Around The World, Part 15: Making waves

I’ve been investing a lot of effort in the generation of plausible land. But the game is all about sailing, so most of the screen will be filled with water, not land. It’s time to take that smooth blue plane that served as the sea, and make it look better!

The rendering of the water will largely be done in a shader. Godot shows changes to shader code live in the editor itself, but it doesn’t hot-reload modified shaders into the running game. So to shorten the feedback cycle, I created a little test scene in Blender and imported it into Godot:

The test scene, with a flat water surface

This also reveals the carrack that I modeled earlier. For reference: it’s about 25 meters long.

Sines

If you’re even slightly mathematically inclined and you think of waves, you think of sines. So let’s start with that: a single sine wave, moving along the water. I’m using the very same mesh data as for the terrain to also render the water surface, but with a vertex shader that moves the vertices up and down. The wave propagation speed is determined from its wavelength a simple formula: frequency = sqrt(gravity * 2 * pi / wavelength). This is only valid in deep water, but I’m using it for shallows too. Here’s the result (I’m too lazy to create videos of every step, so you’ll have to make do with static images):

A single sine wave

The effect is… underwhelming. Part of the reason is that the mesh normals aren’t modified: they still point straight up. This affects things like shadows and reflections.

Fortunately, it’s easy to compute the derivative of a sine wave, which can be used to compute the gradient, which in turn can be used to compute the normal.

A sine wave, but with proper normals

I’m doing all this in the fragment shader, so it’s done finely per pixel rather than coarsely per mesh vertex. This will make a big difference in visual quality later on, when we add finer details.

Transparency

The shape of the waves is not yet great, but there’s a more pressing problem: this blue sheet doesn’t look like water, but more like a corrugated solid surface. It lacks the transparency and reflections that are characteristic of real water.

Real water gets more opaque the deeper it gets, because more and more light is absorbed along the way to the bottom and back up again. We can emulate that: since we’re reusing the terrain mesh to render the water, each vertex already knows the local water depth from its y coordinate. So let’s use that to create exponential attenuation:

With added transparency

Note that this is only an approximation, because it’s not the water depth that matters, but the length of the path that the light takes towards the bottom. If you’re looking at an oblique angle, that path might be (much) longer than the actual depth. But we’re not using very oblique angles in this game, so the approximation works well enough.

However, because shallow water is now almost fully transparent, it’s difficult to see where the water ends and the land begins. In reality, there would be some foam caused by breaking waves. We can easily add that: wherever the water depth (including wave height) is less than 1 meter, render the water as opaque white:

With surf

The surf definitely does not look amazing. It could do with a bit of texture and randomization. But it’s not awful and it serves the gameplay purpose, so it’s good enough for the moment.

Wave shape

As I discovered while I was working on this, there is a lot of scientific literature on waves. One thing that immediately becomes clear: ocean waves are not sines. A better model is the so-called trochoidal wave or Gerstner wave:

Trochoidal wave
Source: Wikipedia by Kraaiennest, CC-BY-SA 4.0

As you can see, each surface particle doesn’t just move up and down; it actually moves in a circle. It’s tempting to have our mesh vertices play the role of surface particles, and move the vertices horizontally as well as vertically. It looks good, but it has a significant drawback: the water surface is no longer a simple height field. In other words, given a coordinate in the world, it’s very hard to compute what the water height at that point is. And we’ll need that computation later, when we want the ship to float on the surface and move in response to it.

So we’re going to settle for an approximation. The very first chapter of the very first GPU Gems book describes one way to do this: shift the sine wave vertically to be between 0 and 1, raise it to some power, then shift it back. (Note that the web rendering of the book is slightly broken: the π symbol is missing from some equations.) A power of 2 looks good to me:

With approximated trochoid shape

Calculating the derivative is slightly more involved now, but it’s still just high school calculus. We can vary the exponent to make the waves look more or less pointy, but higher exponents flatten the valleys too much, so this model is not perfect. I can always revisit it if needed.

More waves

The corrugated solid look is gone, but the waves still look extremely straight and artificial. Real ocean waves are affected by all sorts of random processes, so they aren’t so regular. We’ll take another page out of the GPU Gems book, and add four such waves together, with different amplitudes, wavelengths and speeds – and most importantly, moving in slightly different directions. They add up to a much more irregular surface:

Multiple waves

It may not be immediately clear from these static images, but the interaction between waves and land does look rather weird. Basically, there isn’t any: the waves simply pass through the land and disappear. In the above image, you can see a relatively tall wave cutting a straight path through the islands.

In reality, waves tend to get taller in shallow water:

Wave propagation animation
Source: Wikipedia by Régis Lachaume, CC-BY-SA 3.0

However, that by itself will only make the problem worse. There’s another effect at play which counteracts the growing waves: when they get too tall, they break and topple forwards. Breaking waves are more difficult to model, so I don’t want to go there for the moment. Instead, I’ll just reduce the amplitude in shallows, all the way down to zero at the waterline:

Attenuating amplitude in shallows

That gives a much calmer and more convincing look, especially in motion. The definition of “shallows”, by the way, is also in the literature: water is considered shallow if the depth is less than half the wavelength. This means that shorter waves will travel farther towards the waterline before getting extinguished.

Fine details

We’re getting there, but a real ocean surface doesn’t stop at a sum of just four waves. There are also waves of much shorter wavelengths running around and interfering with each other, giving water its characteristic sparkling and shimmering reflections:

A real ocean surface
Source: Pixabay by jingoba

Since we’re having fun summing powers of sines anyway, why not sum some more, adding shorter and shorter waves to the mix? The effect is… not quite what I was hoping for.

Smaller sines

Due to the smaller wavelengths, there are clearly visible repeating patterns now. The same actually happens for the larger waves as well, but it’s not visible in this small-scale test scene.

Can we fix this with more sine waves? Only up to a point, as it turns out. After over an hour of tinkering with parameters, I got somewhat better results:

Smaller sines, improved

I might eventually have to throw in some simplex noise. What’s holding me back is a limitation in Godot: even though the simplex noise algorithm makes it trivial to compute the gradient of the noise alongside the actual value for little to no cost, Godot only exposes the value and not the gradient.

On a sphere

Did I mention that everything is harder on a sphere? This includes waves.

First off, the y coordinate of a vertex is no longer equal to the height of the terrain. We could do a calculation using the vertex position, but remember that I’m using a floating origin, so this is harder than it seems. Instead, I’m just passing the terrein height (negative for water depth) in a custom vertex attribute.

A more subtle problem is how to represent waves. Imagine a single wave all the way around the equator, travelling north. At the equator, all looks good, and the wave crests will look nice and parallel. But the farther north you go, the more they are distorted; at the north pole, the waves would seem to converge inwards from all around you!

A related problem, which also applies to flat planes, is: how to adjust the waves when wind speed changes? We can’t just change the wavelength of an existing wave, because as GPU Gems mentions: “Even if it were changed gradually, the crests of the wave would expand away from or contract toward the origin, a very unnatural look.”

But GPU Gems also has a solution: individual waves live only for a limited amount of time, fading in at the start of their lifetime and out at the end. “Therefore, we change the current average wavelength, and as waves die out over time, they will be reborn based on the new length. The same is true for direction.”

This also solves the problem of distorted waves on the sphere: as long as the player moves slowly enough that new waves are born around their point of view, they’ll never get to a point where the waves appear to converge or diverge significantly.

Anyway, after getting all that sorted out, the water renders nicely in the game:

Water in the game

(This might be the first screenshot I’m sharing of the game itself! Please ignore the ugly HUD. It’s all placeholder.)

I’m well aware that it doesn’t look great yet. But at least it appears… watery?