I’ve been wanting to write a nice water shader for quite some time, and in the last few weeks I’ve actually had time to do just that. I got the inspiration from the Nvidia GPU gems article on vertex animations with Gertsner waves (see sources). I used that as a sort of base, and added the additional features I wanted on top of it.
I wanted to make a shader that could handle most kinds of water, everything from calm ponds to rough seas. With that in mind I decided to try and implement the following features:
- Vertex animation with adjustable Gerstner waves.
- Depth dependent transparency.
- Subsurface scattering.
- Foam on that matched the peaks in the geometry.
- Foam where the water interacts with other geometry.
Most of these featured can be seen in the image below:
I decided to go with a Unity surface shader, since I wanted to utilize the Unity-PBS shading for my material. Water is quite complicated when it comes to light and reflections, so I would rather have Unity do that for me.
The main setup of the shader goes as follows:
- Vertex animation with Gerstner waves that adjust the vertex position and normal. A total of 5 waves (with adjustable parameters) can be combined to create a wave landscape.
- Two different normal maps with matching height maps are then added on top of the vertex animations. This is crucial so simulate the chaos/noise that occurs in water, especially in rougher seas.
- A foam color is then added to the wave crests. The crest factor is calculated along with the Gerstner waves (although this could really be improved).
- Foam color is also added with the peaks in the height map in order to get some extra noise in the foam that also correlates with the geometry.
- Water depth fog, refraction and object interaction foam is then calculated by sampling the depth texture and comparing it to the pixel depth.
- The subsurface scattering is created by extending the Unity light function in the surface shader. Its basically an inverted diffuse function. This is in no way accurate, but looks good enough.
Creating large water surfaces
It’s one thing to create a small lake, it’s another thing to simulate an ocean covering your scene. In order to do this (see Image 1, 2 and 3) you have to set up your mesh tiles to have proper LOD levels. The water close to the camera needs to have rather high-poly meshes. Otherwise the vertex animations and height map additions will look bad. On the other hand, water far away from the camera can be massively simplified (using a separate but similar shader). The vertex animations still need to be there, but you don’t need the ZWrite prepass or any of the fog, refraction or intersection foam. Since none of this is needed the shader can run in one pass, and since the mesh tiles can be very low poly it can be dynamically batched by Unity.
Suggestions for improvement:
- Particle system for splashes on the wave crests. This would no doubt add a lot to the overall effect. I don’t have time to create it right now though.
- Better interaction with shallow shores. High waves will look weird when they hit shallow shorelines.
- Get rid of the ZWrite prepass. Right now its necessary to get rid of blending artifacts when the animated mesh blends with itself. The unfortunate thing is that I also have to do the vertex animation in this pass, which is rather expensive.
- Get rid of one of the normal/height maps. You really only need to use one, and then sample it again with another set of UVs.
NOTE 1: The code is a bit of a mess at places still, but I thought I’d share it anyway. I’ll update it over time as I make work more on this water.
NOTE 2: If you want to use this in your game, keep in mind that the setup can be a bit tricky. First of all, make sure your normal maps match your height maps(!!) Secondly, if you notice that your normals start looking weird, adjust the steepness and amplitude of the vertex waves. Right now there is nothing controlling how multiple waves combine their separate steepness, they simply add them up, which can cause normals to basically point downwards.
NOTE 3: The shader called Water_simple.shader is the simplified version you can use for higher LOD levels.
- Nvidia gpu gems article on Gerstner waves: https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch01.html
- Catlike coding water article (for water fog and refraction): https://catlikecoding.com/unity/tutorials/flow/looking-through-water/
- LOD meshes in Unity: https://www.youtube.com/watch?v=ifNyVS2_6f8
Also a big thanks to Erland Körner at Snowprint studios for some highly valuable input on how to create this shader!