Water Shader
- jamesghholt
- Mar 30, 2022
- 4 min read
Updated: Jun 18, 2022
As our game is based on a beach, a suiting water shader is needed. It's also a large focus within the game, so creating a shader with many layers of customisability- to be easily handled by other group members- and accurate to our style was a must. Below is a small example of it in action.

Here's the shader in it's entirety, I'll try break down it's functionality:

This section focuses on colouring the water and defining the waters material properties, all are made with customisability in mind, through the use of parameters.
Depth Fade is used to hide unsightly seams between translucent objects, when intersecting with opaque ones.

Frequently used with water shaders, or with deep fog - as assets lose opacity, and smoothly blend into one another. Simulating an effect of depth. In this case depth fade is used as a mask for albedo and opacity. They are lerped between two albedo inputs to create a somewhat faded "layered" look. Both layers can be independently manipulated in opacity and colour through parameters in instanced materials.
The various parameters allow artists to control the stylisation of the water; how it's perceived and interacts with the environment. Later on there is a exaggerated example of this.

Here are two examples of the nodes above in action, one exaggerated to show the effect of "Depth Fade".

Similar to the fire VFX, the normals share a distortion input. Handled by a channel packed noise texture, this noise is also panned ("Panner" node), to create constant shifting distortion across the water's rippling surface. Both the scale of the noise UV's and the speed at which it pans are defined through parameters.
The normal texture is sourced from Unreal Engine, both scale and speed can be manipulated. Normal intensity can be changed, I usually use a different method of extracting the R/G (X/Y) channels, changing their individual intensity (greyscale is much easier to manipulate), then appending all channels (RGB) back together. However, this new method is much more efficient and neat. Instead, a "Lerp" is used, the two inputs are the normal texture and a 3 vector (0,0,1). A completely blue normal map will appear flat; removing the blue create the opposite effect -- exaggerating the red and green channel -- therefore exaggerating the normals.

Below are normals being used, with little distortion, low opacity and tweaked parameters. Pushing the realism of the water.

The distortion noise is used as the foamy shoreline, and is also distorted by itself. The top line simply pans the noise and manipulates the greyscale map to a suitable look, for which it can be later added to the bottom line of nodes. Which focus on creating a "pulsating" effect, this is controlled by a "Sine" node, and this pulse will be our shoreline ripples.

Below is the pulsing effect. But how does this odd effect create shorelines?

Through the implementation of a node called "DistanceToNearestSurface". Which needs mesh distance fields active to function, but what are these? To represent a Static Mesh's surfaces, a Signed Distance Field (SDF) is used. It works by storing the distance to the nearest surface in a volume texture. For every point on the exterior of the mesh is considered positive distance and any point inside the mesh stores a negative distance.

Each level that you create is made up of all these Mesh Distance Fields for your placed Actors. When Mesh Distance Fields are generate, they are done so "offline" using triangle raytracing that stores the results in a volume texture. Because of this, mesh distance field generation cannot be done at runtime. This method computes the Signed Distance Field rays in all directions to find the nearest surface and stores that information. Here's a visualisation of the mesh distance fields.

Because of this, any assets placed within the water will instantly have a "shoreline" effect. Meaning the scene is much quicker to build, and outputs a dynamic effect in response.
Below is an extremely stylised variation of the shoreline, with scale high and distortion rather low. Simplifying the noise.

As expected, the ocean has waves. To displace the mesh of the water, tessellation and displacement are used. Flat tessellation is used, to simply split up the triangles, providing more geometry to create accurate displacement. However, tessellation is performance heavy, so I disabled it at a later date.
The code below uses a "Sine" wave to displace the mesh, similar to a Sine wave. The scale and speed of said wave can be controlled. Below is a exaggerated example.


Refraction is the term used to describe the change in direction of a light wave due to a change in its transmission medium. In other words, when light comes in contact with certain surfaces, like water or glass, the light will get slightly bent because those surfaces affect the speed at which light travels through them. The best example of Refraction can be seen by placing part of a pencil in water. Using the normal map, of the water ripples, this effect can be mimicked. Below is an example of it in action, distorting the silhouette of a elongated cube.


However we did run into some issues. Our cell shader cannot mimic the silhouette of the asset being distorted. As seen in the example below -- because of this refraction was removed from the project.

I'm very happy with the final results of the water shader, it's many layers of customisation provided artists creative freedom with it's application. It fits comfortably within the scene, adds a layer of dynamic movement, to a mostly static environment, and it's colour breaks the mostly green landscape.
References
Comments