feat(renderer): distance-impostor reflection probes — raymarched probe reflections with real depth (#705) - #767
Merged
drsnuggles8 merged 3 commits intoAug 10, 2026
Conversation
…e reflections with real depth (#705) Reflection probes now bake a linear radial-distance cubemap alongside colour and the lit passes raymarch reflection rays against it per pixel (Szirmay-Kalos et al., Eurographics 2005 — re-derived), so a wall two metres away and the sky no longer reflect identically. Hybrid ladder: SSR (on-screen) -> distance-impostor probes -> global sky prefilter, emerging from existing pass order with zero SSR changes. - Bake: ReflectionProbe_Distance.glsl rasterizes the scene's opaque casters (borrowed from the DDGI enumeration via a new aux caster sink) into an RG32F cube-face target around the probe; readback builds a CPU ReflectionProbeDistanceField (R32F @128^2, far-plane miss sentinel, hand-built MAX-mip chain, dMax) stored on the probe's EnvironmentMap. - GPU: new TextureCubemapArray (engine + GL, glCopyImageSubData layer copies); ReflectionProbeArray manager owns the radiance/distance arrays, probe UBO (58), per-cluster probe bitmask SSBO (53) and the ReflectionProbeCull.comp cull on the existing 32x18x24 froxel grid (self-contained slice params, works with Forward+ off). - Shading: include/ReflectionProbes.glsl — cheap max-mip visibility reject, 32-step coarse march + 6 bisections, blended multi-probe result — consumed by both deferred variants and both forward PBR shaders via new calculateIBLPrefiltered/calculateCombinedAmbient- Prefiltered seams (BRDF split untouched). Cube-array slots stay slot-based under bindless via PublishTextureOffsetAndBind (the DDGI-atlas pattern). m_BlendDistance is finally consumed. - Scene::ApplyReflectionProbeOverride becomes diffuse-only when a global environment exists (full-trio override kept for no-sky scenes), so a probe miss falls back to the live sky rather than the dominant probe's own radiance. - Three raymarch traps found by the contract tests / evidence PNGs, all fixed and pinned: far-sentinel crossings reported as hits (all-sky probes), the march bound landing one inside-bias short (silent misses on head-on rays), and occluded starts bogus-hitting a captured occluder's shell (dark crescents on probe-hidden surfaces) — the march now skips leading outside samples until the ray re-enters the visible region. - Evidence: ReflectionProbeParallaxVisualEvidenceTest (corridor with colour-coded walls + mirror floor, 4 poses x 3 probe states, PNGs committed): positional reflection split 224.7 vs 7.6 on the legacy direction-only path; off-screen yellow wall reflected at (178,178,61) vs (51,51,51) probe-off. Both evidence tests now render without editor gizmo/grid/axis chrome. 19 CPU contracts incl. analytic-room convergence and a GLSL<->C++ constant parity pin. Full suite 5586/5587 (the known-red Atmosphere #754 only). New sandbox scene ReflectionProbeCorridor.olo; guide docs/agent-rules/distance-impostor-reflection-probes.md. Closes #705. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdded distance-impostor reflection probes with CPU distance fields, probe baking, GPU cubemap arrays, clustered culling, renderer integration, corridor visual evidence, and contract tests. ChangesDistance-impostor reflection probes
Sequence Diagram(s)sequenceDiagram
participant Scene
participant ReflectionProbeBaker
participant ReflectionProbeArray
participant ReflectionProbeCull
participant DeferredLightingPass
Scene->>ReflectionProbeBaker: Bake probe cubemap and collect casters
ReflectionProbeBaker->>ReflectionProbeBaker: Capture six-face distance field
Scene->>ReflectionProbeArray: Submit probe render data
ReflectionProbeArray->>ReflectionProbeCull: Dispatch clustered probe culling
ReflectionProbeArray->>DeferredLightingPass: Bind radiance, distance, UBO, and grid resources
DeferredLightingPass->>ReflectionProbeArray: Republish resources before lighting draw
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review pass on PR #767 — all findings verified valid and fixed: - ReflectionProbeArray: EnsureArrays rejects >MAX_PROBES loudly instead of silently clamping (a clamp would corrupt UBO layer indices); growth re-upload bounded by both old and new capacity; UploadLayer requires exact distance-mip agreement (a partial chain would leave a previous occupant's texels where the cheap reject samples); profiling scopes. - ReflectionProbeBaker: the distance-capture restore is a scope guard (also runs on throw) and restores the pre-capture viewport instead of leaving the 128^2 capture viewport active. - ReflectionProbeDistanceField: SampleNearest fails safe to the sky sentinel on an empty field; dropped the unused radiusOut lambda param. - OpenGLTextureCubemapArray: layer count validated against GL_MAX_ARRAY_TEXTURE_LAYERS; storage allocation error-checked so a failed create reports IsLoaded()==false; upload/copy/readback drain- then-check GL errors and fail the layer; destructor no-ops on failed construction (no bogus tracker untrack); GL_PACK_ALIGNMENT handled on readback + GLsizei bound checked; RGBA16F memory accounting counts the 8 B/texel resident size, not the 16 B/texel client upload. - TextureCubemapArray::GetStaticType is AssetType::None (constexpr noexcept) — reusing TextureCube could let AssetType-keyed dispatch mistake an array for a TextureCubemap. - Renderer3D: ReflectionProbeArray registered in DebugLiveGpuOwningStatics() for RendererShutdownTest coverage. - Tests: the NearRedWall / ObliqueFromBlueEnd acceptance angles now carry luma + positional colour-band contracts (bands placed from the rendered frames); PNG reload verification memcmps every pixel; IWYU includes added. Validated: 60/60 across ReflectionProbe*, RendererMemoryTracker*, ShaderBindingLayout*, RendererShutdown*. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…baseline test guard (#705) - ReflectionProbeArray::UploadLayer validates the prefilter's mip count against the radiance array before copying, mirroring the distance- chain rule: CopyLayerFromCubemap copies min(mips), so a shorter source would leave a previous occupant's radiance in the upper array mips. - ReflectionProbeParallaxVisualEvidenceTest asserts the legacy baseline floor bands are non-black before the improvement comparison — a black legacy floor would make splitLeg ~0 and pass the "measurably better" contract vacuously, and the whole-frame luma check cannot see it (the emissive walls dominate the mean). Skipped (with rationale in review): exact FBO/cull snapshot-restore in the baker's CaptureStateGuard — the deliberate-restore contract (render-pass-published-state.md §1, the DDGI pass idiom this mirrors) prescribes restore-to-scene-defaults, and querying the current FBO binding has no boundary-legal facade without touching RendererAPI (owned by in-flight #691 Phase 6). Validated: 22/22 ReflectionProbe* tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
drsnuggles8
deleted the
feature/renderer-distance-impostor-reflection-probes-705
branch
August 10, 2026 05:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #705 (already closed with the acceptance evidence; this lands the implementation).
What
Reflection probes bake a linear radial-distance cubemap alongside colour, and the lit passes raymarch reflection rays against it per pixel (Szirmay-Kalos et al., Approximate Ray-Tracing on the GPU with Distance Impostors, Eurographics 2005 — re-derived from the paper, no GPL reference code used). A wall two metres away and the sky no longer reflect identically, and the reflection ladder is now SSR (on-screen) → distance-impostor probes → global sky prefilter — emerging from existing pass order with zero SSR changes (SSR composites later by confidence lerp; the ambient term it lerps over IS the probe/sky fallback).
How
ReflectionProbeBaker::CaptureDistanceFieldrasterizes the scene's opaque casters (borrowed from the DDGI caster enumeration via a newRenderer3D::SetAuxCasterSink, collected during the existing warm-up render) into an RG32F cube-face target with the newReflectionProbe_Distance.glsl; readback builds a CPUReflectionProbeDistanceField— R32F @ 128², far-plane miss sentinel, hand-built MAX-mip chain (the cheap reject needs an upper bound), per-probedMax— stored on the probe'sEnvironmentMap. Encoding contract documented inReflectionProbeDistanceField.hand regex-pinned against the GLSL twin.TextureCubemapArray(engine interface + GL impl withglCopyImageSubDatalayer copies);ReflectionProbeArray(aTiledForwardPlus-shaped manager) owns the radiance/distance arrays, the probe UBO (binding 58), the per-cluster probe bitmask SSBO (53) andReflectionProbeCull.compon the existing 32×18×24 froxel grid — with its own copy of the slice params so probes keep working when Forward+ is off.include/ReflectionProbes.glsl: cheap max-mip visibility reject → 32-step coarse march + 6 bisections (the paper's reflection budget) → parallax-corrected prefilter lookup, blended across covering probes (m_BlendDistanceis finally consumed). Consumed by both deferred variants and both forward PBR shaders through newcalculateIBLPrefiltered/calculateCombinedAmbientPrefilteredseams — the BRDF split is untouched, so probe and sky pixels stay photometrically consistent. The cube-array slots (TEX 14/15) stay slot-based in every variant viaPublishTextureOffsetAndBind(the DDGI-atlas pattern; allowlisted inBindlessShaderPipelineTest).Scene::ApplyReflectionProbeOverridebecomes diffuse-only when a global environment exists (Renderer3D::OverrideGlobalIrradiance), keeping the specular miss fallback the live sky; the full-trio override is kept for no-sky scenes so indoor-only setups still get a BRDF LUT + prefilter source. Stated per the issue: the two mechanisms now split by domain (per-camera diffuse, per-pixel specular) rather than silently disagreeing.Three raymarch traps, found by the tests/evidence and pinned
AllSkyEnvironmentIsAMissNotAFarPlaneHit).kProbeMarchSlackRel/Abs(ConvergesToTheAnalyticHitAcrossASphereRoomfailed on exactly this first).OccludedStartSkipsTheOccluderAndHitsTheRealSurface).Verification (rendering rule: all three layers)
ReflectionProbeDistanceFieldTest.cpp, L1): raymarch convergence vs closed-form sphere/box intersections, miss cases, occluded starts, MAX-mip conservatism, GL cube addressing, std140 layout, GLSL↔C++ constant parity.ReflectionProbeParallaxVisualEvidenceTest, L8): corridor with colour-coded walls (RED −X / GREEN +X / BLUE −Z / YELLOW +Z) + roughness-0.08 mirror floor, 4 poses × 3 probe states, PNGs committed underOloEditor/assets/tests/visual/ReflectionProbeParallax_*.png. Reflections land under their walls (left strip RGB 176/63/62, right strip 63/176/63); the off-screen yellow wall reflects at (178,178,61) vs (51,51,51) probe-off — the SSR-failure case; positional split 224.7 vs 7.6 with the distance field stripped (the pre-Renderer: distance-impostor reflection probes — raymarched probe reflections with real depth #705 path), asserted golden-free. The pre-existingReflectionProbeVisualEvidenceTeststays green; both evidence tests now disable the editor's gizmo/grid/axis chrome so the PNGs show reflections, not authoring overlays.olo_shader_errors= 0 after all shader edits; new sandbox sceneScenes/ReflectionProbeCorridor.ololoads (11 entities) and renders with a clean unbaked-probe fallback.AtmosphereVisualEvidenceTest(AtmosphereVisualEvidenceTest: NightClear/NightOvercast goldens drift (RMSE 13.08 / 9.40 vs threshold 8) — permanently red #754) baseline. The 71 regenerated evidence PNGs were pixel-diffed against HEAD per the photometric-parity rule: same-binary reruns are bit-stable (0-pixel diff), diffs vs HEAD are FP-level drift from the hoisted specular fetch (max RMSE 2.8 outside the two intentionally-changed probe images; signed means ≤0.22/255; no structured shifts).Known limitations (documented in
docs/agent-rules/distance-impostor-reflection-probes.md)Distance impostors assume the environment is piecewise-representable from the probe centre — rooms/corridors/open terrain work; densely self-occluding scenes (forests) alias at silhouettes; probe placement matters. Diffuse still uses the per-camera dominant-probe override; terrain/water keep the global reflection source.
Follow-up logged on #607: an
olo_reflection_probe_bakeMCP tool (the bake is inspector-button-only, so live agents can't exercise the raymarched path end-to-end).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests