Builtin Library
readme.md
exposure system
Exposure in stops, and eye adaptation — the image settles when the scene gets brighter or darker instead of clipping or going black.
Without it the mapping from light to pixels is fixed. A scene lit for daylight goes black when the sun sets; a scene lit for an interior blows out the moment a door opens. Every other option — brightening the lights, dimming the sky — moves the lighting itself, which changes what the scene is rather than how it is photographed.
Two halves, usable separately:
- Compensation — a manual offset in stops, the knob for exposing a shot for its shadows or its highlights.
- Adaptation — the exposure follows the scene on its own, and takes time doing it.
Using it
Exposure is a component — author it once and it persists with the scene:
local id = entity.spawn("exposure").id
entity(id).component.add("AutoExposure", {
auto = true, -- follow the scene
targetGrey = 0.18, -- photographic middle grey
meter = "centre", -- which part of the frame to read
})
Every field is live: edit one in the inspector and exposure follows. With
auto off and compensation at 0 the passes are released entirely.
For code that changes exposure at runtime — a cutscene, going underwater, a photo mode — reach for the module directly:
local exposure = require("@builtin::systems.exposure.exposure")
exposure.set({ compensation = -1.5 }) -- any subset; the rest is kept
exposure.tick(dt) -- adaptation is a rate
exposure.get() -- the settings in force
exposure.active() -- are the passes running
exposure.clear() -- off, settings kept
tick exists because adaptation moves per second. The component calls it for
you; code driving the module directly calls it once a frame.
What the knobs do
A stop is a power of two — the unit compensation, minEV and maxEV are
all in. +1 doubles the light reaching the image, +8 multiplies it by 256. The
default is the neighbourhood to author in; the accepted range is the width of
what the frame can represent, and a value outside it is refused naming the
field and the range rather than taken.
| field | effect | default | accepted |
|---|---|---|---|
compensation | Manual exposure in stops. +1 doubles the light reaching the image, -1 halves it. Applied on top of any adaptation. | 0 | -12..12 |
auto | Whether exposure follows the scene on its own. | true | — |
targetGrey | The luminance adaptation aims to put the metered average on. 0.18 is photographic middle grey. | 0.18 | 0.001..1 |
minEV / maxEV | Bounds on how far adaptation may push exposure, in stops — the floor it may stop down to and the ceiling it may open up to. A maxEV of 8 permits 256x of adaptation gain. | -4 / 4 | -12..12 |
speedUp | How fast exposure RISES, per second, when the scene has gone darker. | 0.6 | 0 or above |
speedDown | How fast exposure FALLS, per second, when the scene has gone brighter. | 2.5 | 0 or above |
meter | Which part of the frame is read: full / centre / spot. | "centre" | those three |
meterRadius | How wide the metered region is, where 1 reaches the middle of an edge and the corners sit near 1.42, so 2 covers the whole frame. | 0.6 | 0.001..2 |
minEV and maxEV bound the adaptation alone — how many stops it may move
away from the exposure the scene is lit for. A scene stepping between an
interior and daylight is served a stop or two either side of the defaults; a
ceiling several stops above them hands adaptation a gain large enough to carry
the whole frame to white, which is a picture the scene is not lit for rather
than a fault in a material or a render pass.
speedUp and speedDown name the direction exposure moves, not the scene.
Exposure rises when the scene has gone darker, and that is the slower default on
purpose — the eye adapts to darkness far more slowly than to light, which is
why walking into a cave should read as a moment rather than a switch.
meter matters more than it looks. The thing a viewer is looking at is rarely
the thing filling the most pixels: a bright sky over a dark street will stop the
street down until it is unreadable if the whole frame is metered equally.
How it renders
Two compute passes after lighting: a meter that reduces the frame to a 32x32 grid of tile log-luminances, and an adaptation pass that moves the live exposure toward the scene's reading.
Averaging happens in log space, because the eye's response is logarithmic and a handful of very dark pixels would otherwise drag the whole frame's exposure up.
The live exposure lives in a buffer the GPU writes and reads back the next frame. That persistence is the whole mechanism behind an image that settles rather than snaps.
exposure.module reads the adapted stop back each tick and writes it to the
chain's display pass as __display__'s exposure property — the same knob a
hand-set exposure is.
Files
exposure.package/
AutoExposure.component/ -- the scene's exposure, authored and persisted
exposure.module/ -- exposure.set / .tick / .get / .clear / .active
exposure.renderFeature/ -- the meter and adapt passes