A Deferred Point Light In WebGL


Your browser does not support the canvas tag. This is a static example of what would be seen.
  

Building on an earlier example which demonstrated a deferred directional light I'm hoping to get a deferred point light working this time.

Point lights have a lot in common with directional lights, so the changes needed to cover the differences should be minimal. The main differences are that point lights have a defined shape and size, unlike directional lights which are infinite. The volume of influence for a point light can be represented as a sphere more or less. A point light has a variable light direction, where for each shaded pixel we need to calculate the vector to the the light. Finally a point light requires an implementation of falloff or attenuation, so the quantity (intensity) of light hitting a surface reduces with distance from the light.

The first change is therefore to swap our full-viewport quad for a sphere mesh. Unlike the quad we used for the directional we'll transform the sphere by the camera matrices and position it in the world such that it neatly overlaps the volume of influence for the light. If I tint that volume red it looks more or less like this...

You can see from the image that we are shading a whole bunch of pixels that represent the background (far Z) which in this example will never be influenced the light. There is a further optimization we could use here using the stencil buffer which would allow us to isolate the pixels we really need to shade more precisely. I might cover that in a future post but for now we'll just put up with the overdraw.

One thing to note is that we actually need to increase the radius of the sphere a little to ensure that the closest point rather than the farthest point on our mesh properly fits to the radius of our light. I found by trial and error a scale factor of 1.07 works well here and copes with various levels of tessellation well. This chart shows what we are working against here... where plotting straight edges along the curve at the edge of our circle leads to us failing to cover the region of influence properly, meaning the region between the red and blue lines is not shaded. Moving out the blue line a little fixes this.

Onto the shader changes, the light direction (L) which was previously a constant is simply calculated from the position, meaning we upload the position rather than direction to the shader for this light type. We still have an eye position that we reconstruct from depth, so we can simply do this, remembering to normalise the result.

  vec3 L = normalize(u_lightPosition - eyePos.xyz);

The only other big change is that we need to account for falloff (attenuation). I've added a uniform that represents the radius of the light, stored in the X value of a light-params vector. We can maybe use the other fields to add spot light properties later. The code that follows implements an inverse square as used in various commercial engines, including UE4, and which is documented here [page 12]. This has a nice property which is that the radius of the light is well defined, which is important for keeping on top of overall performance, as opposed to other approaches where the radius is a function of light intensity where we would tend to lose control of the size of our lights.

  float radius = u_lightParams.x;
  float falloff = clamp(1.0 - pow(d / radius, 4.0), 0.0, 1.0);
  falloff = (falloff * falloff) / (d * d + 1.0);

Finally, as with earlier post the material textures used here were sourced form freepbr.com.