While experimenting with the great framework three.js I ran into the need of simulating some light setups with quadratic decay (which is the natural way for light to decrease its intensity over distance). However I soon learned that three.js does not support this feature yet. This article describes a proposal to address this issue.

From Wikipedia:

Three.js is a lightweight cross-browserJavaScript library/API used to create and display animated 3D computer graphics on a Web browser. Three.js scripts may be used in conjunction with the HTML5canvas element, SVG or WebGL. The source code is hosted in a repository on GitHub.

Currently three.js support 2 types of lights which can be configured to have an intensity decay over distance: PointLight and SpotLight. Depending on the constructor parameter ‘distance’, they can be configured to either having constant intensity over distance (distance = 0) or to fade linearly from the maximum intensity at the light’s position to zero at the specified distance. The natural way for lights to fade out in Nature is quadratic, so I tried to hack three.js so that I could make some simulations with this sort of decay.

## Use cases

Later on I will go over some aspects of the implementation. For now here you have a couple of examples that use the new quadratic decaying lights and compare them with the same light setup using linear decaying lights. There is a very important issue to keep in mind, and it is related to the gamma output setup. For the comparison to be fair we have to deactivate gamma ouput for the linear setup and activate it for the quadratic setup. I’ll address this point a little later.

### Example A

Go to Example A (WebGL support needed)

In example A you can see an array of three point lights at different distances from a plane, upon which they cast a light pattern. We have four cases, 2 of them are the ones we are comparing: from left to right those are the 2nd (quadratic lights) and the 3rd (linear lights). I’ve shown the 4 cases to take into account how gamma output influences the results. The 1st and 2nd array are rendered with gamma output on, while the 3rd and 4th are depicted with gamma output off. For each pair, the left one is made up of linear lights, the right one with quadratic lights. All in all, the meaningful cases are the 2nd (gamma on, quadratic) and the 3rd(gamma off, linear).

I recommend to go to the link on a web browser that supports webGL and play around with the controls at the right top corner.

Leaving the gamma issue aside for the time being, we have two arrays of three point lights set at different distances from the plane. On the left, quadratic decay. On the right, linear decay.

A side view helps to visualize the position of the lights:

We can also set the light array at a constant distance from the plane in order to compare:

We have set up lights so that the maximum intensity without saturation (1.0) is exactly achieved at the brightest point of the plane. This turns out to be the point of the plane that stands closer to the nearest light. For quadratic lights there is no more to configure (only position and intensity), but for linear lights, we still have an additional parameter to configure, the distance at which the intensity fades out completely. We have set it up so that the most distant light hardly lights up the plane.

We can observe that the quadratic lights have a longer, softer tail, and blend mildly. Linear lights fade out abruptly and blend harshly.

### Example B

Go to Example B (WebGL support needed).

In example B we have set up a single light source (to choose between PointLight or SpotLight) in the middle of a plain room with a hemisphere on the floor. As before, on the left we have quadratic decay (gamma on), on the right linear decay (gamma off).

Above we have the results for a single point light. For both cases the distance at which 1.0 intensity is achieved is the same. However, while in the quadratic case (left) there is nothing more to configure, in the linear case we can play around with the “linear fade” parameter, which sets at which distance (from the point with 1.0 intensity) the light goes to 0.0 intensity. We can see that for this “linear fade” setting, the light distribution on the room is quite similar  except for the region very close to the light source. Let’s see what happens when we make the decay tail shorter for the linear case.

In this case we get a harsher fadeout of the light, in a chiaroscuro fashion, which seems to be less natural. In any case it is an interesting effect that could be used with aesthetic intentions.

We can also try the spot light type.

Here we can see that the spot nature of the light source hides the burned out zones in its vicinity.

## Gamma

I’ll try to explain now why I chose the gamma setup above (on for quadratic, off for linear). I’ve been convinced by the explanantions about it on the book Learning Modern 3D Graphics Programming by Jason L. McKesson (Copyright © 2012 Jason L. McKesson).

From Chapter 12, section “Linearity and Gamma”,  http://www.arcsynthesis.org/gltut/Illumination/Tut12%20Monitors%20and%20Gamma.html

When we first talked about light attenuation, we said that the correct attenuation function for a point light was an inverse-square relationship with respect to the distance to the light. We also said that this usually looked wrong, so people often used a plain inverse attenuation function.

Gamma is the reason for this. Or rather, lack of gamma correction is the reason. Without correcting for the display’s gamma function, the attenuation of 1/r2effectively becomes (1/r2)2.2, which is 1/r4.4. The lack of proper gamma correction magnifies the effective attenuation of lights. A simple 1/r relationship looks better without gamma correction because the display’s gamma function turns it into something that is much closer to being in a linear colorspace: 1/r2.2.

Keep in mind that three.js case does not implement an inverse 1/r relationship as suggested in the quote, but a negative slope linear f(r) = Intensity(1-r/Distance). In any case I agree with the reasoning above in favour of using quadratic decay with gamma correction activated.

## Light setup

In three.js, we create a PointLight or SpotLight passing the following parameters to the constructor:

THREE.PointLight = function ( hex, intensity, distance ) {
...
THREE.SpotLight = function ( hex, intensity, distance, angle, exponent ) {

Those which are relevant for light decay with distance are:

• intensity (default 1.0).
• distance (default 0.0). If non-zero, light will attenuate linearly from maximum intensity at light position down to zero at distance. (If zero, light remains at constant intensity at all distances).

So we can depict this behaviour as follows:

For linear decay case we have a finite value at the origin (the point where the light is located). But for quadratic decay, at distance=0.0 we have a singularity (infinity). In addition, for the linear case there is a distance where the light fades out completely, unlike the quadratic case, where there light would only fade out completely at an infinite distance.

So if we want to reuse the constructor interfaces for PointLight and SpotLight for the quadratic type of decay we will have to:

• redefine the meaning of the parameters intensity and distance
• avoid the singularity in the origin, so that the shaders do not try to compute an infinite value of intensity
• add a new optionial parameter at the end of the parameter list to serve as a flag in case we want to have quadratic decay instead of the default linear decay

This way we could implement quadratic decay for both type of lights without disrupting the default linear behavior.

This can be done modifying the quadratic decay function so that it gives a constant intensity from the origin to a certain distance, and fading quadratically beyond that point. The closer zone with constant intensity can be thought of the inner part of a bulb, i.e., a small portion of space where there is a extremely high light intensity and no objects are placed. Objects to be lit by this type of light will be placed further away (outside the bulb), where the quadratic decay is in place and where intensity is within the range 0.0 to 1.0.   So in summary, for the quadratic case the parameters have this meaning:

• distance, the radius of the “light bulb”,
• intensity, the light intensity within the “light bulb”, constant from the origin to distance

In addition we need a third parameter, by the end of the constructor parameter list ( so that it is optional ) which if true, will select quadratic decay for that light. If false or not present (ensuring backward compatibility), it will use the default decay (linear).

In the diagram we can observe three regions that this type of light produces: the “light bulb” region, the “white zone” ( where color saturation happens ) ,and then a region that extends to infinity in which intensity goes from maximum 1.0 to 0 (at infinity) in a quadratic decay fashion.

## Light helpers

There is an inconvenience when using quadratic lights the way we have described. What we have in mind when lighting with a decay light is a distance from the light source at which the scene is optimally lit (i.e., intensity is 1.0) and then assume that the objects of interest will be behind that distance. However, to get the quadratic light from the proposed object constructor, we have to provide with two different values: the bulb radius and the intensity at the bulb. The first one can be as small as we wish, the only constraint is that the bulb is smaller than the objects we want to lighten. However, the second one, intensity at the bulb, must be calculated so that the light intensity becomes 1.0 at the optimal distance. Indeed, the optimal distance will usually be much greater than the constructor parameter “distance”, as the intensity at the bulb will be much greater than the intensity at the optimal distance (intensity 1.0).

So I came up with a wrapper for the PointLight and SpotLight constructor so that we could specify the meaningful optimal distance and optionally the desired intensity at this (optimal) distance (1.0 by default).  This wrapper can also be used for the linear decay case, if we take care of adding an extra parameter (fade_distance), which is the distance, measured from and behind the optimal distance, at which the light fades out completely.


THREE.SmartPointLight = function ( params ) {
/**
* Wrapper for PointLight.
* parameters     -->    object with specifications (parameters)
*   color           : light color, instanceof Color
*                     default = white
*   main_intensity  : intensity at main_distance (should be between 0.0 and 1.0)
*                     default = 1.0
*   main_distance   : distance at which intensity is as specified by the parameter 'intensity'
*                     default = 1.0
*   fade_type       : 'constant', for constant intensity (no decay)
*                     'linear',     linear decay until, intensity becomes 0.0 by fade_distance
*                     for distances < near_distance
*                     default = 'constant'
* For LINEAR PointLight:
*   fade_distance   : distance from main_distance where light intensity becomes 0.0
*                     default = 1.0
*     near_distance :  For shorter distances than this, light intensity remains constant.
*                      Think about it like the "light bulb radius", you do not put objects
*                      within this distance. The default value tries to be a useful one
*                      in most situations: change only if needed.
*                      default = 0.01
*/
if (typeof params === 'undefined') params = {};
var color         =     params.color instanceof THREE.Color ?
params.color : new THREE.Color(0xffffff);
var main_intensity     =     typeof params.main_intensity === 'number' &&
! (params.main_intensity < 0.0) ?
params.main_intensity : 1.0;
var main_distance    =     typeof params.main_distance === 'number' &&
! (params.main_distance <= 0.0) ?
params.main_distance : 1.0;
var near_distance    =     typeof params.near_distance === 'number' &&
! (params.near_distance <= 0.0) ?
params.near_distance : 1.0;
var    hex,
intensity,
distance,
hex = color.getHex();
distance = near_distance;
intensity = Math.pow( main_distance/near_distance, 2 ) * main_intensity;
} else if ( fade_type === 'linear' ) {
intensity = (1 + main_distance / fade_distance ) * main_intensity;
} else {
// fade_type should be 'constant' if we got here
distance = 0;
intensity = main_intensity;
}
THREE.PointLight.call( this, hex, intensity, distance, quadratic );
}

## Implementation

The complete implementation can be found in my github space.

The key change is in the fragment shaders. For instance, here you have an excerpt from the output of the command

\$ git diff

for the file WebGLShaders.js, where a nested if is included to evaluate if the light is quadratic or linear, and applying either the linear or quadratic calculation.

@@ -15984,8 +16030,13 @@ THREE.ShaderChunk = {
"vec3 lVector = lPosition.xyz - mvPosition.xyz;",

"float lDistance = 1.0;",
-                               "if ( spotLightDistance[ i ] > 0.0 )",
-                                       "lDistance = 1.0 - min( ( length( lVector ) / spotLightDistance[ i ] ), 1.0 );",
+                               "if ( spotLightDistance[ i ] > 0.0 ) {",
+                                       "if ( spotLightQuadratic[ i ] ){",
+                                               "lDistance = min( pow( spotLightDistance[i]/length(lVector), 2.0 ),1.0 );",
+                                       "} else {",
+                                           "lDistance = 1.0 - min( ( length( lVector ) / spotLightDistance[ i ] ), 1.0 );",
+                                       "}",
+                               "}",

## Conclusions

• While for quadratic lights we need to setup only the main distance at which they produce the maximum intensity without saturation (1.0), for linear lights we have to define an additional parameter. This is the incremental distance at which light intensity fades out completely (goes to 0). So quadratic lights seem easier to setup.
• Quadratic lights seem to produce more natural results. Linear lights results may go from quite natural to sketchy (chiaroscuro style) depending on how we adjust the additional parameter (the decay tail to be long or or short).
• Quadratic lights should be used to light things that are placed at a certain distance from the light source, from where the light is gentle. The decay is soft when things are a certain distance from the source. However they are hard and saturated when we approach the light source.
• Linear lights  can be used to light objects that are placed close to the source of light. In the near field properly configured linear lights do not saturate and tend to be soft. On the other hand, at further distances, they may go to complete darkness (being hard or dramatic).