Simple Water Shader in Unity

Heya, I’m Lin Reid, programmer on Limit Theory, and I’m going to show y’all how to make a water shader in Unity! This is totally non-Limit-Theory related… just writing some shaders for fun 😂

This tutorial is really a general graphics programming tutorial in disguise. The techniques we’re going to learn in this post- vertex animation and using depth textures- are basically applicable to any platform. However, I do go over a few of the quirks with getting camera depth textures to work in Unity so that you can make it work too.

These are the two possible end results, applied to an adorable Boston Terrier model made by artist Kytana Le (please ignore crappy gif quality):

Notice how both have a foam line where the dog touches the water (but with different styles for each) and animated waves. We’re going to learn how to do both. Let’s start with the foam line!

Also, for reference, here’s the complete code for the shader:

–> Link to final code for Unity Water Shader

UPDATE: I now also have a tutorial for an ice shader that covers a distortion effect that looks GREAT with this water shader, like in the gif below. Finish this tutorial first, then follow the ice shader to add the distortion pass! ;0

watergif


Foam Line using Depth

The way we create this foam line around the dog is by reading the depth at every vertex on the mesh, and using that depth value to output a color. Specifically, we read the camera’s depth texture to find out how far away each vertex is from the camera. When an object is in the water, it shortens the distance. In the shader I wrote, we then have two options for how to use that depth value to create the foam line- one using the depth value as a gradient, and the other using the depth value to sample a ramp texture.

Here’s the Unity documentation on general depth textures and the camera depth texture, which you may want to keep handy during this tutorial.

Let’s get started by setting up our scene. Firstly, we need to enable the depth texture mode on the main camera. Annoyingly, there isn’t an option for this in the inspector- we have to write a script and attach this script to the camera. Here’s mine, in C#:

using UnityEngine;

[ExecuteInEditMode]
public class DepthTexture : MonoBehaviour {

  private Camera cam;

  void Start () {
    cam = GetComponent<Camera>();
    cam.depthTextureMode = DepthTextureMode.Depth;
  }

}

If you do this correctly, you should see a tiny message at the bottom of your camera properties in the inspector:

cameraProperties (2)

Now, we’re ready to write the shader! Let’s start by sampling the depth value at each vertex and using that to output a test color, just to make sure we’re reading the depth values correctly. (If you’re unfamiliar with any of the functions used, check the Unity documentation on shader built-in functions.)

Shader "Custom/Water"
{
SubShader
{
Pass
{

CGPROGRAM
// required to use ComputeScreenPos()
#include "UnityCG.cginc"

#pragma vertex vert
#pragma fragment frag
 
 // Unity built-in - NOT required in Properties
 sampler2D _CameraDepthTexture;

struct vertexInput
 {
   float4 vertex : POSITION;
 };

struct vertexOutput
 {
   float4 pos : SV_POSITION;
   float4 screenPos : TEXCOORD1;
 };

vertexOutput vert(vertexInput input)
  {
    vertexOutput output;

    // convert obj-space position to camera clip space
    output.pos = UnityObjectToClipPos(input.vertex);

    // compute depth (screenPos is a float4)
    output.screenPos = ComputeScreenPos(output.pos);

    return output;
  }

  float4 frag(vertexOutput input) : COLOR
  {
    // sample camera depth texture
    float4 depthSample = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, input.screenPos);
    float depth = LinearEyeDepth(depthSample).r;

    // Because the camera depth texture returns a value between 0-1,
    // we can use that value to create a grayscale color
    // to test the value output.
    float4 foamLine = float4(depth, depth, depth, 1);

    return foamline;
  }

  ENDCG
}}}
To test this:
  1. Create a material using this shader
  2. Apply that material to a flat plane
  3. Use another object (in my case, a doggo) to intersect the plane

Note that the intersecting object MUST be able to use shadows in order to contribute to the depth texture. This is a weird Unity quirk- read the documentation for more info.

You should come up with something similar to the image below. The white area encompasses most of the texture, and grey areas mark where the depth value was < 1, where the intersecting object shortened the distance to the camera:

depthTest

If you’ve made it this far, then awesome! You’re 90% of the way there, especially if you’ve been dredging through Unity’s obstacle course of camera depth texture rendering XD
Next, let’s apply our depth value to create a smooth gradient around intersecting objects. Let’s add a few parameters to our properties to customize what the gradient looks like:
Properties
{
  // color of the water
  _Color("Color", Color) = (1, 1, 1, 1)
  // color of the edge effect
  _EdgeColor("Edge Color", Color) = (1, 1, 1, 1)
  // width of the edge effect
  _DepthFactor("Depth Factor", float) = 1.0
}

Now, let’s modify the fragment shader to create the gradient.

The line where we create foamLine is a bit confusing. Generally, what we’re trying to do is transform the depth value to something more usable to our shader, which tells us about how far way from the plane the position is, and not just from the camera. (The depth value before applying this transformation is relative to the camera’s view point.) I referenced this open source code by Daniel Zeller to be able to figure out how to do this transformation, and to be honest I still don’t understand the wizardry behind subtracting the screen position w value from depth, but that’s the magic of shaders sometimes.

In addition, we multiply by the _DepthFactor to have a bit more customization over the resulting value, and we saturate the value to clamp it from 0-1. We subtract the resulting value from 1 (still resulting in a 0..1) value so that the foamLine value is larger the “deeper” it is.

float4 frag(vertexOutput input) : COLOR
{
  float4 depthSample = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, input.screenPos);
  float depth = LinearEyeDepth(depthSample).r;
  
  // apply the DepthFactor to be able to tune at what depth values
  // the foam line actually starts
  float foamLine = 1 - saturate(_DepthFactor * (depth - input.screenPos.w));
  
  // multiply the edge color by the foam factor to get the edge,
  // then add that to the color of the water
  float4 col = _Color + foamLine * _EdgeColor;
}
 Here’s what the output of the shader should look like now:
waterShader

 

Now, my personal favorite of the two gives more of a cel-shaded effect, which I think looks nice with the cel-shaded pupper. I created this effect by using the depth value to sample a ramp texture, just like the cel shader used on the dog. I used the following ramp texture, which you can feel free to use too:
ramp2
Add a ramp texture to your properties, and modify the fragment shader to sample the ramp texture instead of the edge color.
float4 frag(vertexOutput input) : COLOR
{
  float4 depthSample = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, input.screenPos);
  float depth = LinearEyeDepth(depthSample).r;

  float foamLine = 1 - saturate(_DepthFactor * (depth - input.screenPos.w));
  // sample the ramp texture
  float4 foamRamp = float4(tex2D(_DepthRampTex, float2(foamLine, 0.5)).rgb, 1.0);
  
  float4 col = _Color * foamRamp;
}
Your doggo should now look like this:
celWaterShader

Common Issues

If you can’t get correct depth texture output at all:

  1. Make sure your intersecting object is using a material that can cast shadows.
  2. Make sure your intersection object has “cast/receive shadows” turned on in the inspector.
  3. Make sure you attached your C# depth texture script to your main camera.
  4. Fiddle with the near/far view planes on your camera.

If you’re getting weird output on your depth highlight:

  1. If you’re sampling a ramp texture, make sure it’s clamped.
  2. If you’re already animating the water, make sure you’re calculating vertex position BEFORE checking depth.

Wave Animation

And now, the waves! If you’ve never written an animated shader before, this should be a fun one to start with. The secret ingredient to animating a shader is to add Time to your algorithms. Unity gives you a built-in value for time called _Time. The algorithm looks like this:

  1. Sample a noise texture for a random value, which creates the non-uniform-ness of the waves
  2. Create a wave value by taking sin(time * noiseValue), which creates the oscillating up-and-down motion
  3. Add the wave value to the vertex position

There are many different ways to create random values in shaders, so you can use whichever method you prefer for step #1. If you’re new to shaders and want to skip writing your own noise function, however, you can just sample a noise texture like this one:

noise

Taking sin(time) ensures that our random value oscillates back and forth with time, which is what creates the wave-like motion. The randomness then ensures that the motion is different for each vertex.

Here are the properties you need to add:

float _WaveSpeed;
float _WaveAmp;
sampler2D _NoiseTex;

And here’s what the vertex shader code looks like:

// convert to camera clip space
output.pos = UnityObjectToClipPos(input.vertex);

// apply wave animation
float noiseSample = tex2Dlod(_NoiseTex, float4(input.texCoord.xy, 0, 0));
output.pos.y += sin(_Time*_WaveSpeed*noiseSample)*_WaveAmp;
output.pos.x += cos(_Time*_WaveSpeed*noiseSample)*_WaveAmp;

Your water should now be animating!!! Try adding more objects to show off that foam line, and experiment with your WaveSpeed and WaveAmp.

waveShader1

 

Common Issues

If your water mesh is clipping like this:

waterclip

.. which mine was, add a little fudge factor to the y-coordinate of your animation. There might be a more professional way to solve this issue, but I’m not sure what it is!

Here’s what that fudge factor looks like. _ExtraHeight is a float defined in Properties{}:

output.pos.y += sin(_Time*_WaveSpeed*noiseSample)*_WaveAmp + _ExtraHeight;

 


Fin

WOOHOO!! If you’ve gone through the whole tutorial and made it here, then your output should look something like the header image. You can see that I personally preferred the cel-style foam line for the water. 🙂 For referece’s sake, I’ve included the entire shader code below.

–> Link to final code for Unity Water Shader

If y’all have any questions about writing shaders in Unity, I’m happy to share as much as I know. I’m not an expert, but I’m always willing to help other indie devs 🙂

Good luck,

Lindsey Reid @so_good_lin

PS, here’s the Unity graphics settings for this tutorial.

Advertisements

Published by

Linden Reid

Game developer and tutorial writer :D

49 thoughts on “Simple Water Shader in Unity”

  1. Hi, there is one thing that i can’t really figure out. I’m fairly new to shaders though, so sorry in advance if I ask about something obvious.

    in vertex program:

    //convert vert world pos to cameras clip space
    output.pos = UnityObjectToClipPos(input.vertex);
    //in -> clipped vert pos , out -> text coordinate for doin screenspace mapped texture sample
    output.screenPos = ComputeScreenPos(output.pos) –> so, our vertex first got clipped to cameras space and then we got screenspace texture coordinate from it, right? UV coords basicaly?

    and then, in fragment progam:

    float4 s = UNITY_PROJ_COORD(input.screenPos); –> ok, what happens here? Following Unitys documentation:

    “UNITY_PROJ_COORD(a) – given a 4-component vector, return a texture coordinate suitable for projected texture reads. On most platforms this returns the given value directly. ”

    So, we gave that unity_proj_coord macro our verts position in screenspace texture coordinates and then what did we receive exactly? I can’t wrap my head around it really 🙂

    Like

    1. This is a really good question. I just realized the call to UNITY_PROJ_COORD is a bit of an artifact from trying to get this shader to work, and I should probably remove it from the tutorial XD But it does have one use!

      Some platforms seem to have an issue with calling SAMPLE_DEPTH_TEXTURE_PROJ directly from a screen position. If we look at the actual defines for Unity’s built-in shaders, UNITY_PROJ_COORD actually returns the exact same value on the majority of platforms:

      #if defined(SHADER_API_PSP2)
      #define UNITY_BUGGY_TEX2DPROJ4
      #define UNITY_PROJ_COORD(a) (a).xyw
      #else
      #define UNITY_PROJ_COORD(a) a
      #endif

      This isn’t just the PSP2, but for, it seems like, a few different platforms with a similar issue.

      For better reference on any of Unity’s helper functions like this, I highly recommend downloading the built-in shaders (https://unity3d.com/get-unity/download/archive) and looking at what they’re actually defined as in code 😀

      Like

  2. Could a PBR lighting model pass be applied to the shader (such as the one in the standard surface shader) to create a more realistic art style for the water?

    By the way great work! I just discovered your site it’s awesome!

    Like

  3. The camera script has compile errors in unity 2017.3

    Error CS0411: The type arguments for method ‘UnityEngine.Component.GetComponent()’ cannot be inferred from the usage. Try specifying the type arguments explicitly. (CS0411) (Assembly-CSharp)

    Like

    1. You are correct that we don’t have to supply a LOD for this texture fetch. However, for reasons I don’t completely understand, using the basic tex2d() function doesn’t work in these vertex shaders.

      Here’s the best explanation I could find, from https://gamedev.stackexchange.com/questions/114851/why-cant-i-sample-a-texture-in-a-vertex-shader :
      “tex2D() is really a shortcut that says ‘figure out the right mip level to sample automatically’ – in a fragment shader this is done using implicit derivatives, but those aren’t available at the vertex stage”

      Like

  4. Firstly, thank you for this tutorial, it’s fantastic! Would you mind posting the values of the depth factor, wave speed and wave amplitude that you used to create the gifs? I can’t seem to find the right numbers to make it look similar.

    Like

    1. Heya, I’m going to release the entire Unity project (with the materials and tuned values in a scene!) when I open my Patreon later this week. Just keep fiddling with them! 🙂

      Like

  5. Hello Lindsey! First off, I would like to thank you for sharing this tutorial with everybody, for a student this is gold 🙂

    Anyway, I am a modeler so I have little understanding of coding, and also I am relatively new to Unity, so what I am about to ask may sound stupid.

    I created and attached the script for the camera. Then I created a “Standard Surface Shader” filling the text as written in the tutorial to sample the depth value of the vertexes to obtain an output test color.

    I built the code and everything was fine except for two “errors”:

    ________

    1. ‘frag’: function must return a value
    Compiling Vertex program
    Platform defines: UNITY_ENABLE_REFLECTION_BUFFERS UNITY_USE_DITHER_MASK_FOR_ALPHABLENDED_SHADOWS UNITY_PBS_USE_BRDF1 UNITY_SPECCUBE_BOX_PROJECTION UNITY_SPECCUBE_BLENDING UNITY_ENABLE_DETAIL_NORMALMAP SHADER_API_DESKTOP UNITY_COLORSPACE_GAMMA UNITY_LIGHT_PROBE_PROXY_VOLUME UNITY_LIGHTMAP_FULL_HDR

    2. undeclared identifier ‘foamline’
    Compiling Vertex program
    Platform defines: UNITY_ENABLE_REFLECTION_BUFFERS UNITY_USE_DITHER_MASK_FOR_ALPHABLENDED_SHADOWS UNITY_PBS_USE_BRDF1 UNITY_SPECCUBE_BOX_PROJECTION UNITY_SPECCUBE_BLENDING UNITY_ENABLE_DETAIL_NORMALMAP SHADER_API_DESKTOP UNITY_COLORSPACE_GAMMA UNITY_LIGHT_PROBE_PROXY_VOLUME UNITY_LIGHTMAP_FULL_HDR

    __________

    Furthermore, by applying the shader to a standard material it just turns purple.

    I beg pardon for the wall of text, and totally understand if you’d rather not waste time on it.

    Liked by 1 person

    1. I found the problem whilst looking into it myself. It’s suppose to be ‘return foamLine’ not ‘return foamline’. I hope this helps. I am creating my own water shader for a uni project so thank you for your tutorials. They are a massive help in my research.

      Like

  6. Good evening, thank you so much for all your tutorials! I’m completely new in shaders and that’s very handy to understand it through your explanations!

    I’ve got some problems just before the step “adding a ramp texture”. I can see my models (I use for example a cube with the default Material and it casts and receives shadows and the free Horse model on the Asset Store) through the water but the texture of each model are not visible anymore. It’s like only the color set for “Edge Color” is visible on each model (I use 0.1 as a Depth Factor value). I also checked every point of your list of common issues but everything is ok…

    Like

  7. Hi, amazing tutorial! I’m so glad I found it! I have a few questions about it.

    Where you’ve written:
    float4 depthSample = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, input.screenPos);
    float depth = LinearEyeDepth(depthSample).r;

    I understand that depthSample is the actual value from the depth map, so then what exactly does LinearEyeDepth do to it, and why do we need that value?

    My second question comes from this piece:
    float foamLine = 1 – saturate(_DepthFactor * (depth – input.screenPos.w));

    My question pertains to input.screenPos.w. What is this value, exactly? Is it the distance from the camera to a vertex on the water object? Unity’s documentation says that ComputeScreenPos() calculates a texture coordinate, so what does the w value represent?

    Thank you for the tutorial, and thank you for your help!

    Like

  8. Hi Lindsey! Thanks for the great tutorial! This was my first go at creating a depth texture and it feels pretty powerful already.

    Question: This tutorial is written for a perspective camera. Have you tried to get this effect working on an orthographic one? I’m trying but having some difficulty. I removed the `linearEyeDepth` call since in an orthographic perspective we already have it. The part that I can’t figure it out is how to correctly calculate `foamLine`. It looks like there’s some kind of transformation missing on `input.screenPos.w`. Whatever `_DepthFactor` I use I still get a `foamLine` of 1 (when there’s a non-zero `depth`). Any ideas? Thanks in advance!

    Like

    1. You DM’d me on Twitter with the same question, so let me know if you still need help! For anybody reading this- I don’t think this will work with an orthographic perspective, since orthographic cameras don’t render depth the same way.

      Like

      1. Hello I am the one who is studying shader in Korea! I tried to implement your water and ice shaders while searching for shader-related information.
        If the camera is orthographic when it implements a water shader, how does it work differently?
        And I’ve implemented the same code for the ice shader, but it’s not like ice.
        I do not have a place to put a Bump Ramp in the tutorial, but I guess that’s the difference. What should I put in?

        Like

  9. Hi! Wonderful tutorial, the water looks great and helps a designer like me understand behind the scenes with the intricacies of programming. But I was just wondering: would it be possible to curve the ‘foam lines’ when the edges collide with things like corners?

    Like

    1. Can you be more specific about what defines a ‘corner’ and how a curve would work? Keep in mind that you’ll have to write rules for the shader even clearer than you can define them to me 😉

      Like

  10. Hey Lindsey, thank you very much for sharing your knowledge ! One can learn a lot and good tutorials on shaders are really rare. I am a complete beginner with this but I tried to recreate a modified version of your water shader with a surface shader. Is there a disatvantage with using surface shaders for this kind of task ?
    Here is what I came up with, in case it could be interesting
    https://github.com/Luukezor/unity-shader-learning/blob/master/Shaders/S_WaterSimpleSurface.shader

    Like

  11. I have tried this multiple times. Following your instructions & settings and even just cutting and pasting the code (to ensure I didn’t make any mistakes).
    But still, the shader is just white for me, it’s just a big white plane ….

    Any ideas?

    Like

  12. Hi Lindsey!

    Thank you so much for this amazing tutorial. I did have one question though. I couldn’t seem to figure it out. For some reason when I use LinearEyeDepth() to retrieve the depth value it doesn’t work for me. However, I got it working by substitute the function with Linear01Depth(). Do you know what possibly be the cause of it? No one else on this blog seems to have this issue.

    Thank you vert much

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s