GPU Instancing with Unlit shader broken because "affected by different forward lights"

Hi guys,
I am experiencing an annoying issue when trying to create a Unlit shader supporting GPU Instancing.
It works fine if I don’t put any lights, or if all my objects are affected by the same lights. In this example, my two red sphere use my custom unlit shader and are properly GPU instanced :
3887659--330529--Screenshot_1.png

But if I put a light (like a spotlight) which only affects one of my sphere, it will break the instancing.
Here is the setup in the editor scene view :
3887659--330532--Screenshot_12.png

And the broken instancing result:

Here is the test shader I am using :

Shader "Unlit/CustomUnlit"
{
    Properties { }

    SubShader
    {
        LOD 100
   
        Pass
        {
            ZWrite Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog
            #pragma multi_compile_instancing


            #include "UnityCG.cginc"
      
            struct v2f
            {
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };
       
            v2f vert (appdata_base v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);

                o.vertex = UnityObjectToClipPos(v.vertex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }
       
            fixed4 frag (v2f i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);

                fixed4 col = float4(1,0,0,1);
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

I am using a deferred camera. But you can see in the Frame Debugger that after the “RenderDeferred” pass, it draws an additional “RenderForwardOpaque” pass which seems to be the issue. Because if I use a standard build-in shader, it works and the “RenderForwardOpaque” doesn’t appear in the Frame Debugger:

Note that I don’t care about lighting on my Unlit shader. I don’t want it to cast or receive shadows/lights.

What am I missing?
Thanks

This is a long time “quirk” (aka bug) in Unity with unlit objects. For forward rendered objects, Unity sorts them internally depending on the real time lights that overlap them. Unity doesn’t actually check to see if a shader uses those lights or not before doing this.

If you’re using the deferred rendering path, one work around is to make your unlit shader deferred compatible.

Shader "Unlit/Deferred Unlit"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Tags { "LightMode"="Deferred" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ UNITY_HDR_ON
            #pragma multi_compile_instancing
           
            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
           
            UNITY_INSTANCING_BUFFER_START(Props)
                UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
            UNITY_INSTANCING_BUFFER_END(Props)
           
            v2f vert (appdata_full v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);

                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }
           
            void frag (v2f i,
                out half4 outDiffuse : SV_Target0,           // RT0: diffuse color (rgb), occlusion (a)
                out half4 outSpecSmoothness : SV_Target1,    // RT1: spec color (rgb), smoothness (a)
                out half4 outNormal : SV_Target2,            // RT2: normal (rgb), --unused, very low precision-- (a)
                out half4 outEmission : SV_Target3           // RT3: emission (rgb), --unused-- (a)
                )
            {
                // fill the main gbuffers with empty data
                outDiffuse = half4(0, 0, 0, 1);
                outSpecSmoothness = half4(0, 0, 0, 0);
                outNormal = half4(0, 1, 0, 1);

                UNITY_SETUP_INSTANCE_ID(i);

                // color
                fixed4 col = tex2D(_MainTex, i.uv) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

                // support for deferred HDR
                #ifndef UNITY_HDR_ON
                    col.rgb = exp2(-col.rgb);
                #endif
                // write "unlit" color to emission buffer, which ends up being the frame buffer
                outEmission = half4(col.rgb, 1);
            }
            ENDCG
        }
    }
}

Deferred lighting doesn’t care about real time lights when writing the gbuffers, so it doesn’t do the per-light sorting that forward rendering does. Thus you can have your objects correctly instance regardless of the lights you have in your scene. However if you use light probes it’ll still break unless you remember to turn off light probes on the renderer components.

For a more general case solution, you’ll have to manually draw your objects manually using DrawMeshInstanced to avoid lighting entirely.

2 Likes

Many thanks @bgolus for your answer. It would be useful to add this information in the documentation, because I don’t remember having read something about it.

I tried your shader workaround and it works. To make it work also when using Forward rendering, do I have to simply add a “LightMode”=“ForwardBase” pass which basically does the same thing? But there is no way to make GPU Instancing work in my case (1 light affects one of my object) with Forward rendering, right?

Thanks for your suggestion about using DrawMeshInstanced.
The cool thing about using the “standard rendering pipeline” is that Unity handles a lot of things such as culling or ordering. Since DrawMeshInstanced must be called in one shot for all the instanced objects, I would need to handle culling myself for example, which makes it much more complicated.

Thanks

You’d think so, but no. Forward rendered objects are always separated by overlapping dynamic lights. The forward base pass can still have per vertex lights, which still requires unique values per object. It’d be nice if Unity had a way mark if a shader doesn’t need any localised lighting information … but it doesn’t.

I got similar problem but with Particle/StandardUnlit shader.
In deffered rendering particles were affected by lights. (draw calls)
Some solution is to move particles to different layer and on every light disable this layer.

A follow up on this, if I am using forward rendering and real-time point light, are you saying there is no way for me to avoid the extra drawcalls (eg. gpu instancing are broken apart due to multiple light sources), even though my shader ignore the additional lights?

It seems the only proper solution, is to put my objects on a different layer so they are not affected by point lights?

It’s been a year and a half, I’d test it and make sure it’s still broken.

That should work, yeah. Alternatively render your objects manually with DrawMeshInstanced calls.

My only problem is I am using Unity Terrain, which put trees and grass on the same layer as the terrain itself, so I can’t easily exclude grasses from point lights while keeping additional light on terrain.

Tough position to be in… (would probably need to duplicate the terrain and render separately…)

By the way, it’s still “broken” (or by-design) for me on Unity 2019.2 + Built-in pipeline, even if I have removed the forward add pass, gpu instancing still draw grasses in separate batches, due to “affected by different forward lights”; but I can see in the frame debugger, the lighting data read by shader appear to be exactly the same… from vertex lighting to the main light…