How To: Implement Shadow Mapping

This example demonstrates how to use depth textures (that is, shadow maps) to create dynamic shadows in a scene. These shadows move when a light source moves, and are accurate against both flat and irregular surfaces.

To create shadows with shadow mapping, you first create a depth texture showing the depth of all objects in the scene from the point of view of the light source. Then when rendering pixels in your final scene from the camera's point of view, you compare the distance from the pixel to the light source with the depth value encoded in the shadow map. When the depth of the rendered pixel is higher than the value in the shadow map, that pixel is in shadow.

To render the scene, this sample uses a technique from a customized Effect file. For more information, see How To: Draw a Model with a Custom Effect.

The Complete Sample

The code in this tutorial illustrates the technique described in the text. A complete code sample for this tutorial is available for you to download, including full source code and any additional supporting files required by the sample.

To Create a Shadow Map Effect

  1. First, load a custom effect that supports shadow mapping. In this example, the effect has two major techniques: one for creating a shadow map, and one for rendering a scene using the shadow map to draw shadows. A simple third technique is used to draw the light source. The effect is encapsulated in its own class.

    MyEffect = ShadowMapEffect.LoadEffect(Content);
  2. In your game's LoadContent method, load the Models and Effect for your scene. Remap the Models to use your custom Effect according to How To: Draw a Model with a Custom Effect.

  3. In LoadContent, create a RenderTarget2D and DepthStencilBuffer for your shadow map using the techniques described in How To: Create a Depth Texture.

  4. In your game's Update method, set the position of your light source and your camera as parameters to the Effect. You must also set the view and projection matrices for both the shadow map and the camera itself.

    MyEffect.mLightView.SetValue(Matrix.CreateLookAt(LightPos, bounds.Center, Vector3.Up));
  5. Next, in your game's Draw method, create a shadow map for the scene using the techniques described in How To: Create a Depth Texture.

    map = CreateShadowMap();
  6. After creating the depth texture in Draw, set it into an appropriate sampler value in your shadow mapping effect.

    // Set our shadow map into our effect
  7. Next, draw your scene using a shader that supports shadow mapping. The shader will apply ambient light only where the light casts a shadow.

    // Render our scene

To Render Shadows in HLSL with a Shadow Map

  1. Rendering shadows with a shadow map in HLSL requires per-pixel lighting. The vertex shader for this effect performs a normal world/view/projection transformation on the POSITION supplied by the video card. It also transforms the NORMAL. Both of these values are passed as output. The UV coordinate for the texture is passed through to the pixel shader, along with a copy of the untransformed POSITION value.

    VS_OUTPUT RenderShadowsVS(
         float3 position : POSITION,
         float3 normal : NORMAL,
         float2 vTexCoord0 : TEXCOORD0 )
         VS_OUTPUT Output;
         //generate the world-view-projection matrix
         float4x4 wvp = mul(mul(g_mWorld, g_mCameraView), g_mCameraProj);
         //transform the input position to the output
         Output.Position = mul(float4(position, 1.0), wvp);
         //transform the normal to world space
         Output.vNormal =  mul(normal, g_mWorld);
         //do not transform the position needed for the
         //shadow map determination
         Output.vPos = float4(position,1.0);
         //pass the texture coordinate as-is
         Output.TextureUV = vTexCoord0;
         //return the output structure
         return Output;
  2. The first task for the pixel shader is calculating the normal lighting equation for the pixel. In this example, only diffuse lighting is calculated.

    PS_OUTPUT RenderShadowsPS( PS_INPUT In ) 
        PS_OUTPUT Output;
        // Standard lighting equation
        float4 vTotalLightDiffuse = float4(0,0,0,1);
        float3 lightDir = normalize(g_LightPos-In.vPos);  // direction of light
        vTotalLightDiffuse += g_LightDiffuse * max(0,dot(In.vNormal, lightDir)); 
        vTotalLightDiffuse.a = 1.0f;
  3. Next, the pixel shader gets the position of this pixel on the shadow map. This is necessary because the shadow map is rendered from the light's perspective, and this pixel is rendered from the camera's perspective. The result of GetPositionFromLight is a screen coordinate. This is then converted into a UV coordinate so it can be accessed from the sampler containing the shadow map.

    // Now, consult the ShadowMap to see if we're in shadow
    float4 lightingPosition = GetPositionFromLight(In.vPos);// Get our position on the shadow map
    // Get the shadow map depth value for this pixel   
    float2 ShadowTexC = 0.5 * lightingPosition.xy / lightingPosition.w + float2( 0.5, 0.5 );
    ShadowTexC.y = 1.0f - ShadowTexC.y;
  4. Next, the shadow depth of this pixel is accessed using the tex2D function to access the shadow map. Then the depth of this pixel is determined by dividing the Z value of the position with the W value. This is subtracted from 1.0 before comparison because the shadow depths are similarly subtracted from 1.0. For more information see CreateDepthTexture.

    float shadowdepth = tex2D(ShadowMapSampler, ShadowTexC).r;    
    // Check our value against the depth value
    float ourdepth = 1 - (lightingPosition.z / lightingPosition.w);
  5. Now the shadow depth and true depth are compared. Floating-point precision errors can cause a banding effect if the two values are compared directly, so the shadow depth is decreased slightly to ensure that pixels that should be lighted are lighted. If this pixel is indeed in shadow, the diffuse portion of the lighting is reset to black.

    // Check the shadowdepth against the depth of this pixel
    // a fudge factor is added to account for floating-point error
    if (shadowdepth-0.03 > ourdepth)
        // we're in shadow, cut the light
        vTotalLightDiffuse = float4(0,0,0,1);
  6. Lastly, the total lighting equation is computed by adding the diffuse and ambient components together. This is the output returned from the pixel shader.

        Output.RGBColor = tex2D(MeshTextureSampler, In.TextureUV) * (vTotalLightDiffuse + g_LightAmbient);
        return Output;

Community Additions