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:
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);
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:
- 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.
- 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.
- 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
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.oloandDDGIVisualEvidenceTestboth 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:
OloEditor/assets/shaders/DDGI_Relight.glslcomputes the bounce term as the previous frame's irradiance at the cached hit point:ddgiSampleIrradiance(OloEditor/assets/shaders/include/DDGICommon.glsl) opens with: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.
bounceEreturns 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::EstimateIrradianceon a scene built from the same box table (DDGIReferenceParityTest):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.glslpins its storage convention in a header comment ("the atlas stores full irradiance E", via the ratio estimatorE = π·Σ(wL)/Σ(w)), which is exactly what the tracer integrates.Confirmed by construction
DDGIReferenceParityWideVolumeTestruns the identical room, lights and reference, changing only the volume bounds so the wall slabs fall inside it: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
ddgiIsInsideVolumeis 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: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