Improving Efficiency

But we can do much better then light sized quads, it still covers a lot of pixels where the lighting contribution is zero, even for a simple point light with a spherical shape, the corners of the quad are wasted. For spotlights it’s even worse. We could try a oriented bounding box which would help (but not much) the spotlight case but wouldn’t help point lights. What we really want is the 2D projection of the volume where the lighting contribution isn’t zero.

We create a mesh that encloses the light affecting volume with any pixels found to be in the interior of the volume executing the light shader. The hardware is very good at projecting geometry, but our requirements for light volumes are different from the standard mesh projection. There are 2 major unusual requirements that light shaders have,

  1. Each pixel most be hit once and once only. If the light volume causes the light shader to be executed more than once it will be equivalent to having n lights affecting this pixel.
  2. The near and far clip planes must not effect the projected shape. We need the projected geometry not to be clipped at the near and far plane as this will cause holes in our lights.

The only change to the light shader due to using 3D geometries rather than quads is that the we must use projective texture lookups to access the G-Buffer as the uv coordinates are now being interpolated in 3D space.

These requirements are similar to shadow volumes techniques, which have become robust recently and are quite complicated. We are able in many cases to use much simpler (almost trivial) solutions but in the most extreme cases we will need to use similar techniques to shadow volumes.

Convex Light Volumes

The first and most important thing to note is that if the light shader geometry is closed and convex the solutions to the problems are much simpler. The first problem is solved due to an often forgotten (in these days of fast depth buffers) fact about convex objects, the only hidden surface removal needed for a closed convex object is back face culling [13]. In other words, for convex volumes the first problem is completely removed by just using back or front face culling.

The second problem is also greatly simplified, I was very careful to say back or front face culling, as when depth buffering is off and no clipping occurs the same pixels will be covered whether you cull front or back faces. The correct pixels will also be covered if you are rendering back faces and clipping the near plane but not the far plane and if you are rendering front faces and clipping the far plane but not the near plane. Unfortunately there is no easy solution if the geometry is clipped by both the far and near plane, so ideally we would like to guarantee that will never happen. We can’t remove the near plane, but we can effectively remove the far plane by placing it at infinity.

Placing the far plane at infinity has very little side effects (I’ll leave the mathematical treatment to better people than I [14][15]), but means we can now render our convex geometries with an infinite far plane and front face culling and have the correct pixels hit once and once only, regardless of clipping.

Convex volumes cover the vast majority of lights shaders (e.g. spheres for point lights, cones for spotlights, etc.) and we can adapt them to use the fast z-reject hardware that is usually available.

Light Shader Occlusion Optimisations

Click for a bigger version

Modern hardware usually has some kind of hierarchical depth buffer, which is able to reject pixels very quickly if the pixel shader result isn’t visible. By enabling this capability for our light shaders, we will only pay for lighting that can actually be seen on a per-pixel basis. I.e. if a light is largely occluded by a wall, we will only pay for any pixels not covered by the wall.

The basis of using occlusion culling with light shaders is that the depth buffer used for the creation of the G-Buffer is available at no cost (this is only true if the resolution of the G-Buffer is the same as destination colour buffer and that we are using the same projection matrix for the geometry shaders and light shaders). If you are using light sized quads, it simple a matter of rendering the quad at a distance of the closest point on the light shader along the view direction (or the near clip plane if the closest point would be behind it) and enabling the depth test without depth writes. If the depth test fails then something was in front of the closest point of the light and therefore the light at that pixel couldn’t be seen by the viewer.

The problem comes when we try and combine the occlusion test with using geometry to represent the light shader. Even with convex geometries, there is a conflict between our solution of front face culling and needing the depth at the closest point. To use the depth of the closest points we must render front faces which will fail when clipping the near plane (which we can’t remove). Ultimately we need to produce a cap at the near plane to fix the hole produced by clipping, but this can be a difficult and expensive CPU operation.

My solution is much simpler; I simply turn off the occlusion culling if the light shader hits the near plane and just render the back faces without depth testing. Its means some pixels run the pixel shader unnecessarily but it’s very cheap on the CPU and the actual difference is usually only a few pixels.

Concave Light Volume

Concave light shaders are useful for making complex and hard edged lights. The problem is closely related to shadow volumes, so it’s probably worth implementing a robust shadow volume technique which have been researched extensively and are now very robust and modifying them for use on concave light volumes. The only major change is to add the lights volume is such as way that everything in the exterior is in shadow.