How to prevent transparent mesh from drawing over parts of itself which are nearer the camera

Hi,

I’m working on a water shader which has vertex displacement to create waves. When the waves are high enough the mesh behind can draw over the waves which are in front.

As far as I can tell, this is a complicated issue called “Order Independent Transparency”.

As I understand it, the shader doesn’t write to the z-buffer and the verts are rendered in the order that they are stored in the vertex array of the mesh. So the wave in front is drawn and then the wave behind overwrites that outpuf.

My best guess about how to solve this is to have the shader do the vertex displacement and then write to a custom z-buffer. Then I can do the actual surface shader part and only render the parts that are in front. This will mean that the transparency won’t be completely accurate because I wont be able to see the ocean through wave peaks but that’s probably fine.

I’m overwhelmed with the amount of information I’ve found and the sheer range of different possible solutions and I really need help discussing this problem within the specific context of what I’m working on so that I can understand what is happening and what my options are.

Here is my shader:

Shader "Custom/Ocean"
{
    Properties
    {
        _WaterColour ("Water Colour", Color) = (0, 0, 0, 0)
        _WaterFogColourLow ("Water Fog Colour Low", Color) = (0, 0, 0, 0)
        _WaterFogColourHigh ("Water Fog Colour High", Color) = (0, 0, 0, 0)
        _WaterMaxHeight ("Water High Colour End", float) = 10
        _WaterFogDensity ("Water Fog Density", Range(0, 2)) = 0.1
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _AtmosphereColour ("Atmosphere Colour", Color) = (0, 0, 0, 0)
    }
    SubShader
    {
        Tags { "RenderType" = "Transparent" "Queue" = "Transparent"}

        Cull OFF
     
        GrabPass { "_WaterBackground" }

        CGPROGRAM
        #include "WaterShaderUtils.cginc"
        #include "CustomFog.cginc"
        #pragma surface surf Standard alpha vertex:vert finalcolor:FinalColour
        #pragma target 5.0
        #define TWO_PI 2 * UNITY_PI

        struct Input
        {
            float2 uv_MainTex;
            float4 screenPos;
            float3 worldPos;
        };

        float4 _WaterColour;
        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        float _EditorTime;
        float3 _PositionOffset;
        float4 _AtmosphereColour;
        float _FullAtmosphereDepth;

        struct LinearWave
        {
            float Amplitude;
            float Wavelength;
            float Frequency;
            float2 Direction;
        };

        #ifdef SHADER_API_D3D11
        StructuredBuffer<LinearWave> _LinearWaveBuffer;
        int _NumLinearWaves;
        #endif

        float3 GetDisplacementAtPosition(float3 p, LinearWave wave, inout float3 ddX, inout float3 ddZ)
        {
            float2 d = wave.Direction;
            float a = wave.Amplitude;
            float l = wave.Wavelength;
            float f = wave.Frequency;

            float pd = dot(p.xz, d);
            float theta = (TWO_PI * pd / l) - (f * _EditorTime);
            float k = (a * TWO_PI / l);
            float sinTheta = sin(theta);
            float cosTheta = cos(theta);
            float3 vec = float3(-sinTheta * d.x, cosTheta, -sinTheta * d.y);

            ddX += k * d.x * vec;
            ddZ += k * d.y * vec;

            return a * float3(cosTheta * d.x, sinTheta, sinTheta * d.y);
        }

        void vert (inout appdata_full v)
        {
            #ifdef SHADER_API_D3D11

            float3 ddX = float3(0, 0, 0);
            float3 ddZ = float3(0, 0, 0);
            float4 vertexWorld = mul(unity_ObjectToWorld, v.vertex);

            for    (int i = 0; i < _NumLinearWaves; i++)
            {
                v.vertex.xyz += GetDisplacementAtPosition(
                    vertexWorld.xyz + _PositionOffset,
                    _LinearWaveBuffer[i],
                    ddX,
                    ddZ);
            }

            float3 tangent = normalize(float3(1, 0, 0) + ddX);
            float3 biTangent = normalize(float3(0, 0, 1) + ddZ);
            v.normal = normalize(cross(tangent, -biTangent));
         
            #endif
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Albedo = _WaterColour.rgb;
            o.Alpha = _WaterColour.a;

            float3 emissionCol = ColorBelowWater(IN.screenPos, IN.worldPos) * (1 - _WaterColour.a);
            o.Emission = emissionCol;
        }

        void FinalColour (Input IN, SurfaceOutputStandard o, inout fixed4 colour)
        {
            float distance = length(_WorldSpaceCameraPos - IN.worldPos);
            float depth = distance / _ProjectionParams.z;

            colour.rgb = lerp(colour.rgb, _AtmosphereColour, saturate(depth / _FullAtmosphereDepth));

            float3 fogColour = LerpToFog(colour.rgb, distance);

            colour.rgb = fogColour;
            colour.a = 1;
        }
        ENDCG
    }
}

I’ve done some more research and I’m trying to follow Ben Golus’ advice in this post:

So I’m trying to write a pass before my surface shader which figures out the shape of the mesh and writes to the depth buffer. Then I can do a following pass which does the rest.

To write the pass I’m following Farfarer’s example here:

I cannot get this to work though because I don’t have a good enough understanding of what I’m doing. I don’t get how there can be a fragment shader in the first pass and other fragment shaders in following passes? Which one is going to decide the output colour? What effect do those passes have on the surface shader at the end?

I’ve modified my shader to look like this, if anyone can help me understand or link me to some good reliable material then I’d be super grateful.

Shader "Custom/Ocean"
{
    Properties
    {
        _WaterColour ("Water Colour", Color) = (0, 0, 0, 0)
        _WaterFogColourLow ("Water Fog Colour Low", Color) = (0, 0, 0, 0)
        _WaterFogColourHigh ("Water Fog Colour High", Color) = (0, 0, 0, 0)
        _WaterMaxHeight ("Water High Colour End", float) = 10
        _WaterFogDensity ("Water Fog Density", Range(0, 2)) = 0.1
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _AtmosphereColour ("Atmosphere Colour", Color) = (0, 0, 0, 0)
    }
    SubShader
    {
        Tags { "RenderType" = "Transparent" "Queue" = "Transparent"}

        Cull OFF
      
        GrabPass { "_WaterBackground" }

        Pass
        {
            ZWrite On
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            #define TWO_PI 2 * UNITY_PI

            fixed4 _Color;
            float _EditorTime;
            float3 _PositionOffset;
            float4 _AtmosphereColour;
            float _FullAtmosphereDepth;

            struct LinearWave
            {
                float Amplitude;
                float Wavelength;
                float Frequency;
                float2 Direction;
            };

            #ifdef SHADER_API_D3D11
            StructuredBuffer<LinearWave> _LinearWaveBuffer;
            int _NumLinearWaves;
            #endif

            float3 GetDisplacementAtPosition(float3 p, LinearWave wave, inout float3 ddX, inout float3 ddZ)
            {
                float2 d = wave.Direction;
                float a = wave.Amplitude;
                float l = wave.Wavelength;
                float f = wave.Frequency;

                float pd = dot(p.xz, d);
                float theta = (TWO_PI * pd / l) - (f * _EditorTime);
                float k = (a * TWO_PI / l);
                float sinTheta = sin(theta);
                float cosTheta = cos(theta);
                float3 vec = float3(-sinTheta * d.x, cosTheta, -sinTheta * d.y);

                ddX += k * d.x * vec;
                ddZ += k * d.y * vec;

                return a * float3(cosTheta * d.x, sinTheta, sinTheta * d.y);
            }

            void vert (inout appdata_full v)
            {
                #ifdef SHADER_API_D3D11

                float3 ddX = float3(0, 0, 0);
                float3 ddZ = float3(0, 0, 0);
                float4 vertexWorld = mul(unity_ObjectToWorld, v.vertex);

                for    (int i = 0; i < _NumLinearWaves; i++)
                {
                    v.vertex.xyz += GetDisplacementAtPosition(
                        vertexWorld.xyz + _PositionOffset,
                        _LinearWaveBuffer[i],
                        ddX,
                        ddZ);
                }

                float3 tangent = normalize(float3(1, 0, 0) + ddX);
                float3 biTangent = normalize(float3(0, 0, 1) + ddZ);
                v.normal = normalize(cross(tangent, -biTangent));
              
                #endif
            }
          
            // I added this fragment shader because the shader just shows the pink error shader without it
            // I just return 0 because I don't want this pass to decide the colour. I just want this pass
            // to calculate the vertex positions and then write to the z buffer.
            // This seems wrong though. Clearly I'm missing something.
            half4 frag() : SV_Target
            {
                return 0;
            }
            ENDCG
        }

        ZTest LEqual

        CGPROGRAM
        #include "WaterShaderUtils.cginc"
        #include "CustomFog.cginc"

        #pragma surface surf Standard alpha finalcolor:FinalColour
        #pragma target 5.0

        struct Input
        {
            float2 uv_MainTex;
            float4 screenPos;
            float3 worldPos;
        };

        half _Glossiness;
        half _Metallic;
        float4 _WaterColour;

        fixed4 _Color;
        float4 _AtmosphereColour;
        float _FullAtmosphereDepth;

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Albedo = _WaterColour.rgb;
            o.Alpha = _WaterColour.a;

            float3 emissionCol = ColorBelowWater(IN.screenPos, IN.worldPos) * (1 - _WaterColour.a);
            o.Emission = emissionCol;
        }

        void FinalColour (Input IN, SurfaceOutputStandard o, inout fixed4 colour)
        {
            float distance = length(_WorldSpaceCameraPos - IN.worldPos);
            float depth = distance / _ProjectionParams.z;

            colour.rgb = lerp(colour.rgb, _AtmosphereColour, saturate(depth / _FullAtmosphereDepth));

            float3 fogColour = LerpToFog(colour.rgb, distance);

            colour.rgb = fogColour;
            colour.a = 1;
        }
        ENDCG
    }
}