Visualizing the number of lights on an object

I’m trying to implement a debug shader to visualize the number of lights a given object is receiving. I have a basic solution: an additive fragment shader pass that tints the fragment a little each time it runs. However, this does not lead to very good differentiation between light counts, and it only works up to the 4 pixel light cap (see attached image, with 0-4 lights left-right)

Ideally, different light counts would be displayed as very different colors, but in order to achieve this I would need information about the existing color inside the fragment shader. I might be able to do this by reading/writing a render texture in each pass, but I’m not sure if I’m on the right track.

Any suggestions/examples would be greatly appreciated.

For reference, here is the basic shader I’m currently using:

Shader "Unlit/LightCountVisualization"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "black" {}
    }
   
    SubShader
    {
        Tags { "RenderType"="Opaque" }
       
        LOD 100

        // Render base objects black
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            Blend One OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 color: COLOR;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float4 color: COLOR;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.color = v.color;
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = (fixed4(0,0,0,1));
               
                return col;
            }
            ENDCG
        }
        // Add tint for each light
        Pass
        {
            Tags { "LightMode" = "ForwardAdd" }
            Blend SrcColor One
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 color: COLOR;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 color: COLOR;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.color = v.color;
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = (fixed4(0.25,0.1,0.1,0));
               
                return col;
            }
            ENDCG
        }
    }
}

You’ve got the right idea, but the important thing to understand is there’s no way to do this in a way that’s going to be pretty, and that’s okay.

Really all you need to care about is that you’re outputting useful information. In this case the light count. You can easily count up to 255 lights by outputting 1.0/255.0 as the color value instead of 0.25 of 0.1.

But obviously that doesn’t get you very easily viewable data, as it’s a gradient from 0 to 1.0 over very small steps. The trick is you need another shader that reads that output and recolors it. You can do this as a post process, or (though I only recommend this for debugging, not a shader you use in a shipping product) using a grab pass.

So the idea is you have your forward add pass output fixed(1.0/255.0, 0, 0, 1). Then you have a grab pass:

GrabPass { }

Then you have a shader that reads the grab pass contents, decodes the number of lights, and colors it however you want.

        Pass {
            Blend One Zero

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct v2f
            {
                float4 grabPos : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            v2f vert(appdata_base v) {
                v2f o;
                // use UnityObjectToClipPos from UnityCG.cginc to calculate 
                // the clip-space of the vertex
                o.pos = UnityObjectToClipPos(v.vertex);
                // use ComputeGrabScreenPos function from UnityCG.cginc
                // to get the correct texture coordinate
                o.grabPos = ComputeGrabScreenPos(o.pos);
                return o;
            }

            sampler2D _GrabTexture;

            half4 frag(v2f i) : SV_Target
            {
                // get light count from the grab pass
                uint numLights = round(tex2Dproj(_GrabTexture, i.grabPos).r * 255.0);

                // convert count to color by checking the integer bit values
                half3 color = half3(numLights & 1, numLights & 2, numLights & 4);
                // 0 = black
                // 1 = red
                // 2 = green
                // 3 = yellow (red & green)
                // 4 = blue
                // 5 = purple (red & blue)
                // 6 = cyan (green & blue)
                // 7 = white
                // 8 = black again and repeats, ish

                return half4(color, 1.0); 
            }
            ENDCG
        }

In the above I’m converting the number of lights to a color by using the raw bits of the uint itself, but you could also do something like divide the numLights by 4 so the color changes after 4 lights, and modify get a gradient, like:

half3 color = half3((numLights/4) & 1, (numLights/4) & 2, (numLights/4) & 4);
half brightness = pow(frac((float)numLights / 4.0)  + 0.33, 2.2);
return half4(color * brightness, 1.0);

Or you could sample a gradient texture, or a texture flipbook with some numbers to display the count, or you can use this function (if you convert it from glsl to hlsl) to render numbers without any texture.

This is perfect, thank you so much! There’s just one other issue - 4 of the lights in the scene color in a sort of bounding box, rather than only coloring the geo they are illuminating. I suspect this has something to do with pixel vs vertex lights, but I’m not sure how to resolve it.

6090540--661503--Annotation 2020-07-14 131733.jpg

You can’t. That’s how Unity’s lighting works. When rendering additive lights it only renders a screen space rectangular area that the light’s bounds cover.

That’s a shame - the lights on the left and right side of that screenshot (the ones illuminating the green cube and the red cylinder) are behaving the way I would want … there’s no way to get that behavior for the other lights?

There is not, no. What you have above is accurately showing how many lights are affecting each pixel of that object. The rest of the mesh is not rendered, and there’s no way to disable that functionality to render the whole mesh instead.