GLSL Programming/Unity/Light Attenuation

This tutorial covers textures for light attenuation or — more generally spoken — textures as lookup tables.

It is based on. If you haven't read that tutorial yet, you should read it first.

Texture Maps as Lookup Tables
One can think of a texture map as an approximation to a two-dimensional function that maps the texture coordinates to an RGBA color. If one of the two texture coordinates is kept fixed, the texture map can also represent a one-dimensional function. Thus, it is often possible to replace mathematical expressions that depend only on one or two variables by lookup tables in the form of texture maps. (The limitation is that the resolution of the texture map is limited by the size of the texture image and therefore the accuracy of a texture lookup might be insufficient.)

The main advantage of using such a texture lookup is a potential gain of performance: a texture lookup doesn't depend on the complexity of the mathematical expression but only on the size of the texture image (to a certain degree: the smaller the texture image the more efficient the caching up to the point where the whole texture fits into the cache). However, there is an overhead of using a texture lookup; thus, replacing simple mathematical expressions — including built-in functions — is usually pointless.

Which mathematical expressions should be replaced by texture lookups? Unfortunately, there is no general answer because it depends on the specific GPU whether a specific lookup is faster than evaluating a specific mathematical expression. However, one should keep in mind that a texture map is less simple (since it requires code to compute the lookup table), less explicit (since the mathematical function is encoded in a lookup table), less consistent with other mathematical expressions, and has a wider scope (since the texture is available in the whole fragment shader). These are good reasons to avoid lookup tables. However, the gains in performance might outweigh these reasons. In that case, it is a good idea to include comments that document how to achieve the same effect without the lookup table.

Unity's Texture Lookup for Light Attenuation
Unity actually uses a lookup texture  internally for the light attenuation of point lights and spotlights. (Note that in some cases, e.g. point lights without cookie textures, this lookup texture is set to  without  . This case is ignored here.) In, it was described how to implement linear attenuation: we compute an attenuation factor that includes one over the distance between the position of the light source in world space and the position of the rendered fragment in world space. In order to represent this distance, Unity uses the $$z$$ coordinate in light space. Light space coordinates have been discussed in ; here, it is only important that we can use the Unity-specific uniform matrix  to transform a position from world space to light space. Analogously to the code in, we store the position in light space in the varying variable. We can then use the $$z$$ coordinate of this varying to look up the attenuation factor in the alpha component of the texture  in the fragment shader: Using the texture lookup, we don't have to compute the length of a vector (which involves three squares and one square root) and we don't have to divide by this length. In fact, the actual attenuation function that is implemented in the lookup table is more complicated in order to avoid saturated colors at short distances. Thus, compared to a computation of this actual attenuation function, we save even more operations.

Complete Shader Code
The shader code is based on the code of. The  pass was slightly simplified by assuming that the light source is always directional without attenuation. The vertex shader of the  pass is identical to the code in  but the fragment shader includes the texture lookup for light attenuation, which is described above. However, the fragment shader lacks the cookie attenuation in order to focus on the attenuation with distance. It is straightforward (and a good exercise) to include the code for the cookie again. If you compare the lighting computed by this shader with the lighting of a built-in shader, you will notice a difference in intensity by a factor of about 2 to 4. However, this is mainly due to additional constant factors in the built-in shaders. It is straightforward to introduce similar constant factors in the code above.

It should be noted that the $$z$$ coordinate in light space is not equal to the distance from the light source; it's not even proportional to that distance. In fact, the meaning of the $$z$$ coordinate depends on the matrix, which is an undocumented feature of Unity and can therefore change anytime. However, it is rather safe to assume that a value of 0 corresponds to very close positions and a value of 1 corresponds to farther positions.

Also note that point lights without cookie textures specify the attenuation lookup texture in  instead of  ; thus, the code above doesn't work for them. Moreover, the code doesn't check the sign of the $$z$$ coordinate, which is fine for spot lights but results in a lack of attenuation on one side of point light sources.

Computing Lookup Textures
So far, we have used a lookup texture that is provided by Unity. If Unity wouldn't provide us with the texture in, we had to compute this texture ourselves. Here is some JavaScript code to compute a similar lookup texture. In order to use it, you have to change the name  to   in the shader code and attach the following JavaScript to any game object with the corresponding material:

In this code,  and   enumerate the texels of the texture image while   and   represent the corresponding texture coordinates. The function  for the alpha component of the texture image happens to produce similar results as compared to Unity's lookup texture.

Note that the lookup texture should not be computed in every frame. Rather it should be computed only when necessary. If a lookup texture depends on additional parameters, then the texture should only be recomputed if any parameter has been changed. This can be achieved by storing the parameter values for which a lookup texture has been computed and continuously checking whether any of the new parameters are different from these stored values. If this is the case, the lookup texture has to be recomputed.

Summary
Congratulations, you have reached the end of this tutorial. We have seen:
 * How to use the built-in texture  as a lookup table for light attenuation.
 * How to compute your own lookup textures in JavaScript.