Christopher Nylén

Checking WebGPU…

Verifying that this browser can create a GPU device.

How this showcase works

A deferred-style GPU pipeline with an implementation of holographic radiance cascades for real-time global illumination. Written in Rust with the wgpu graphics API and compiled to WebAssembly. The implementation uses the amitabha implementation by the author as a reference.

Radiance cascades in brief

To learn more about radiance cascades it's recommended to read the paper by the author: Holographic Radiance Cascades for 2D Global Illumination (Freeman, Sannikov, and Margel, 2025). This video by SimonDev is also a great explanation. Radiance cascades are a multi-level probe system for global illumination. Each level stores radiance over a set of angular intervals, and coarse levels are merged into finer ones, so distant light is resolved without tracing long rays. Rays are assembled from short intervals instead of conventional path tracing, which makes the algorithm single-shot and scene-agnostic: no denoising and no temporal reprojection of the final image. The holographic part is a further development by the original author which aims to improve in areas which the original algorithm struggles, such as with hard shadows.

World-space probes instead of screen-space

The main departure from the implementations I studied is where the probes live. Most demos anchor the cascade to the camera and inject lighting through the G-buffer. That works until a light leaves the screen or moves: the cascade's domain no longer contains the emitter, and the result depends on view-space handoffs and screen-space sampling. I anchor the cascade to the or level instance in world space instead. It covers the entire active room at a fixed world spacing independent of camera position, zoom, and window size so offscreen and moving lights are correct by construction, with no handoff and no G-buffer injection of the light field. The tradeoff for this is that maps need to be hard capped at some size or risk exploding the frame budget. A 128×128-tile room at the default 0.5-tile spacing is a 256×256 probe grid. This runs at about 0.96ms on my GTX 1080Ti. Another issue with using world-space probes is also that color bleed becomes significantly more finicky to get right, As you can't just sample the G-Buffer result anymore.

What it took to get there

Getting to that design meant discarding several approaches. A camera-anchored cascade needed a separate path for offscreen lights; extending the view with a margin made the cost scale with screen pixels and zoom and crushed the framerate. A per-probe direct pass for offscreen emitters worked, but its cost grew with distance and its soft shadows were harder to keep consistent. A coarse map-anchored cascade dimmed lights near the view edge by up to ~25% and risked double-counting. Injecting coarse radiance back into the merge chain failed outright: the chain attenuates an injected far-field value by roughly a factor of sixty, producing beams and dead zones. The lesson was that offscreen light must be computed by a domain that actually contains the emitter, or else the quality would have to suffer in some way or another. As such the world-anchored cascade became the answer, where the room is the domain.

Using the G-buffer for materials, not for the light field

The deferred G-buffer still matters as the source of surface albedo. Walls sample their own tile colour, entities their tint, so a red wall bleeds red into the room. Because surfaces leave the view constantly, each cell and entity persists the last albedo it was sampled with, so indirect colour survives offscreen instead of falling back to a grey approximation. Two bugs here were instructive: sampling the G-buffer at clamp-to-edge gave offscreen faces border texels, and sampling an entity at its own center made every quadrant's ray hit the entity, multiplying the feedback loop gain by four and diverging above a gain of roughly 0.79. The fix was to sample at face midpoints just outside the footprint and to gate rays that start inside an entity. The downside to this is that entities do not give off as much color bleed as tiles if they are not emissive or very large.

Colour bleeding and indirect light through temporal accumulation

Global illumination is accumulated over frames. Each frame, surfaces and non-emissive entities deposit their incoming light times albedo into an indirect buffer, which the tracer reads at hit points the next frame, one bounce per frame, converging to multi-bounce transport as it iterates. A Lambertian factor and a loop gain of exactly albedo × gain keep the feedback stable, and empty floor cells contribute through a world-fixed Gaussian gather, which is the main transport path in open rooms. A small additive term lifts every albedo so a pure red surface under pure green light still picks up colour instead of going black.

A fluid simulation as real-time volumetric fog

The demo also runs an incompressible fluid solver as a compute pass advection with projection, driven by the mouse in the force/fog tool and by an ambient animation. Its dye buffer is bound to the lighting system as participating media: the tracer marches per-chunk mean densities, skipping inactive blocks through an activity mask, and multiplies transmittance into every lighting event. The fog is therefore simulated in real time and genuinely shadows the light by path length, rather than being a screen-space overlay.

References