# A better point light attenuation function

### The problem

So, recently I was adding lights support to the martian-droid-colony-building project I’ve been working on for a while. I used CPU-based clustered shading, storing the lights & clusters data in buffer textures, and managed to support about 4K lights at 60 FPS on my machine. *(I’m not using compute shaders and SSBO’s because I’m stuck with OpenGL 3.3 so that even prehistoric microwaves can run my game; I’ll probably add support for those through OpenGL extensions later.)* Here’s a twitter thread with some intermediate results, and here are some final screenshots:

For light clustering to work, I need to clip the light’s area of influence by a certain distance. The physically correct point light attenuation function (how much light some point in scene receives) is

where A is the light’s amplitude (intensity) and d is the distance from the light to an illuminated point. However, this function introduces a singularity at d = 0, meaning that points too close to the light will receive unbounded amounts of lighting. This is an artefact of the point light abstraction: real lights are not points, they are physical objects of nonzero size. We don’t want to throw away this nice abstraction, though, so we add a fake term that removes this singularity:

This has the added benefit that A is now the maximal light intensity (achieved at d = 0). This function (red) isn’t physically correct at small values of d, but is close to the correct one (blue) for large distances:

However, this attenuation formula doesn’t let us control the fallof, i.e. how fast does the intensity decrease with distance. This won’t be physically correct either, but it allows for a greater artistic flexibility. So, introduce another parameter R:

R is precisely the distance where the light intensity is half the maximal intensity. Larger values of R make the light attenuate (decrease in intensity) slower, increasing the effective light’s area of influence. Here, the green function has R = 1 and the purple one has R = 3:

Finally, we can add another parameter Q, for which I don’t have an intuitive explanation, but it allows us to control the shape of attenuation more precisely:

Small values of Q introduce a cusp at d = 0, producing a faster fallof and a slightly different shape for the attenuation function (the blue one has Q = 1):

Use can play with all this functions here.

This type of function is usually presented in APIs/engines in a slightly different, arguably cleaner form (but with less intuitive parameters):

I.e. light attenuation is the inverse of a quadratic function of distance. See e.g. here or the docs for OpenGL 1.0 lighting model.

There are other popular attenuation functions: some introduce a sharp cutoff near d = 0, some replace the point light with a spherical area light, etc. What I don’t like about all these options is that no formula gives a good way of constraining the light’s influence to a certain radius: all these formulas are non-zero for arbirtarily large distances!

### The solution

So, I set off to find a different formula that

- Looks a bit like the physically correct
- Is exactly zero at a certain distance R, so that I can use that distance for light-cluster intersections
- Has zero derivative at distance R (otherwise the lightness will have a C^1 discontinuity, leading to a noticeable gradient edge)

What I came up with looks incredibly simple:

where is the normalized distance, and F allows you to control how fast it decays. It allows to separately control the maximum intensity A and the maximum radius R. You can play with this formula here. It is also fast to compute, since it doesn’t use square roots, exponents, and other stuff, just a bit of arithmetic. Here’s how it looks like for A = 2, R = 5, F = 1 (red) and F = 4 (blue):

For distances less than R/2 this function looks about the same as , while for larger values it gradually goes to zero and is exactly zero at distance R. Note that it doesn’t have a sharp cusp at d = 0, which can be for an artistic reason of wanting the attenuation to behave roughly like a spherical area light source near the light. For a version with cusp (which I’m actually using), you can simply remove the square in the denominator:

Here’s how it looks like for the same values of A = 2, R = 5, F = 1 (red) and F = 4 (blue):

For the sake of completeness, here’s a GLSL implementation. Note that we need to check for d < R, otherwise we’d get wrong lightness values at larger distances.

```
float sqr(float x)
{
return x * x;
}
float attenuate_no_cusp(float distance, float radius, float max_intensity, float falloff)
{
float s = distance / radius;
if (s >= 1.0)
return 0.0;
float s2 = sqr(s);
return max_intensity * sqr(1 - s2) / (1 + falloff * s2);
}
float attenuate_cusp(float distance, float radius, float max_intensity, float falloff)
{
float s = distance / radius;
if (s >= 1.0)
return 0.0;
float s2 = sqr(s);
return max_intensity * sqr(1 - s2) / (1 + falloff * s);
}
```

That’s it! Hope you’ll find it usefull and thanks for reading.