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
- Gerstner waves
- Lighting
- Normal maps
- Noise map
- Transparancy
- The enviroment
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 myself.
I started by creating variables for the x and z size of the mesh and a variable with a range between 0 and 1 for the distance between the vertices. These can all be changed in the inspector. After that I created a method and used a double for loop for the vertex placement and distance between the vertices.
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 , 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. Since a quad consists of 2 triangles, and 1 triangle has 3 points, 6 points are needed to create 1 quad. This does mean that some vertices will be the same. In the example I drew below, points 1 and 4 of the first triangle are on the same position as points 2 and 3 respectively.

The code to draw the quads works the same way.
private void CreateQuads()
{
int pointsperQuad = 6;
int[] quads = new int[xSize * zSize * pointsperQuad];
int point2 = 1;
int point3 = 2;
int point4 = 3;
int point5 = 4;
int point6 = 5;
for (int ti = 0, vi = 0, z = 0; z < zSize ; z++, vi++)
{
for (int x = 0; x < xSize; x++, ti += pointsperQuad, vi++)
{
quads[ti] = vi;
quads[ti + point4] = quads[ti + point3] = vi + 1;
quads[ti + point5] = quads[ti + point2] = vi + xSize + 1;
quads[ti + point6] = vi + xSize + 2;
}
}
mesh.triangles = quads;
}
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.
I wrote the code to create a basic sine wave, but added some variables for the height, length, x and z direction and speed. With this I can manipulate every aspect of the wave.
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;
}
Wave Length: 1, Wave Speed: 1, Wave Amplitude: 1, Wave Shift: 1
Wave Length: 0.2, Wave Speed: 3, Wave Amplitude: 5, Wave Shift: 5
Gerstner wave
Now I had a decent looking sine wave, but it didn’t look like ocean water yet. The next step for me was to transform 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.
I first 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.
I used 2 * PI to calculate a circle around a certain point, then divided that by a wave parameter so I can manipulate the size of the circle. Visually it changes the size of the waves.
float euler = 2 * UNITY_PI;
float k = euler / _Wave.z;
I also needed a way to manipulate the speed at which the circle completes, which visually would change the speed of the waves. To do this I normalized the direction of the waves and multiplied the size of the circle by the dot product of the normalized direction and the worldposition the plane plus a parameter wavespeed so I could change the speed manually.
I also added a variable to change the distance between the waves.
float2 d = normalize(_Wave.xy);
float f = k * (dot(d, worldPos.xz) + _WaveSpeed * _Time.y);
float a = _Wave.w / k;
for the x and z position I multiplied the x and y values of the normalized wave direction respectively by the distance between the waves multiplied by the cosine of the wavespeed. for the y I multiplied the distance between the waves by the sine of the wavespeed.
i.position.x += d.x * (a * cos(f));
i.position.y = a * sin(f);
i.position.z += d.y * (a * cos(f));
x Direction: 0.23,Direction: 0.5, Wave Length: 5, Wave Amplitude: 0.8, Wave Speed: 1
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.
More waves
To create something that would represent ocean water I decided to use multiple Gerstner waves. To do this, I moved the code to create gertner waves a separate function. Now I only had to call the function for the amount of Gerstner waves I wanted. I had to change up the code a bit so it could be used in a funtion. The code is essentially the same though.
I called the function 4 times to add 4 differerent Gertner waves. I can change all of these in the inspectre
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;
Wave 1: x Direction: 1, y Direction: 0, Steepness: 0.31, Wave Length: 44, Wave Speed: 2
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.
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 add specular lighting to the shader. I separated the lambertion (dot product from the diffuse light) so I could use it for the specular light as well.
float3 lambert = saturate(dot(N,L));
I 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 variable and used this to calculate the size of the specular light. The specularlight itself is the returned value from the specularlight raised by the power of the size of the light.
float4 frag(Interpolators i) : SV_TARGET
{
// Diffuse lighting
float3 N = normalize(i.normal);
....
// 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 );
}
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. this is the result
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 look like a flat surface . 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. Since I wanted to use bump maps I also needed to add values for the tangents and the bitangent. The bitangent is the cross product of the tangent and the normal.
in the vertex shader I set the bump maps as TRANSFORM_TEX. I calculated the bitangent and then multiplied that by the transparency of the tangent multiplied by the WorldTransformParams.
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;
In the fragment shader I 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 for the lighting as well so it takes the difference in height on the bump map into account.
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.
Better Color
Generally, if you look at ocean water the top of the waves has a different color because it is more transparent. At moment the plane has a singular color so that needs to change.
I created a new variable h where I calculated the difference between the maximum and minumum height of the waves. After that I used changed the way the color is decided by lerping between 2 colors and using saturate for the h variable.
float h =(_MaxHeight - i.wPos.y) / (_MaxHeight - _MinHeight);
fixed4 tintColor = lerp(_WaterColor.rgba, _WaterColor2.rgba, saturate(h));
After playing around with it and changing a few small things in the inspector, this is what it looks like.
Noise map
Ther biggest problem with my waves was that it looked quite nice, but they were very repetitive and there wasn’t any randomness to it like waves in real life. To fix this I decided to add a noise map. The definition of a noise map that I found on the website www.noisemap.ltd.uk(Roger Tompsett and Alan Williams. z.d.) is “a map of an area which is coloured according to the noise levels in the area“. After some research I decided to go with a simplex noise. I stumled onto an artical by called Perlin vs. Simplex(Keith Peters,z.d) where he explains in detail what a simplex noise is. I found the fast noise light from auburn on Github and decided to use this one. I downloaded the HLSL library and imported this to my project. I imported the library into the shader and created a variable in the vertex shader that holds the Simplex noise itself. I created another variable that holds the simplex noise that I wanted to use..
I wanted to increase the amount of noise on the map. The bigger the uv values, the more extreme the noise is. So I multiplied the x and y values of the uv. I also multiplied the uv by _Time.w because I wanted the noise to scroll. This way it would create even more randomness of the waves.
Uv not multiplied
uv * 1000
Uv * 100000
I noticed that the noise map would take a value from -1 to 1. This meant that the waves would go lower as well as higher which I didn’t like. I decided to add 1 to the noise and divide everything by 2. Now it would only take a value from 0 to 1. Now it was time to tweek the values until I was satisfied with the results. In the end I chose to multiply it by 1000.
fnl_state noise = fnlCreateState();
noise.noise_type = FNL_NOISE_OPENSIMPLEX2
float noiseValue = (fnlGetNoise2D(noise, v.uv2.x * 1000 + _Time.w, v.uv2.y * 1000 + _Time.w ) +1)/2;
v.vertex.y *= noiseValue;
Transparancy
At the moment the waves looks good, but the water itself looks a bit like slime because it is completely opague. This is easily fixed by changing the RenderType to Transparent anf adding Blend SrcAlpha OneMinusSrcAlpha to the pass. Now I only needed to add a variable so i could change how transparent th water would be.
Enviroment
The only thing left to do is to make everything look nice. Right now the water is floating into space so I started working on an eviroment. I downloaded a boat model from Turbosquid and imported it into the project. The next thing I did was creating a terrain and sculting it. I added some mountains and made sure there was no way you could see into the abbyss. A simple first person character was created and placed on the boat. This way you could walk around a bit and see the water from the angle it was meant to be viewed at. As a small detail I added a duplicate of the boat to the scene and placed it underwater as if it had sunk to the bottom of the ocean. Now my project was done.
What could be done
Obviouly there is still that could be done with this shader. It isn’t fully optimized for example. Adding something like LOD’s to slowly reduce the waves the further you are from the player could be a big improvement. Boyency could also be added but that would be C# programming and not HLSL so I left that out. The waves could technically also be improved with added the FFT (fast Fourier Transform) formula to combine gertner waves, but that was too difficult for me. I was stuck on this for weeks and couldn’t figure it out so I decided to move on. Overall I am very happy with the results.
Extra
I decided to play around with the shader a bit and found out it is actually quite versitile. You can make completely different substances than water with waves. here are 2 examples just for fun.
Lava
Slime
Bibliography
- Freya Holmér. (2021, 26 Februari) Shader Basics, Blending & Textures • Shaders for Game Devs [Part 1] [Video] https://www.youtube.com/watch?v=kfM-yu0iQBk&t=8608s
- Freya Holmér. (2021, 26 Februari) Healthbars, SDFs & Lighting • Shaders for Game Devs [Part 2] [Video] 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) E to the power of pi explained in 3.14 minutes | DE5 [Video] https://www.youtube.com/watch?v=v0YEaeIClKY
- 3Blue1Brown. (2018, January 26) But what is the Fourier Transform? A visual introduction. [Video] https://www.youtube.com/watch?v=spUNpyF58BY&t=823s
- Catlike Coding. (z.d.) Procedural Grid. Consulted on 3 October 2022 from https://catlikecoding.com/unity/tutorials/procedural-grid/
- Catlike Coding. (z.d.) Waves. Consulted on 3 October 2022 from https://catlikecoding.com/unity/tutorials/flow/waves/
- Roger Tompsett and Alan Williams. (z.d.) What is Noise Mapping?. Consulted on 29 October 2023 from https://www.noisemap.ltd.uk/home/what%20is%20noise%20mapping.html
- Auburn. (z.d.) FastNoiseLite. Consulted on 30 October 2023 from https://github.com/Auburn/FastNoiseLite
- Keith Peters. (z.d.) Perlin vs. Simplex. Constulted on 29 October 2023 from https://www.bit-101.com/blog/2021/07/perlin-vs-simplex/
- Helarts. (2015, November) Until Texture Transparant Shader. Consulted on 30 October 2023 from https://discussions.unity.com/t/unlit-texture-transparent-shader/153160/3
- Brackeys. (27 October 2019) FIRST PERSON MOVEMENT in Unity – FPS Controller https://www.youtube.com/watch?v=_QajrabyTJc
- CodeMotion. (2017, April 12) An introduction to Realistic Ocean Rendering through FFT – Fabio Suriano – Codemotion Rome 2017 [Video] 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.) Rendering 6, Bumpiness. Consulted on 3 October 2022 from https://catlikecoding.com/unity/tutorials/rendering/part-6/
- Pensionerov, I.P. (z.d.) FFT-Ocean. https://github.com/gasgiant/FFT-Ocean
Leave a Reply