Quadratic Lights WebGL

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. The following article describes a proposal of implementation 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

Example A

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 would be the 2nd (quadratic light) 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, as I said, 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, which is the distance at which the intensity fades out completely. We have set it up so that the more 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

Example B

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


We have displayed above the results for a single point light. For both cases the distance at which 1.0 intensity is achived is the same. However, while in the quadratic case (left) there is nothing more to configure, in the linear case we can play around the 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.

quadlights_sol_1_b_02In 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.


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 a 1/r relationship as suggested in the quote, but a f(r) = Intensity(1-r/Distance). In any case I agree with the reasoning above in favour if using quadratic decay and 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 lights fades out completely, unlike the quadratic case, where there is no distance where light fades out completely.

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 compute an infinite value of intensity for the origin
  • 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 not 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 withing the range 0.0 to 1.0.   So in summary, now the parameters mean:

  • 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).



Setting up main distance and intenstity. Additionally, tail distance for linear lights.

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
 *                     'quadratic', quadratic decay, intensity remains constant
 *                     for distances < near_distance
 *                     default = 'constant'
 * For LINEAR PointLight:
 *   fade_distance   : distance from main_distance where light intensity becomes 0.0
 *                     default = 1.0
 * For QUADRATIC PointLight
 *     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 fade_type        =     params.fade_type === 'constant' ||
                        params.fade_type === 'linear' ||
                        params.fade_type === 'quadratic' ?
                        params.fade_type : 'constant';
    var fade_distance    =     typeof params.fade_distance === 'number' &&
                        ! (params.fade_distance <= 0.0) ?
                        params.fade_distance : 1.0;
    var near_distance    =     typeof params.near_distance === 'number' &&
                        ! (params.near_distance <= 0.0) ?
                        params.near_distance : 1.0;
    var    hex,
    hex = color.getHex();
    if ( fade_type === 'quadratic'){
        distance = near_distance;
        intensity = Math.pow( main_distance/near_distance, 2 ) * main_intensity;
        quadratic = true;
    } else if ( fade_type === 'linear' ) {
        distance = main_distance + fade_distance;
        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 );





  • 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.
  • The quadratic lights seem to produce more natural results. Linear lights results may go from ‘natural’ to sketchy (chiaroscuro style) depending on how we adjust the additional parameter (the decay tail to be long or or short).
  • Quadratic lights are meant to light things that are placed at a certain distance from the light source, from where the light is gentle. They are soft when things are a certain distance from the source. However they are hard and saturated when we approach the light source.
  • Linear lights are meant to light objects that are placed close to the source of light. In the near field 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).


  • Results
  • The importance of Gamma
  • Extending PointLight for quadratic decay
  • Do not mix up with attenuation









  • Quadratic Lighs for CanvasRenderer ?



No confundir “atenuación” con “difusión?”












No confundir “atenuación” con “difusión?”








Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

Aviso de cookies