I mentioned in my previous post that calculating shadows for directional lights was the simplest case of shadow mapping. Well, I’m proud to announce that it’s no longer the case, since the process has become a lot more complex now thanks to cascading shadow maps.
Shadow mapping for directional lights (lights that are very far away from the scene and which rays are considered to be parallel) is fairly straightforward in theory. The shadow map is computed by calculating the closest distance from a light source to the objects in our scene. And it affects the entire scene, so every single object must be processed when calculating the shadows. An orthographic projection is used in this case to simulate the parallel rays.
But the problem with this approach is that it doesn’t scale well when there are multiple objects spread all over the place, since the resolution of the shadow texture is limited. Yes, we could increase the texture resolution but the GPU has a hard limit there too (I think it’s 8k for high-end GPUs at the time of this writing).
Cascading shadow maps work by following a simple idea: we need shadows that are closer to the camera to have the greatest resolution. At the same time, we don’t really care if the shadows that are far away look pixelated (up to a point, of course). Therefore, we need to generate multiple shadow maps, each of them processing a different section of the scene, using a slightly different projection to render different objects based on their distance to the camera.
For example, let’s assume our camera has a maximum viewing distance of 1000 units (meters, feet, light-years, etc…). Then, we could split the scene objects into four groups based on the distance from any object to the camera. Each of those groups will be rendered into a different shadow textures, as follows:
- Group 1: objects that are less than 10 units away from camera. These are the objects that are closest to the camera and the ones ending up with the higher resolution shadows.
- Group 2: objects that are less than 100 units away from the camera. These objects should still get a pretty decent shadow resolution.
- Group 3: objects that are less than 500 units away from the camera. For these objects, the shadow resolution won’t be great, but it might not be that bad either.
- Group 4: objects that are farther than 500 units. These are the objects in the background. Here, the shadows will look really pixelated and smaller objects might not even cast shadows at all.
Here’s how it looks like in action:
Notice how shadows look pixelated at first since they are farther away from the camera, but they start to look better and better as the camera gets closer.
The following video shows how the different objects are grouped together into cascades of different colors:
Notice how shadows become less and less pixelated as the camera gets closer.
If you paid attention on those videos above, you might have noticed that some shadows disappear completely when the camera gets closer (look at the cyan cascade in the second video). The reason for that problem is very simple to explain: for each cascade, we need to "zoom into" the objects that we care about. So, only objects that are currently visible from the point of view of the camera are actually processed when computing the shadow maps. It might be possible that one object is affected by shadows from objects that are not visible from the current viewpoint. In the videos above, some of the cubes get culled by the camera when they're not visible and therefore not rendered in the shadow map. I still need to fix this behavior.
If you want to know more about this technique, here’s an excellent article from GPU Gems 3 which I used as basis for my implementation.