Forum begins after the advertisement:

Underwater effect shader from our Subnautica series

  • 31 August 2023: We recently fixed a bug where our Patrons were not getting their titles displayed properly on their posts.

Home Forums Video Game Tutorial Series Creating an Underwater Survival Game in Unity Underwater effect shader from our Subnautica series

Viewing 2 posts - 1 through 2 (of 2 total)
  • Author
  • #11635

    Hi everyone, if you are looking to create an underwater effect like this:

    Underwater effects for our Underwater Survival Game (like Subnautica)

    You can use the Image Effect shader code and MonoBehaviour code below. Be sure that both the Shader file and MonoBehaviour file have the same name, as the MonoBehaviour will automatically link itself to the Shader file.


    Shader "Hidden/CameraUnderwaterEffect"
            [HideInInspector] _MainTex ("Texture", 2D) = "white" {}
            [HideInInspector] _DepthMap("Texture", 2D) = "black" {}
            [HideInInspector] _DepthStart("Depth Start Distance", float) = 1
            [HideInInspector] _DepthEnd("Depth End Distance", float) = 300
            [HideInInspector] _DepthColor("Depth Color", Color) = (1,1,1,1)
            [HideInInspector] _WaterLevel("Water Level", Vector) = (0.5, 0.5, 0)
            // Disable backface culling (Cull Off),
            // depth buffer updating during rendering (ZWrite Off),
            // Always draw a pixel regardless of depth (ZTest Always)
            // No culling or depth
            Cull Off ZWrite Off ZTest Always
                #pragma vertex vert
                #pragma fragment frag
                #include "UnityCG.cginc"
                sampler2D _CameraDepthTexture, _MainTex, _DepthMap;
                float _DepthStart, _DepthEnd;
                Vector _WaterLevel;
                fixed4 _DepthColor;
                struct appdata
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                struct v2f
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                    float4 screenPos: TEXTCOORD1;
                // We add an extra screenPos attribute to the vertext data, and compute the 
                // screen postion of each vertex in the vert() function below.
                v2f vert(appdata v)
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.screenPos = ComputeScreenPos(o.vertex);
                    o.uv = v.uv;
                    return o;
                // This function is run on every pixel that is seen by the camera.
                // Hence, it is responsible for applying the post-processing effects onto
                // the image that the camera recieves.
                fixed4 frag(v2f i) : SV_Target
                    fixed4 col = tex2D(_MainTex, i.screenPos);
                    if (i.screenPos.y > _WaterLevel.x * i.screenPos.x + _WaterLevel.y - _WaterLevel.x * 0.5) return col;
                    // We sample the pixel in i.screenPos from _CameraDepthTexture, then convert it to
                    // linear depth (depth is stored non-linearly) that is clamped between 0 and 1
                    float depth = LinearEyeDepth(tex2D(_DepthMap,i.screenPos.xy));
                    // Clip the depth btween 0 and 1 again, where 1 is if the pixel is further
                    // than _DepthEnd, and 0 os of the pixel is nearer than _DepthStart.
                    depth = saturate((depth - _DepthStart) / _DepthEnd);
                    // Scale the intensity of the depth colour based on the depth by lerping it
                    // between the original pixel colour and our colour based on the depthValue of the pixel
                    return lerp(col, _DepthColor, depth);


    using UnityEngine;
    public class CameraUnderwaterEffect : MonoBehaviour
        public LayerMask waterLayers;
        public Shader shader;
        [Header("Depth Effect")]
        public Color depthColor = new Color(0, 0.42f, 0.87f);
        public float depthStart = -12, depthEnd = 98;
        public LayerMask depthLayers = ~0; //All layers selected by default
        Camera cam, depthCam;
        RenderTexture depthTexture, colourTexture;
        Material material;
        bool inWater;
        // Start is called before the first frame update
        void Start()
            cam = GetComponent<Camera>();
            //Make our camera send depth information (i.e. how far a pizel is from the screen)
            // to the shader as well
            //cam.depthTextureMode = DepthTextureMode.Depth;
            //Create a material using the assigned shader
            if (shader) material = new Material(shader);
            //Create render textures for the camera to save the colour and depth information
            //prevent the camera from rendering onto the game scene
            depthTexture = RenderTexture.GetTemporary(cam.pixelWidth, cam.pixelHeight, 16, RenderTextureFormat.Depth);
            colourTexture = RenderTexture.GetTemporary(cam.pixelWidth, cam.pixelHeight, 0, RenderTextureFormat.Default);
            //Create depthCam and parent it to main camera
            GameObject go = new GameObject("Depth Cam");
            depthCam = go.AddComponent<Camera>();
            go.transform.position = transform.position;
            //copy over main camera settings, but with a different culling mask and depthtexturemode.depth
            depthCam.cullingMask = depthLayers;
            depthCam.depthTextureMode = DepthTextureMode.Depth;
            //Make depthCam use ColorTexture and depthTexture
            //and also disable depthCam so we can turn it on manually.
            depthCam.SetTargetBuffers(colourTexture.colorBuffer, depthTexture.depthBuffer);
            depthCam.enabled = false;
            //Send the depth texture to the shader
            material.SetTexture("_DepthMap", depthTexture);
        private void OnApplicationQuit()
        private void FixedUpdate()
            //get the camera frustum of the near plane.
            Vector3[] corners = new Vector3[4];
            cam.CalculateFrustumCorners(new Rect(0, 0, 1, 1), cam.nearClipPlane, cam.stereoActiveEye, corners);
            //check where the water level is, without factoring in rolling (as we cannot)
            //check how far submerged we are into the water, using corner[0] and corner[1],
            //which are the bottom left and top left corners respectively.
            RaycastHit hit;
            Vector3 start = transform.position + transform.TransformVector(corners[1]), end = transform.position + transform.TransformVector(corners[0]);
            Collider[] c = Physics.OverlapSphere(end, 0.01f, waterLayers);
            if(c.Length > 0)
                inWater = true;
                c = Physics.OverlapSphere(start, 0.01f, waterLayers);
                if (c.Length > 0)
                    material.SetVector("_WaterLevel", new Vector2(0, 1));
                    if(Physics.Linecast(start,end,out hit, waterLayers))
                        //get the interpolation value (delta) of the point the linecast hit
                        //the reverse of a lerp function gives us the delta.
                        float delta = hit.distance / (end - start).magnitude;
                        //set the water level
                        //use 1 - delta to get the reverse of the number
                        //e.g. if delta is 0.25, the water level will be 0.75.
                        //this is because the linecast is done from above the water, and the delta is the percentage of screen that is not submerged.
                        material.SetVector("_WaterLevel", new Vector2(0, 1 - delta));
                inWater = false;
        //Automatically finds and assigned inspector variables so the script can be immediately used when attached to a gameobject
        private void Reset()
            //Look for the shader we created
            Shader[] shaders = Resources.FindObjectsOfTypeAll<Shader>();
            foreach(Shader s in shaders)
                if (
                    shader = s;
        //This is where the image effect is applied
        private void OnRenderImage(RenderTexture source, RenderTexture destination)
            if (material && inWater)
                //Update the depth render texture
                //We pass the information to our material
                material.SetColor("_DepthColor", depthColor);
                material.SetFloat("_DepthStart", depthStart);
                material.SetFloat("_DepthEnd", depthEnd);
                //Apply to the image using blit
                Graphics.Blit(source, destination, material);
                Graphics.Blit(source, destination);

    To use the scripts above to apply the underwater effect to your camera, you will need to:

    1. Attach the CameraUnderwaterEffect script to your Camera GameObject.
    2. Add a Trigger Collider to your water body that will represent the water’s volume, and assign it to a layer (I recommend “Water”).
    3. Duplicate your water plane, and set it to a Y-scale of -1, so that it faces down. To make it not show up in-game, create a new layer (e.g. “Water Surface”) and assign the down-facing water place to it. Then, in your game Camera, set the Culling Mask to exclude the “Water Surface” layer.
    4. Set the Water Layers property in the CameraUnderwaterEffect component to include “Water” and “Water Surface”
    5. When you drop your camera into the collider that represents the water’s volume, the underwater shader effect will apply.

    There is still a lot of work we are planning to do to improve upon the underwater effect. We are currently working on a free Asset Pack that will make it easier to apply the underwater effect. Keep your eyes peeled!

    Here is a video summarising the instructions above:

Viewing 2 posts - 1 through 2 (of 2 total)
  • You must be logged in to reply to this topic.

Go to Login Page →

Advertisement below: