Screen Space Contact Shadow Algorithms in Games
Screen-space contact shadows (SSCS) are a vital real-time rendering technique used in game development to ground dynamic objects and resolve the visual detachment—often called "Peter Panning"—caused by standard shadow map bias. By casting short-range rays directly across the camera’s screen-space depth buffer toward a light source, rendering engines can generate high-frequency micro-shadows at geometry contact points. This article breaks down the primary algorithms and optimization techniques employed to implement contact shadows from depth buffers in modern game engines.
Uniform Screen-Space Linear Ray Marching
The foundational algorithm for screen-space contact shadows is uniform linear ray marching. In this approach, each visible pixel's world-space position is determined by unprojecting the depth buffer value. The engine then projects a ray from the surface position toward the light source directly into screen space (UV and depth coordinates).
The ray marches forward in fixed-size increments along the 2D screen vector while computing the expected linear depth at each step. At every increment, the algorithm samples the screen-space depth buffer and compares the stored depth against the ray’s current depth:
- Intersection: If the ray's depth is greater than the sampled surface depth (meaning the ray is behind a rendered surface), an occlusion is registered, and the pixel is shadowed.
- Miss: If the ray travels its maximum defined distance without intersecting the surface, the pixel remains unshadowed.
Because linear ray marching performs an identical step count per pixel regardless of scene complexity, it is computationally predictable but can miss thin occluders if the step size is too coarse, or cause performance bottlenecks if the step size is too fine.
Hierarchical-Z (Hi-Z) Ray Tracing
To accelerate ray traversal and reduce depth texture bandwidth, many engines implement Hierarchical-Z (Hi-Z) ray tracing. This algorithm constructs a mipmap pyramid of the depth buffer, where each higher mip level stores the minimum (or maximum, depending on the depth convention) depth value of a 2x2 pixel footprint.
Instead of stepping linearly across full-resolution depth, the Hi-Z algorithm navigates this spatial hierarchy:
- The ray begins traversal at an intermediate or fine mip level.
- The algorithm checks whether the ray intersects the bounding box of the current cell.
- If the ray does not intersect the cell's conservative depth bounds, the algorithm advances the ray to the boundary of the cell and moves up to a coarser mip level to skip empty space.
- If an intersection is detected within the cell's bounds, the algorithm steps down to a finer mip level to locate the exact intersection surface.
Hi-Z tracing drastically reduces the number of texture reads required to trace long rays, though the traversal overhead often makes it more suitable for screen-space reflections, while contact shadows frequently rely on optimized short-range linear variants.
Dithered Ray Marching with Temporal Filtering
To maintain high performance without sacrificing visual fidelity, game developers frequently utilize dithered ray marching combined with Temporal Anti-Aliasing (TAA). Instead of running dozens of samples per pixel, engines limit the ray steps to a small budget—often between 4 and 16 steps per ray.
To hide the resulting spatial banding artifacts, a pseudo-random or blue-noise offset is added to the starting position of each ray. This converts coherent banding into high-frequency noise. The noisy shadow mask is then resolved over multiple frames using temporal reprojection, which blends current and previous frames using motion vectors. This pipeline yields smooth, artifact-free contact shadows at a fraction of the computational cost of brute-force sampling.
Thickness Heuristics and Occlusion Falloff
Depth buffers represent only a 2.5D capture of the scene, containing only the frontmost surfaces visible to the camera with no information regarding back-face geometry or real-world thickness. A standard depth comparison assumes that any geometry extends infinitely into the screen, which can cause background objects to cast massive, incorrect shadows.
Contact shadow algorithms resolve this by introducing a thickness threshold:
\[\text{Occlusion} = (\text{RayDepth} > \text{SampledDepth}) \land (\text{RayDepth} - \text{SampledDepth} < \text{Threshold})\]
If the ray penetrates beyond this thickness threshold, the algorithm assumes the ray has passed behind the object rather than through it, treating the sample as unoccluded. Engines also apply a distance-based attenuation function that smoothly fades out the contact shadow as the ray approaches its maximum screen distance, blending it seamlessly into standard cascaded shadow maps.
Cross-Bilateral Filtering
After ray marching, a spatial post-processing pass is often applied to clean up any remaining high-frequency dithering patterns. Standard Gaussian blurs cannot be used because they cause shadows to bleed across disconnected geometry edges.
Instead, a cross-bilateral blur is employed. This filter weights neighboring shadow samples not only by screen distance, but also by linear depth and world-space normal differences. Samples that deviate significantly in depth or normal from the center pixel are discarded, preserving sharp, physically accurate shadow boundaries against geometry edges while smoothing flat surfaces.