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. My goal was to create a water system that will react to the wind force and wind direction. 

For my research I used a lot of sources from unity and other projects from developers. I wanted to use shader graph, because it was easier to create something fast with no deep knowledge of shaders. But to show that I know how to create shaders in code, I will use this report to make a comparison between shader graph and code. At the end of this report, I will make a recommendation between using shader graph or code. 

Sea of thieves

Table of contents

  1. What makes the sea in games look so good?
  2. Visuals
    1. Depth camera
    2. Refraction
    3. Reflection
    4. Spectacular Highlights
  3. Waves 
    1. Perlin Noise
    2. Gerstner waves
    3. Comparing methods
    4. Wind
  4. Final Result
    1. Conclusion
    2. Improvements to be made

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 refractions, 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, this can be seen on these images.

Then there are the reflections, I won’t use any objects on the water so reflections are not that necessary but to create a more realistic water system I will implement them. With the reflections on you can see the skybox in the water, in my skybox I’ve used a simple texture with some clouds, so the goal is to show these clouds in the water.

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, and I will explain them in this paragraph. 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 Camera

A key component is the depth camera. 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 compare it how it is done in shader graph. And finally I will show the before and after.

struct Input 
{
    float4 screenPos;
    float cameraDepth;
    float normalStrength;
};
 
sampler2D _CameraDepthTexture;
float _Strength;
 
 
void vert (inout appdata_full v, out Input o)
{
     UNITY_INITIALIZE_OUTPUT(Input, o);
     COMPUTE_EYEDEPTH(o.cameraDepth);
}
 
void surf (Input IN, inout SurfaceOutputStandard o) 
{
    float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(IN.screenPos)));
    float depth = 1.0 - saturate(_Strength * (sceneZ - IN.cameraDepth));
    o.Albedo = depth;
    o.Alpha = normalStrength;
}

This allows us to check depth based on the camera position, making it great for water transparency and color. By creating and lerping two colors using depth the transition between deep and shallow water will be realistic. There are a few properties that can be changes:

  • Depth
  • Strength
  • Shallow Color
  • Deep Color

With the depth property you can change the limit of the depth, if the value is high the depth will be shallow, this means that the shallow color is more prominent than the deep color. The strength will change the lerping effect. If the strength is high the lerping effect will be much stronger. 

Minimal Depth
More Depth

2.2 Refractions

As I explained earlier the refractions are small ripples on the water. This is done with two normal maps moving over each other. In this paragraph I will show how it is done in code and shader graph and what the pros and cons are with the method I used. And how I overcame the cons. I will show the before and after and what kind of settings you can play with to give it a different look.

Shader Graph Refractions

For creating the refractions of the water, we need two different normal maps. These normal maps are moving, in the Divide settings you can alter the movement speed of the normal maps. In the Tiling and Offset settings, you can change the size of the normal maps, if the size is very big there are no repeating patterns but the downside of this is that the detail gets lost. When decreasing the size, there is a lot more detail in the water, the downside is that there are much more repeating patterns. 

Low Detail, no repeating patterns
High detail, more repeating patterns

2.3 Reflections

In water there are of course reflection. In unity there are multiple options to add reflection but each one is different, and some are unnecessary in the scene I use. For example, there are no moving objects in de scene, so there is no need for moveable reflections, only baked reflections. Here it will be explained what kind of reflection I used and how it is done in shader graph and how it could be done in code. 

For the reflections I used a reflection probe. A reflection probe is like a camera that captures a spherical view of its surroundings in all directions. The captured image is then stored as a Cubemap that can be used by objects with reflective materials. One of those reflective materials is the water. 

Reflections of the clouds
Shader "C4Cat/CubeMap" {
   Properties {
         _MainTex("Main Map", 2D) = ""{}
      _Cube("Reflection Map", Cube) = "" {}
      _reflectionAmount("Reflection amount", Range (0.0,1.0)) = 0.3
   }
   SubShader {
      Pass {  
         CGPROGRAM
         uniform samplerCUBE _Cube;
         uniform sampler _MainTex;  
         uniform fixed _reflectionAmount;
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
            float2 uv : TEXCOORD;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float3 normalDir : TEXCOORD0;
            float3 viewDir : TEXCOORD1;
            float2 uv : TEXCOORD2;
         };
         vertexOutput vert(vertexInput input)
         {
            vertexOutput output;
            
             output.uv = input.uv;
            float4x4 modelMatrix = _Object2World;
            float4x4 modelMatrixInverse = _World2Object;
            output.viewDir = mul(modelMatrix, input.vertex).xyz
               - _WorldSpaceCameraPos;
            output.normalDir = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }
         float4 frag(vertexOutput input) : COLOR
         {
            float3 reflectedDir =
               reflect(input.viewDir, normalize(input.normalDir));
            return lerp(tex2D(_MainTex,input.uv),texCUBE(_Cube, reflectedDir),_reflectionAmount);
         }
         ENDCG
      }
   }
}

2.4 Spectacular highlight

A specular highlight is the bright spot of light that appears on shiny objects when illuminated, with water it’s almost the same. The difference with water is that water is translucent. Only at the tops of the waves does the light bounce back. This gives you bright spots of light. Spectacular highlight belongs to the normal maps. Without the normal maps it is not possible to show the highlight in the water. If we use the shader graph from refractions we only have to add a smoothness value to the Fragment _Smoothness. This is because the normal maps were already added to get the refraction animation. This way we don’t have to add a normal map again.

Before
After

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 some types of waves you can implement in your game. It is not the easiest thing to implement and therefore it will be shown how I have done it in shader graph, but it will also be shown how it could be done in code. I will show the different kind of patterns gerstner waves could create and what I have chosen for my project. 

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. 

float3 GerstnerWave (float4 wave, float3 p, inout float3 tangent, inout float3 binormal) {
float steepness = wave.z;
float wavelength = wave.w;
float k = 2 * UNITY_PI / wavelength;
float c = sqrt(9.8 / k);
float2 d = normalize(wave.xy);
float f = k * (dot(d, p.xz) - c * _Time.y);
float a = steepness / k;

tangent += float3(-d.x * d.x * (steepness * sin(f)),
	d.x * (steepness * cos(f)),
	-d.x * d.y * (steepness * sin(f)));

binormal += float3(-d.x * d.y * (steepness * sin(f)),
	d.y * (steepness * cos(f)),
        -d.y * d.y * (steepness * sin(f)));

return float3(d.x * (a * cos(f)),
	      a * sin(f),
              d.y * (a * cos(f)));
}

This is the code for creating a wave pattern, but this is only for one wave pattern. If we want to make it more realistic, we need a second wave. To add support for a second wave, all we have to do is add another wave property and invoke Gerstner Wave a second time. If we want to make our shader more complex, we can add a third wave. We only need invoke the wave a third time. We can do this as much as we like.

_WaveA ("Wave A (dir, steepness, wavelength)", Vector) = (1,0,0.5,10)
_WaveB ("Wave B (dir, steepness, wavelength)", Vector) = (0,1,0.5,15)
	
SubShader {
float4 _WaveA, _WaveB;

void vert(inout appdata_full vertexData) {
float3 gridPoint = vertexData.vertex.xyz;
float3 tangent = 0;
float3 binormal = 0;
float3 p = gridPoint;

p += GerstnerWave(_WaveA, gridPoint, tangent, binormal);
p += GerstnerWave(_WaveB, gridPoint, tangent, binormal);

float3 normal = normalize(cross(binormal, tangent));
		vertexData.vertex.xyz = p;
		vertexData.normal = normal;
}

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

The result can be seen in the video. I am quite happy with my result, for only 2.5 weeks of work. The water reacts to the wind, waves are visible, reflection on the water, the light reacts with the water, correct colors in the water, foam at the coast.

What I miss is the foam on the crest of the waves, also the rolling waves near the coast. I have chosen for the Perlin Noise because this was the best option with high wind forces. Also, I wanted to implement splashes of water, but this is done best when interacting with an object. I’ve learned a lot about shader over the past few weeks. You can do endless things with shaders, and you are not only limited to adding a normal map or vertex displacement.

I didn’t expect so much math involved. Without examples, this project would also have been very difficult. But the great thing is that you don’t have to reinvent the wheel. This allows you to add new parts to your project quite quickly. The difficult thing is to make all systems work with each other. I think if I spend more time with shaders that you get a better overview, how everything is put together and how everything works.

The advantage of shader graph was that you can quickly make a well-functioning shader. There are countless examples of shader graph on the internet. In terms of documentation, it is disappointing. The disadvantage of shader grahph is that different systems are quite difficult to merge and it quickly becomes cluttered if you have added many parts.

Code, on the other hand, is a bit more difficult to get a fast working shader. With little knowledge of the shader, it can be difficult to get a good working shader right away. There is a lot of documentation about shaders, this is also because shader graph is still quite new. It is also easier to put different parts together.

In the end I chose shader graph because I had very little time and with shader graph it was a lot easier to get a working shader. There is of course a challenge because there is not a lot of documentation to be found about it. It was also a challenge to make different water systems work with each other. If I had to give a recommendation I would choose code, because you can quickly build up the knowledge to make a nice shader. I quickly ran into problems merging different systems together, and I think if I had done it in code I could have done more. So for anyone who hesitates between shader graph or code, I would choose code. Code is much more accessible and plenty of documentation available.

4.1 Improvements to be made

  • Foam on the crest of the waves
  • Water that reacts to objects
  • Splashes of water when it interacts with objects
  • Perlin Noise far from the coast, Gerstner near the coast

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

Related Posts