GI: baked irradiance volumes for diffuse GI

Closes #157 item 4 — indirect diffuse. The last item in docs/decisions/bistro-visual-fidelity.md (phase 7).

Design: docs/ADR/ADR-0018-diffuse-gi-irradiance-volumes.md.

Problem

The only diffuse indirect term in scene.frag.glsl is the distant environment:

vec3 irradiance  = evalSH(N, ubo.env_sh) * intensity;
vec3 diffuse_ibl = irradiance * albedo / PI * (1.0 - metallic);

env_sh is nine order-2 SH coefficients projected from the scene's one HDRI. It has no notion of position. A fragment under the Bistro's awning, a fragment inside the shop, and a fragment in open sunlight all receive the same hemisphere of sky. SSAO darkens that term but cannot colour it — no red spill from the awnings onto the stucco, no bounce from the storefront onto the pavement — and interior surfaces, which see almost no sky, get a full sky term SSAO can only attenuate.

Why irradiance volumes

Of #157 item 4's four options, three are unavailable or a poor fit:

  • DDGI / RTXGI needs hardware ray tracing. SDL3 GPU exposes none on any backend — the same constraint that decided ADR-0017.
  • Lightmaps need a second UV set and an atlas. Bistro ships no lightmap UVs and the importer has no unwrapper; building one is a larger project than the GI.
  • Voxel cone tracing is expensive and aliasing-prone, and adds a voxelization pass to a renderer that is already submission-bound at this scale (#158).

Baked irradiance volumes are the only option that reuses machinery already in the repo, and they match the static/dynamic call ADR-0017 already made for this scene.

Design summary

  • IrradianceVolumeComponent — box volume, spacing, blend_distance, priority, intensity, AssetRef(.irradiance). Same field shape as ReflectionProbeComponent / PostProcessVolumeComponent, so volume resolution reuses postprocess.resolveVolumes' existing box/blend/priority model.
  • .irradiance asset, magic TIRR, versioned from the first release per ADR-0012. Per probe: order-2 SH irradiance (9 RGB coefficients), mean distance and mean squared distance for a Chebyshev visibility test, and a validity flag.
  • Bake as a TaskManager job (ADR-0016) with progress and cancel — not the per-frame trickle reflection probes use. A 16×8×16 grid is 2,048 probes × 6 faces. Extract reflection_probes.captureFace into a shared subsystems/render/cube_capture.zig; ADR-0017's bake becomes a second caller. Reuse the nine-term SH basis in assets.computeIrradianceSh, adding a cube-face integrator alongside the existing equirect one. Two or three iterations, each reading the previous one's irradiance, give multi-bounce.
  • Runtime: one fragment storage buffer, not 3D textures. Chebyshev weighting and backface rejection need per-corner weights, so hardware trilinear filtering is useless and the eight corners are fetched by hand anyway — at which point a storage buffer costs one binding regardless of coefficient count, where 3D textures would cost 9 samplers plus 2 for visibility on top of the 10 the scene shader already binds. Scene lights already set the precedent at set=2, binding=10.
  • Blend then evaluate: weight the eight probes' coefficient sets into one (72 madds), then call the existing evalSH once. Exact for a linear basis, and one evaluation instead of eight.
  • The existing ambient * occlusion * ssao multiply stays — the volume supplies where the light comes from, SSAO the contact detail below the probe grid's resolution.

Scheduling

Scheduled after #158's performance phases: this adds frame cost to a scene that can't afford it yet, and it should land behind one of #160 (closed)'s feature toggles.

Acceptance

  • A volume can be placed, sized and baked from the Inspector, with progress and cancel
  • Interior surfaces are lit by the room, not by the sky — terrace and interior before/after pairs against the ORCA reference
  • No visible light leak through the exterior/interior wall (the hardest case in this sample)
  • Scenes with no volume render byte-identically to today
  • Frame cost measured with the volume on and off (#159 (closed)'s harness)