Dynamic water shader

Youri Berentsen

For this project I wanted to create a realistic water system. I hesitated between a realistic sea or waterfall system. When I looked up some examples, I was immediately attracted to the game sea of thieves. The game sea of thieves has a very realistic water system, with reflections, refractions, realistic waves that react to the wind. When I compare sea of thieves with Red Dead Redemption 2, the two water systems are very different from each other. Red Dead Redemption 2 has very static water that doesn’t react to the environment, only to the player or objects but not wind, rain or waves.

 

Sea of thieves

Table of contents

  1. What makes the sea in games look so good?
  2. Visuals
    1. Depth
    2. Foam
    3. Refraction (Surface Distortion)
  3. Waves
    1. Perlin Noise
    2. Gerstner waves
    3. Comparing methods
  4. Final Result
    1. Conclusion

1. What makes the sea, in games, look so good?

When we look at the game the sea of thieves you can see one of the most realistic water systems in a game. There are reflections, there is depth in the water, there are small waves, big waves, refractions, foam on top of the waves and it interacts with the environment. When we use all these systems, we can create a realistic sea. In the next paragraph we will go deeper in all these systems. First, we start with the depth and how it is used in the water. Without the depth, the whole water will be one color. In real life you can see color changes in the water, for example: the water near the coast will be lighter than water in the middle of the ocean. 

The next step will be the surface noise and distortion, these are the small ripples on the water that will increase when the wind force is higher. In small lakes where there is no wind the reflections on the water are much smaller than compared to an open ocean.

The next paragraph will be the movement of the sea, The main component will be the waves. For creating the waves there are different ways, some systems have their pros and cons, but for this shader I will use Gerstner Waves. For the final paragraph I will the result, my recommendation and what kind of improvements can be made.

I have used many different examples and sources. There are countless ways and systems to make water with shaders, but these systems must be able to work with each other. That is why I first looked up many examples for myself and researched which types of systems they use. I made a list of what I wanted to add to my project. After this I could start looking for sources that belonged to the different systems, for example: Gerstner waves or Normal map displacement.

2. Visuals

2.1 Depth

A key component is the depth. Depth is crucial because various other features of water depend on it, such as watercolor, transparency and foam. As I explained earlier the watercolor near the coast will be lighter than far into the ocean. First, I will explain how it is done in code and then I will show the before and after.

First the current depth of the pixel is calculated. This depth value is obtained from a depth texture, _CameraDepthTexture, which stores the depth information of the scene from the camera’s perspective. The depth is in a non-linear range from 0 to 1. Then the non-linear is converted to a linear depth. This is done with the “LinearEyeDepth” function. 

Based on the depth of the pixel, it then calculates the difference between the water surface’s depth and the object/floor behind it. This difference is then used in the next calculation. To change the color of the water base on the depth and to determine the amount of foam to display on the surface. 

With this we can display a different color near the coast, so that it will look like the water is shallow near the coast. As seen on the images below. 

Minimal Depth
More Depth

2.2 Foam

The foam and surface distortion (Refractions) are somewhat intertwined with each other. First is the code for the foam explained and in 2.3 we will continue the code with the surface distortion. 

Foam Code

The foam intensity is derived from the angle of the difference between the water surface and the normal surface. These are the “existingNormal” and “i.viewNormal”. Based on this angle, a distance for the foam is calculated, which is then used to determine the intensity of the foam. 

When the water is stationary it will look kind of strange, normally the foam is only visible when the waves are hitting the shoreline. So, if we add some surface distortion the water will be more alive. 

Foam near the coast

2.3 Surface Distortion

As I explained earlier the surface distortion are small ripples on the water. This is done with two textures, one for the noise and one for the distortion and movement.

The surface noise is generated by a texture (“_SurfaceNoise”), and is influenced by a different texture (“_SurfaceDistortion”). The SurfaceNoise texture is just a simple Perlin Noise texture, the SurfaceDistortion looks like a wave pattern.

The distortion is then added to a UV, this is used for creating a motion over time. The surface noise is then applied through a smoothstep function to create a smooth transition from foam to water. 

In summary, the foam is first created based on the difference between the water surface and the ground surface. Then the surface noise is generated by a texture that adds detail to the water surface. This noise is then further distorted to create the appearance of small waves or ripples. As seen on the image. 

Texture Distortion

3. Waves

In this paragraph I will explain the different kind of waves there are and what kind of waves are needed to give a realistic feel. These are the systems that I have researched for creating waves. 

  • Gerstner waves
  • Perlin Noise
  • Sinus function

3.1 Perlin Noise

Perlin noise can be used to create waves. This is done with the movement of the perlin noise. The perlin noise are the lows and highs of the wave, when moving, it looks like waves in the water. It has some disadvantages compared to other wave systems. Perlin Noise is a very good for making height displacement in the water. If we look at an image of heavy seas far away from the coast, you won’t see rolling waves, but height displacements. The downside of perlin noise is that it can’t properly simulate rolling waves. For this we need another system.

Heavy Seas
Shader Graph Perlin Noise

3.2 Gerstner waves

Gerstner waves are a mathematical model that effectively simulate the dynamics of large bodies of water such as oceans. Unlike traditional sine and cosine waves, they offer pointed crests and flat troughs, more accurately mimicking real-life waves.

Gerstner wave

This is how a Gerstner wave looks like from the side. The benefit of this wave system is that the top of the wave moves not only on the X axis but also on the Y axis. If we use a sine wave the disadvantage of this is that the waves won’t move on the Y axis, only on the X axis. 

For the Gerstner wave we use 3 properties 

 _WaveAmplitude: This is the height of the Gerstner wave.

 _WaveFrequency: This is the frequency of the wave, indicating how packed the wave crests are.

 _WaveDirection: This is a vector that represents the direction the wave is moving in.

The Gerstner Wave function calculates the height of the wave at a specific position and time. 

waveDirection: The direction of the wave is normalized to convert it into a vector, which simplifies calculations.

 wavePhase: The phase of the wave is calculated by taking the dot of the wave direction and the position, and then adding the time. The frequency of the wave is factored into this calculation.

 The function returns the wave height at the given position and time, which is calculated by multiplying the wave amplitude by the sine of the wave phase.

The vert function, called for each vertex of the plane, deforms the plane according to the Gerstner function.

   – waveHeight: The height of the Gerstner wave is calculated at the current vertex position (in the xz plane) and the current time.

   – displacedVertex: The y-coordinate of the current vertex is displaced based on the calculated waveHeight.

   – o.distortUV and o.noiseUV: The UV coordinates for the distortion and noise textures are computed. These textures are used later in the fragment shader to create more visual complexity on the water surface.

   – o.viewNormal: The normal of the vertex in view space is calculated.

3.3 Comparing methodes

Over the course of the project, I have tried to implement multiple different wave systems. The Sine wave is a very simple way for creating waves, it can be a placeholder, but I should never be used for creating realistic waves. In my opinion is Perlin Noise a nice and simple system for implementing waves. I should not be used for creating rolling waves near the coast, it is a better system for creating height displacement in heavy seas. 

If we compare Perlin noise to Gerstner waves, Gerstner waves are the most realistic of the three. It has a lot of similarities compared to real waves. When looking at real waves, the crest of the wave always remains at a fixed height. The crest of the wave does move along the X-axis but remains at a fixed height of the Y-axis. This is the same with Gerstner waves. An advantage of this system is that multiple waves can be added. This provides more detail in the water and waves. I would use the Gerstner waves for water near the coast and Perlin noise for water deep in the ocean. 

3.4 Wind

To make the water react dynamically to the wind, a C# script is needed. In this script you can set the parameters needed for the wind force and direction. If the wind force is very low, the water should react as little as possible, there are few waves, fewer ripples on the water and the smoothness is high. For every increment the water has to react more to the wind, higher waves, more ripples and less smoothness. Because I have determined the parameters for each system in advance, I can easily adjust them in the script. When the wind force is high, the Perlin noise speed is increased, Normal map displacement is increased, Smoothness is decreased. In the UI I placed a slider that influences the wind force, all parameters that influence the water are linked to the slider. If I raise the slider, all parameters are adjusted correctly, so that the water seems to react to the wind.

Making the water react to the wind direction is even easier. The only thing that needs to be adjusted is the direction of the normal map movement and the perlin noise direction.

4. Final result

In my first attempt, which lasted for 2.5 weeks, I achieved some satisfying results with my shader using shader graph. I was able to create realistic water effects such as reacting to wind, visible waves, water reflections, and proper color representation. I also managed to add foam at the coast. However, there were a couple of aspects that I felt were missing. First, I couldn’t generate foam on the crest of the waves, and secondly, I struggled with implementing rolling waves near the coast.

One challenge I faced during this process was the amount of math involved. Without examples and references, it would have been even more difficult. Fortunately, there are many resources and code examples available, making it easier to add new elements into my shader. The main difficulty was that all the different systems worked together.

For my second attempt, I decided to abandon shader graph and rewrite the entire shader code myself. I specifically focused on implementing the Gerstner waves in code. This allowed me to have more control over the shader. While shader graph provided a quicker way to create a functional shader, merging different systems became challenging and led to clutter.

I saw that there was a lot of documentation available for shader coding, which made the process a lot easier. With the ability to piece different parts together more easily, I found coding to be the preferable option. If I were to make a recommendation, I would suggest choosing code over shader graph. Although my second attempt required me to invest more effort and code a lot more, I believe I could achieve more by writing the shader in code.

Overall, both experiences taught me valuable lessons. Shader graph allowed for quicker prototyping, but coding offered greater control and customization.

Sources

Flick, J. (2018, 25 juli). Waves. CatLikeCoding. Geraadpleegd op 11 april 2022, van https://catlikecoding.com/unity/tutorials/flow/waves/

How to implement Gerstner Waves with Shader Graph in Unity. (2020, 2 augustus). YouTube. Geraadpleegd op 11 april 2022, van https://www.youtube.com/watch?v=Awd1hRpLSoI

Making a Water Shader in Unity with URP! (Tutorial). (2020, 7 juli). YouTube. Geraadpleegd op 11 april 2022, van https://www.youtube.com/watch?v=gRq-IdShxpU

Technologies, U. (z.d.-a). Unity – Manual: Normal map (Bump mapping). Unity. Geraadpleegd op 11 april 2022, van https://docs.unity3d.com/Manual/StandardShaderMaterialParameterNormalMap.html

Technologies, U. (z.d.-b). Unity – Manual: Water in Unity. Unity. Geraadpleegd op 11 april 2022, van https://docs.unity3d.com/530/Documentation/Manual/HOWTO-Water.html

Wikipedia contributors. (2022a, maart 27). Dispersion (water waves). Wikipedia. Geraadpleegd op 11 april 2022, van https://en.wikipedia.org/wiki/Dispersion_(water_waves)

Wikipedia contributors. (2022b, april 11). Wind wave. Wikipedia. Geraadpleegd op 11 april 2022, van https://en.wikipedia.org/wiki/Wind_wave

Lindenreid. (n.d.). Unity-Shader-Tutorials/water.shader at master · lindenreid/Unity-Shader-Tutorials. GitHub. Retrieved May 31, 2023, from https://github.com/lindenreid/Unity-Shader-Tutorials/blob/master/Assets/Materials/Shaders/water.shader

Unity Toon Water Shader Tutorial at Roystan. (n.d.). Retrieved May 31, 2023, from https://roystan.net/articles/toon-water/

Related Posts