From our sponsor: Ready to show your plugin skills? Enter the Penpot Plugins Contest (Nov 15-Dec 15) to win cash prizes!
Today we’d like to share some WebGL experiments with you. The idea is to create a very realistic looking rain effect and put it in different scenarios. In this article, we’ll give an overview of the general tricks and techniques used to make this effect.
Getting Started
If we want to make an effect based on the real world, the first step is to dissect how it actually looks, so we can make it look convincing.
If you look up pictures of water drops on a window in detail (or, of course, observed them in real life already), you will notice that, due to refraction, the raindrops appear to turn the image behind them upside down.
You’ll also see that drops that are close to each other get merged – and if it gets past a certain size, it falls down, leaving a small trail.
To simulate this behavior, we’ll have to render a lot of drops, and update the refraction on them on every frame, and do all this with a decent frame rate, we’ll need a pretty good performance – so, to be able to use hardware accelerated graphics, we’ll use WebGL.
WebGL
WebGL is a JavaScript API for rendering 2D and 3D graphics, allowing the use of the GPU for better performance. It is based on OpenGL ES, and the shaders aren’t written in JS at all, but rather in a language called GLSL.
All in all, that makes it look difficult to use if you’re coming from exclusively web development — it’s not only a new language, but new concepts as well — but once you grasp some key concepts it will become much easier.
In this article we will only show a basic example of how to use it; for a more in depth explanation, check out the excellent WebGl Fundamentals page.
The first thing we need is a canvas
element. WebGL renders on canvas
, and it is a rendering context like the one we get with canvas.getContext('2d')
.
<span class="hljs-tag"><<span class="hljs-title">canvas</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"container"</span> <span class="hljs-attribute">width</span>=<span class="hljs-value">"800"</span> <span class="hljs-attribute">height</span>=<span class="hljs-value">"600"</span>></span><span class="hljs-tag"></<span class="hljs-title">canvas</span>></span>
<span class="hljs-keyword">var</span> canvas = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"container"</span>);
<span class="hljs-keyword">var</span> gl = canvas.getContext(<span class="hljs-string">"webgl"</span>);
Then we’ll need a program, which is comprised of a vertex shader and a fragment shader. Shaders are functions: a vertex shader will be run once per vertex, and the fragment shader is called once per pixel. Their jobs are to return coordinates and colors, respectively. This is the heart of our WebGL application.
First we’ll create our shaders. This is the vertex shader; we’ll make no changes on the vertices and will simply let the data pass through it:
<script id="vert-shader" type="x-shader/x-vertex">
// gets the current position
attribute vec4 a_position;
void main() {
// returns the position
gl_Position = a_position;
}
</script>
And this is the fragment shader. This one sets the color of each pixel based on its coordinates.
<script id="frag-shader" type="x-shader/x-fragment">
precision mediump float;
void main() {
// current coordinates
vec4 coord = gl_FragCoord;
// sets the color
gl_FragColor = vec4(coord.x/800.0,coord.y/600.0, 0.0, 1.0);
}
</script>
Now we’ll link the shaders to the WebGL context:
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createShader</span>(<span class="hljs-params">gl,source,type</span>)</span>{
<span class="hljs-keyword">var</span> shader = gl.createShader(type);
source = <span class="hljs-built_in">document</span>.getElementById(source).text;
gl.shaderSource(shader, source);
gl.compileShader(shader);
<span class="hljs-keyword">return</span> shader;
}
<span class="hljs-keyword">var</span> vertexShader = createShader(gl, <span class="hljs-string">'vert-shader'</span>, gl.VERTEX_SHADER);
<span class="hljs-keyword">var</span> fragShader = createShader(gl, <span class="hljs-string">'frag-shader'</span>, gl.FRAGMENT_SHADER);
<span class="hljs-keyword">var</span> program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragShader);
gl.linkProgram(program);
gl.useProgram(program);
Then, we’ll have to create an object in which we will render our shader. Here we will just create a rectangle — specifically, two triangles.
<span class="hljs-comment">// create rectangle</span>
<span class="hljs-keyword">var</span> buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
<span class="hljs-keyword">new</span> <span class="hljs-built_in">Float32Array</span>([
-<span class="hljs-number">1.0</span>, -<span class="hljs-number">1.0</span>,
<span class="hljs-number">1.0</span>, -<span class="hljs-number">1.0</span>,
-<span class="hljs-number">1.0</span>, <span class="hljs-number">1.0</span>,
-<span class="hljs-number">1.0</span>, <span class="hljs-number">1.0</span>,
<span class="hljs-number">1.0</span>, -<span class="hljs-number">1.0</span>,
<span class="hljs-number">1.0</span>, <span class="hljs-number">1.0</span>]),
gl.STATIC_DRAW);
<span class="hljs-comment">// vertex data</span>
<span class="hljs-keyword">var</span> positionLocation = gl.getAttribLocation(program, <span class="hljs-string">"a_position"</span>);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, <span class="hljs-number">2</span>, gl.FLOAT, <span class="hljs-literal">false</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
Finally, we render the whole thing:
gl.drawArrays(gl.TRIANGLES, <span class="hljs-number">0</span>, <span class="hljs-number">6</span>);
And this is the result:
After that, you can play with the shaders to get a hang of how it works. You can see a lot of great shader examples on ShaderToy.
Raindrops
Now let’s see how to make the raindrop effect. First, let’s see how a single raindrop looks:
Now, there are a couple of things going on here.
The alpha channel looks like this because we’ll use a technique similar to the one in the Creative Gooey Effects article to make the raindrops stick together.
There’s a reason for the color: we’ll be using a technique similar to normal mapping to make the refraction effect. We’ll use the color of the raindrop to get the coordinates of the texture we’ll see through the drop. This is how it looks without the mask:
From this image, we’ll use data from the green channel to get the X position, and from the red channel to get the Y position.
Now we can write our shader and use both that data and the drop’s position to flip and distort the texture right behind the raindrop.
Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.
Raining
After creating our raindrop, we’ll make our rain simulation.
Making the raindrops interact with each other can get heavy fast — the number of calculations increase exponentially with each new drop — so we’ll have to optimize a little bit.
In this demo, I’m splitting between large and small drops. The small drops are rendered on a separate canvas
and are not kept track of. That way, I can make thousands of them and not get any slower. The downside is that they are static, and since we are making new ones every frame, they accumulate. To fix that, we’ll use our bigger drops.
Since the big drops do move, we can use them to erase smaller drops underneath them. Erasing in canvas is tricky: we have to actually draw something, but use globalCompositeOperation='destination-out'
. So, every time a big drop moves, we draw a circle on the small drops canvas using that composite operation to clean the drops and make the effect more realistic.
Finally, we’ll render them all on a big canvas
and use that as a texture for our WebGL shader.
To make things lighter, we’ll take advantage of the fact that the background is out of focus, so we’ll use a small texture for it and stretch it out; in WebGL, texture size directly impacts performance. We’ll have to use a different, in-focus texture for the raindrops themselves. Blur is an expensive operation, and doing it in real time should be avoided – but since the raindrops are small, we can make that texture small as well.
Conclusion
To make realistic looking effects like raindrops we need to consider many tricky details. Taking apart a real world effect first is the key to every effect recreation. Once we know how things work in reality, we can map that behavior to the virtual world. With WebGL we can aim for good performance for this kind of simulation (we can use hardware accelerated graphics) so it’s a good choice for this kind of effect.
We hope you enjoyed this experiment and find it inspiring!
Great! 🙂
This is amazing!
WOW – Absolutely amazing… Great job!
This is insane…!
This is beyond awesome! This is amazing!
This is great! We need a snowing version too 🙂
have u find snowing version?
Oh, it’s awesome, I want to learn it
This is truly awesome! Thank you for laying out how you came to the conclusion how it works instead of “just” posting the result!
Thank you for the great effect. I think it can be used in weather applications.
Waouw ! Amazing work ! Thank you for sharing this 🙂
Amazing. Your work is awesome!
Fantastic! Finnaly, I’h add some JS&it’s begin work in real time… Year, there is rainy everytime.
Works beautifully on my Surface Pro 3, really nice coding too. Thanks for writing it up
No word.
Nice work with metaball concept + shader + webgl! Already saw your amazing work on CodePen and Dribbble, keep on doing great stuff Lucas!
very nice, i’m starting using webgl too, but i’m using threejs library
Really impressive, good work!
A M A Z I N G and curated work <3
best regards from Canary Islands.
This is fantastic work!
This is so detailed work, I would love to learn WEb GL . Codrops always come with the best creative ideas all the time.
Good Job and keep up the good work.
MY FAN IS SCREAMING! LOL. (awesome work!)
omfg
Hey guys, why not as Chrome Extension?)
Now this is jaw droping, scary , and even sad for me. I mean – I am far behind 🙁
OH MY….. THIS IS UNBELIEVABLE!!!!! SOOOOOOOO REALLLL…
I always expect great ideas here, but this is next level. Seriously creative, pushing the boundary of whats possible in a browser.
Fantastic work
Looks amazing when first loaded, but the longer you leave it running the less real it looks because the smaller drops accumulate too fast and are not cleaned up by the larger drops consistently enough to make it appear as if the small drops are interacting properly.
This feels good
This is just awesome! Thanks for sharing, Lucas.
WOW!
Simplesmente: Fodástico!
That’s absolutely amazing!
One of the coolest effects I have seen.
Great work! Thanks for sharing this experiment and overview. Can’t wait to play with it more tonight, I find it inspiring indeed! 🙂
Lucas, great work, any plans for adding a snow effect?
Worked great on my iPhone. Really amazing effect!
Brilliant!
the source (zip) doesn’t work on my computer but the demo from your website , yes ! I dont know why ….
I use the last Google Chrome
An idea ?
Thanks
You’ll need to put it on Localhost.
Seriously cool demo! Works fine on Firefox (Mac).
this is so cool but the hazardous rain is more cooler !
Gotta try something like this, then figure out how to use Modernizr to only load it if it’s gonna work 🙂
This is amazing, how smooth I can’t believe it! Thanks!
This is so awesome – can you make this a chrome extension/new tab page? Or just a page that actually uses the real weather for the current location. That’d be so awesome 🙂
so cool
This is awesome
awesome and hard work!
in demo better to move city layout, not a “glas”. cuz feels like its not a window, but some raincoat))
Could you add snow too?
Exactly what i am searching for this…
Nice one….