Previous Post

Water Rendering

Next Post
Water Rendering
1 / 13
DESCRIPTION

In this post I'll dive into the rendering techniques used in the new water system in my Forest project:

normal maps

displacement map + compute shader

planar reflections

fresnel reflections

absorption

distortion

murkiness

caustics

We'll start with the basics and work our way up.

Geometry

First we'll add a flat plane to the world, which we'll simulate water effects on later.

Then we'll use the same forward lighting code as the rest of the world to add sunlight, shadows and fog:

Normal Maps

The normals are all pointing up by default, so we'll sample a water normal map in the fragment shader to add some detail to the water surface.

I'm using the normal map created by Cebbi over at BlenderArtists

Paired with the lighting code, we get this bumpy looking water:

Then we'll move the normal map over time to simulate moving water. Since the normal map repeats, there aren't any seams:

To disguise this linear movement, we'll sample the normal map again in a different direction, and then average the two normal values:

This is pretty turbulent, so for this lake we'll slow it down and dampen the normals:

Displacement Map

The next step is to be able to interact with the water, so that objects cause ripples when they intersect with the water surface.

First we'll create a texture, where each pixel represents the height (red) and velocity (green) of a portion of the lake. The texture is 1024x1024 pixels and the lake is 150x150 world units, so each pixel represents roughly 0.14 world units.

Then we'll create a compute shader that simulates wave physics on this texture. For each pixel, the shader will read the height and velocity of all nearby pixels and average them.

The output is stored in a 2nd texture, since we can't read and write from the same texture without causing artifacts. The next frame, it will read from the 2nd texture and write back to the 1st, and so on.

When fish swim through the water, they'll add a small upward bump to the height of the pixel at their location. When a rabbit or player walks around the edge of the lake, they'll reduce the height of the pixel at their location. The compute shader then averages this out over time, causing ripples.

Here we can see fish moving through the water, and rabbits running around the edges of the lake:

To display this visually, we'll adjust the normals of the water based on the height of the displacement map. We can calculate the normal of a pixel by reading the height of its 2 neighbouring pixels, and using this GLSL formula:

Now as rocks drop in the water, we can see ripples in the sun light.

This looks good from above, but from a shallow angle we can see it's just a flat plane.

To change the height of the water, we need to subdivide the water plane into a grid of triangles. I use 1024x1024 grid when the camera is close to the water, and swap to a 256x256 grid when further away.

In the vertex shader we'll adjust the Y position of each vertex based on the height in the displacement map.

Now as rocks fall in the water, we can see ripples form in 3D:

Reflections

Currently the water is a murky blue oil that only reflects the sunlight, whereas it should really reflect the world around it.

To reflect the world in the water, we need to render it again again from underwater looking up. We'll create a second camera underneath the water, and then flip it's pitch so it's looking upwards.

Then we'll render the scene again using the reflection camera and save the render result to another texture.

Rendering the entire scene again is slow, so we'll render it at half resolution, disable shadows, and won't render the terrain. Since the reflections are distorted, we can get away with this cheaper rendering.

To sample this texture in the water shader, all we need to do is flip the screen's UV coordinates vertically so the reflection is mirrored, and then sample the reflection texture.

The next step is to distort the reflection based on the ripples. There's no fancy re-projection here, it just offsets the UV by the water's normal value.

Fresnel

Currently we can only see reflections - all the underwater geometry is covered up.

If you try looking at your phone/computer screen from different angles - you'll notice the reflection is clearer when looking from the side. This is the Fresnel effect — at shallow angles, surfaces reflect more light.

To implement this in our shader, we'll check the difference between the camera-to-pixel angle, and the pixel's normal. Then, we can reduce the pixel's alpha from 1.0 at grazing angles, to 0.05 when looking straight down.

Absorption

The yellow sand on the lake floor is adding a golden hue to the reflection. To fix this we'll use Beer-Lambert absorption, which affects how water absorbs different colours.

Red is absorbed the quickest, then green, then blue:

However, we can't just 'edit' the underwater colour, since we're only rendering a transparent water plane over the top.

So rather than using alpha blending, we'll copy what we've rendered so far (without water) to another texture. Then we'll sample from this other texture to get the underwater colour.

Then we can disable alpha blending and perform the blend manually:

Distortion

The water surface is rippling but we can still see the underwater geometry clearly. We need to distort the underwater geometry too, just like the reflection.

We'll distort screenUV using the water's normal (like in the reflection code), but without mirroring it vertically.

However! The top of the wooden pole is being distorted by the water behind it.

To prevent this, we need to check if the distorted pixels we're sampling from are above the water, and if so disable distortion. To do this we'll compare the depth of the distorted pixel against the depth of the water surface:

Murkiness

Currently the water is very clear, so we'll simulate murkiness by interpolating into the water colour based on how far underwater the pixel is.

This is a simple fade from 0% murkiness at the surface, to 95% murkiness at 4 units deep.

Caustics

The last effect is caustics, which simulates how light is concentrated at certain parts of the lake floor, due to the angle of the water surface.

These caustics are an approximation - I'm not using the water normal or displacement map to create these bright spots.

Instead I used this caustic function which creates bright values near the edges of voronoi cells. I used this in Sector's Edge years ago and can't remember where I got it from.

Then we multiply the underwater colour by the result of this function.

Remaining work

I'm not entirely happy with shadows. Currently there are two shadows - one on the surface and one underwater.

I feel like it should be casting a volumetric shadow through the water instead. I'll have to do some research and figure out how this should work.

I also need to clean up the shader and optimise it where I can, as I'm sure there's room for improvement.

For Patrons this shader is available on the new_forest branch, and will be merged back to main once I've added support for older GPUs (currently this forest renders using some OpenGL 4.6 features). It may not run on your machine as it is still a work in progress.

Thanks for reading - I'd love to hear your feedback on this water system and anything else you think I should add to it.

Full resolution screenshots and videos are available here.

vercidium PATREON 2 favs
VIEWS1
FILES13 files
POSTEDMar 9, 2026
ARCHIVEDMar 9, 2026