That Water Though

One of the things I'm most proud of in this game is the 2d water simulation, here's a run-down of how I programmed it!

Posted by on

One of the things I've noticed people really enjoy seeing are gorgeous water effects, so I decided to gently stroke this obsession by creating my own water effects. But I, as many have before me, will show you the full effect at the beginning and describe the entire process for those interested in how it all works.

So we begin, here is a gif (on the post, most I'll just link to conserve your precious loading times) of the semi-final look (I'll still be tweaking and adding more to it later):

As you can see, the water flows dynamically, ripples follow anything that moves in the water around, and raindrops create little ripples!

Now, how does one accomplish such an effect you might ask? Well it's actually surprisingly simple! I started... with an algorithm, as many of these projects seem to start. That magical few systems of equations that mysterious and wondrously create dazzling effects or, you know, make a path or something. The algorithm operates over a large 2d array of floats (or ints if you're in to that). I set it up to work on float values ranging from 0 to 1. Here's the code:

`left = waterMap1[i-1][j] or 0;right = waterMap1[i+1][j] or 0;up = waterMap1[i+1][j] or 0;down = waterMap1[i-1][j] or 0;waterMap2[i][j] = (left + right + up + down) / 2 - waterMap2[i][j];waterMap2[i][j] = min(max(waterMap2[i][j] + .00005f, 0f), 1f);draw WaterMap2swap WaterMap2 and WaterMap1`Holy really simple code, Batman! The left, right, up, and down values are set to the respective locations in the buffer of the previous frames values, except for at the edges they are always set to 0 (or in my case 0.1 to add some constant turbulence to the water). First I got this algorithm to work over a small array, and converted that array into a texture so I could visualize how it was working. I started off both of the buffers with a randomly generated perlin noise so I could see how the waves were working.

Now that surpassed my expectations! With little to no effort you get a water effect that can easily support ripples that reflect of the edges and waves that pass through each other!

Now I just had to up-scale it to work with the water in my game. I gave each tile that contained water a 32 by 32 array of floats to store the height of the water at each pixel in the tile, then when it does the algorithm, it checked to see if the tiles around it also contain water, and if they do continue the simulation on to those tiles instead of calling it a day at it's borders. This way I don't have to simulate water over the entire screen, just on the tiles that actually contain it. After I finally got this working (after a few bugs) it ended up like this:

Now this is looking great! The only big issue now is that water, at least the water I'm used to, is most certainly not black and white. I also wanted to add in normal maps so the water reacts to light the way it should (I can do a separate post just about normal maps if people really want it). Calculating the normal maps is pretty easy once you have an array of all the heights of the water, all you have to do is:

`Vector3 Normal = new Vector3( ((leftUp - rightUp) + 2 * (left - right) + (leftDown - rightDown)) / 3 + .333f, ((leftUp - leftDown) + 2 * (up - down) + (rightUp - rightDown)) / 3 + .333f, 1);`where each variable is the just value of the pixel at that relative location. The (/3 + .333f) part is to scale down the normal because otherwise the waves were far too pronounced. Now I just had to throw that new array into a texure and throw it in the shader and we're in business!

Next, instead of drawing it as a gradient of black to white and set up a new gradient of blue to a different blue to make it look more like water.

Another thing I did before this next gif was allow raindrops to create ripples in the surface of the water. All you have to do is figure out which pixel you want the ripple to start at and you:

`waterMap[x][y] = .9f;`You make it .9f there because you want to add energy to the system with a larger number than is around it. You can create bigger ripples (like in the earlier gifs) by making all the pixels in an area set to .9 or 1 (in the first gif it sets a circle of radius 5px to 1).

Now we have this:

and this right when rain is starting:

So now we have some pretty nice-looking water, but it's still a little flat. We need to add some variation to the texture of the water the is irrespective of the waves themselves. To accomplish this I simply draw the water texture using an offset of the water texture we were using before, color modified slightly by the height-map of the water and with the normal maps added.

The result is this:

Now all we have to do is make this semi-transparent so you can see fish under the water, the lake bed, and reflections from objects (which I still have to make based off the waves, but I will do that soon enough). The part is easy, you just draw it semi-transparent. But I also wanted to add an effect where objects moving through the water would leave trailing ripples. This also turned out to be very easy. You just iterate over the collision box of the object when it's in water, and you add a small amount to the waterMap at each of those pixels.

After that you get this:

A few more tweaks later and you get the gif at the top of the post! There's still a bit to do with this but I'm very excited about how well it's turning out so far.

Next time! We eat more code!

Very interesting article.

Lovely article, cancerous title.

I got the basic ripples working / raindrops... but they don't reflect off the land / edges... how would I do that?

Author

Sorry, I just noticed this comment!

The ripples only reflect off the edge of the square you are calculating the water for. However, if you want them to reflect off of a specific surface in the middle of the water, you have to make sure the values for the Water Map at those points are always 0. If you don't compute the flow at those point and consider them 0 to all other points, then they will automatically reflect off those areas.