Clipping / "Screen door" transparency in deferred rendering?

Hi, I’m using deferred rendering in my project, and have run into an issue with “fake” transparency.

Previously in forward rendering, clipping was used to do cheap transparency:

Unfortunately this doesn’t work in deferred (which does make sense at a basic level), and shows the camera clear colour:

Is there a way around this? I struggled to find anything at all about this on the forums or online, so seems like something that’s either trivial to solve, or there’s a basic alternative I’m missing.

The shader code is as below. The clipping is done via the distance from the camera to the surface of the shader.

Shader "Project/DitheredDiffuse"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Diffuse Texture", 2D) = "white" {}
        _DitherTex("Dither",2D) = "white" {}

        _MinDistance ("Minimum Fade Distance", Float) = 0
        _MaxDistance ("Maximum Fade Distance", Float) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque"  }
        Tags { "LightMode" = "Vertex" }
        LOD 200

        CGPROGRAM
        #pragma surface surf WrapLambert

        half4 LightingWrapLambert (SurfaceOutput s, half3 lightDir, half atten)
        {
            half NdotL = dot (s.Normal, lightDir);
            half diff = NdotL * 0.5 + 0.5;
            half4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten);
            c.a = s.Alpha;
            return c;
        }

        struct Input
        {
            float2 uv_MainTex;
            half4 color : COLOR;
            float4 screenPos;
        };
   
        sampler2D _MainTex;
        sampler2D _DitherTex;
        fixed4 _Color;

        float4 _DitherTex_TexelSize;
        float _MinDistance;
        float _MaxDistance;

        void surf (Input IN, inout SurfaceOutput o)
        {
            // value from the dither pattern.
            float2 screenPos = IN.screenPos.xy / IN.screenPos.w;
            float2 ditherCoordinate = screenPos * _ScreenParams.xy * _DitherTex_TexelSize.xy;
            float ditherValue = tex2D(_DitherTex, ditherCoordinate).r;

            //get relative distance from the camera
            float relDistance = IN.screenPos.w;
            relDistance = relDistance - _MinDistance;
            relDistance = relDistance / (_MaxDistance - _MinDistance);

            //discard pixels accordingly
            clip(relDistance - ditherValue);

            o.Albedo
                = tex2D (_MainTex, IN.uv_MainTex).rgb
                * _Color
                * IN.color;

           
        }
        ENDCG
    }
    FallBack "Diffuse"
}

It appears the custom lighting function “LightingWrapLambert” is causing this issue.
If using the regular Lambert lighting, no issue occurs.

Not sure how to resolve that, presumably need to also clip in the lighting function, as the default lambert function must be doing this, but where can I find the code for that?

Still struggling with this, not sure whar in the lighting function is messing with the depth buffer.

Deferred rendering generally only supports one shading model. The “Standard” shading model in the case of Unity’s BIRP. When you use the built in Lambert shading in a Surface Shader, it approximates Lambert with 0.0 smoothness and black specular Standard shading, which isn’t the same as Lambert, but close enough for most people. Humorously the Standard shading model can 100% match Lambert using a smoothness of 1.0 and a black specular, but that’s not how Unity chose to implement it.

When you use a custom lighting model, Unity has to render that object using forward rendering instead. And basically the deferred rendering path is entirely skipped… or so you might think.

Many things in Unity make use of the camera depth texture. When using deferred rendering, the depth texture is generated while rendering the deferred GBuffers. This means all forward rendered opaque surfaces are still rendered into the GBuffers, but do so using their shadow caster pass. Since your surface shader does not have a custom shadow caster pass, it renders into the GBuffer depth as fully solid, leading to the weirdness you’re seeing.

The solution is… add addshadow to your #pragma surface line so your Surface Shader generates a custom shadow caster!

At least it should be. Unfortunately it’s not quite that straightforward, as there are two issues with doing that. The first one is a long time bug with Surface Shaders that screenPos isn’t calculated for Surface Shader generated shadow caster passes. So you have to use a vertex function to calculate it manually instead.

The second issue is now your object’s real time shadows will also potentially get dithered, not based on the player’s camera, but based on the light position. Though that might not be an issue for you if you’re not using real time shadows. If you are, you have to try to detect if the shadow caster pass is being used to render during the shadow map rendering or for camera depth rendering, which isn’t 100% accurate.

My suggestion to you is, stick with forward rendering and don’t use deferred. Looking at your screenshots it doesn’t seem like you’re using a lot of dynamic lights, or intend to use the Standard shading model on most surfaces. Those are the only reasons to use deferred.

Really helpful, thankyou - though oddly the additional step of calculating the screenpos wasn’t needed. Maybe the bug has been resolved?

Fortunately not using real time shadows yes, so this is a perfect solution.

I do take your point on using forward rendering, but I am making heavy use of dynamic lights, so this is really great to have.

Hmm, I maybe spoke too soon - other transparent objects (cutout / clip transparency) in the background seem to be drawn on top of the dithered objects:

All the shaders have “addshadow”, but I’m unclear how the screenpos would play into this.

Switching the lighting model back to “Lambert” from my custom “WrapLambert” fixes this issue - what’s going on there?

Do I need some extra computation in:

half4 LightingWrapLambert (SurfaceOutput s, half3 lightDir, half atten)
{
    half NdotL = dot (s.Normal, lightDir);
    half diff = NdotL * 0.5 + 0.5;
    half4 c;
    c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten);
    c.a = s.Alpha;
    return c;
}

The basic lambert lighting model seems to work flawlessly when you set it (even if like you say, it’s fake lambert with a standard shader), so not sure why this custom model is different and what needs to change.

I don’t quite understand this? Shaders with the custom lighting model are being lit deferred, with arbitrary number of dynamic lights per mesh (I tried over 50).

Edit: I’m not sure the above is true actually, maybe the editor was accidentally displaying it without the custom lighting model… Yes, using my custom lighting model only allows 8 lights.

Deferred shaders supports Clip function. What you looking for is probably it + you’ll need to add dithering effect before clip, shading models not matters.