ReSTIR — Reservoir-based Spatiotemporal Importance Resampling

  • Bitterli et al. 2020 — one of the most impactful real-time rendering papers.
  • Enables high-quality direct lighting with thousands of lights at real-time rates.
  • Parent: PathTracer Learning

The Problem ReSTIR Solves

  • Direct lighting with many lights
    • Naive: sample one random light per pixel → high variance with many lights
    • Better: sample proportional to light contribution → need to evaluate all lights
    • ReSTIR: efficiently find good light samples by resampling candidates
  • Key insight
    • Resampled Importance Sampling (RIS): generate M candidates, select 1 proportional to target PDF
    • Reservoir: compact data structure to maintain RIS state
    • Temporal reuse: reuse reservoir from previous frame (free extra samples)
    • Spatial reuse: share reservoirs with neighbors (more free samples)

Resampled Importance Sampling (RIS)

  • Goal: sample from target distribution p̂(x) when direct sampling is hard
  • Algorithm
    • Generate M candidates {x_1, ..., x_M} from source distribution q(x)
    • Select one candidate with probability proportional to w_i = p̂(x_i) / q(x_i)
    • The selected sample approximates sampling from
  • Weight formula
    • W = (1/M) * Σ w_i — sum of weights
    • Unbiased contribution weight: W_selected = W / p̂(x_selected)
  • In practice for direct lighting
    • q(x) = uniform over all lights (easy to sample)
    • p̂(x) = L_e(x) * G(x) * V(x) — emission × geometry × visibility
    • Visibility V(x) is expensive — defer to final evaluation

The Reservoir Data Structure

  • Compact structure per pixel
    struct Reservoir {
        LightSample y;      // current selected sample
        float w_sum;        // sum of weights seen so far
        int M;              // number of candidates processed
        float W;            // unbiased contribution weight
    };
    
  • Streaming update (one candidate at a time)
    void update(Reservoir r, LightSample x, float w):
        r.w_sum += w
        r.M += 1
        if random() < w / r.w_sum:
            r.y = x   // replace selected sample
    
  • Merging two reservoirs
    Reservoir merge(Reservoir a, Reservoir b):
        combined = new Reservoir()
        update(combined, a.y, p̂(a.y) * a.W * a.M)
        update(combined, b.y, p̂(b.y) * b.W * b.M)
        combined.M = a.M + b.M
        combined.W = combined.w_sum / (p̂(combined.y) * combined.M)
        return combined
    

ReSTIR DI Algorithm (per frame)

  • Pass 1: Initial candidates
    • For each pixel: sample M=32 light candidates from q (uniform)
    • Store in reservoir
  • Pass 2: Temporal reuse
    • Reproject previous frame’s reservoir to current pixel
    • Merge current reservoir with reprojected reservoir
    • Cap M to prevent unbounded growth (M_max = 20 typically)
  • Pass 3: Spatial reuse
    • For each pixel: sample K=5 neighboring pixels
    • Merge their reservoirs into current pixel’s reservoir
    • Apply MIS weights to avoid bias from different pixel domains
  • Pass 4: Shading
    • Evaluate visibility for final selected sample (one shadow ray)
    • Compute shading: L = f_r * L_e * G * V * W

Bias in ReSTIR

  • Naive spatial reuse is biased
    • Neighbor’s reservoir was selected for neighbor’s domain, not current pixel
    • Using it directly introduces bias (incorrect PDF)
  • Correction methods
    • MIS-based resampling (Talbot 2005) — correct but expensive
    • Pairwise MIS (Bitterli 2022) — efficient and unbiased
    • 1/M heuristic — biased but fast, often acceptable

ReSTIR GI (Global Illumination)

  • Extends ReSTIR to indirect lighting
  • Each reservoir stores a full path segment (not just a light)
  • Temporal and spatial reuse of path segments
  • Much more complex bias correction needed
  • Paper: “ReSTIR GI: Path Resampling for Real-Time Path Tracing” (Ouyang et al. 2021)

Implementation Notes

  • Two reservoir buffers needed (ping-pong for temporal)
  • Spatial reuse needs neighbor reservoirs from same frame (not current pass)
  • Use a separate spatial pass reading from temporal output
  • Visibility reuse: cache shadow ray results to avoid redundant traces