this is an Excellent article, the way its set out with the vids and pics. very technical but not rambley. I learnt alot out of it.
Bob would have been proud
Congratulations Lukas:) Amazing piece of 3D parametric shader:) thank You for sharing Your knowledge:)
Hailey Williams created a super detailed tutorial for UE4 users, showing how you can create water surfaces with ‘Gerstner Waves’.
Hello there! My name is Hailey Williams and I’m just writing up a quick tutorial on the basics of creating a ocean or water shader using something called a Gerstner Wave. Water is one of the hardest materials to get looking right because of the larger wave movement, the way light scatters across it (specularity), and the micro details across the surface that help make it believable. This will just be a brief overview of the steps and methods to create my water shader. I’m not breaking down every single step since then this would be like 2 million pages long, but I hope you learn something and if you have any questions please feel free to reach out check out my Portfolio or shoot me an email!
Some quick notes about this tutorial:
To create the gerstner wave function I used a mathematical equation found in GPU Gems in combination with this tutorial. This is some pretty complicated math that I won’t fully breakdown in this tutorial since the tutorial I used is way more helpful then I could ever be.
I won’t be going into texture creation, the tutorial will be dealing only with my process within Unreal Engine 4’s Material Editor.
This water is still a work-in-progress, there are plenty of adjustments to be made and things I would like to add, so I will be talking about next steps and I will be updating the tutorial here.
I’m not going into translucency due to performance issues, but know that translucent materials are also a totally viable option for water but are much more costly.
1: Creating the Gerstner Wave Function
So the biggest thing about my water is that all of the larger wave movement is controlled by a Gerstner Wave. A Gerstner Wave is basically a modified Sine Wave. However, unlike a sine wave it has sharper peaks and flatter valleys (where as a sine wave has identical peaks and valleys). Gerstner Waves are used in pretty much all CG water and fluid simulations, so there’s lots of documentation. A quick search for Gerstner Wave Equations brings up GPU Gem’s Equation for Gerstner Waves:
While this looks really intimidating at first, you can literally translate this equation into a series of notes inside of the Unreal Editor and create a Material Function. For those who don’t know, a material function is a little snippet of a Material graph that can be saved in packages and reused across multiple Materials. This is really nice because it is a really efficient way to reuse common calculations like fresnel, world aligned textures, and more. Unreal comes with some premade material functions but we are going to create our own for the Gerstner Wave.
Since the math looks pretty technical I’ve just provided screenshots that break down the nodes needed for each part of the formula and also explain the parameters needed to create this function. For a more in-depth look at understanding the math, please check out the tutorials I referenced above! Here is an image of the full material function to get an idea of how everything connects together.
One big note: something that is critical for material functions is the input node, these are used so that when you use a material function within a material, you can put in your own numbers, so it parametrizes the function you create.
Dᵢ (Direction) – First, we are determining the direction the wave will move with a 3 vector constant. A 3 vector constant is made up of 3 channels, Red, Green, and Blue, which is usually used to create a color. However, these 3 channels ALSO correlate to the X, Y, and Z axis in 3D space. For this calculation we aren’t looking at the Z axis for movement, so we just are using R and G.
Wᵢ (Wavelength) – Wavelength is how far apart or how “tight” the waves will appear next to each other
ഴᵢt (Speed) – Determines how fast the waves will be moving
Qᵢ (Steepness) – How steep the crests of the wave will be (0-1 parameter, over 1 will cause the displaced geo to create a “loop”)
Aᵢ (Amplitude) – How high the waves will be
2: Base Color Gradient
So once the Material Function is built, it’s time to start building the base for the material. Before plugging in the Function though, I first created a gradient between two colors so you can more accurately see how the displacement is working. This works by using Absolute World Position to look at where (or how high) the material is in the actual level is. We then mask it off to just the Blue (B) channel so it is only looking at the Z axis. The subtract is choosing what height to start the transition between the two colors. Then you multiply it by a very small number to control how soft or harsh the gradient is (the smaller the number, the softer the transition is).
The last thing to do is plug this string of nodes into the Alpha channel of a Lerp node. A “lerp” node (linear interpolation), basically just blends between the two colors where the A channel represents everything black in the Alpha and the B channel represents everything white. Since we created a gradient with the absolute world position, it will treat “B” as the “top” of the gradient and “A” as the bottom. One final note: if you forget to put a clamp into the alpha channel, this will not work. It will visibly mess up colors.
3: Add Displacement from Material Function
Now that we have a gradient to show off the displacement, it’s time to actually tessellate the mesh. Since the material function is ready to go, we just need to create a couple different instances of our function and use different parameters to get the basic movement of the water. I like to play around with these different parameters on the fly until I get something that “feels” right.
For my final water I only needed 4 different instances of the material function, but I’d say 4-6 variations is a good range for the gerstner wave function. Also, since it’s important to be able to make changes instantly, you want to make all of your input notes Parameters so you can change them and have the material update in real time. Below you can see how playing with the parameters can give the water movement very different feelings. One note: With the direction parameter, leave the blue channel at 0 because the gerstner wave function is already handling the height of the waves. Also, don’t max out the numbers with a full value of “1,” you will get better results.
Once you have the 4-6 instances, you need to use several Add nodes to add them together and then plug them into the World Displacement channel of the material. Also, you need to make sure you have tessellation turned on in your material. Here are the settings that I found worked best for me:
4: Adding Panning Normal Maps
So by now, the big read is working as expected. The gerstner wave function is handling all of the large movement and wave shape, but we still need to add normal map detail to really sell this surface as water. Because I’ve made water before, I created a custom material function for 4-way panning normals (pictured above) to help speed up my process. The idea is to just have one texture moving in 4 different directions with slight scale and speed variation to give the illusion of water movement. One thing that I did add that’s a little unique is the ability to put in and overall directional panner. I use this panner to mimic the direction inputs from the gerstner wave function to get the movement of the normals to look more believable with the movement of the waves. You can see a successful use of this in my classmate, Max Frorer’s biome.
So once you have the 4-way distortion working inside the material function, it’s a simple matter of plugging in the normal maps and playing around with the parameters to get an effect that looks good. Like with the displacement, I make all of my inputs parameters so I can tweak them on the fly. For the base of the water, I found I only needed 3 different normal maps: one “medium sized” one to add some variation to the wave shapes, one “macro” to add some choppiness to water, and a “micro” detailing normal to add small variation to break up the shapes and bounce light/reflection around. These all get added together and plugged into the Normal channel of the material.
Once the normal map is working, I added some of the height information from the medium wave normal maps into the tessellation/displacement from the gerstner wave function. I multiplied the normal map information by a very small number and then used a VertexNormalWS (World Space) node to give it information for tessellation. Multiplying this by a parameter gives quick control of how intense the normal map tesselation is. This gets added in with the gerstner function and plugged into the World Displacement channel in the material. You can also plug this into the World Position Offset channel in the material to amplify the effect.
5: Reflection + Specularity via Emissive
Because this water is going to take up more than 50% of my screen space, I made the decision to keep this material opaque instead of translucent. However, I wasn’t quite getting the look I was wanting so I ended up leveraging the Emissive Color channel to create input some custom reflections and custom specularity to get the sparkling effect that water has. Below are two gifs: one with just the custom specularity and the other with the “final-in-progress” with reflections, specularity, and a super low metallic value.
To get the reflection, I put a Scene Capture Cube in my level to take a snapshot of the level to use as a base for the reflections. By taking the normal map information, multiplying that down, and plugging it into a Reflection Vector, the snapshot is distorted by the panning normals. Then, a Fresnel Function is used to make the edges of the the material more reflective than the interior.
The Custom Specular calculation I’m using takes a 3 vector constant that is approximately the same angles as the directional light in the level, and transforms them to world space. A Fresnel that is using some of the normal map information is multiplied against a power node to determine how strong the “glow” from the specularity will be and how tightly it will follow the normal map information. That is then multiplied by whatever color you are wanting the spec to look like. Then you multiply this whole string of nodes by a 1 vector to determine how intense it will be. I used another Absolute World Position gradient with a lerp to make it stronger at the crests of the waves. This is then added together with the reflections and plugged into the emissive channel.
Finally, I set the Roughness and Specular channel to 0 and set the metallic value of the water very low (0.01) and dropped a Sphere Reflection Capture in the level to get the “sparkle” on the water.
6: What’s Next
So that finishes the breakdown of what I have so far. Like I said at the beginning, this is still very much a work in progress I will be developing over the next few months as part of my senior thesis at Ringling College of Art + Design. Some next steps I’m planning to take is foam on the crests of some of the waves, edge detection with ripples, and possibly faking depth by using a masked material.
I hope you found this breakdown somewhat helpful, and even if you aren’t planning on making water learned something that you can apply to future projects! If you have any questions please feel free to reach out to me on Twitter, check out my Portfolio, or shoot me an email! ! I will be updating this tutorial with my process on Tricount. Thanks for taking the time to check this out :^)