Liquid shader front/back face rendering order issue

So I’m making a liquid shader that takes a mesh and renders only the values below a certain threshold, like this:

My issue is that the backfaces are overlapping the frontfaces at the given fill rate.

I’d love to understand why.

Here’s my code and an image of what it looks like

Shader "Renatus/Liquid"
{
    Properties
    {
        [HDR]_LiquidColor("Liquid Color", Color) = (1,1,1,1)
        _FillAmount ("Fill Amount", Range(0, 1)) = 0.4
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent"}
        Blend SrcAlpha OneMinusSrcAlpha
        Cull Off
        ZWrite Off

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            float4 _LiquidColor;
            float _FillAmount;

            struct MeshData
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct Interpolator
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 world : TEXCOORD1;
            };

            Interpolator vert (MeshData v)
            {
                Interpolator o;
               
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.world = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = v.uv;
                return o;
            }

            float4 frag (Interpolator i, fixed facing : VFACE) : SV_Target
            {
                float fragWorldY = i.world.y;
                float centerWorldY = mul(unity_ObjectToWorld, float4(0, 0, 0, 1)).y;
                float distanceFromFragToCenter = fragWorldY - centerWorldY;;
                float fillAmount = step(distanceFromFragToCenter, _FillAmount * 2 - 1);
                float4 liquid = _LiquidColor * fillAmount;
                float4 foam = fillAmount;

                return facing > 0 ? liquid : foam;
            }

            ENDCG
        }
    }
}

MinionArt’s example shader is alpha tested. Or more specifically it’s using AlphaToMask On, which is enabling alpha to coverage and desktop GPUs fall back to regular alpha testing when MSAA is not enabled.

If you’re looking to you alpha blending you must use a two pass shader which renders the back faces first and then the front faces, or approximate the back face using an analytical or precomputed SDF of some kind.

Why? Because correct and efficient sorting of transparency is an unsolved problem for real time rendering.

1 Like

Thanks for the answer! I’ve spent some time trying to understand it and have now decided to lean for the two pass solution. My issue now though is doing the right semantics for two passes.

I followed this mega simple tutorial here from 2020: https://www.codinblack.com/shader-pass-and-multi-pass-shader/
and it does not work for me. Like, I literally copy pasted the code, and the outline did not appear. Is it cause I’m in URP w forward renderer? Is there some new semantic for two pass shaders that I’m not aware of?

Here’s my code for me trying to do two passes on my shader. One with the liquid pixel stuff, and the second being all white.

Shader "Renatus/Liquid"
{
    Properties
    {
        [HDR]_LiquidColor("Liquid Color", Color) = (1,1,1,1)
        [HDR] _FoamColor ("Foam Color", Color) = (1,1,1,1)
        _FillAmount ("Fill Amount", Range(0, 1)) = 0.4
        _FoamWidth ("Foam Width", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off

        Pass
        {
            Cull Back
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            float4 _LiquidColor;
            float4 _FoamColor;
            float _FillAmount;
            float _FoamWidth;

            struct MeshData
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct Interpolator
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 world : TEXCOORD1;
            };

            Interpolator vert (MeshData v)
            {
                Interpolator o;
            
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.world = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = v.uv;
                return o;
            }

            float4 frag (Interpolator i, fixed facing : VFACE) : SV_Target
            {
                float fragWorldY = i.world.y;
                float centerWorldY = mul(unity_ObjectToWorld, float4(0, 0, 0, 1)).y;
                float distanceFromFragToCenter = fragWorldY - centerWorldY;
                float fill = ( _FillAmount * 2 - 1);
                float liquidMask = step(distanceFromFragToCenter, fill);
                float foamMask = step(distanceFromFragToCenter, fill+(_FoamWidth * 0.2)) - liquidMask;

                float4 liquid = _LiquidColor * liquidMask;
                float4 foam = _FoamColor * foamMask;
            
                return liquid + foam;
            }

            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct MeshData
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct Interpolator
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            Interpolator vert (MeshData v)
            {
                Interpolator o;
            
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            float4 frag (Interpolator i, fixed facing : VFACE) : SV_Target
            {
                return 1;
            }

            ENDCG
        }
    }
}

The image below shows that nothing changed.
6987206--824924--Screen Shot 2021-03-29 at 3.34.45 PM.jpg

Yes. URP does not support multi pass shaders. Or more specifically does not support two lit passes in a single shader. You can only do a single unlit pass and a single lit pass per shader. For URP you have to do it with a Render Feature or multiple materials.

For clarification, you can have one pass with Tags{ "LightMode" = "UniversalForward" } and one pass with no "LightMode" tag or Tags{ "LightMode" = "SRPDefaultUnlit" }.

Thanks bgolus! I made it work with by using the LightMode passes. Although since the intention of these passes has to do with light I feel like there’s probably something wrong with my approach. Perhaps you might know of this?

Also, there are some calculations that I have to do for both passes in the fragment function, yet the calculations are to attain the same output value. Is there a way to pass this output value from one pass to another as a uniform variable (I think that’s the term)?

float4 frag (Interpolator i) : SV_Target
{
// [...]
// evil calculations creating values A, B, and C
// [...]
float outputIwantInBothFragPasses = A + B + C;
}

Instead of doing the calculations in both fragment functions

No.

I mean, technically yes, but not in a way that’ll be faster than calculating it in both pass separately.

Generally speaking uniforms are assigned by the CPU before rendering begins and cannot be changed by the GPU afterwards. There are some tricks using structured buffers or render textures that can be abused to pass data between passes, in which case the uniforms aren’t being changed per-say, but rather the data in the object assigned to a uniform. But this is can be complicated when being used in a real world setup and, as I said, isn’t likely to be faster than just calculating the data again. GPUs can do a lot of math very fast, usually way faster than passing data around.

1 Like

Ok thanks for all the help!

To anyone wondering, here’s the liquid shader code.

Shader "Renatus/Liquid"
{
    Properties
    {
        [HDR]_LiquidColor("Liquid Color", Color) = (1,1,1,1)
        [HDR] _FoamColor ("Foam Color", Color) = (1,1,1,1)
        [HDR] _EdgeRingColor ("Edge Ring Color", Color) = (1,1,1,1)
        [HDR] _FresnelColor ("Fresnel Color", Color) = (1,1,1,1)
        _FillAmount ("Fill Amount", Range(0, 1)) = 0.4
        _FoamWidth ("Foam Width", Range(0, 1)) = 0.5
        _FresnelPower ("Fresnel", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off

        Pass
        {
            Tags {"LightMode"="UniversalForward"}

            Cull Back
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            float4 _LiquidColor;
            float4 _EdgeRingColor;
            float4 _FresnelColor;
            float _FoamWidth;
            float _FillAmount;
            float _FresnelPower;

            struct MeshData
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct Interpolator
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 world : TEXCOORD1;
                float3 normal : TEXCOORD2;
            };

            Interpolator vert (MeshData v)
            {
                Interpolator o;
               
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.world = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.uv = v.uv;
                return o;
            }

            float4 frag (Interpolator i, fixed facing : VFACE) : SV_Target
            {               
                // World position of current fragment
                float fragWorldY = i.world.y;

                // World position of the objects' center
                float centerWorldY = mul(unity_ObjectToWorld, float4(0, 0, 0, 1)).y;
                float distanceFromFragToCenter = fragWorldY - centerWorldY;

                // Amount to fill               
                float t = sin(_Time.y * 0.75) * 0.05;
                float fill = ((_FillAmount + t) * 2 - 1);
               
                // Mask creations
                float liquidMask = step(distanceFromFragToCenter, fill);
                float foamMask = step(distanceFromFragToCenter, fill+(_FoamWidth * 0.2)) - liquidMask;

                // Adding Color
                float4 liquid = _LiquidColor * liquidMask;
                float4 foam = _EdgeRingColor * foamMask;
               
                // Adding Fresnel
                float fresnelAnimation = 1 - (_FresnelPower * (sin(_Time.y) * 0.3) + 0.4);
                float fresnelMask = pow(saturate(1-dot(normalize(_WorldSpaceCameraPos - i.world), normalize(i.normal))), (1-fresnelAnimation) * 8);
                float4 fresnel = fresnelMask * liquidMask * _FresnelColor;

                return liquid + foam + fresnel;
            }

            ENDCG
        }

        Pass
        {
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            float4 _LiquidColor;
            float4 _FoamColor;
            float _FoamWidth;
            float _FillAmount;

            struct MeshData
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct Interpolator
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 world : TEXCOORD1;
            };

            Interpolator vert (MeshData v)
            {
                Interpolator o;
               
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.world = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = v.uv;
                return o;
            }

            float4 frag (Interpolator i) : SV_Target
            {
                // World position of current fragment
                float fragWorldY = i.world.y;

                // World position of the objects' center
                float centerWorldY = mul(unity_ObjectToWorld, float4(0, 0, 0, 1)).y;
                float distanceFromFragToCenter = fragWorldY - centerWorldY;

                // Amount to fill               
                float t = sin(_Time.y * 0.75) * 0.05;
                float fill = ((_FillAmount + t) * 2 - 1);
               
                // Mask creations
                float liquidMask = step(distanceFromFragToCenter, fill);
                float foamMask = step(distanceFromFragToCenter, fill+(_FoamWidth * 0.2)) - liquidMask;

                return (liquidMask + foamMask)* _FoamColor;
            }

            ENDCG
        }
    }
}

Thanks so much for posting! That fixed all my problems. On Built-In renderer I simply deleted Tags {“LightMode”=“UniversalForward”}, and it worked :slight_smile: