Custom Dynamic Lighting Attenuation

Info: Unity 2023.2.11f1, URP 16.0.5 (copied to local Packages folder). *

*You have to copy the Render Pipeline Core, Universal, and Shader Graph packages to your local packages folder, otherwise it’ll just use the base ones I think, reverting all changes you do.
**I’m doing the same thing on Unity 6000 with URP 17, still seems to work.

I’m looking to adjust the attenuation for point lights in two ways. Firstly to change the attenuation distance by a scalar, and secondly to change the attenuation exponent.

I’ve been digging around in the lighting.hlsl file for a while and either I’m more blind than usual or I’m missing where this can be changed. The goal is to have those 2 values be changeable in realtime. Where should these be set (or multiple places, don’t need to do it separately for Forward/Def?), and what’s the best way to pass those values along?

If we assume a super-simple point light attenuation as NdotL * (1 / pow(distance, 2)), you get this:

The first change is to the distance, scaling that factor by A. In this case, a factor of 1/3. So what you see is NdotL * (1 / pow(distance / 3), 2).

The second change is an adjustment to the attenuation exponent. In these 2 pics, I use exponent values of 1 and 3.

NdotL * (1 / pow(distance, 1))


NdotL * (1 / pow(distance, 3))

Demo pics done with an unlit Shader Graph. I don’t really want to do that since it means everything that relies on that in particular (and I’d have to hand-re-create Unity’s BRDF lighting model!), and I’d rather make small changes to the lighting calcs in one place and have everything else run Unity’s ‘default’ lighting model.

OK, so I’ve got the control I need for the lighting attenuation scalar and exponent, just need to pass some values to it.

For those interested, the code is in RealtimeLights.hlsl in the Universal RP / Shader Library folder.

float DistanceAttenuation(float distanceSqr, half2 distanceAttenuation)
{
// We use a shared distance attenuation for additional directional and puctual lights
// for directional lights attenuation will be 1

// get a safe distance attenuation here, doesn't like being done elsewhere...
distanceSqr = sqrt(distanceSqr); // attenuation is now linear in meters

// custom distance scalar
float scaleFactor = 1.0;
distanceSqr = distanceSqr * scaleFactor;

// custom falloff exponent
float falloffExponent = 2.0;
distanceSqr = pow(distanceSqr, falloffExponent); // now, to control the exponent

float lightAtten = rcp(distanceSqr);
float2 distanceAttenuationFloat = float2(distanceAttenuation);
// Use the smoothing factor also used in the Unity lightmapper.
half factor = half(distanceSqr * distanceAttenuationFloat.x); // default one
half smoothFactor = saturate(half(1.0) - factor * factor);
smoothFactor = smoothFactor * smoothFactor;
//smoothFactor = 1.0; // smoothfactor is the fading for max range of point lights. Remove it and the light range does nothing.
return lightAtten * smoothFactor;
}
2 Likes

Well, it looks like I can get there using Shader.SetGlobalFloat to pass the value along to the hlsl file, and adding these lines at the top of the file, line ~10…

float _LightScalar;
float _LightAttenuationExponent;
1 Like

@Jesus saves…time. I’ve been looking something to modify the attenuation to achieve more of a comic look while still incorporating lights.

Attached are two pictures using the default values (scalar of 1 and attenuation exponent of 2), one where the spotlight is low intensity (doesn’t blow out colors but really only hits things directly in front of it, and one with a high intensity spotlight, which “hits” a lot more things given its range, but the colors are blown out.

I modified the scalar to and attenuation exponent to be close to zero (0.01) and produced the third picture, which I’m very happy with for the spotlight.

One issue I see immediately is the point light now has “jaggies” towards the end of its range. I see this with the spotlight as well if I bring the range in, but since I will always be shining them on some kind of background, I just max the range out to be > than whatever I’m hitting and I get nice crisp lines.

I’m guessing that with default values you don’t see this because normally the falloff is near zero by the time it hits the end of the range.

My “bandage” fix was to just change the red cave glow point light to a spotlight, and point it at the cave entrance. This works in this instance and gives me the clean lines I want, but it wouldn’t work for say a campfire where someone is standing in between it and the camera (should cast a shadow going towards the camera).

Any idea on how I might avoid the jaggies and still be able to use point lights? Some kind of function that makes it falloff intensely close to the max range, but is otherwise fairly consistent?




I think those are a result of your fall-off, which is now near nonexistent, reaching the ‘range’ limit of your light, as well as scaling the position of the edge-of-light falloff (see below). Can you check the range of the point light in your scene? That probably also affects things like tiled rendering, since it might use that range rather than when attenuation is zero, and just assumed the light will be near zero by then.

The line near the bottom of the function deals with fading the edge of the point light at its range:
half factor = half(distanceSqr * distanceAttenuationFloat.x); // default one

If you mess with distanceSqr, then you mess with the distance from the light that edge falloff happens.
I suspect if you feed it an un-altered distanceSqr rather than the altered one, you should see a fade in light intensity over the last ~10% of the light range, even with point lights.

Note that if you want to CHANGE TO A NEW ATTENUATION FUNCTION (as in, write your own, and leave the existing one, or even just add new parameters to the existing one), you need to change its use in:

Deferred.hlsl, line 36
//float attenuation = DistanceAttenuation(distanceSqr, punctualLightData.attenuation.xy)…and so on and so forth
as well as the one in RealtimeLights.hlsl
//float attenuation = DistanceAttenuation(distanceSqr, distanceAndSpotAttenuation.xy);

The Deferred.hlsl was tricky to find, it’s in UniversalRP/Shaders/Utils.

So I tried a few options, see the equations pictured below. In this example, the light intensity is 1, and the range is about 25. Default Unity Cube and Capsule, and Unity Spheres spawned via Shuriken Particle System.

In the graph below, x axis is distance, y axis is light intensity.
9715750--1388737--2024-03-22 10_55_17-Desmos _ Graphing Calculator.png

Here’s the 1/(d^2).

You can see the point light near the ground emits a very bright light, brighter than intensity 1. Note the red line in the graph above.

Here’s 1/((d+1)^2).

The bonus point here is that no matter how close you get to the point light, the intensity never is above 1, or in the end result the intensity is never above the color*intensity you set in the Light Inspector. This is the blue line in the graph above.

And here’s (1 - saturate(d/r))^2.

You should be able to get range from distanceAttenuation.x somehow (???) in the normal function, with just a little re-arranging, that seems to be the range value. This basically scales the light as a gradient from distance 0 to distance (range), then inverse square of that. So if you want point lights to have a much larger area of coverage, and actually do something near their max range, this might be the way to go.

It’s the green line in the graph. It’s higher intensity for further out, and never creates super-bright spots where the intensity is over 1 so it should be speckle-proof.

Here’s some other ideas for different curves I found, maybe some of these would be nice to play with?
https://lisyarus.github.io/blog/graphics/2022/07/30/point-light-attenuation.html

The variables for those might be set globally, declare them at the top of the hlsl file like in post #3 and set them in a script.

Anyway, here’s a pic of the (1 - saturate(d/r))^2 light in scene. See how it covered the entire range (25 units) of the point light, unlike the normal one which would need to have the intensity set to >10 before it did anything above 20m away.

Hi, is it possible you could post an image of your DistanceAttenuation function using the saturated distance/distanceAttenuation.x ? I cant seem to get the same results.

So after a bit of googling apparently light range as a usable number is rsqrt(distanceAndSpotAttenuation.x) ( according to this and this ). In fact that second link looks pretty useful in general.

I’ll clean up a script and see if I can post it soon.

Could you post what exactly you changed? I want my spotlight to not be so bright up close and have a more linear dropoff.

OK, so I think I’ve got the syntax right. Haven’t tried all lighting setups, so tell me if it’s wrong (or if there is even any use to distanceAndSpotAttenuation.y, it doesn’t seem to be used?)…
I’m on Unity 6000.0.1f1, so the line numbers may be slightly different. So I’ve noted where the values should go, relative to existing code.

NOTE:
This function will give the brightness/intensity you set in the inspector at distance 0. So if the intensity is 2, and it’s about 0.0001m away from a surface, the brightest you’ll see is 1.999.

From there, it’s a smooth exponential falloff from distance 0 to light range distance. So if you set intensity to 1, and range to 12, then brightness at distance 0 = 1, distance 6 = 0.25, and distance 12 = 0.

Strictly speaking this is not physically correct. Even generally speaking, it isn’t (forgive me Carmack, for I have sinned). But it’s a nicer, soft, 100% efficient use of the light range use of point lights.

In RealtimeLights.hlsl
Change the DistanceAttenuation function at line 47 to:
float DistanceAttenuation(float distanceSqr, float range)
{
float distance = sqrt(distanceSqr);
float distance01 = saturate(1 - (distance / range));
float lightAtten = pow(distance01, 2);
return lightAtten;
}

And then create this at line 154, between all the other floats like lightVector, distanceSqr, etc…
float range = rsqrt(distanceAndSpotAttenuation.x);
And then change the attenuation function at line ~157 to be:
float attenuation = DistanceAttenuation(distanceSqr, range) * AngleAttenuation(spotDirection.xyz, lightDirection, distanceAndSpotAttenuation.zw);

In Deferred.hlsl
Again, define range at line 32 in amongst the other floats like lightVector, etc…
float range = rsqrt(punctualLightData.attenuation.x);
And again, change the attenuation function at line ~36 to be:
float attenuation = DistanceAttenuation(distanceSqr, range) * AngleAttenuation(punctualLightData.spotDirection.xyz, lightDirection, punctualLightData.attenuation.zw);



9908187--1431696--3m range spotlight.png

2 Likes

(check the inspector in the pic above, I think if you re-created that with the default light you’d get a blown-out bright part of intensity ~100 just below the point light, which is 0.1m off the ground. if you assume intensity = 1 / distance squared, 1 / 0.1^2 = 100. You’d lose a little brightness in the reflection/angle, but starting with a base brightness of 100 you can see why little fires/torches/etc don’t do well in URP)

And it seems to work with Spot Lights as well.


9908187--1431696--3m range spotlight.png

1 Like

Made a slight change to the Attenuation function (now has a saturate) to make it work nicer with Forward and Forward+. Deferred was adding a safeguard I hadn’t noticed, but it’s now stable across forward, forward+ and deferred.

1 Like

There might be a better way to do this.

Instead of modifying all the lines in Deferred and RealtimeLights, you might be able to just change the attenuation function only in RealtimeLights.

I’m not sure on the exact syntax for the float2 (it might be a half2, and with a different name, but whatever)… This means all the changes are in one function in one file, so it’s easier to maintain.

In RealtimeLights.hlsl

float DistanceAttenuation(float distanceSqr, float2 distanceAndSpotAttenuation)
{
float distance = sqrt(distanceSqr);
float range = rsqrt(distanceAndSpotAttenuation.x);
float distance01 = saturate(1 - (distance / range));
float lightAtten = pow(distance01, 2);
return lightAtten;
}

2 Likes

This is great thanks. I guess to make lights linear falloff again it needs the 1 line to sqrt the distance back at start! This clunky change seems to do it for me.
Not sure about what the smoothFactor is though - I guess can be removed if linear falloff.

Of course allowing per light or custom changes to SRPs should be a feature already!

My use case is for fill lights on procedural geometry. Default lights just blow out the center in order to get anything bright enough! Sqrt of distance perhaps even better

image

The SmoothFactor is I think a counter to if you’re using the new, accurate lighting + a small light range. There might still be light at (say) 5m range, and it’s there to fade that out.

In your case, yes, I’d keep everything in that script except change the line from:
float lightAtten = pow(distance01, 2);
to
float lightAtten = pow(distance01, 0.5); // fiddle with that number, maybe 0.33 or 0.7 instead?

If you like 0.5, then
float lightAtten = sqrt(distance01);
would be marginally faster, depending on how smart the compiler code is.

1 Like

I think there is a better way to do this, which still requires quite a bit of custom lighting code but it doesn’t need to modify the URP package and probably ends up being more performant.

So in Lighting.hlsl there is a function called GetAdditionalPerObjectLight. Inside of it is the where the per light distance calculation and distance attenuation are taking place. This function is literally constructing the Light struct we are getting the light information from.

float3 lightVector = lightPositionWS.xyz - positionWS * lightPositionWS.w;
float distanceSqr = max(dot(lightVector, lightVector), HALF_MIN);

half3 lightDirection = half3(lightVector * rsqrt(distanceSqr));
half attenuation = DistanceAttenuation(distanceSqr, distanceAndSpotAttenuation.xy) * AngleAttenuation(spotDirection.xyz, lightDirection, distanceAndSpotAttenuation.zw);

Copy that function into a custom hlsl file and fork the shader function down to that point. Then you have full control and you can do anything to the distance attenuation without the need to undo anything because you have access to the values before they are even modified.

Its not that many functions actually:
UniversalFragmentCustom → GetAdditionalLightCustom → GetAdditionalPerObjectLightCustom

But yeah that will only ever work if you write your own shaders instead of using the built-in ones.
(I’ve implemented that into shadergraph too, that is a whole other beast though)

Okay, I got Jesus’s latest solution working pretty nicely for my spot light. Although the light isn’t really shining on the walls and the floor, it does shine all the way to the further walls as can be seen here.