Point Light in V-F shader

I attempt to write a V-F shader for receiving point light without calculating vertex normal. The resultant color on the mesh changes smoothly per point light intensity, until I change the point light range or their positions. The problem is better demonstrated in the video.

The left sphere uses built-in diffuse shader; the middle particle system and the right sphere use my own shader.

Apparently once the point light range hits the mesh’s or particle system’s bounding boxes, they are instantly lit. I thought the absence of normal were the culprit, so I tried Shade4PointLights from UnityCG.cginc instead, but the result isn’t better either. I tried moving the point light function from vertex shader to fragment shader, but no luck.

Shader "Particles/Receive Point Light" {
    Properties {
        _TintColor ("Tint Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
        _AmbientPow ("Ambient Power", Range(0,1)) = 0.5 // Can be used in HDR effect. Intensity greater than 2 causes glitch in HDR rendering.
        _Glow ("Intensity", Range(0, 127)) = 1
    }
    SubShader {
        Tags {"LightMode" = "ForwardBase" "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Opaque" }
        LOD 100
        Cull Back
        ZWrite Off
        Lighting On
        //Blend SrcAlpha OneMinusSrcAlpha

        Pass {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"
            #include "UnityShaderVariables.cginc"
            #include "AutoLight.cginc"
            #include "UnityDeferredLibrary.cginc"

            half4 _TintColor;
            sampler2D _MainTex;
            half4 _MainTex_ST;
            half _AmbientPow;
            half _Glow;

            struct vertIn {
                float4 pos : POSITION;
                float4 normal : NORMAL;
                half2 uv : TEXCOORD0;
                fixed4 color : COLOR;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float4 normal : NORMAL;
                half2 uv : TEXCOORD0;
                fixed4 color : COLOR;
                half4 worldPos : TEXCOORD1;
            };

            v2f vert (vertIn v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.pos);
                o.uv = TRANSFORM_TEX(v.uv,_MainTex);
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.normal = v.normal;
                o.color = v.color * _TintColor;
                o.color.rgb += _Glow + ShadeSH9(half4(worldNormal,1)) * _AmbientPow;
                o.worldPos = mul(unity_ObjectToWorld, v.pos);
               
                o.color.rgb += unity_LightColor[0].rgb * (1 / distance(float3(unity_4LightPosX0.x, unity_4LightPosY0.x, unity_4LightPosZ0.x), o.worldPos.xyz)) * (1 / unity_4LightAtten0.x);
                /*
                o.color.rgb += Shade4PointLights(
                    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
                    unity_LightColor[0].rgb, unity_LightColor[1].rgb,
                    unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                    unity_4LightAtten0, o.worldPos, v.normal
                );
                */
                return o;
            }
           
            fixed4 frag (v2f f) : SV_Target {
                fixed4 col = tex2D(_MainTex, f.uv) * f.color;
                //col.rgb += unity_LightColor[0].rgb * (1 / distance(float3(unity_4LightPosX0.x, unity_4LightPosY0.x, unity_4LightPosZ0.x), f.worldPos.xyz)) * (1 / unity_4LightAtten0.x);
                /*
                col.rgb += Shade4PointLights(
                    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
                    unity_LightColor[0].rgb, unity_LightColor[1].rgb,
                    unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                    unity_4LightAtten0, f.worldPos, f.normal
                );
                */
                return col;
            }
        ENDCG
        }
    }
}

Yep, this is working as intended.

The attenuation function used by these lights is different than the one used by the ForwardAdd pass, and has an infinite range. The “LightAtten” value is some value they’ve calculated to approximate the fall off so the brightness is similar within the actual range of the light. I also seem to remember in the past the light’s brightness and falloff where packed into the single attenuation value, with very bright lights not actually being any brighter, just getting a larger range. Luckily today the light’s brightness is not part of the atten value.

Because this has bothered me too, I believe you can extract the range with this:

float range = (0.005 * sqrt(1000000.0 - unity_4LightAtten0.x)) / sqrt(unity_4LightAtten0.x);

2 Likes

float range = (0.005 * sqrt(1000000 - unity_4LightAtten0.x)) / sqrt(unity_4LightAtten0.x);
float attenUV = distance(float3(unity_4LightPosX0.x, unity_4LightPosY0.x, unity_4LightPosZ0.x), f.worldPos.xyz) / range;
float atten = tex2D(_LightTextureB0, (attenUV * attenUV).xx).UNITY_ATTEN_CHANNEL;

That appears to match Unity’s internal lighting atten calculations exactly. That’s a lot of sqrts to do though for each light, but might not be too bad if you do the range and “attenUV” calculations in the vertex shader. You can also skip the texture sample with this:

float atten = saturate(1.0 / (1.0 + 25.0*attenUV*attenUV) * saturate((1 - attenUV) * 5.0));

It’s not a perfect match due to the original being texture based, but you’ll be hard pressed to notice a difference in use.

3 Likes

Both of your atten formulas work like a charm. I realized the built-in point light range is intentionally large to take vertex normal into consideration, I just had no idea to fake my own point light range.

1 Like

For per vertex lighting having a softer tail on the attenuation can certainly be beneficial. The obvious side effect is that sudden “entirely lit” issue.

1 Like

Now that I have added all 4 point lights to the shader, there is a glitch when more than 1 non-important point light is ligliting the object. I guess there is no foolproof way to fix it than changing all point lights to important or switching to deferred…

Shader "Particles/Alpha Blended Point Lights" {
    Properties {
        _TintColor ("Tint Color", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
        _AmbientPow ("Ambient Power", Range(0,1)) = 0.5 // Can be used in HDR effect. Intensity greater than 2 causes glitch in HDR rendering.
        _Glow ("Intensity", Range(0, 127)) = 1
    }
    SubShader {
        Tags {"LightMode" = "ForwardBase" "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }
        LOD 100
        Cull Back
        ZWrite Off
        Lighting On
        Blend SrcAlpha OneMinusSrcAlpha

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

            half4 _TintColor;
            sampler2D _MainTex;
            half4 _MainTex_ST;
            half _AmbientPow;
            half _Glow;

            struct vertIn {
                float4 pos : POSITION;
                float4 normal : NORMAL;
                half2 uv : TEXCOORD0;
                fixed4 color : COLOR;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                //float4 normal : NORMAL;
                half2 uv : TEXCOORD0;
                fixed4 color : COLOR;
                half4 worldPos : TEXCOORD1;
                half4 attenUV : TEXCOORD2;
            };
          
            float attenUV (float lightAtten0, float3 _4LightPos, float3 _worldPos) : SV_Target {
                float range = (0.005 * sqrt(1000000 - lightAtten0)) / sqrt(lightAtten0);
                return distance(_4LightPos, _worldPos) / range;
            }
          
            float atten (float _attenUV) : SV_Target {
                return saturate(1.0 / (1.0 + 25.0*_attenUV*_attenUV) * saturate((1 - _attenUV) * 5.0));
            }
          
            float attenTex (sampler2D _LightTextureB, float _attenUV) : SV_Target {
                return tex2D(_LightTextureB, (_attenUV * _attenUV).xx).UNITY_ATTEN_CHANNEL;
            }
          
            v2f vert (vertIn v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.pos);
                o.uv.xy = TRANSFORM_TEX(v.uv,_MainTex);
                o.color = v.color * _TintColor;
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.color.rgb += _Glow + ShadeSH9(half4(worldNormal,1)) * _AmbientPow;
              
                o.worldPos = mul(unity_ObjectToWorld, v.pos);
              
                o.attenUV.x = attenUV(unity_4LightAtten0.x, float3(unity_4LightPosX0.x, unity_4LightPosY0.x, unity_4LightPosZ0.x), o.worldPos.xyz);
                o.attenUV.y = attenUV(unity_4LightAtten0.y, float3(unity_4LightPosX0.y, unity_4LightPosY0.y, unity_4LightPosZ0.y), o.worldPos.xyz);
                o.attenUV.z = attenUV(unity_4LightAtten0.z, float3(unity_4LightPosX0.z, unity_4LightPosY0.z, unity_4LightPosZ0.z), o.worldPos.xyz);
                o.attenUV.w = attenUV(unity_4LightAtten0.w, float3(unity_4LightPosX0.w, unity_4LightPosY0.w, unity_4LightPosZ0.w), o.worldPos.xyz);
              
                //o.color.rgb += _LightColor;//Deferred
              
                /*
                o.normal = v.normal;
                o.color.rgb += Shade4PointLights(
                    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
                    unity_LightColor[0].rgb, unity_LightColor[1].rgb,
                    unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                    unity_4LightAtten0, o.worldPos, v.normal
                );
                */
              
                return o;
            }
          
            fixed4 frag (v2f f) : SV_Target {
                float4 _atten;
                //float _atten.x = attenTex(_LightTextureB0, f.attenUV.x);
                _atten.x = atten(f.attenUV.x);
                _atten.y = atten(f.attenUV.y);
                _atten.z = atten(f.attenUV.z);
                _atten.w = atten(f.attenUV.w);
              
                fixed4 col = tex2D(_MainTex, f.uv.xy) * f.color;
                col.rgb += unity_LightColor[0].rgb * (1 / distance(float3(unity_4LightPosX0.x, unity_4LightPosY0.x, unity_4LightPosZ0.x), f.worldPos.xyz)) * _atten.x;
                col.rgb += unity_LightColor[1].rgb * (1 / distance(float3(unity_4LightPosX0.y, unity_4LightPosY0.y, unity_4LightPosZ0.y), f.worldPos.xyz)) * _atten.y;
                col.rgb += unity_LightColor[2].rgb * (1 / distance(float3(unity_4LightPosX0.z, unity_4LightPosY0.z, unity_4LightPosZ0.z), f.worldPos.xyz)) * _atten.z;
                col.rgb += unity_LightColor[3].rgb * (1 / distance(float3(unity_4LightPosX0.w, unity_4LightPosY0.w, unity_4LightPosZ0.w), f.worldPos.xyz)) * _atten.w;
              
                return col;
            }
        ENDCG
        }
    }
}
2 Likes

I still found this old thread useful today!

The attenuation formula posted works great, but is for one light only.

In my use case I needed a replacement for the builtin Shade4PointLights function, with corrected attenuation. I managed to make that work so the function is here in case anyone else who finds this thread (like I did recently) might find it useful.

            float3 Shade4PointLightsCustom (
                float4 lightPosX, float4 lightPosY, float4 lightPosZ,
                float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
                float4 lightAttenSq,
                float3 pos, float3 normal)
            {
                // to light vectors
                float4 toLightX = lightPosX - pos.x;
                float4 toLightY = lightPosY - pos.y;
                float4 toLightZ = lightPosZ - pos.z;
                // squared lengths
                float4 lengthSq = 0;
                lengthSq += toLightX * toLightX;
                lengthSq += toLightY * toLightY;
                lengthSq += toLightZ * toLightZ;
                // don't produce NaNs if some vertex position overlaps with the light
                lengthSq = max(lengthSq, 0.000001);

                // NdotL
                float4 ndotl = 0;
                ndotl += toLightX * normal.x;
                ndotl += toLightY * normal.y;
                ndotl += toLightZ * normal.z;
                // correct NdotL
                float4 corr = rsqrt(lengthSq);

                ndotl = max (float4(0,0,0,0), ndotl * corr);

                // attenuation
                // THIS PART IS ALSO MODIFIED
                //float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
                // More correct attenuation derived from
                // https://discussions.unity.com/t/679554/2
                float4 range = (0.005 * sqrt(1000000 - unity_4LightAtten0)) / sqrt(unity_4LightAtten0);
                float4 attenUV = sqrt(lengthSq) / range;
                float4 atten = saturate(1.0 / (1.0 + 25.0 * attenUV * attenUV) * saturate((1 - attenUV) * 5.0));

                float4 diff = ndotl * atten;
                // final color
                float3 col = 0;
                col += lightColor0 * diff.x;
                col += lightColor1 * diff.y;
                col += lightColor2 * diff.z;
                col += lightColor3 * diff.w;
                return col;
            }
3 Likes

Hey thanks a lot!
Your code was really helpful,

1 Like

8666118--1167165--upload_2022-12-15_22-28-4.png

You guys are absolute geniuses! Thanks, got it working over there!

Hello. Here is an exact formula to calculate the range from attenuation:

float range = 5.0 * (1.0 / sqrt(unity_4LightAtten0.x));

It’s very close the formula @bgolus posted, but they differ a bit close to 0 range.


This graph shows 3 plots. The blue and black plot show a roundtrip of atten_to_range(range_to_atten(range)), which should just simplify to range. The blue plot uses the formula posted above, the black plot is the one posted by bgolus. The red plot is the error value of bgolus’ approximation. This probably doesn’t matter much in practice since for light ranges this small, the difference is going to be quite negligible.

A recent bug investigation had me looking into this area, so I figured I’d post this in case it is useful to anyone.

2 Likes