Shadow Caster Culling |
Rendering shadow maps can be expensive in large scenes. Directional lights, like the sun, usually cover the entire level. Modern shadow mapping algorithms, such as cascaded shadow maps, limit the distance up to which shadows are rendered. Still, this may leave us with hundreds or thousands of shadow casting objects that need to be rendered into a shadow map. Shadow caster culling reduces the number of shadow casters further by removing shadow casters that do not contribute to the final image.
This topic contains the following sections:
The OcclusionBuffer implements shadow caster culling for the main directional light source. It is activated by passing the directional light node to the Render method. Multiple directional lights with shadows are not supported.
// Render the occluders into the occlusion buffer. // lightNode is the main directional light that casts shadows. occlusionBuffer.Render(occluders, lightNode, context); // Perform occlusion culling and shadow caster culling on the // list of scene nodes. occlusionBuffer.Query(sceneNodes, context);
Shadow-casting nodes that are culled are internally marked as hidden. The ShadowMapRenderer will automatically skip these scene nodes.
For background information about shadow caster culling see Stephen Hill and Daniel Collin: "Practical, Dynamic Visibility for Games", GPU Pro 2 and Nick Darnell: "Hierarchical Z-Buffer Occlusion Culling – Shadows"
Shadow caster culling involves several steps:
Light Frustum Culling: As a first step the bounds of the shadow casters are compared with the light frustum. Shadow casters that do not intersect with the light frustum are marked as hidden.
(Note: A spotlight can be represented as a single frustum; an omnidirectional point light as six 90° frustums. In case of a directional light, the light frustum is an orthographic frustum, which needs to be large enough to cover the viewing frustum and catch all shadow casters in front of the light source.)
Light Occlusion Culling: The occluders are rendered from the light's perspective and the bounds of the shadow casters are tested against the occluders. Shadow casters that are occluded from the light's perspective are marked as hidden.
Camera Frustum Culling: Next, the extent of the shadow is estimated. The resulting shadow volume is compared with the viewing frustum. If the shadow volume does not intersect with the viewing frustum, the shadow caster is marked as hidden.
Camera Occlusion Culling: The shadow volume is tested against the occluders from the camera's perspective. When the shadow volume is occluded, the shadow caster is marked as hidden.
Shadow Caster vs. Receiver Test: (1) - (4) gets rid of most shadow casters that do not contribute to the final image. However, there is still a chance that a shadow "just passes through" the view and that the shadow is not cast on an object visible inside the viewing frustum. Bittner et al. have proposed a culling method that solves this issue (see "Shadow Caster Culling for Efficient Shadow Mapping"):
This step assumes that we have already performed occlusion culling for the objects in the scene. Only objects that are visible from the main camera are potential shadow receivers. We can render all potential shadow receivers as seen from the light source into a mask. Then we can compare the bounds of the shadow casters with this mask. Only shadow casters that overlap with a visible shadow receiver needs to be rendered into the shadow map.
The new OcclusionBuffer implements (1) - (4) for one directional light, which is usually the sun. OcclusionBufferRender creates a HZB for the active camera (which is set in the render context) and a HZB for the directional light. OcclusionBufferQuery performs occlusion culling and shadow caster culling on the given list of scene nodes. Shadow casters that are culled are marked, see flag SceneNodeIsShadowCasterCulled. This flag is automatically check by the ShadowCasterQuery. In custom scene queries this flag has to be checked explicitly.
Step (5) is not implemented. It is also not desirable in some cases, for example: When a single shadow receiver stretches across the entire level (e.g. a landscape mesh), then (5) has no effect. When rendering volumetric effects, (5) might remove shadow casters that are actually needed in the shadow map.
Local lights (point lights, spotlights, …) are culled as a whole as part of the regular occlusion culling. The remaining, visible lights are usually small and have a manageable number of shadow casters – shadow caster culling is not necessary/efficient.
The OcclusionBuffer supports two modes:
The property ProgressiveShadowCasterCulling determines whether progressive shadow caster culling is active.
"Progressive" shadow caster culling is more aggressive than "conservative" shadow caster culling, but may cause problems: In some cases it is not possible to estimate the correct extent of the shadow volume. A shadow caster might be culled, even though its shadow should be visible. Shadows can start to flicker.
The problem is caused by steps (3) and (4) because they require the extent of the shadow volume. The bounds of the shadow caster are defined, but the extent of the shadow volume, i.e. the distance at which the shadow ends in the scene, is not available. It is estimated by reading the maximum depth from the HZB during occlusion culling. Using this solution the extent of the shadow volume is the distance from the shadow caster to the furthest occluder behind the shadow caster. However, this method is unreliable if an object is both a shadow caster and an occluder at the same time. In this situation the following problems can occur:
In these cases the estimated extent of the shadow volume is too small and the shadow caster may be culled, which causes flickering or missing shadows. Both cases are rare, but they can be examined in the OcclusionCullingSample (see Samples).
The following solutions can be applied to prevent missing/flickering shadows: