Last saturday I published a clip on twitter highlighting the new wavy look of the water in the game. That was just a small improvement over what had been coded for a long time but it wasn't so glaring to me at first. Today I'm going to explain how I created the water shader by breaking down the basic principle behind it.
When I started desiging the game it was obvious I would have needed water sections. Almost all metroidvanias have them and the Amazon rainforest wouldn't feel right without a big river running through it. However, the game is 2D. While it's not technically 2D in coding terms, it's still rendered through an ortographic camera. This means that most effects that require the calculation of view and light angle simply don't work, because an ortographic camera views each pixel at the same angle, that is perpendicular to the XZ plane in my case. In this situation, the classic approach of applying a normal map to a plane and let the translucent material work its magic just isn't available.
In this blog post @superyateam describes an approach that can get you the best of both worlds, but It couldn't get me the extreme pixel crispness I wanted. For this reason, I decided to replicate most of the effects a perspective camera provides for my very limited orthographic camera. I started looking into the subject and found out the guys making The Siege and Sandfox already did it. In this devlog post they briefly describe the process of distorting the pixel data coming from the SceneColor node adding values to the ScreenPosition coordinates. In the end, the secret is just there, so what do I have to add? A whole lot of nodes, actually.
I'll give you a quick run-down: all the distortion wizardy happens in the top chain.
First I create UVs that map nicely along the world coordinates. If I map the distortion to the normal texture coordinates of the box I'm going to use to render the water they will just feel stretched as soon as I stretch the box to cover a wider area, and I don't want that. With this method, the effect won't look too noisy or just completely off-center.
After that, I apply some panning if I want the water to scroll. Using parameters I can easily tweak other aspects of the shaders according to the scrolling speed. Making the water feel more chaotic or (like my tweet clip) control waves on the surface.
At this point, I have my world coordinates set up. I first multiply them with a couple of parameters in case I want a more "relaxed" look, then hook them up in a flipbook node. What this node does is cycle through a texture like it was made from many smaller textures, and display them in sequence. This creates an animated texture without the need of expensive fractals. I'll explain the process to create a texture like this in a future post. The values obtained from the texture are in the range 0->1, so I multiply them by 2 and subtract 1 to bring them in the more useful -1 -> 1 range.
What if I want the water to feel stronger or much more "dense" than usual? I just increase the magnitude of the distortion with a multiply operation. Lava, oil, gasoline and jello have very different refractive indexes and this will help me nail that look. In this block I also take into account the actual width of the camera. This is necessary because if my camera is rendering a smaller area of the game, for example 240 units, my distortion will feel much weaker, therefore I need to increase it by the correct amount. For reference, my cameras are set to 320 unreal units wide.
That's it. Adding the distortion to the scene coordinates is just the last part of the process.
After this I actually make some color conversions and overlay other textures to give the water its foamy and shimmering look, but you can do anything you want, from tinting the water to the color blue or green for acid and red for lava.
Remember this distortion tecnique comes in handy for many more effects and not just water. I used it for fire heat, wind, droplets, glass and much more.