Shadow Mapping

Since the last SVN commit, I have been working on adding shadows to Kourjet. The two main techniques are shadow volumes (aka stencil shadows) and shadow mapping. Some years back, shadow volumes were the standard mainly because they do not require lots of extra resources and the hardware requirements are rather low (just need an 8 bit stencil buffer to make it work). However, as of today, shadow mapping has taken the preference. Hardware is now more powerful and this technique offers many more possibilities of improvement such as soft shadows for instance. As a consequence, I have decided to start implementing shadow mapping and leave shadow volumes for later.

Engine Architecture Changes

In order to allow for more flexibility, the rendering has been segregated into Render Path objects. These objects describe a rendering technique and take as input a Scene Graph object that allows them to retrieve the lights, camera, renderable objects, … using Query objects.

Even though the algorithms used are very naive, it is very easy to refine the implementations. For instance, the default scene graph object is just a plain vector containing the scene objects. However, Octrees, BSP, Portals or other object management structures will simply need to implement the Scene Graph interface and can then work with any Render Path. Similarly, you can add Render Path implementations without touching the scene graph structures: as of now in Kourjet, you get a render path that displays objects to the main back buffer (using a scene graph query to return objects in the camera frustum). You also get a render path that updates the shadow maps associated to the lights (using a scene graph query that returns all objects that could be casting shadows in the camera frustum).

In short, this opens quite a lot of possibilities and, most of all, allows the rendering to be separated from the scene traversal or other algorithms.

A First Shadow Mapping Implementation in Kourjet

I have decided to start with shadow mapping for point lights for different reasons:

  • It requires Cube Textures and this was missing in Kourjet, giving me the opportunity to add it to the engine
  • It requires rendering to cube textures and I thought I would give a try to the single pass cube map rendering using Geometry Shaders (see below for some drawbacks)
  • It does not require to do any other calculation than just rendering the scene from the light position using a very simple perspective projection (for directional lights, you need to compute the projection according to the camera, … and I wanted to keep cascaded shadow maps for the next task)

So here is the result:

A first shot at getting shadow mapping in Kourjet

A first shot at getting shadow mapping in Kourjet

The above image shows some spheres and cubes on top of a plane. There are 3 point ligths casting shadows on the whole scene plus one ambient light. The frame rate as I had expected is still very low (15 FPS) but nothing is really helping it and there is a lot of room for improvement:

  • First the objects are not culled at any time. Which means that every object is rendered 28 times:
    • 1 time for each point light for the shadow map rendering pass (3 times in this case), which is in fact 6 times because we are rendering to 6 cube texture faces → 24 times!
    • 1 time for each light for the main lighting/shadowing pass (4 times in this case)
  • The shadow maps are updated each frame. This could probably distributed over 3/4 frames (for instance update only 1 shadow map per frame).
  • There is one shadow map per light. Which is quite annoying if the plan is to have plenty of point lights. There are various solutions in which I am thinking about:
    • Having a single shadow map for all point lights and use only a single renderpath: instead of updating all maps in a render path and then rendering the scene for all lights in another render path, the idea would be to have a single render path that will render each light at once (update shadow map to get shadows for this particular light and right after that render the light contribution for this light in the scene).
    • Having a pool of shadow maps (say 4 or 5). Each time we want to render the scene, we pick only the 4/5 lights that are around the camera and have the most contribution to the scene and update the shadow maps for those lights. However I feel this solution could introduce problems of shadows popping in the view.
  • Cube Textures are rendered in a single pass using geometry shaders. While this clearly saves CPU cycles, this is also very inefficient because most of the geometry could be culled. In the above example, if we were rendering only one face of the cube texture each time, we could skip rendering the top face (objects are all below the lights) and skip at least 2 to 3 more faces with a basic frustum culling. For instance, if you look at the light in the left of the screenshot above, we would be able to skip the left side of the cube (no objects to the left of the light) and render problably only one object for the down face (the plane) and one for the front face (the sphere located closer to us).

Apart from performance improvements to be done, there are also some quality improvements to be implemented in the next release. To start with, a very common problem with shadow maps: aliasing. This could be improved by implementing some sort of soft shadow algorithm (starting from the simplest fake one, just blur the shadow map).

Some aliasing can be seen where the shadows are stretched even though a 512x512 texture is used

Some aliasing can be seen where the shadows are stretched even though a 512x512 texture is used

Some links about shadow techniques that got me started

  • The GPU Gems chapter about shadows on nvidia.com
  • Various shadow techniques explained briefly on devmaster.net
  • Some ideas on fighting the aliasing problem on gamedev.net
  • Discusion about the usefulness of single pass cube map rendering on gamedev.net
  • General article about shadow mapping on wikipedia
  1. No comments yet.

  1. November 29th, 2009
  2. November 30th, 2009