Animating water tiles part 2: Experimenting with Shaders.

So, last week I spend time on figuring out different animations in terms of raster animation that is then baked into the final tile. This week, I investigated a different approach to game graphics: Shaders.

Shaders are formally tiny programs that are specially written for the graphics card. In game engines, shaders are most prominently used to calculate how a 3d object should be rendered, or shaded. But they’re also used in 2d graphics a lot. Krita for example uses shaders to make the canvas perform better, handling cursor and to display HDR images. Godot also allows applying shaders to 2d graphics, even specific tile graphics. So I spent some time playing with that.

In the case of water, there’s a ton of stuff out there on water shaders in 3d. But I am interested in water shaders in 2d, to spruce up the water effect I had going. So I set up a tileset in Godot, with my water tiles as an ‘animated texture’ as one of the spritesheets/tileatlasses used for the tileset. Godot allows individual textures that are part of a tileset to have a shader assigned to them, so the following visual shader was set up:

Image displaying the full visual shader graph.

This may seem like a very overcomplicated thing, but visual programming always looks overcomplicated, as each block in visual programming is a single operation. I chose to use visual programming here because I imagine there’s people out there for whom regular programming is a bit intimidating, so here’s a confirmation that what I am about to do can be done in the visual programming interface. Let’s break down what is done here!

Blending

Starting out, these are the water tiles we have:

Basic water tiles that animate on the edges.

We overlay these over terrain on a seperate tile layer:

Water tiles overlaid over different types of terrain, like dirt, grass, and pebbles.

This looks a bit dull, so the first order is to blend them with the underlying tiles. We do this in two passes: once with multiply, and once with overlay, so that the contrast gets increased nicely, as is typical of wet things in the real world. So we go from the most basic ‘canvas item’ visual shader:

Most basic visual shader, connecting the uv to the texture, and then the texture to the output.

To this:

Visual shader graph that blends the water tiles twice into the background. First is multiplied and second is overlay. These are both blended 50%.

In the above, you can see two things not mentioned. First is that the preview is useless here. The second is that we also use vector interpolation to blend the multiply and the overlay 50% with the underlying texture. This is to avoid oversaturated colors.

Next up, we’ll add some clouds to give the effect of an overhead sky. I want to show the overlaid area only when there’s no cloud shadow, so you get the effect of sun reflecting on the water. For this we’ll use a simple black and white texture.

Visual graph showing how to use clouds to blend between the two versions of water and thus get a reflection effect.

We’re getting to the point where the visual shader graph is a bit too big to really fit, so subsequent images are going to only show a small part, and you’ll have to imagine to integrate them into the whole.

So, the next step is getting the clouds to move. This is as simple as taking the ‘time’ input and using a vector op to add it to the uv input of the clouds. (The uv being a coordinate system that the texture is mapped onto) However, we’ll want to control in which direction the clouds move. We can just use sine and cosine to calculate x and y modification from a radian angle, but most people prefer degrees for input. So we’ll need to convert those degrees to radians. Furthermore, we will also need to make sure the input is limited to 360 degrees.

To do this, we start with a ScalarUniform we’ll name ‘Angle’. Then, whenever you use this shader, Godot will present you with a little input labeled ‘Angle’ that you can set to anything. Another one is ‘speed’, to indicate how fast the clouds go. The visual shader graph is the following:

Visual shader graph showing how to make the clouds move through a user-input angle.

In the above image, we take the angle, use the modulus/remainder operator to limit the value to 360, then multiply by 3.1427(visual shader editor doesn’t have constants) and divide by 180 to get the corresponding radians. The radian value then has sine and cosine functions applied to it to get the x and y coordinates. These are normalized values, so we can just multiply them with the time vector. The time vector is the time interpolated with 0 using the ‘speed’ uniform as the interpolation factor. That is, if the speed is high, the resulting value will be higher, if the speed is low the resulting value will be low. The resulting value is our time vector, which is then multiplied with the angle vector and that is then added to the screen uv, which is what causes the movement animation.

Water flow

For water flow, we can use a flicker animation and overlay that. Then, similar to the clouds we can move them in a given direction. But first, the flicker animation is 1/8th in size compared to the animated texture. We’ll need to apply a transformation matrix to the uvs of the flicker texture here.

visual shader graph showing how to align and compose flickers onto the water tiles.

In the above graph there’s still the time stuff, but we’ve hidden it offscreen for now. Once the UVs are aligned properly, you can use the alpha of the flickers to interpolate/blend them onto the main texture.

So, one of the things that I really wanted to figure out was whether I could use flowmaps to control the water direction. There’s been several papers on this, with the most notable one being the valve paper, where flow maps weren’t just a simple but cool result, but also helped players find their way through a map. I tried to glean most of my info from this useful tutorial on the subject.

Gif showing flow distortion using a flowmap.

So, in the above we have a flowmap texture(that is aligned to the original water texture, more on that later), this is then decomposed into xyz values. The xyz values are decoded from their 0 to 1 range to the proper -1 to 1 range so they’re proper vectors. Finally, the result is multiplied with the time vector, and then added to the other UV modifications(offscreen). This is following the initial part of that tutorial, giving the simple ‘distortion’ effect (the tutorial also recommends adding a fraction function so that the time doesn’t repeat into infinity doing weird things in the shader).

Shader graph showing rotation

Above we replace the vector op ‘multiply’ node with an attempt to rotate the time vector with the vector from the flow map, following the second part of the flowmap tutorial. I was unsure how to do this, as I kept thinking to myself ‘surely, they’re not expecting us to manually multiply the matrices?’, but after some search this youtube video demonstrated that indeed, in the visual shader graph the matrices of the two vectors do need to be multiplied manually. Note that the first vector compose, and two decomposes are unnecessary here, but are just there to help visualize.

In the end, I would’ve liked it if I could have created a second tilemap where I would create the flow using tiles, and then used that to inform the distortion, but that doesn’t seem to be possible. So while it is possible to create a flowmap effect in Godot’s visual shader, I am not sure how to effectively create and align a flowmap to objects in the game engine.

Other

If you add in masks to emphasize the edges, and a mask to make the sparkles in the center less strong, you end up with something like this:

Final result of all the different parts.

Meanwhile, on the OGA forums, BlueCarrot was able to get a much simpler and easier to use flow effect going by just animating center tiles, so next stop is to experiment with that instead. I just figured I’d document everything I’ve learned up till now. 🙂

Overall, it was a little surprising that the Godot visual shader graph, which is for people not experienced with coding didn’t really have easy ways to generate a transformation matrix from vectors or radians from degrees, or even have access to common maths constants like pi. This makes the shader graph surprisingly barebones. The above shader could probably also be optimized, but right now this was more of a ‘how would we go about it’ rather than worrying about speed.

Author: Wolthera

Artist, Krita manual writer, Color Management expert and also busy with comics creation.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.