Skip to content

Renderer: DDGI's infinite-bounce term is silently dead when the probe volume excludes the walls #751

Description

@drsnuggles8

Found by the offline reference path tracer (#709 / #750) on its first run against DDGI.

Summary

A DDGI probe volume fitted to a room's air silently disables the entire infinite-bounce feedback term. Probe irradiance comes out at roughly half of ground truth, with no visual signature beyond "the GI looks a bit dim".

Air-fitted is the natural authoring — and it is what OloEditor/SandboxProject/Assets/Scenes/DDGITest.olo and DDGIVisualEvidenceTest both do today (volume [-7, 0.5, -5] … [7, 5.5, 5] inside walls at ±8 / ±6).

Mechanism

A two-line chain, invisible from either end:

  1. OloEditor/assets/shaders/DDGI_Relight.glsl computes the bounce term as the previous frame's irradiance at the cached hit point:
    vec3 bounceE = ddgiSampleIrradiance(u_PrevIrradiance, u_CurrVisibility, u_ProbeData,
                                        hitPos, N, -texelDir);
  2. ddgiSampleIrradiance (OloEditor/assets/shaders/include/DDGICommon.glsl) opens with:
    if (!ddgiIsInsideVolume(worldPos))
    {
        return vec3(0.0);
    }

Every cached hit point is on a surface. A volume fitted to the interior air therefore excludes every wall, floor and ceiling in the room — which is to say every surface the bounce light was supposed to come from. bounceE returns zero for all of them, on every probe, every frame.

Neither line is wrong in isolation. The volume test is there for a good reason (a lookup outside the grid has no probes to interpolate), and reading the previous atlas at the hit point is the correct formulation of the feedback. It is the interaction with a plausible authoring choice that produces the failure.

Measurements

Enclosed room (~8 × 4 × 8), one point light, albedos 0.55–0.6, 60 convergence frames. Probe irradiance read straight from the pass's FP16 atlas, compared against PathTracer::EstimateIrradiance on a scene built from the same box table (DDGIReferenceParityTest):

probe DDGI reference (4 bounces) reference (direct only) DDGI/full DDGI/direct
(0, 0.8, 0) 0.230 0.446 0.246 0.52 0.93
(0, 3.2, 0) 0.238 0.443 0.237 0.54 1.003
(−3, 3.2, 0) 0.342 0.560 0.341 0.61 1.005
(3, 3.2, 0) 0.097 0.236 0.098 0.41 0.989

DDGI's direct transport is essentially exact — within 1% at three of four probes. It contributes zero multi-bounce.

The comparison is in physical units with no fudge factor: DDGI_BlendIrradiance.glsl pins its storage convention in a header comment ("the atlas stores full irradiance E", via the ratio estimator E = π·Σ(wL)/Σ(w)), which is exactly what the tracer integrates.

Confirmed by construction

DDGIReferenceParityWideVolumeTest runs the identical room, lights and reference, changing only the volume bounds so the wall slabs fall inside it:

volume DDGI/direct DDGI/full
fitted to the air 0.93 – 1.005 0.41 – 0.61
enclosing the walls 1.74 1.12

With the walls inside, the bounce term comes alive and DDGI lands within 12% of full multi-bounce ground truth — a perfectly respectable result for a real-time probe field. So the machinery works; it is only ever being handed hit points it refuses to answer for.

Why this wasn't fixed in #750

ddgiIsInsideVolume is also the guard that keeps the feedback loop contractive (docs/adr/0007-ddgi-hit-point-cache-gather.md). Any fix trades against that stability, so it is a design call for whoever owns #632 rather than a patch to land alongside an unrelated instrument. Options, roughly in increasing order of invasiveness:

  1. Authoring rule only — document "enclose the surfaces you want bounce light from" and enforce it (an editor warning when a volume's bounds do not contain the geometry its probes see). Zero risk, but relies on every author getting it right.
  2. Clamp the bounce lookup instead of zeroing it — sample at the nearest point inside the volume rather than returning 0. Keeps the interpolation well-defined; needs thought about whether it stays contractive at the boundary.
  3. Bias-margin the sample volume for the feedback path only — expand the inside-test by roughly the probe spacing for bounceE (not for the lit-pass sampler). Most targeted, and the boundary probes' Chebyshev visibility already handles the leak case.

Whatever is chosen, DDGIReferenceParityTest / DDGIReferenceParityWideVolumeTest (landing in #750) pin the behaviour from both sides: fix the bounce term and the wide-volume test keeps passing while the air-fitted assertions start failing against the direct-only reference — i.e. the tests will tell you it worked.

Meanwhile

Author DDGI volumes to enclose the surfaces you want bounce light from, not just the space the camera moves through. Recorded in docs/agent-rules/reference-path-tracer.md §5.

Related: #632 (realtime DDGI), #707 (DDGI v2), #709 / #750 (the reference path tracer that found this).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrobustnessProduction hardening / shipping robustness

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions