Shadow Mapping Sample

This sample shows you how to implement basic shadow mapping from a directional light where the view and projection of the shadow map adapt to the viewing frustum of the viewer's camera. You can use the sample to shadow a large scene with multiple dynamic objects that cast dynamic shadows.

What is Shadow Mapping?

Shadow mapping refers to a shadowing technique where you can use a texture to store an object's distance from a light. When you render the scene, you can then use the distance from the light to determine if the pixel being rendered is behind the value stored in the shadow map.

Shadow mapping requires you to render the scene twice. The first step is to render all objects that may cast shadows from the point of view of the light. These objects are called occluders. This means you create a view and projection matrix that positions the scene at the light and in the direction of the light. The depth of the objects is stored in a render target. A common format to use for the render target is a 32-bit floating-point SurfaceFormat.Single, which allows for 32-bit precision for the depth value of objects. Some graphics cards do not support this format, however, so if you are using this type of card, you must use a 16-bit floating-point SurfaceFormat.HalfSingle. Older graphics cards do not support floating-point render targets at all and require a different surface format such as SurfaceFormat.Rgba32. In these situations, pack the distance into the four 8-bit channels.

The second step is to render the scene normally from the perspective of the camera. Determine the distance of each pixel from the light, and then sample the value stored in the shadow map. If the distance in the shadow map is closer than the distance from the light to the pixel being drawn, you know the pixel is occluded by another object. Shade the pixel to show that it is in shadow.

The preceding figure shows how to render a scene from both the light's perspective and the camera's perspective. The red rectangle represents the final back buffer render. The blue rectangle represents what is stored in the shadow map from the perspective of the light. Note how only certain parts of the gray rectangle behind the orange sphere are in shadow.

There are some common artifacts when you render a scene with shadow maps. One is related to the surface format used to store the depth values. The value you use limits how precisely your scene can determine distances from the light source. This leads to incorrect shadows around areas that have similar distances from the light source. This problem is most evident on faces that cast shadows on themselves—an artifact called shadow acne. The following figure is an example of shadow acne. Note that a bias normally is used to offset the values to correct some of these issues.

Another common artifact stems from the limit of pixels in the render target used and the area that the render target covers. If you are rendering a large area to a single render target, as we are in this sample, the amount of space covered by a single texel can be quite large. As you zoom closer into a shadow, you will notice large blocks around the edges that should be smoothed. This condition, called aliasing, is caused by the lack of precision the shadow map has over the area. The following figure shows an example of aliasing.

Another artifact occurs only when you draw a part of an object onto the shadow map, which results in holes in the shadow. Often this happens when an object is on the edge of the shadow map. In this sample, it happens when the camera moves behind the model; the camera's frustum does not contain parts of the model. Thus, the shadow map does not contain parts of the model either as the following example shows.

Sample Overview

Remember that the first step of the shadow mapping algorithm is to render the scene from the point of view of the light. To do this, you need a view and projection matrix. To make the most efficient use of the resolution in our render target, the view and projection matrices should correspond to a frustum that is as small as possible. In this sample, calculate the view and projection matrices that correspond to the smallest possible bounding box around the camera's view frustum. This enables the shadow map to adapt in such a way that it contains only what is visible to the user. At the same time, it is still able to cover the entire viewable area.

The preceding figure shows the camera's frustum. The corners of the camera frustum are used to find the smallest bounding box that will fit the frustum in the direction of the light. Although this technique is a basic shadow mapping technique for a directional light, it suffers from a number of limitations, which are discussed later in Limitations of the Sample.

Sample Controls

This sample uses the following keyboard and gamepad controls.

Action Keyboard control Gamepad control
Rotate the camera UP ARROW, DOWN ARROW, LEFT ARROW, and RIGHT ARROW Right thumb stick
Move the camera W, S, A, and D Left thumb stick
Exit the sample ESC or ALT+F4 BACK

How the Sample Works

The sample has four main parts:

  • Creating the render target and depth buffer for the shadow map
  • Calculating the light's view projection matrix CreateLightViewProjectionMatrix
  • Creating the shadow map CreateShadowMap
  • Drawing the scene by using the shadow map DrawModelsWithShadow

In order to create a texture that stores the depth of the objects in the scene, you need to use RenderTarget2D to create a render target for that purpose. The sample uses a large floating point texture SurfaceFormat.Single with width and height of 2048. This gives you more precise results when you render it to the shadow map. Also, you need to use DepthStencilBuffer when you write to the shadow map. This stores the z values of the rendered scene, which enables you to store only the closest point to the light in your shadow map.

To calculate the view projection matrix of the light source, CreateLightViewProjectionMatrix finds the least fitting BoundingBox around the camera's frustum in the direction of the light. It does this by rotating the camera’s frustum corners by the direction of the light. This puts the corners in light space. The light's position is then calculated by finding the middle of the back panel of the bounding box where the back of the box is the Min Z of the box.

To create the view matrix, transform the light’s position from light space into world space by using the inverse of the light's rotation. The view matrix is then constructed using the light's position and the direction of the light from its position. The projection is calculated from the size of the bounding box created earlier with the X direction being the width, the Y direction being the height, and the Z direction being the near and far plane distances. Since you are using a directional light, you should use an orthographic matrix. Because the light rays are parallel, the shadows need to be parallel, too. The light’s view and projection matrices are multiplied together to get the light’s view projection matrix.

To create the shadow map, CreateShadowMap first calls SetRenderTarget to set the render target on the graphics device. Then save the current depth stencil buffer, and set the shadow depth stencil buffer on the graphics device. The render target is then cleared to white, since 1 would be the farthest away an object could be in the shadow map. Anything closer than the far plane will set a lower value in the shadow map. The CreateShadowMap technique in the shader draws all of the geometry that will cast shadows in the scene. The CreateShadowMap_VertexShader moves the vertices into light space. The CreateShadowMap_PixelShader then writes the depth value out to the shadow map.

Finally, you need to render the scene from the camera’s perspective. Use DrawWithShadowMap for this purpose. Use the DrawWithShadowMap technique in the shader to draw each of the models in the scene. DrawWithShadowMap_VertexShader transforms the vertices by the world matrix and stores the result in Output.WorldPos. This value is then used in DrawWithShadowMap_PixelShader to determine the location of the pixel in light space. The position of the pixel being drawn is then compared to the value stored in the shadow map, which is also in light space. If the depth of the pixel is greater than that of the value stored in the shadow map, you know the pixel being drawn is behind something else—an occluder—that is rendered to the shadow map. Therefore, you know the pixel should be in the shadows.

Limitations of the Sample

The technique demonstrated is very basic and has visible artifacts. As a consequence of using the camera's frustum as the basis of what should be in the shadow map, the entire scene rendered to the shadow map has the same precision. This leads to objects that are far away from the camera having the sample amount of pixels in the shadow map. Objects that are close to the camera need to have a higher precision to prevent aliasing. There are a number of techniques that address the precision and aliasing issues found in this sample. If you want to make changes, you have two options. You can increase the size of the shadow map, or you can shrink the size of the viewing frustum.

Objects outside the view frustum are not always drawn to the shadow map, which leads to off-screen objects that do not cast shadows into the viewable area. An easy but not perfect solution is to expand the bounding box created for the light's projection matrix so it includes a larger area. There are more advanced solutions that find all of the possible occluders and create a bounding box that will fit them and the viewing frustum correctly.