Mobile water with "Depth Alpha" or "Depth Opacity" or "Darker stuff deeper"?

Hey guys. I have been spending a good amount of time and effort trying to find/create a solution for mobile devices to use water with “depth alpha” or “depth opacity”, where objects that are placed under the water, are only visible nearest the surface of the water, and the deeper they get, the more and more “hidden under the water” the object becomes visually to the player.

This might not sound like its that complicated, but in practice, I’ve yet to find a solid tutorial, code snippet, or open source shader that shows how to achieve this effect. I’ve seen things like the free open source ocean system that has had a few different authors working on it, as well as plenty of water examples that don’t have any different rendering no matter how deep the object gets.

An example of a (bad) workaround might be to layer 5 or so planes/quads on top of one another, each with slight transparency, so that the deeper the objects were, the less transparency there would be visible, like you see here:

But that is obviously not the solution I’m looking for, it looks bad, and probably would even have rendering problems on some mobile devices… another less than perfect solution might be to manually change the alpha of objects the lower they get in the water… but this also is not an optimal solution and wouldn’t work for objects not specifically decided to be adjusted at lower depths.

So how in the world do you guys do this? I’m not asking for a asset store package to solve this problem (although I’d be interested to see/hear how they did it for those water assets available in the asset store) but I’m hoping somebody can actually shed some light on specifically what worked, whether you wrote a custom shader, or used shader forge, or did something else entirely!

How did you make your mobile water “darker” at lower depths? Thanks ahead of time for any info and tips that might help me figure this out!

EDIT: another thing I’ve heard of is “schlicks fresnel equation” - which I don’t know anything about, but perhaps would help me?

EDIT2: forgot to mention, the “pro water” is too performance heavy for my target devices, so even though it might have options to do this sort of stuff (not sure) I found it was just too slow even on default settings for mobile devices.

Well, the way it’s done is by using the depth buffer (_CameraDepthTexture) to find the distance of all objects in the scene to the camera (this automatically disregards the water itself) and comparing that to the distance of the water to the camera. This is what the pro water assets do, but they also factor in fresnel calculations, reflections, refraction and potentially displacement. A simple shader that would fade from nothing to a colour could look like this;

Shader "Custom/Depth Fade" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _Depth ("Depth Fade", Float) = 1.0
        _Fix ("Depth Distance", Float) = -0.09
    }
    SubShader {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 200
       
        ZWrite Off

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
           
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _CameraDepthTexture;

            half _Depth;
            half _Fix;

            fixed4 _Color;

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                //float3 worldNorm : TEXCOORD1;
                //float3 viewDir : TEXCOORD2;
            };

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                o.uv = ComputeScreenPos (o.pos);
                //o.worldNorm = UnityObjectToWorldNormal (v.normal);
                //o.viewDir = WorldSpaceViewDir (v.vertex);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                float2 uv = i.uv.xy / i.uv.w;

                //float rim = saturate (dot (normalize (i.viewDir), i.worldNorm));

                half lin = LinearEyeDepth (tex2D (_CameraDepthTexture, uv).r);
                half dist = i.uv.w - _Fix;
                half depth = lin - dist;

                //return lerp (half4 (1,1,1,0), _Color, saturate (depth * _Depth * rim));
                return lerp (half4 (1,1,1,0), _Color, saturate (depth * _Depth));
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

The stuff that’s been commented out is a bit more intensive, but it counters the ‘condensing’ effect you get when looking at glancing angles to the surface of the water. Take note however; if you are using Forward rendering, you have to manually enable the depth texture per camera via scripting. If you don’t, it won’t be generated and you’ll just get a blank object.

1 Like

Ok, I’ve tried this shader, and created this little script to set depth mode on the main camera:

using UnityEngine;

[ExecuteInEditMode]
public class EnableDepthTexture : MonoBehaviour
{
    void Start()
    {
        Camera.main.depthTextureMode = DepthTextureMode.Depth;
    }
}

And this is the result:

EDIT: I see this on the camera component (it has a notice at the bottom):

Is this the expected result? Should I use a different setting with DepthTextureMode? I’ll keep experimenting, but any more info is appreciated :slight_smile:

EDIT 2: Ok, had to adjust the alpha of the color of the plane, then it looks proper:

Thank you very much good sir/ma’am!

If your water is always at a given height, set that as a global variable and do the fade to your “deep water” color in the object, not in the water. Then you don’t need the depth tex and your water shader just handles specularity.

1 Like

Hmm, forgive me @brownboot67 but I don’t know much about shaders and don’t understand all the terminology and stuff.

-Your saying that you can do it without the depth texture on the camera, if your water doesn’t move (doesn’t have waves or multiple heights in diff areas?) by setting the water height in the shader - is that correct?

-How do you go about fading the object then, like would you need to do some checks again in the shader to find out the height of the object?

-Would you need to assign values (like the “deep water” color/alpha values) on a per-object basis if you did it this way?

Thanks for your advice!

First question: you could have waves. It’d be problematic if you have like a lake at the top of a mountain, and an ocean at the bottom of the mountain.

Over simplification incoming…

Step 1: set your water height and deep water color as globals. (you might also want like “water opacity” as a global, whatever)
Step 2: make a shader for your objects and declare your globals.
Step 3: in your object shader fade to the deep water color based on vertex world position.

Ta-da. No alpha, no depth tex. And because everything in “deep water” is the same color, nobody knows!

That ought to get you there.

3 Likes

See also mario sunshine

See any game before 2005 with water?

3 Likes

If you are going to go with the depth buffer based shader, I would recommend disabling shadow casting for the water object. If you don’t want to do this manually per object, you can just remove the fallback at the end of the shader.

If you still couldnt solve your issue, here is my pack in the assetstore with an alternative method.
TOZ Water
If you watch the video, around 10th minute, you will see depth painting.

1 Like

Thanks for the info @brownboot67 - that makes sense I think, I might try working on something like that.

@aubergine - your asset does seem to be pretty full featured!

I’m curious if it would work in the situation I’m trying to use it for, which is to show “fish” and other underwater stuff, ever so slightly, from the surface of the water, if they are near it. Imagine your standing on a dock overlooking a pond or river… and you can see fish that are close to the surface, but not the ones who are deep in the water. From what I could tell in the video on the asset store, it looks like you also have a depth texture approach, is that correct?

The reason I ask, is in the part of the video where you show the “painting” on the edges of the water near the beach, it seems like your creating transparency for specific areas, rather than the entire water surface, so does that mean you would not see objects just under the surface, if they were not near the edges of the water area? If you could still see objects slightly beneath the surface, then what exactly is the purpose of painting the edges like that?

Hi all,

Thanks @Namey5 - this is exactly the effect I’ve been looking for - and using the shader code above I’ve got this effect going on (see below). It works really well in the editor but I’m having an issue with it on android- as you can see from the gifs. The device is an old Galaxy Note II, with GLES 2.0. The opaque objects all use the standard shader Mobile/Vertex Lit (direction lights only)…

3393677--266877--BuildyRoadLogs2.gif Editor

3393677--266878--BuildyRoadLogs2Android.gif Android

Cheers,
Al.

1 Like

That’s poor depth precision. Don’t use a depth mask use the vertex position.

2 Likes

Thanks @brownboot67 . I’m a newb at shader writing but after a load of research I’ve managed to get it working using your suggestion - with the results looking almost exactly like the depth buffer version… and working beautifully on mobile :slight_smile:

3398302--267434--BRLogsNew.gif

1 Like

Nice @Alan-Ward - would you mind sharing the code?

Ok here’s the shader. I’ve cobbled this together from various sources…

It’s a basic diffuse vertex lit vertex/fragment shader - similar to standard “mobile/diffuse” but doesn’t account for shadows.

I’ve made it a little more flexible now - you can set whether lighting is included in the fade or whether it’s just flat colour, and also specify a colour tint that will modify fade :

Properties
_Maintex = your 2D texture map
_colorToFade = the ‘deep water’ colour at maximum fade
_colorModifier = a colour tint that’s added to the texture during the fade, %50 Gray = no tint
_useLightingInFade = positive values = true, negative = false
_fadeDepth = the y units to fade across
_fadeStart = the y world height to start the fade

Add this shader to the objects that you want to fade, and enjoy…

Shader "Custom/FadeToColour"
{
     Properties
     {
        _MainTex ("Texture", 2D) = "white" {}
        _colorToFade("Fade to Color", Color) = (1,1,1,1)
        _useLightingInFade("Use Lighting in Fade", Int) = 1
        _colorModifier("Color Modifier", Color) = (0.5,0.5,0.5,0.5)
        _fadeDepth("Fade Depth", Float) = 1.0
        _fadeStart("Fade Start Y", Float) = 0.0
     }
 
     SubShader
     {
        Tags {"Queue"="Geometry" "LightMode"="ForwardBase"}
        LOD 200
  
           Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
 
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;

            half4 _colorToFade;
            half4 _colorModifier;
            float _fadeDepth;
            float _fadeStart;
            fixed _useLightingInFade;

            struct v2f
            {
                float4 position : POSITION;
                float2 uv : TEXCOORD0;
                float4 wPos : TEXCOORD1;
                float4 col : COLOR0;
                float3 normal : NORMAL;
            };
            v2f vert (v2f v)
            {
                v2f o;
                // set Wpos to be vertex world postion for y checking
                o.wPos = mul (unity_ObjectToWorld, v.position);

                o.position = UnityObjectToClipPos(v.position);
                o.uv = v.uv;
                o.normal = v.normal;
          
                //lighting - basic vertex shading with one light, no shadows
                float4x4 modelMatrixInverse = unity_WorldToObject;
                float3 normalDirection = normalize(float3(mul(float4(v.normal, 0.0), modelMatrixInverse).xyz));
                float3 lightDirection = normalize(float3(_WorldSpaceLightPos0.xyz));
                float3 diffuseReflection = float3(_LightColor0.xyz) * max(0.0, dot(normalDirection, lightDirection));
                o.col = float4(diffuseReflection, 1.0) + UNITY_LIGHTMODEL_AMBIENT;

                //return the v2f output structure
                return o;
            }
 
            half4 frag (v2f i) : SV_Target
            {
                //get the texture colour
                half4 texel = tex2D (_MainTex, i.uv);
                //get the world height
                half vertHeight = i.wPos.y;
                //fade if below fade height
                if (vertHeight < _fadeStart)
                {
                    //work out how deep we are in the fadeDepth for use in lerp
                    half vertDepth = abs(_fadeStart - vertHeight);
                    //calculate the colour tint - 50% gray = no tint
                    half4 tint = _colorModifier - (0.5,0.5,0.5,0.5);

                    if (_useLightingInFade > 0)
                        //lerp between texture color and complete fade depending on depth - with lighting.
                        return (lerp (_colorToFade, (texel * i.col ) + tint, saturate ((_fadeDepth-vertDepth)/_fadeDepth))) ;
                    else
                        //or ignore lighting - flat shade the Fade
                        return (lerp (_colorToFade, texel + tint, saturate ((_fadeDepth-vertDepth)/_fadeDepth))) ;
                }
                else
                {
                    //otherwise just return the texture colour affected by lighting
                    return texel * i.col;
                }
            }
            ENDCG
        }
     }
     Fallback "Diffuse"
}
2 Likes

Hi all - just FYI - I edited the shader slightly so that the colour tint works to darken the colours too. 50% Gray is now set as the “no tint” colour. Hope this makes sense.
Cheers,
Al.