Skip to content

feat(sdf3d): IBL / environment reflections on the SDF path (ADR-0060 Phase 2) - #359

Open
jeffcrouse wants to merge 3 commits into
mainfrom
adr-0060-phase2-sdf-ibl
Open

feat(sdf3d): IBL / environment reflections on the SDF path (ADR-0060 Phase 2)#359
jeffcrouse wants to merge 3 commits into
mainfrom
adr-0060-phase2-sdf-ibl

Conversation

@jeffcrouse

Copy link
Copy Markdown
Member

Builds on the merged Phase 1 (#357). Makes SDF metaballs reflect a real environment — the last big visual gap between the SDF path and the mesh path.

The discovery

Reading the code first revealed Vivid's IBL was entirely dormant: the ENVIRONMENT-fragment machinery + a fallback cube existed in Render3D, but no operator ever produced an environment, so meshes had no reflections either. So Phase 2 couldn't be "bind the existing cubemaps" — it had to build the source. (This also delivers ADR-0060's Phase 3 goal.)

What's in it

  • New Environment operator — bakes a procedural dark-stage sky or a loaded equirect .hdr into irradiance + GGX-prefiltered cubemaps + a split-sum BRDF LUT (all render-pass based, cached), emits the ENVIRONMENT fragment. HDR: stbi_loadf → RGBA16Float → an equirect→cube pass.
  • Shared create_ibl_bind_group_layout() (gpu_3d.h) + a custom_ibl fragment flag — so SDF3D's group-1 IBL layout is group-equivalent to Render3D's group 2, and Render3D's existing real + fallback IBL bind groups bind onto the SDF pipeline unchanged.
  • SDF3D gets a group-1 IBL slot + the split-sum ambient/specular terms in its shader (reusing Phase 1's F0/NdotV), gated on has_environment. Render3D binds the scene IBL (or the black fallback) at group 1 on the custom SDF dispatch — one added block.
  • blob demo reflects a committed royal_esplanade 1k HDRI (Poly Haven, CC0, 1.6 MB in media/blob/); BLOB_HDRI=/path overrides, BLOB_HDRI='' uses the procedural sky.

Verification

  • Full ctest green, 0 GPU validation errors (cross-op bind-group compatibility holds).
  • A Render3D mesh sphere, an SDF3D sphere, and the blob metaballs all reflect the environment — procedural dark-stage sky (cyan/magenta studio reflections on the wet liquid metal) and real CC0 HDRIs (studio / day / night / royal_esplanade). All were byte-black on the SDF path before.
  • ABI stays additive (a bool fragment field; group-1 is pipeline-internal). Vendored gpu_3d.h snapshots synced (guard passes).

Follow-up (not blocking)

Skybox/IBL render without tone mapping, so a bright HDRI blows out the background (the reflection is correct) — an exposure/tonemap control is a good later add.

🤖 Generated with Claude Code

…Phase 2)

Vivid's IBL was dormant — the ENVIRONMENT-fragment machinery + a fallback cube
existed in Render3D, but no operator produced an environment, so neither meshes
nor SDF surfaces got reflections. Phase 2 builds the source and wires it into the
raymarched SDF path (finishing ADR-0060's unification; folds in Phase 3's
'one environment + procedural fallback').

- New Environment operator: bakes a procedural dark-stage sky OR a loaded
  equirectangular .hdr into irradiance + GGX-prefiltered cubemaps + a split-sum
  BRDF LUT, and emits the ENVIRONMENT scene fragment. HDR path: stbi_loadf ->
  RGBA16Float -> an equirect->cube pass; the procedural sky is the asset-free
  fallback.
- Shared create_ibl_bind_group_layout() in gpu_3d.h + a custom_ibl flag on
  VividSceneFragment, so SDF3D's group-1 IBL layout is group-equivalent to
  Render3D's group 2 and Render3D's existing real/fallback IBL bind groups bind
  onto the SDF pipeline unchanged (no new fallback machinery).
- SDF3D: a group-1 IBL slot on the pipeline + split-sum ambient/specular terms in
  the shader (reusing the Phase 1 F0/NdotV/roughness), gated on has_environment.
  Render3D binds the scene IBL (or the black fallback) at group 1 on the custom
  SDF dispatch.
- blob demo reflects a committed royal_esplanade 1k HDRI (Poly Haven, CC0);
  BLOB_HDRI=/path overrides, BLOB_HDRI='' selects the procedural sky.

Verified: full ctest green, 0 GPU validation errors; a Render3D mesh sphere, an
SDF3D sphere, and the blob metaballs all reflect the environment (procedural +
real 1k/4k HDRIs). ABI stays additive; vendored gpu_3d.h snapshots synced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Production gate (core): ✅ PASS

  • 83/83 tests passed (0 failed) in 16.51s

jeffcrouse and others added 2 commits August 17, 2026 09:00
The split-sum BRDF LUT was generated with roughness = 1 - uv.y but sampled at
vec2f(NdotV, roughness), so the roughness axis was FLIPPED — every IBL surface
read the BRDF response for (1 - its roughness). Generate it as roughness = uv.y
to match the sampler. Fixes the energy/appearance of all IBL (mesh + SDF).

Also bump the base/prefiltered cube to 256^2 and the GGX prefilter to 256 samples
for a crisper reflection.

Known limitation (follow-up): the single-mip GGX prefilter shows coherent-sample
rings on a near-perfect mirror (roughness -> 0); glossy surfaces (the blob demo)
are clean. The proper fix is PDF-based mip sampling of the environment, which
needs a base-cube mip chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in an HDR hall

A dedicated ADR-0060 Phase 2 showcase: a near-mirror SDF sphere reflecting a
loaded royal_esplanade equirect HDRI via image-based lighting, with the same
environment drawn as the skybox behind it. An orbit camera (Clock-driven) plus a
faint tarnish (subtle roughness smudges + fine scratches, committed maps) make it
obviously a rotating reflective object; no direct lights — everything on the ball
is reflected environment. The slight gloss also hides the single-mip prefilter
rings a perfect mirror would show.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant