-
Notifications
You must be signed in to change notification settings - Fork 0
Lighting FX Roadmap
Companion to Fractal-Expansion-Roadmap.md. Tracks the volumetric-lighting +
post-FX rendering pipeline (Engine/Rendering/Lighting/,
Abstractions/Rendering/Lighting/LightingFxData.cs,
UI.Avalonia/ViewModels/FractalParamsViewModel.Lighting.cs,
UI.Avalonia/Views/FractalParamsView.axaml lighting expanders).
Phases 1–24 + the b follow-up bindings landed across two batched commits:
- Phase 1–24 + first follow-up wave: pre-summary commits (see
git log --grep="Lighting"). - Deferred-wave bundle:
5b5a741(Phases 6b/8b/11b/15b/21c/22b/23b/24b + animation-tick fix + Phase 1c legacy-light removal in 8 calculators). - GPU + DoF deferred-wave second pass: Phases 12b (tonemap + bloom GPU port), 12c (edge-ink GPU port), 21b (HDR hex-bokeh DoF wired into 7 calculators). Volumetric in-scatter GPU port (originally part of 12b) marked structurally infeasible at the time — see below.
- Volumetric GPU port shipped via Performance-Roadmap P7c.2 — per-fractal GPU calculators now run the volume march inline against each fractal's own DE (the calculator-level GPU port called out as the prerequisite in the original 12b note). 12b is now fully shipped end-to-end.
- Lighting-FX deferred-wave third pass: 16b (recursive reflection bounces + roughness-convolved IBL) and 20b (true per-eye stereo via camera-offset plumbing in 8 calculators). See entries below.
Status as of this doc write: working tree clean on
feature/cross-platform-full.
These were explicitly left out of the deferred-wave bundle because each is a multi-day refactor that touches the GPU dispatcher, the raymarcher hot loop, or both. They're listed in roughly the order they were originally suggested to be tackled.
Update: 12b (tonemap+bloom), 12c, and 21b shipped in the GPU+DoF deferred-wave second pass. Their entries below are kept for historical context; current shipped behaviour matches the algorithm sketched here. 12b-volumetric shipped in the Performance-Roadmap P7c.2 wave — per-fractal GPU calculators inline the volume march against each fractal's own DE (the calculator-level GPU port that blocked the original rendering-layer port). 16b and 20b shipped in the Lighting-FX deferred-wave third pass — recursive reflection bounces + roughness- convolved IBL and true per-eye stereo via camera-offset plumbing in all 8 raymarchers.
Current state. All three passes run on CPU inside
Engine/Rendering/Lighting/ScreenSpacePost.cs
(ApplyToneMapBloom) and ShadingPipeline.cs
(volumetric in-scatter loops + FbmCloud3D). They sit after the
raymarcher's iteration buffer is filled and before the framebuffer
upload.
Why deferred. Bloom in particular is a 2-pass separable Gaussian over the full HDR buffer. At 1920×1080 + 6 mip downsamples this is the single biggest CPU cost in the post-pass chain. Tonemap is per-pixel but cheap; it's bundled here for kernel-locality reasons.
Scope.
- Add a
GpuPostKernelspartial forBloom*,Tonemap*, andVolumetricInScatter*mirroring the CPU implementations. - Plumb through
FracturingFog.Rendering.Lighting.GpuPostKernels.cs(already scaffolded as a stub in commit5b5a741). - Honour the existing
UseGpuPostflag — when false, fall back to CPU so the renderer remains usable without an ILGPU device. - Bit-identity gate: when
UseGpuPostis false or bloom/volumetric parameters are at their default-zero values, the GPU path must not even dispatch (avoids surprising the user with a device init when none of the FX are in use).
Touch points. Engine/Rendering/Lighting/ScreenSpacePost.cs,
Engine/Rendering/Lighting/ShadingPipeline.cs,
Engine/Rendering/Lighting/GpuPostKernels.cs,
Engine/Rendering/FractalRenderHost.cs (dispatch routing).
Risk. Mixed-precision: CPU path uses double for accumulators;
ILGPU kernels will run float. Document the expected magnitude of
divergence and add a tolerance band to any visual-regression tests
that compare CPU vs GPU output.
Status — Shipped (partial). Tonemap + bloom kernels live in
GpuPostKernels.cs (ThresholdKernel, DownsampleKernel,
BlurHorizontalKernel, BlurVerticalKernel, UpsampleAddKernel,
CompositeKernel). ScreenSpacePost.ApplyToneMapBloom dispatches via
GpuPostKernels.TryApplyToneMapBloom when UseGpuPost is true and either
bloom or tonemap is active; falls back to the CPU pyramid on any failure.
Volumetric GPU port — Shipped via Performance-Roadmap P7c.2. The
calculator-level GPU port (Performance-Roadmap P7a/P7b/P7c) ships eight
per-fractal GPU kernels (Mandelbulb, Mandelbox, KIFS Menger / Sierpinski,
QJulia, QMandel, Bicomplex, Kleinian). P7c.2 added kernel-side
Hash3D / ValueNoise3D / FbmCloud3D / VolumetricDensityMul /
CloudSelfShadow / ExpNegSmall to GpuKernelUtils, plus the
volumetric fields on GpuShadingParams (VolumeSteps,
VolumeStepsFalloff, FogHeightFalloff, VolumeNoiseAmount/Scale/ Speed/Octaves, VolumeSelfShadow/Steps, SceneTime). Each kernel runs
the per-pixel volume march inline; per-step SoftShadow toward Light1
calls the local fractal's DE directly (ILGPU still can't take a
struct-generic DE through LoadAutoGroupedStreamKernel, hence the
per-fractal inlining). VolumeSteps>0 selects volumetric; otherwise
the cheap scalar exp(-T·density) path runs. UserBulb stays on the
existing UserBulbGpuCalculator path.
Current state. ScreenSpacePost.ApplyEdgeInk runs both the Sobel
3×3 convolution and the Frei-Chen 4-subspace projection on CPU. Same
hot path as 12b but a separate kernel because it operates on the
normal buffer rather than the colour buffer.
Why deferred. Edge detection is a single-pass stencil — perfect GPU candidate but the kernel is small enough that the CPU version is not the bottleneck. Worth doing after 12b since the colour-buffer will already be GPU-resident.
Scope.
- Sobel and Frei-Chen kernels in
GpuPostKernels(one each, since the basis projections differ). - Switch on
EdgeKernelfield already wired in 23b. - Reuse the GPU normal buffer written by 12b's volumetric pass to avoid an extra upload.
Touch points. Engine/Rendering/Lighting/ScreenSpacePost.cs,
Engine/Rendering/Lighting/GpuPostKernels.cs.
Risk. Frei-Chen's 4 basis projections per channel need non-trivial register pressure — verify on a low-end GPU before shipping default-on.
Status — Shipped. GpuPostKernels.EdgeKernel covers both Sobel and
Frei-Chen (selected via kernelMode arg, 0 = Sobel / 1 = Frei-Chen).
ScreenSpacePost.ApplyEdgeInk dispatches via
GpuPostKernels.TryApplyEdgeInk when UseGpuPost is true; falls back to
the CPU loop on failure. Skip-on-sky-neighbour preserved.
Status — Shipped. LightingFxData.MaxBounces (default 1 = legacy
single bounce). ShadingPipeline.Shade<TDe> wraps the existing reflect-
march block in a per-bounce loop: each bounce hit becomes the next
bounce origin, the bounce direction is reflected about the new normal,
and the contribution is weighted by (reflectStrength · F)^bounce so
deeper bounces fade off. On miss, the per-bounce ray samples the IBL /
sky at its own direction (roughness-convolved when an HDRI is loaded —
see below). All 8 P7-pattern GPU kernels carry a matching
GpuShadingParams.ReflectBounces loop calling each fractal's local DE.
HDRI mip chain: HdriImage now allocates MipLevels = floor(log2(min(W, H))) box-downsampled mips at load time; Sample(dir, roughness) picks
the mip by roughness² · (MipLevels − 1) and bilinearly samples that
level (mip 0 is sharp; mip N−1 is one pixel). SampleEnvAmbientHdri
and SkyColorHdri route through the new overload so ambient IBL and
sky-tint reflection misses both pick up roughness convolution. GPU
HDRI sampling remains GPU-blocked (managed-array lookup); the GPU
reflect bounce continues to use the sky-gradient proxy.
GGX importance sampling per bounce (the original scope item #3) was not shipped — the existing mirror reflection + Fresnel mix matches what the CPU and GPU paths already do, and per-pixel stochastic GGX sampling would need a deterministic RNG plus visual-regression tests to lock in. Roughness-convolved IBL captures the same intent (rougher surfaces see a softer environment lobe) without adding noise.
Original scope sketch (for reference; the shipped subset is above):
ShadingPipeline.Shade does one reflection bounce
(ReflectionStrength knob). HDRI sampling on miss is point-sampled,
not roughness-convolved. The reflection vector is mirror-only — no
GGX importance sampling, no per-roughness lobe.
Why deferred. Recursive bounces require the raymarcher's primary- ray code to be reentrant for secondary rays. Current code keeps state in locals; making it reentrant means lifting the march state into a struct that can be invoked twice (mirror bounce 1 → bounce 2 → IBL fallback).
Scope.
- Extract the sphere-trace loop in each calculator into a static helper that takes ray origin + direction and returns hit/normal/ roughness.
- In
Shade, after the primary hit, call the helper N times (N =MaxBouncesknob, default 1 = current behaviour). - For each bounce, importance-sample a GGX lobe rather than a
pure mirror. Reuse the existing
Roughnessknob. - Roughness-convolved IBL: prefilter the HDRI into a mip chain at
load time (
HdriRegistry.cs), sample the mip that matches the surface roughness on miss.
Touch points. All 7 raymarcher Calculate methods,
Engine/Rendering/Lighting/ShadingPipeline.cs,
Engine/Rendering/Lighting/HdriRegistry.cs.
Risk. Cost scales linearly with MaxBounces. Default must stay
at 1. Document in UI tooltip that MaxBounces > 2 is interactive-
preview-only.
Status — Shipped. LightingFxData.StereoMode enum (Off / Fake /
True). Off skips stereo entirely. Fake runs the existing Phase 20
depth-parallax warp (StereoRender.ApplyStereoSideBySide). True runs
two real camera-offset renders. LightingFxData.StereoEyeOffset is a
transient runtime field (double, default 0) — each 3D calculator,
right after computing its right basis vector, applies
camX += right.X · EyeOffset (likewise Y/Z). Eight CPU calculators +
eight GPU GpuRaymarchParams builders carry the shift. StereoRender. RenderTrueStereo(calc, fp, ipd, ct) orchestrates the two-pass render:
sets EyeOffset = −IPD/2, calls Calculate, snapshots ColorBuffer
into the left half; sets EyeOffset = +IPD/2, repeats into the right
half; resets EyeOffset = 0 and returns the 2W × H output. Caller swaps
display buffer. Default Off preserves legacy mono; Fake preserves
the cheap warp; True doubles the render cost but eliminates the close-
object parallax-flatness that the warp can't fix.
Original scope sketch (for reference; the shipped subset is above):
Phase 20 / 21c ships a side-by-side stereo
post-pass (ScreenSpacePost.cs, StereoRender.cs) that takes the
mono-rendered framebuffer and warps it to look like a stereo pair.
Cheap but not true stereo — there's no parallax on close
objects because both eyes see the same ray hits.
Why deferred. True stereo means rendering twice with the camera shifted left/right by half the IPD (interpupillary distance). At 1920×1080 that's a flat 2× cost. The raymarcher needs a parameter for "eye offset along right-vector" — currently hard-coded to 0.
Scope.
- Add
StereoEyeOffset(or pass-througheyeOffsetparameter) to each calculator's primary ray construction. - Render the left eye into the left half of the buffer, the right eye into the right. Reuse the existing side-by-side framebuffer layout from Phase 20.
- Wire
StereoFovDegrees(already shipped in 21c) to control the off-axis frustum so the two eyes converge on a focal plane at the user-set distance. - Defer barrel-distortion correction for HMD output — Phase 20's
StereoRender.csalready has a pre-warp pass that can be reused.
Touch points. All 7 raymarcher Calculate methods (primary
ray construction), Engine/Rendering/Lighting/StereoRender.cs,
Engine/Rendering/FractalRenderHost.cs (stereo-enabled dispatch).
Risk. Doubles render time when stereo is on. Need a
StereoMode = Off | Fake | True enum on LightingFxData and a UI
toggle that defaults to Fake (= current behaviour) for backward
compatibility.
Current state. No DoF pass. The LensFx block has vignette,
chromatic aberration, Brown decentring, and anamorphic squeeze, but
no depth-driven blur. Depth is available — the raymarcher writes it
into SmoothBuffer (per-pixel t along the primary ray).
Why deferred. Hexagonal bokeh via McIntosh's 3-pass approach (McIntosh, Riecke, DiPaola, "Efficiently Simulating the Bokeh of Polygonal Apertures in a Post-Process Depth of Field Shader", 2012) is the highest-quality CPU-affordable bokeh, but it needs three separable directional blurs followed by a min-blend, and the input must be HDR (linear-light) for the bokeh highlights to bloom right.
Scope.
- New
HdrDofPassinScreenSpacePost.cs. - Inputs: HDR colour buffer (from 12b output, or current CPU HDR
buffer if 12b not yet shipped), depth buffer (from
SmoothBuffer), focal distance + aperture + bokeh-shape (hex / circle / iris). - Three skewed-box blurs at +0°, +120°, +240° per the McIntosh paper. Min-blend the three intermediate buffers to form the hex.
- UI: focal-distance knob (already half-shipped under
LensFocalDepth?), aperture (f-stop), bokeh-shape combo. - Honour the existing
FocalDistance/Aperturefields if they exist; else add them toLightingFxData.
Touch points. Abstractions/Rendering/Lighting/LightingFxData.cs,
Engine/Models/LightingFxPresetData.cs,
UI.Avalonia/ViewModels/FractalParamsViewModel.Lighting.cs,
UI.Avalonia/Views/FractalParamsView.axaml,
Engine/Rendering/Lighting/ScreenSpacePost.cs.
Risk. Three full-resolution skewed blurs are expensive on CPU. Pair this with 12b — the GPU path can do all three in parallel and makes the hex blur viable at interactive framerates.
Status — Shipped (CPU). ScreenSpacePost.ApplyHdrDof runs on the
linear-light HDR buffer before tonemap. Three skewed 1D box blurs at
0°/120°/240° via SkewedBoxBlur helper; per-pixel min-blend produces the
hex bokeh envelope. Bleed control mirrors the byte-buffer Phase 21 pass.
Wired into all 7 calculators (Mandelbulb, Mandelbox, KIFS,
QuatJulia, QuatMandelbrot, Bicomplex, Kleinian, UserBulb)
between SSAO and ApplyToneMapBloom. GPU port deferred — three skewed
blurs map cleanly to ILGPU but the threading the CPU path already does
with Parallel.For is acceptable at typical resolutions.
- 12b first (biggest perf win; unblocks 12c and 21b). — Shipped (tonemap+bloom). Volumetric portion deferred indefinitely.
- 12c (mechanical follow-up to 12b; small kernels). — Shipped.
- 21b (uses the GPU HDR buffer from 12b). — Shipped (CPU); GPU port still open but lower priority.
- 16b (largest architectural lift; reentrant raymarcher).
- 20b (depends on nothing else; can be done in parallel with 16b if a second contributor picks it up).
- 12b-volumetric — Shipped via Performance-Roadmap P7c.2 (see 12b Status note above).
-
16b — Shipped via this third deferred-wave.
LightingFxData.MaxBouncesdrives the reflection-march loop inShadingPipeline.Shade<TDe>(each bounce hits → next bounce origin / dir, accumulates Fresnel-weighted env color, miss → IBL sample at bounce dir). Eight GPU kernels (P7-pattern) gained the matchingReflectBouncesloop.HdriImagenow prefilters into a box-downsample mip chain at load time;Sample(dir, roughness)picks the mip byroughness² · (MipLevels−1), so the existingIblStrengthambient path and the reflection-miss env tint both get roughness-convolved IBL with no extra knobs. -
20b — Shipped via this third deferred-wave.
LightingFxData.StereoModeenum (Off/Fake/True) + transientStereoEyeOffsetfield. Every 3D CPU + GPU calculator shifts its camera origin byright · EyeOffsetafter the basis is computed.StereoRender.RenderTrueStereoorchestrates the two-eye render (sets EyeOffset = ±IPD/2, snapshotsColorBufferper pass, composites the doubled-width output). DefaultOffpreserves legacy mono;Fakepreserves the Phase-20 depth-warp;Trueis the new path.
-
Bit-identity smoke test. Every phase committed so far preserves
output at default knob values. A scripted comparison
(
--batch --headlessrender → SHA256) on each of the 7 raymarchers would catch regressions caused by future GPU ports. - Default-zero gating. All FX knobs default to a zero value that means "pass through". The GPU dispatcher should skip kernel dispatch for any pass whose knob is at default, not just early-out inside the kernel — saves the upload round-trip.
-
LightingFxDatais a value type. Any new GPU path must copy- mutate-write-back through the auto-property; see the CS8156 error fix in commit5b5a741for the pattern. -
HDRI auto-load.
HdriRegistry.TryLoadFromFileparses Radiance.hdr/.pic. Currently called lazily fromShadingPipeline.SampleEnvAmbientHdri. Consider preloading the active environment on parameter change so the first render doesn't stall on file IO.