Basic Lights as Shader Image Effect - Help Me Understand

I couldn’t think of a good title for this.

I’m experimenting with doing dynamic lighting as an image effect in a very basic way. I can’t seem to get it to work at all.

All I want to do is specify a position in the world where there’s a light, specify its range and intensity. Then the shader should simply brighten every pixel within the range of that light.

So, here’s the shader:

Shader "Custom/Lighting"
{
    Properties
    {
        _MainTex("Base (RGB)", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            Cull Off
            ZWrite Off
            ZTest Always

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
    
            uniform sampler2D _MainTex;
            uniform int _LightCount;
            uniform float3 _Lights[100];
            uniform float _Ranges[100];
            uniform float _Intensities[100];

            struct VertexIn
            {
                float4 pos : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct VertexOut
            {
                float4 pos : POSITION;
                float2 uv : TEXCOORD0;
                fixed3 worldPos : TEXCOORD1;
            };
            VertexOut vert(VertexIn v)
            {
                VertexOut o;
                o.pos = mul(UNITY_MATRIX_MVP, v.pos);
                o.worldPos = mul(unity_ObjectToWorld, v.pos).xyz;
                o.uv = v.uv;
                return o;
            }

            float4 frag(VertexOut o) : COLOR
            {
                half light = 0.0;

                for (int i = 0; i < _LightCount; i++)
                {
                    half dist = distance(o.worldPos, _Lights[i]);
                    half r = _Ranges[i];
                    half intensity = 1 - saturate(dist / r);

                    light = max(intensity * _Intensities[i], light);
                }

                light = saturate(light);
                return tex2D(_MainTex, o.uv) + light;
            }

            ENDCG
        }
    }
}

-I try to compute the world position of the vertex using unity_ObjectToWorld, and pass that to the fragment shader.
-I loop through each light.
-I take the distance between the light (which is in world coordinates) and the world coordinates I computed in the vertex function.
-I get the range. I compute intensity. Using dist / r, if distance is larger than r this should be capped to 1 and produce a final intensity of 0 (light is out of range, no effect). If it’s smaller, it will be closer to 1 and have more effect.
-I multiply intensity by the intensity modifier passed to the shader (ranges from 0 to 1).

Then I store the best light I’ve found so far, because the strongest light is the one that should affect this pixel. Maybe I’ll have to get fancier, but trying to keep it very simple to start with to see if I can get this working.

At the end, I ensure the final value is between 0 and 1 and add the light to the result. This is very wrong and just produces a pure white image. But I just did that so I can see the area the light is affecting.

The problem? It doesn’t affect anything. There’s never any light anywhere, no matter what I make the range of my light or where I position it. I don’t know why. Clearly I don’t understand something. Here’s the C# code just in case it helps:

using UnityEngine;
using UnityEngine.Assertions;

public sealed class Lighting : MonoBehaviour
{
    private const int MaxLights = 100;

    private Material material;

    private Vector4[] lights = new Vector4[MaxLights];
    private float[] ranges = new float[MaxLights];
    private float[] intensities = new float[MaxLights];
    private int lightCount = 0;

    private int lightCountID;
    private int lightsID;
    private int rangesID;
    private int intensitiesID;

    private void Awake()
    {
        material = new Material(Shader.Find("Custom/Lighting"));

        lightCountID = Shader.PropertyToID("_LightCount");
        lightsID = Shader.PropertyToID("_Lights");
        rangesID = Shader.PropertyToID("_Ranges");
        intensitiesID = Shader.PropertyToID("_Intensities");

        SetLight(new Vector3(8.0f, 8.0f), 5.0f, 1.0f);
    }

    public void SetLight(Vector3 pos, float range, float intensity)
    {
        Assert.IsTrue(lightCount != MaxLights, "Exceeded the maximum light count.");

        lights[lightCount] = new Vector4(pos.x, pos.y, pos.z);
        ranges[lightCount] = range;
        intensities[lightCount] = intensity;
        lightCount++;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        material.SetInt(lightCountID, lightCount);
        material.SetVectorArray(lightsID, lights);
        material.SetFloatArray(rangesID, ranges);
        material.SetFloatArray(intensitiesID, intensities);

        Graphics.Blit(source, destination, material);
    }
}

Here I mainly just set the shader data. As you can see, my light is at 8, 8 in world coordinates with a range of 5 and max intensity. My camera is also there, and I see no light.

Thanks for your help!

Assuming you’re trying to implement 2D lights and not reinvent deferred shading, I’m guessing that the world position in the vertex shader isn’t what you’d expect? - When you render an image effect with Graphics.Blit, you’re not dealing with world space. You’re also adding the light to your texture rather than multiplying the texture by the lighting intensity.

Try attach a quad to the camera with your shader instead, as a test, to see if you can see anything?

But this is going to be a very inefficient way to render lots of 2D lights. A more efficient solution is to use a sprite for each light, rendered additively onto a RenderTexture (use a separate layer/camera to do this). Once you’ve got that texture, you can blend it with the Main Camera view, maybe via an image effect, or by rendering the lights texture first and sampling it in the shaders used to render your lit sprites.

1 Like

Thanks, that was quite helpful. I ditched what I was trying and got much closer to what I want with your suggested approach. I’m having some issues making that render texture when it comes to multiple lights near each other (as I posted in another topic here), but working on it!