Shadow Mapping
Shadows are an important visual cue that help humans perceive proximity and shape. When there are no shadows, the visual system cannot form an accurate mental model of how a scene is arranged. Watch this aged video on some psychology experiments to observe how shadows influence our perception.
Our renderers do not have shadows. That's because when we calculate the lighting terms for a fragment, we only consider a few inputs:
- The surface properties like the albedo, normal, and shininess
- The position of the eye
- The properties of the light source
Nowhere in this list is consideration of any other object in the scene. Other objects are what cause shadows. They block or occlude light from reaching the fragment. If we want shadows, we must find a way to determine a fragment's occluders.
Your intuition may tell you to cast a ray from the fragment to the light source and see if it hits any occluders. That's a great idea that is used in a different approach to 3D rendering called raytracing. Sadly, casting rays doesn't really fit the WebGL model, which processes scenes one model at a time. It also requires many ray intersection tests, which are expensive to run per fragment.
Lance Williams, the same individual who invented mipmapping, invented a faster algorithm for computing shadows called shadow mapping. By now you've probably figured out that the word “mapping” means throwing information into textures. True to form, the shadow mapping algorithm stores information about occluders in a texture.
The complete shadow mapping algorithm follows these steps:
- Create a blank texture.
- Whenever the light source moves, render the scene from the perspective of the light source. But store the resulting image in the texture, not in the framebuffer. And don't store any color. Store only the depths. This texture is the shadow map that records how near are the closest surfaces to the light.
- On each frame, render the scene to the default framebuffer.
- In the fragment shader, project the fragment onto the shadow map. All fragments on the line of sight between the light source and the current fragment will project to the same texel. The texture lookup gives the depth of the fragment nearest to the light.
- Compare the current fragment's depth with the closest depth. If the fragment's depth is greater, then it is occluded by a closer fragment. The fragment is in shadow and must be shaded darker.
Read on to learn about each of these steps in detail.