# Ocean Shader

_{Made by Dion Behre - 500746771}

For my R&D project I decided to try to create a realistic ocean with the use of shaders. I had not previously worked with shaders, so everything was quite new for me.

To do list:

- Find references
- Create a mesh
- Basic shader
- Sine waves
- Better looking waves
- Light reflection
- Normal maps

# References

Before I could start with my project, I needed to have some idea on how I wanted my ocean to look. I found a few good videos and screenshots of a real ocean, as well as the ocean of the games **Sea Of Thieves **and** Assassins Creed IV: Black Flag. **These would be my references for my project.

# Creating the mesh

To create the mesh, I used a lot of information from **CatlikeCoding.com** where they have a tutorial on procedural grids. I had to modify it a little bit to suit my needs.

## Vertices

I decided that I did not want to use a standard plane for my ocean shader, so I created my own. I wanted to be able to change the **size of the mesh** and the **distance between the vertices,** so I had to program that in my code.

```
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class CreateWaterMesh : MonoBehaviour
{
public int xSize, zSize;
private Mesh mesh;
public Vector3[] vertices;
[Range(0,1)]
public float distance;
```

```
private void CreateVertices()
{
vertices = new Vector3[(xSize + 1) * (zSize +1)];
for (int i = 0, z = 0 ; z <= zSize; z++)
{
for (int x = 0; x <= xSize; x++, i++)
{
vertices[i] = new Vector3(x * distance,0 ,z * distance);
}
}
mesh.vertices = vertices;
}
```

## Quads

Now that I can place the vertices and change the distance between them, it’s time to **draw the actual mesh** on screen. A mesh consists of **triangles**, and two triangles make a **quad**. I wrote some code to create these quads.

```
private void CreateQuads()
{
int pointsperQuad = 6;
int[] triangles = new int[xSize * zSize * pointsperQuad];
int point1 = 1;
int point2 = 2;
int point3 = 3;
int point4 = 4;
int point5 = 5;
for (int ti = 0, vi = 0, z = 0; z < zSize ; z++, vi++)
{
for (int x = 0; x < xSize; x++, ti += pointsperQuad, vi++)
{
triangles[ti] = vi;
triangles[ti + point3] = triangles[ti + point2] = vi + 1;
triangles[ti + point4] = triangles[ti + point1] = vi + xSize + 1;
triangles[ti + point5] = vi + xSize + 2;
}
}
mesh.triangles = triangles;
}
```

This is the result:

# Basic shader

Now that the mesh was done it was time to start making the shader. The first thing I did was create a new material. I create an **Unlit Shader **and** deleted** almost everything in it. To start of I wanted to create a basic unlit shader that just shows a color on the mesh.

I created a **Color** property and added 2 structs in a **CGINCLUDE. **I did this so I could use these structs in multiple **subshaders** if I wanted to in the future. After this I created a subshader to show a color on my mesh.

# Sine Wave

From here on I knew a little bit what to do, but not enough to create what I wanted. I decided to look for information about how to create one. I found a **YouTube channel** by the name of **Freya Holmér**. On her channel she has a few tutorials about the basics of shader programming. I watched the first video called **Shader Basics, Blending & Textures • Shaders for Game Devs [Part 1] ***(Freya Holmér, 2021)* to get started. I changed the names the structs to make them more logical. After the first video everything made a bit more sense.

From the shader workshop I knew how to make a **sine wave** with shader code, but it wasn’t good enough for me. I couldn’t control anything about the wave. While looking for videos about making ocean water in unity I found a video that did exactly what I wanted to achieve. He started out with explaining everything about sine waves. The video was called **Ocean waves simulation with Fast Fourier transform ***(Jump Trajectory, 2020)***. **By listening to his explanation, I was able to write my own sine wave that I could control however I wanted. I could change the height of the wave**, **the speed at which the wave travelled and the length of the wave. I was also able to make the wave 2-dimensional as well.

After watching the video and trying some things I wrote the following code.

```
Shader "Unlit/Water"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_WaterColor("Water Color", Color) = (0,0,0,1)
_WaveLength("Wave Length", Range(0,20)) = 1.0
_WaveSpeed("Wave Speed", Range(1.0,5.0)) = 1.0
_WaveHeight("Wave amplitude", Range(1.0,5.0)) = 1.0
_WaveShift("Wave shift", Range(1.0,5.0)) = 1.0
}
CGINCLUDE
#include "UnityCG.cginc"
#pragma vertex vert;
#pragma fragment frag;
struct MeshData //Per-vertex mesh data
{
float4 position: POSITION;
float2 uv : TEXCOORD0;
};
struct Interpolators //Data that gets passed from vertex shader to fragment shader
{
float4 position: SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};
sampler2D _Maintex;
float4 _WaterColor;
float _WaveLength;
float _WaveSpeed;
float _WaveHeight;
float _WaveShift;
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
Interpolators vert(MeshData i)
{
Interpolators o;
float3 worldPos = mul(unity_ObjectToWorld, i.position).xyz;
i.position.y += _WaveHeight * sin(_WaveLength * (worldPos.x + worldPos.z) + _WaveSpeed * _Time.w) + _WaveShift;
o.color = _WaterColor;
o.uv = i.uv;
o.position = UnityObjectToClipPos(i.position);
return o;
}
half4 frag(Interpolators v) : SV_TARGET
{
return v.color;
}
ENDCG
}
}
}
```

# Better looking waves

Now I had a decent looking sine wave, but it didn’t look like ocean water yet. The next step for me was transforming this sine wave into a Gerstner wave. With Gerstner waves, the wave doesn’t just move up and down. The vertex points move in a circle. This looks way more realistic than just having a sine wave. To make this happen I had to completely change how I calculate the vertex movement.

The first thing I did was do some research on Gerstner waves. Through the same video **Ocean waves simulation with Fast Fourier transform*** (Jump Trajectory, 2020)* I found another YouTube channel called **3Blue1Brown. **I found a video about a formula that I needed to create Gerstner waves **called Euler’s Formula**. This formula is used to calculate a circle around a certain point. The video from this channel called **E to the power of pi explained in 3.14 minutes | DE5 ***(3Blue1Brown, 2019)* explains this concept in detail.

In my formula I also needed to get the dot product of the object’s **Wordlposition’s** x and z axis and the x and y position that I wanted the wave to go towards. This way I could change the direction of the waves to wherever I wanted them to go. I partially used some of the information on **Catlikecoding** about **waves** *(CatLikeCoding, z.d.)*, but that was all written in a Standard shader, so I had made it work in an unlit shade myself. I also reversed the calculation for the Amplitude of the wave. This way The Gerstner wave will not overlap if the value is under 1.

```
Properties
{
_WaterColor("Water Color", Color) = (0,0,0,1)
_Wave("Wave (dir,length, steepness)", Vector) = (1,0,0.5,10)
_WaveSpeed("wave speed",float) = 1
}
```

```
Pass
{
CGPROGRAM
Interpolators vert(MeshData i)
{
Interpolators o;
float3 worldPos = mul(unity_ObjectToWorld, i.position).xyz;
float euler = 2 * UNITY_PI;
float k = euler / _Wave.z;
float2 d = normalize(_Wave.xy);
float f = k * (dot(d, worldPos.xz) + _WaveSpeed * _Time.y);
float a = _Wave.w / k;
i.position.x += d.x * (a * cos(f));
i.position.y = a * sin(f);
i.position.z += d.y * (a * cos(f));
o.color = _WaterColor;
o.position = UnityObjectToClipPos(i.position);
o.uv = i.uv;
return o;
}
half4 frag(Interpolators v) : SV_TARGET
{
return v.color;
}
ENDCG
}
```

# My biggest struggle

From here I wanted to follow the video again because the water shader in that he created was exactly what I wanted to have. Sadly, I didn’t manage to do it. The next step was to implement a formula called the **Fast Fourier transform**. I tried to understand this formula, but I sadly failed in doing so on time. There were symbols that I had never seen before. Eventually I started to understand the formula itself but had no idea how to use it in shader code. I watched the video multiple times, tried to find more information and more videos about this formula. After 2 weeks I found something that would help me, but I ran out of time and had to give up and find another way.

**Multiple Gerstner waves**

To create something that would represent ocean water I decided to use multiple Gerstner waves. To do this, I moved all the code for the waves to a function. Now I only had to call the function for the amount of Gerstner waves I wanted.

```
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_WaterColor("Water Color", Color) = (0,0,0,1)
[Header(Wave1)]
_WaveA("Wave a (dir,length, steepness)", Vector) = (1,0,0.5,10)
_WaveSpeedA("wave speed a",float) = 1
[Header(Wave2)]
_WaveB("Wave b (dir,length, steepness)", Vector) = (0,1,0.25,20)
_WaveSpeedB("wave speed b",float) = 1
[Header(Wave3)]
_WaveC("Wave c (dir,length, steepness)", Vector) = (0,1,0.25,20)
_WaveSpeedC("wave speed b",float) = 1[Header(Wave3)]
[Header(Wave4)]
_WaveD("Wave d (dir,length, steepness)", Vector) = (0,1,0.25,20)
_WaveSpeedD("wave speed b",float) = 1
}
```

```
struct Interpolators //Data that gets passed from vertex shader to fragment shader
{
float4 position: SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};
float3 GerstnerWave(
float4 wave, float waveSpeed ,float3 p)
{
float steepness = wave.z;
float waveLength = wave.w;
float k = 2 * UNITY_PI / waveLength;
float2 d = normalize(wave.xy);
float c = sqrt(9.8 / k);
float euler = k * (dot(d, p.xz) - c / waveSpeed * _Time.w);
float a = steepness / k;
return float3(
d.x * (a * cos(euler)),
a * sin(euler),
d.y * (a * cos(euler))
);
}
```

```
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
Interpolators vert(MeshData i)
{
Interpolators o;
float3 worldPos = mul(unity_ObjectToWorld, i.position).xyz;
float3 p = worldPos;
p += GerstnerWave(_WaveA, _WaveSpeedA, p);
p += GerstnerWave(_WaveB, _WaveSpeedB, p);
p += GerstnerWave(_WaveC, _WaveSpeedC ,p);
p += GerstnerWave(_WaveD, _WaveSpeedD, p);
i.position.xyz += p;
o.color = _WaterColor;
o.position = UnityObjectToClipPos(i.position);
o.uv = i.uv;
return o;
}
half4 frag(Interpolators v) : SV_TARGET
{
return v.color;
}
ENDCG
}
```

Wave 2: x Direction: 1, y Direction: 1, Steepness: 0.53, Wave Length: 19, Wave Speed: 4

Wave 3: x Direction: 1, y Direction: -1, Steepness: 0.3, Wave Length: 38, Wave Speed: 3

Wave 4: x Direction: 0, y Direction: -1, Steepness: 0.7, Wave Length: 28, Wave Speed: 3

## Lighting

Now that I had the waves I needed to make the mesh look more like water. I had to start with adding lighting to my unlit shader. I didn't know how to do this so I went to the Youtube channel **Freya Holmér** again and looked at her second video in her shader tutorial series. Here she explained how to write lighting code. The video is called **Healthbars, SDFs & Lighting • Shaders for Game Devs [Part 2]** *(Freya Holmér, 2021).*

## Directional Light

The first light I made was a **directional light**. I had to add a **NORMAL **to the structs. In the vertex shader I set the normal to **UnityObjectToWorldNormal**. With this I assigned a value **N **in the fragment shader. I also assigned a value **L**. This value held the direction of the directional light in the scene. I calculated the **dot product** of those two values en returned the vector3 of this. The result was exactly what I wanted.

```
struct MeshData //Per-vertex mesh data
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};
struct Interpolators //Data that gets passed from vertex shader to fragment shader
{
float4 vertex: SV_POSITION;
float3 wPos : TEXCOORD2;
float3 normal : TEXCOORD3;
float4 color : COLOR;
};
```

```
Interpolators vert(MeshData v)
{
...
o.normal = UnityObjectToWorldNormal(v.normal);
o.wPos = mul(unity_ObjectToWorld, v.vertex);
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
```

```
float4 frag(Interpolators i) : SV_TARGET
{
// Diffuse lighting
float3 N = i.normal;
float3 L = _WorldSpaceLightPos0.xyz; //Actually a direction
float3 diffuseLight = saturate(dot(N,L)) * _WaterColor.xyz;
return float4(diffuseLight.xxx,1);
}
```

## Specular Light

I also wanted to have some **specular lighting** in my shader. I **separated the lambertion (dot product **from the **diffuse light**) so I could use it for the specular light as well. I also **normalized the normal** and created a **new variable H** that holds the normalized direction of the light. I calculated the **dot product** of these two and multiplied this by the **lambert higher that 0**. This way the specular light will not be visible from the other side of the object. I added a **_Gloss **component and used this to calculate the size of the specular light.

```
Properties
{
_WaterColor("Water Color", Color) = (0,0,0,1)
_Gloss("Gloss", Range(0,1)) = 1
....
}
```

```
float4 frag(Interpolators i) : SV_TARGET
{
// Diffuse lighting
float3 N = normalize(i.normal);
float3 lambert = saturate(dot(N,L));
float3 diffuseLight = lambert * _WaterColor.xyz;
....
// Specular lighting
float3 H = normalize(L);
float3 specularLight = saturate(dot(H,N)) * (lambert > 0); // Blinn-phong
float specularExponent = exp2( _Gloss * 6 + 1);
specularLight = pow(specularLight, specularExponent );
return float4(specularLight.xxx ,1);
}
```

Now it's time to put both the directional en specular light together. I also **multiplied **the specular light with the **color** because that color has to bounce back.

```
float3 H = normalize(L);
float3 specularLight = saturate(dot(H,N)) * (lambert > 0); // Blinn-phong
float specularExponent = exp2( _Gloss * 11) + 2;
specularLight = pow(specularLight, specularExponent );
specularLight *= _WaterColor.xyz;
return float4(diffuseLight * _WaterColor + specularLight ,1);
}
```

## Normal maps

Now that I have waves and lighting in my shader the next step is to add **normal maps** or **bump maps**. This is needed so the mesh doesn't just look like a flat plane. A normal map is used to simulate height differences, but it dones't actually add them to a mesh. I used a video called **Normal Maps, Tangent Space & IBL • Shaders for Game Devs [Part 3]** *(Freya Holmér, 2021),* as well as a tutorial called **Rendering 6, Bumpiness** *(CatLikeCoding, z.d.)*. I added some properties for the bump maps. The structs also needed some variables for the normal maps. I made the **TEXCOORD's**. Since I wanted to use bump maps I also needed to add values for the **tangents**. I also needed a variable for the **bitangent**. The bitangent is the cross product of the tangent and the normal.

```
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Normal1 ("Texture", 2D) = "bump" {}
_Normal2 ("Texture", 2D) = "bump" {}
_WaterColor("Water Color", Color) = (0,0,0,1)
....
}
```

```
struct MeshData //Per-vertex mesh data
{
....
float4 tangent: TANGENT;
float2 uv1 : TEXCOORD0;
float2 uv2 : TEXCOORD1;
};
struct Interpolators //Data that gets passed from vertex shader to fragment shader
{
....
float2 uv1 : TEXCOORD0;
float2 uv2 : TEXCOORD1;
....
float3 tangent : TEXCOORD4;
float3 bitangent : TEXCOORD5;
....
};
```

in the vertex shader I set the bump maps as **TRANSFORM_TEX**. I also set the **tangent** and calculate the **bitangent.** In the fragment shader Unpack the bump maps so they are ready to be used. Because I want to use multiple bump maps I needed to add them together. I did this by using the **BlendNormals **function. I calculate a matrix with the tangent, bitangent, and the normal. Now that I have the matrix and the blended bump maps, I use these instead of the normal.

```
Interpolators vert(MeshData v)
{
....
o.uv1 = TRANSFORM_TEX(v.uv1, _Normal1);
o.uv2 = TRANSFORM_TEX(v.uv2, _Normal2);
o.normal = UnityObjectToWorldNormal(v.normal);
....
o.tangent = UnityObjectToWorldDir(v.tangent.xyz);
....
o.bitangent = cross(o.normal, o.tangent);
o.bitangent *= v.tangent.w * unity_WorldTransformParams.w;
return o;
}
float4 frag(Interpolators i) : SV_TARGET
{
float3 tangentSpaceNormal1 = UnpackNormal(tex2D(_Normal1, (i.uv1 * _NormalControl1.xy) + _Time.y * _NormalControl1.z));
float3 tangentSpaceNormal2 = UnpackNormal(tex2D(_Normal2, (i.uv2 * _NormalControl2.xy) + _Time.w * _NormalControl2.z));
float3 blendedTangents = BlendNormals(tangentSpaceNormal1, tangentSpaceNormal2);
float3x3 mtxTangToWorld = {
i.tangent.x, i.bitangent.x, i.normal.x,
i.tangent.y, i.bitangent.y, i.normal.y,
i.tangent.z, i.bitangent.z, i.normal.z,
};
// Diffuse lighting
float3 N = mul(mtxTangToWorld , blendedTangents);
....
return float4(diffuseLight * color + specularLight ,1);
}
```

This is the result

Now to move the bump maps and add the waves.

## Bibliography

- Freya Holmér. (2021, 26 Februari)
[Video]**Shader Basics, Blending & Textures • Shaders for Game Devs [Part 1]***https://www.youtube.com/watch?v=kfM-yu0iQBk&t=8608s* - Freya Holmér. (2021, 26 Februari)
[Video]*Healthbars, SDFs & Lighting • Shaders for Game Devs [Part 2]**https://www.youtube.com/watch?v=mL8U8tIiRRg* - Freya Holmér. (2021, 26 Februari)
**Normal Maps, Tangent Space & IBL • Shaders for Game Devs [Part 3]**[Video]*https://www.youtube.com/watch?v=E4PHFnvMzFc&t=6331s* - Jump Trajectory. (2020, 6 December)
*Ocean waves simulation with Fast Fourier transform*[Video]*https://www.youtube.com/watch?v=kGEqaX4Y4bQ&t=535s* - 3Blue1Brown. (2019, July 7)
[Video]**E to the power of pi explained in 3.14 minutes | DE5***https://www.youtube.com/watch?v=v0YEaeIClKY* - 3Blue1Brown. (2018, January 26)
[Video]**But what is the Fourier Transform? A visual introduction.***https://www.youtube.com/watch?v=spUNpyF58BY&t=823s* - Catlike Coding. (z.d.)
. Consulted on 3 October 2022 from**Procedural Grid***https://catlikecoding.com/unity/tutorials/procedural-grid/* - Catlike Coding. (z.d.)
. Consulted on 3 October 2022 from**Waves***https://catlikecoding.com/unity/tutorials/flow/waves/* - CodeMotion. (2017, April 12)
[Video]**An introduction to Realistic Ocean Rendering through FFT - Fabio Suriano - Codemotion Rome 2017***https://www.youtube.com/watch?v=P4G0hn5QhMs* - e-maxx-eng. (2022, June 8)
.**Algorithms for Competitive Programming***https://cp-algorithms.com/algebra/fft.html* - Catlike Coding. (z.d.)
. Consulted on 3 October 2022**Rendering 6, Bumpiness***https://catlikecoding.com/unity/tutorials/rendering/part-6/* - Pensionerov, I.P. (z.d.)
**FFT-Ocean**.*https://github.com/gasgiant/FFT-Ocean*