Reducing Noise in DirectX Ray-Traced Shadows

Real-time ray-traced shadows generated via DirectX Raytracing (DXR) often suffer from severe Monte Carlo noise due to strict per-frame ray budget limits. Reservoir-based Spatiotemporal Importance Resampling (ReSTIR) minimizes this noise by recycling shadow ray candidates across neighboring pixels and previous frames. By combining Resampled Importance Sampling (RIS) with lightweight reservoir data structures, DXR pipelines can evaluate hundreds of candidate light samples per pixel while only tracing one or two actual shadow rays, dramatically reducing penumbra noise and convergence time.

The Challenge of Low-Sample Ray Tracing

In DirectX Raytracing, casting multiple shadow rays per pixel to resolve soft shadows from area lights is computationally prohibitive for high-frame-rate rendering. Tracing only one or two rays per pixel (1–2 SPP) yields high-variance, stippled noise in soft shadow penumbras. Traditional spatial-only denoisers often blur geometry edges or over-smooth contact shadows when attempting to filter this variance. ReSTIR addresses this at the sampling stage rather than relying purely on post-process filtering.

Understanding Reservoirs and Resampled Importance Sampling

A reservoir is a compact, fixed-size data structure that holds:

  • A selected light sample (candidate ray direction/target light source)
  • The sum of weights of all candidate samples considered so far (\(w_{sum}\))
  • The total count of processed candidates (\(M\))
  • The final resampling weight (\(W\))

Using Resampled Importance Sampling, an initial candidate ray is generated. Instead of committing to tracing all potential samples against the scene geometry, candidates are mathematically combined into the reservoir using their unshadowed contribution as an importance metric. This allows a pixel to effectively evaluate a large distribution of samples using minimal memory and processing overhead.

Temporal Reservoir Reuse

Temporal reuse minimizes noise across successive frames by capitalizing on frame-to-frame coherence:

  1. Reprojection: Using motion vectors and depth buffers generated by the G-buffer pass in DirectX, the engine projects the current pixel back to its corresponding location in the previous frame.
  2. History Combination: The current frame's reservoir is combined with the reprojected previous frame's reservoir.
  3. Weight Normalization: The combined reservoir merges their weights (\(w_{sum}\)) and sample counts (\(M\)). To prevent historical samples from dominating dynamic scene changes or disocclusions, \(M\) is typically clamped to a fixed threshold (e.g., 20–30 frames).

This process compounds the effective sample count over time, turning high-frequency temporal flickering in penumbras into a stable, converging soft shadow.

Spatial Reservoir Reuse

Temporal reuse alone cannot resolve noise in newly disoccluded regions or fast-moving shadow edges. Spatial reuse solves this by sharing reservoirs between neighboring pixels within the same frame:

  1. Neighbor Gathering: A compute shader gathers reservoirs from randomly chosen pixels within a defined screen-space radius.
  2. Geometric Validation: To prevent shadow bleeding across depth discontinuities, cross-bilateral weights are applied. Samples are rejected or down-weighted if the neighbor's world-space normal or linear depth diverges significantly from the current pixel.
  3. Stream Merging: Valid neighbor reservoirs are statistically merged into the current pixel's reservoir.

This step acts as an intelligent, visibility-aware filter in sample space rather than color space, rapidly smoothing noise without blurring sharp contact shadow edges.

Execution Within the DirectX Pipeline

In a modern DirectX 12 rendering engine, spatial and temporal reservoir reuse is integrated through structured buffers (UAVs) and compute shaders flanking the DXR dispatch:

  • Generation Pass: A DXR ray generation shader or compute pass evaluates initial candidate lights and writes them into a temporal reservoir buffer.
  • Temporal Pass: A compute shader reads the previous frame's reservoir UAV and blends it with the current generation output.
  • Trace Pass: A lightweight DXR TraceRay() call evaluates visibility only for the single best candidate retained in the reservoir.
  • Spatial Pass: One or more compute shader dispatches perform spatial gathering across the visibility-tested reservoirs.

By moving the noise reduction into the sampling and visibility selection phase, spatial and temporal reservoir reuse achieves clean, physically plausible area light shadows with single-ray budgets in modern real-time rendering.