Click or drag to resize
DigitalRuneShadow Maps

This article covers how shadow maps are created.

This topic contains the following sections:

ShadowMapRenderer

DigitalRune Graphics provides the ShadowMapRenderer for creating shadow maps.

Shadow maps are created by calling Render. For example:

C#
// Select render pass (required by MeshRenderer).
context.RenderPass = "ShadowMap";
// Render shadow maps for the specified list of light nodes.
shadowMapRenderer.Render(sceneNodes, context);

The ShadowMapRenderer takes a list of scene nodes and looks for LightNodes with a Shadow. LightNodes are sorted based on the shadow map type. Then the shadow maps are rendered: For each shadow map, the ShadowMapRenderer prepares a render target and sets the required states. It fetches all shadow casters (meshes, etc.) and renders them into the render target. The resulting shadow maps are stored in ShadowShadowMap.

This method is usually called once per frame – but it is also possible to reuse shadow maps from previous frames (see Shadow Map Caching below).

Shadow map types

The built-in renderer supports StandardShadows, CubeMapShadows, CascadedShadows and CompositeShadows. Support for other shadow types can be added by implementing a new renderer and adding it to the Renderers collection of the ShadowMapRenderer. (See VarianceShadowMapSample in the Samples.)

Render callback

The ShadowMapRendererRenderCallback can be used to define which shadows casters are selected and how they are rendered. If no render callback is set, a default implementation will be used.

The callback can be set when the ShadowMapRenderer is created. Here is an example (similar to the default implementation):

C#
meshRenderer = new MeshRenderer();
shadowMapRenderer = new ShadowMapRenderer(context =>
{
  var query = context.Scene.Query<ShadowCasterQuery>(context.CameraNode, context);
  if (query.ShadowCasters.Count == 0)
    return false;

  meshRenderer.Render(query.ShadowCasters, context);
  return true;
});

In this implementation SceneQueryT is called to get all scene nodes that intersect the camera frustum. (Note: When the render callback is executed, RenderContextCameraNode is a camera representing the light source and not the player.)

The ShadowCasterQuery checks the scene node flags SceneNodeCastsShadows and SceneNodeIsShadowCasterCulled. CastsShadows determines whether a scene node can cast any shadows. IsShadowCasterCulled might be set when occlusion culling is enabled. It indicates whether the shadow is visible from the player's point of view or whether the shadow is occluded. The ShadowCasterQuery also evaluates LOD nodes.

Then a MeshRenderer is used to render the shadow casters into the shadow map.

A user-defined render callback can use a different method to select shadow casters and can use other renderers to draw objects into the shadow map.

Shadow map shaders

In the first examples the RenderPass is set to "ShadowMap" before Render of the ShadowMapRenderer is called. That means, the MeshRenderer in the render callback uses the "ShadowMap" pass to render meshes into the shadow map. This render pass must be present in the material (.drmat file) of the mesh, for example:

<?xml version="1.0" encoding="utf-8"?>
<Material>
  <Pass Name="Default" Effect="SkinnedEffect" Profile="Any">
    …
  </Pass>

  <!-- Render pass which renders a mesh into a shadow map. -->
  <Pass Name="ShadowMap" Effect="DigitalRune/Materials/ShadowMapSkinned" Profile="HiDef" /> 

  <Pass Name="GBuffer" Effect="DigitalRune/Materials/GBufferNormalSkinned" Profile="HiDef">
    …
  </Pass>
  <Pass Name="Material" Effect="DigitalRune/Materials/MaterialSkinned" Profile="HiDef">
    …
  </Pass>
</Material>

Meshes without a "ShadowMap" pass will be ignored and won't be rendered into the shadow map. DigitalRune Graphics contains several predefined effects for rendering shadow maps, supporting alpha-tests, mesh skinning and morphing.

The depth that is written to the shadow map is the normalized light space z value (= distance to the shadow near plane) – except for omnidirectional shadow maps of point lights, where the depth value is the normalized linear distance to the point light. For cascaded shadow maps triangles in front of the cascade are clamped to 0 to make better use of the available depth map precision. This is called "pancaking". When using pancaking we found that 16-bit shadow maps are usually enough. (The property ShadowPrefer16Bit is set by default.)

The "ShadowMap" effects (.fx files) define whether front faces (= triangles facing the light source) or back faces are rendered into the shadow map. The predefined "ShadowMap" effects render front faces.

Some games render back faces, which has the advantage of moving shadow acne problems to the back of a mesh. Back faces are usually not facing the light, hence they are dark and acne is less of a problem. When rendering back faces, shadow bias values must be negative. Avoiding shadow acne by rendering back faces does not work for thin meshes, such as alpha-tested grass. (In the end, we did not find any relevant benefits of rendering back faces and chose to render front faces as the default.)

Shadow map caching

The Shadow class stores the shadow map (property ShadowMap) and all necessary information (e.g. the light's view and projection matrix) for reuse in the next frame. That means, it is not necessary to update all shadow maps every frame.

Further, the flag CascadedShadowIsCascadeLocked can be used to "lock" individual cascades of a cascaded shadow map. Locked cascades will not be updated by the ShadowMapRenderer, the cached shadow map will be reused until the cascade is unlocked.

Shadow map caching can be used to improve performance. For example:

  • Reuse shadow maps from the previous frames if there are no moving objects in the light.
  • Update shadow maps of distant lights less frequently.
  • Update distant cascades less frequently.
  • Do not update all shadow maps in the same frame. Distribute the update of different shadow maps or cascades over several frames.
Recycling shadow maps

To support shadow map caching, shadow maps are not recycled automatically. When the shadow maps are no longer required, they must be recycled manually:

C#
foreach (var node in sceneNodes)
{
  var lightNode = node as LightNode;
  if (lightNode != null && lightNode.Shadow != null)
  {
    renderTargetPool.Recycle(lightNode.Shadow.ShadowMap);
    lightNode.Shadow.ShadowMap = null;
  }
}

If a CompositeShadow is used, you have to enumerate the child shadows:

C#
if (shadow is CompositeShadow)
    foreach(var child in shadow.Shadows)
      … recycle shadow map …

When shadow map caching is not used, it is usually best to recycle the shadow maps each frame as soon as they are no longer needed.