Builtin Library
readme.md
screenSpaceGI system
Indirect diffuse light gathered from the scene that was just drawn.
Baked lightmaps and probe volumes carry indirect light for the geometry that was present when they were baked. Anything else receives none: a character walking past a brightly lit red wall picks up no red, an object moved at runtime neither receives nor contributes bounce, and a scene assembled at runtime has no indirect light beyond a flat ambient term.
This supplies that missing bounce from what is actually on screen. It needs no acceleration structure and no bake, which is what makes it reachable on both native and WASM.
Using it
local id = entity.spawn("screen_space_gi").id
entity(id).component.add("ScreenSpaceGI", {
intensity = 1.0, -- how much of the gathered bounce reaches the image
radius = 2.0, -- how far a surface gathers, in WORLD units
samples = 12, -- gather taps per pixel
})
Every field is live. Setting intensity to 0 turns it off without losing the
rest. For runtime control (a quality slider, a cutscene) drive the module:
local screenSpaceGI = require("@builtin::systems.screenSpaceGI.screenSpaceGI")
screenSpaceGI.set({ intensity = 1.0, radius = 2.0 })
screenSpaceGI.get()
screenSpaceGI.active()
screenSpaceGI.clear()
What the knobs do
| field | effect |
|---|---|
intensity | How much of the gathered bounce reaches the image. 0 turns it off and releases the pass. |
radius | How far a surface gathers, in world units. This is the scale of bounce supplied, so it scales with the scene rather than the screen. |
samples | Gather taps per pixel. More resolves a smoother gather, at linear cost. |
transfer | How much of the averaged incoming radiance is admitted, in [0, 1]. Trims the gather without changing its reach or its colour. |
How it renders
ssgi.renderFeature runs a gather, a filter and a composite after lighting.
The gather is a form-factor sum. Each pixel takes a spiral of taps over a disc
whose screen radius is derived from radius and the pixel's own world
footprint, so the gather stays the same size in the world at every depth. Each
tap becomes a small emitting patch of the surface it belongs to, and the
transfer depends on both ends: how far the receiver's normal turns toward the
patch, and how far the patch's normal turns back. A patch facing away sends
nothing, which is what stops a surface lighting its own back face.
The composite multiplies the gathered irradiance by the receiver's own albedo — light arriving times what the surface reflects — and adds it. This is a term the lighting did not have, not a replacement for one.
Why it does not boil
The sample pattern is a function of the pixel coordinate alone. It carries no frame counter and no time, so a still camera renders the identical gather every frame and the result cannot boil. That is what lets this run without a history buffer or motion-vector reprojection.
The cost is a fixed pattern in the raw gather, which the à-trous filter from
@builtin::systems.denoising.denoiser removes — the same trade rt_ao makes.
Working with baked lighting
A baked lightmap already carries the static bounce for the geometry it was
baked from. Enabling this at full intensity on top of a baked scene therefore
adds that bounce a second time. intensity is the control: a baked scene wants
a lower value, sized to how much of the visible bounce is dynamic, while a
scene lit entirely at runtime can take the full term.
@scene.normal and @scene.material are the deferred G-buffer, so this needs
the deferred path (the default).
Files
screenSpaceGI.package/
ScreenSpaceGI.component/ -- the scene's bounced light, authored and persisted
screenSpaceGI.module/ -- .set / .get / .clear / .active
ssgi.renderFeature/ -- the gather, the filter and the composite