Search Unity

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

Discussion in 'Shaders' started by stephero, Nov 13, 2018.

  1. stephero

    stephero

    Joined:
    Feb 8, 2016
    Posts:
    123
    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 :
    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 :
    Screenshot_12.png

    And the broken instancing result:
    Screenshot_13.png

    Here is the test shader I am using :
    Code (CSharp):
    1. Shader "Unlit/CustomUnlit"
    2. {
    3.     Properties { }
    4.  
    5.     SubShader
    6.     {
    7.         LOD 100
    8.    
    9.         Pass
    10.         {
    11.             ZWrite Off
    12.  
    13.             CGPROGRAM
    14.             #pragma vertex vert
    15.             #pragma fragment frag
    16.             #pragma multi_compile_fog
    17.             #pragma multi_compile_instancing
    18.  
    19.  
    20.             #include "UnityCG.cginc"
    21.      
    22.             struct v2f
    23.             {
    24.                 UNITY_FOG_COORDS(1)
    25.                 float4 vertex : SV_POSITION;
    26.                 UNITY_VERTEX_INPUT_INSTANCE_ID
    27.             };
    28.        
    29.             v2f vert (appdata_base v)
    30.             {
    31.                 v2f o;
    32.                 UNITY_SETUP_INSTANCE_ID(v);
    33.                 UNITY_TRANSFER_INSTANCE_ID(v, o);
    34.  
    35.                 o.vertex = UnityObjectToClipPos(v.vertex);
    36.                 UNITY_TRANSFER_FOG(o,o.vertex);
    37.                 return o;
    38.             }
    39.        
    40.             fixed4 frag (v2f i) : SV_Target
    41.             {
    42.                 UNITY_SETUP_INSTANCE_ID(i);
    43.  
    44.                 fixed4 col = float4(1,0,0,1);
    45.                 UNITY_APPLY_FOG(i.fogCoord, col);
    46.                 return col;
    47.             }
    48.             ENDCG
    49.         }
    50.     }
    51. }
    52.  
    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:
    Screenshot_14.png

    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
     
    Last edited: Nov 13, 2018
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    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.
    Code (CSharp):
    1. Shader "Unlit/Deferred Unlit"
    2. {
    3.     Properties
    4.     {
    5.         _Color ("Color", Color) = (1,1,1,1)
    6.         _MainTex ("Texture", 2D) = "white" {}
    7.     }
    8.     SubShader
    9.     {
    10.         Tags { "RenderType"="Opaque" }
    11.         LOD 100
    12.  
    13.         Pass
    14.         {
    15.             Tags { "LightMode"="Deferred" }
    16.  
    17.             CGPROGRAM
    18.             #pragma vertex vert
    19.             #pragma fragment frag
    20.             #pragma multi_compile _ UNITY_HDR_ON
    21.             #pragma multi_compile_instancing
    22.            
    23.             #include "UnityCG.cginc"
    24.  
    25.             struct v2f
    26.             {
    27.                 float4 pos : SV_POSITION;
    28.                 float2 uv : TEXCOORD0;
    29.                 UNITY_VERTEX_INPUT_INSTANCE_ID
    30.             };
    31.  
    32.             sampler2D _MainTex;
    33.             float4 _MainTex_ST;
    34.            
    35.             UNITY_INSTANCING_BUFFER_START(Props)
    36.                 UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
    37.             UNITY_INSTANCING_BUFFER_END(Props)
    38.            
    39.             v2f vert (appdata_full v)
    40.             {
    41.                 v2f o;
    42.                 UNITY_SETUP_INSTANCE_ID(v);
    43.                 UNITY_TRANSFER_INSTANCE_ID(v, o);
    44.  
    45.                 o.pos = UnityObjectToClipPos(v.vertex);
    46.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    47.                 return o;
    48.             }
    49.            
    50.             void frag (v2f i,
    51.                 out half4 outDiffuse : SV_Target0,           // RT0: diffuse color (rgb), occlusion (a)
    52.                 out half4 outSpecSmoothness : SV_Target1,    // RT1: spec color (rgb), smoothness (a)
    53.                 out half4 outNormal : SV_Target2,            // RT2: normal (rgb), --unused, very low precision-- (a)
    54.                 out half4 outEmission : SV_Target3           // RT3: emission (rgb), --unused-- (a)
    55.                 )
    56.             {
    57.                 // fill the main gbuffers with empty data
    58.                 outDiffuse = half4(0, 0, 0, 1);
    59.                 outSpecSmoothness = half4(0, 0, 0, 0);
    60.                 outNormal = half4(0, 1, 0, 1);
    61.  
    62.                 UNITY_SETUP_INSTANCE_ID(i);
    63.  
    64.                 // color
    65.                 fixed4 col = tex2D(_MainTex, i.uv) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
    66.  
    67.                 // support for deferred HDR
    68.                 #ifndef UNITY_HDR_ON
    69.                     col.rgb = exp2(-col.rgb);
    70.                 #endif
    71.                 // write "unlit" color to emission buffer, which ends up being the frame buffer
    72.                 outEmission = half4(col.rgb, 1);
    73.             }
    74.             ENDCG
    75.         }
    76.     }
    77. }
    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.
     
    Kbpro and asqewfcq2egf like this.
  3. stephero

    stephero

    Joined:
    Feb 8, 2016
    Posts:
    123
    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
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    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.
     
  5. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,074
    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.
     
  6. bitinn

    bitinn

    Joined:
    Aug 20, 2016
    Posts:
    961
    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?
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    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.
     
  8. bitinn

    bitinn

    Joined:
    Aug 20, 2016
    Posts:
    961
    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...)
     
  9. bitinn

    bitinn

    Joined:
    Aug 20, 2016
    Posts:
    961
    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...