Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

restirLighting

Direct light from many small sources at a cost that does not grow with how many there are.

A shading pass that loops every light pays for every light at every pixel, which is why the number of lights has to be capped. Resampled importance sampling breaks that link: each pixel draws a few candidates, keeps ONE of them with a probability proportional to what it would contribute, and carries a weight that makes the survivor stand for the whole set. What it keeps is then reused — by its neighbours this frame, and by itself on the next one — so the set a pixel effectively samples keeps growing while the number of candidates it draws stays where you put it.

A cave of glowing crystals, a street of practicals, a control room of indicator lamps: hundreds of sources, and each pixel looks at about a dozen.

Using it

Put an EmissiveSource on the geometry that glows:

local id = entity.spawn("crystal", { position = { 2, 1, 0 } }).id
entity(id).component.add("EmissiveSource", {
    color = { 0.3, 0.8, 1.0 },
    intensity = 8.0,
    radius = 6.0,
    size = 0.4,
})

Or register sources from a script, which is how a generated scene does it:

local restir = require("@builtin::systems.restirLighting.restirLighting")
for i = 1, 400 do
    restir.set(`lamp{i}`, {
        position = { math.random() * 60 - 30, 1.5, math.random() * 60 - 30 },
        color = { math.random(), math.random(), math.random() },
        intensity = 4.0,
        radius = 5.0,
    })
end

The pass starts with the first source and stops with the last. Settings are on restir.configure, or on a ResampledLighting component for a scene to author:

restir.configure({ candidates = 16, spatialSamples = 6 })

What a pixel pays

candidates + spatialSamples + 1 evaluations, reported by restir.perPixelCost(). None of those terms is the source count.

The estimate is formed on a grid of one cell per 2x2 pixels (resolution = "full" gives every pixel its own cell), across four passes:

PassRuns overDoes
restir_samplegrid cellsdraws candidates sources at random, resamples to one, merges the cell's own history
restir_spatialgrid cellsborrows from spatialSamples neighbours, each evaluated against this cell's surface
restir_resolvepixelsshades the chosen source with the pixel's own normal, roughness and albedo
restir_denoisepixelsaverages that light over the cells around the pixel and composites it

Which source to look at is a low-frequency decision a cell of pixels can share. What the surface does with it is not, and is computed per pixel.

One source per cell is correct on average and different from its neighbour, which is the grain a single sample carries. restir_denoise averages across the cells around a pixel, weighing each by how nearly its surface is this one, so the estimate keeps its value and loses its variance.

mode = "reference" replaces the reservoir lookup with a loop over every registered source at every pixel. That is the picture the resampled mode is estimating and the cost it is measured against — the same shading, the same filter and the same composite, so the two are comparable directly.

Temporal reuse

A cell keeps its reservoir across frames and merges it with the fresh candidates, so a good find keeps paying after the frame that made it. The stored world position is what says the cell is still looking at the same surface; a camera move or an occluder passing in front fails that test and the history is dropped rather than smeared.

historyCap bounds how many candidates the history is allowed to stand for. Without it a converged cell would outvote every new candidate forever and a lighting change would never work its way out. At the default of 20 a change is absorbed over roughly twenty frames.

Removing a source, or turning its intensity down to 0, releases it; the pass stops with the last one.

Where it sits

Sources registered here are additional to the lights the engine holds rows for. The passes run in afterLighting at orders 80-84 and add to the scene as it was drawn, reading position, normal, albedo and metallic-roughness back out of the G-buffer.

A source lights every surface within radius that faces it, along the straight line between them.

One frame, several targets

A render feature's render runs once a frame, and the passes it enqueues run once per render target — the window, an offscreen capture, a camera rendering to a texture. Each pass therefore reads the frame size out of the textures it is handed and works out its own cell grid, so every target resamples at its own resolution. A reservoir carries the grid width it was written under, which is what stops one target's history being read as another's.

The reservoir buffers are sized to the widest grid any target has asked for. A target wider than they have yet been sized for reports the grid it needed, and the frame after that covers it.

Reading what it did

restir.stats() returns the counters the passes keep on the GPU for the most recent frame a read-back has landed for, carrying that frame's number so a repeated read is distinguishable from a fresh one. meanWeight is counted in sixteenths with each cell capped at 16 so the total fits a 32-bit counter; peakWeight alongside it is exact to a thousandth, and is where an estimator that has come loose shows first.