Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commite8beb5604c

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

fieldeffect
intensityHow much of the gathered bounce reaches the image. 0 turns it off and releases the pass.
radiusHow far a surface gathers, in world units. This is the scale of bounce supplied, so it scales with the scene rather than the screen.
samplesGather taps per pixel. More resolves a smoother gather, at linear cost.
transferHow 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