Local exposure
Open the live demo · Read the source · View on GitHub
Stand in a dark room and look at a bright window, and a camera has to choose. Expose for the room and the window turns white. Expose for the window and the room turns black. Auto exposure only moves that one choice around. Local exposure gives each part of the frame its own exposure before the tone curve, so the room and the window can both be seen at once.
Step 1: A dark room #
A room six metres square with a window in the back wall, and a crate in the dark half of the room. The walls are thick slabs that overlap where they meet.
final Material plaster = Material(
name: 'plaster',
baseColor: Vector4(0.75, 0.72, 0.68, 1.0),
roughness: 0.9,
);
// A room six metres square and three high, with walls half a metre
// thick. Every slab runs past the ones it meets, so no corner is a seam.
const double t = 0.5;
const double outer = 6 + 2 * t;
const double side = 3 + t / 2;
// The window, a metre and a half wide, from 0.9 m to 2.3 m up.
const double half = 0.75, sill = 0.9, head = 2.3;
const double jamb = 3 + t - half;
final Scene scene = Scene()
..add(slab(Vector3(outer, t, outer), Vector3(0, -t / 2, 0), plaster))
..add(slab(Vector3(outer, t, outer), Vector3(0, 3 + t / 2, 0), plaster))
..add(slab(Vector3(t, 3 + 2 * t, outer), Vector3(-side, 1.5, 0), plaster))
..add(slab(Vector3(t, 3 + 2 * t, outer), Vector3(side, 1.5, 0), plaster))
// The back wall, in four pieces around the window.
..add(
slab(
Vector3(jamb, 3 + 2 * t, t),
Vector3(-(half + jamb / 2), 1.5, -side),
plaster,
),
)
..add(
slab(
Vector3(jamb, 3 + 2 * t, t),
Vector3(half + jamb / 2, 1.5, -side),
plaster,
),
)
..add(
slab(
Vector3(2 * half, sill + t, t),
Vector3(0, (sill - t) / 2, -side),
plaster,
),
)
..add(
slab(
Vector3(2 * half, 3 + t - head, t),
Vector3(0, (head + 3 + t) / 2, -side),
plaster,
),
)
// A crate in the dark half of the room.
..add(
slab(
Vector3(0.8, 0.8, 0.8),
Vector3(-1.8, 0.4, -1.2),
Material(name: 'crate', baseColor: Vector4(0.6, 0.25, 0.15, 1.0)),
),
);
Step 2: A bright outside #
Through the window you see a large emissive panel, many times brighter than
anything in the room. There is no lamp and no sun, only a weak ambient light,
so the room stays dark. defaultLightWhenUnlit is switched off, or the
engine would add a light of its own to a scene that has none.
final MeshNode sky = slab(
Vector3(8.0, 8.0, 0.1),
Vector3(0.0, 1.5, -5.0),
Material(
name: 'sky',
baseColor: Vector4(0.0, 0.0, 0.0, 1.0),
emissive: Vector3(5.0, 6.0, 8.0),
),
)..shadowCasting = ShadowCastingMode.off;
// No lamp and no sun: the room has only a weak ambient light, and the
// engine is told not to add a light of its own to a scene that has none.
scene
..ambientIntensity = 0.12
..defaultLightWhenUnlit = false;
Step 3: Turn on local exposure #
RenderSettings.localExposure takes a LocalExposureSettings, off by
default. With enabled on, the renderer looks at the frame three times over:
shadowStops brighter, as it is, and highlightStops darker, 2 stops each by
default. At each place it weighs how close each version comes to mid grey,
blurs those weights wide, and gives each place the exposure its weights
choose. The blur ignores edges, so the exposure changes gradually across the
frame instead of cutting a seam at every edge. The price is a soft glow
around the bright window, whose edge is pulled down less than its middle.
strength is how much of that shift applies. Nought is the one global
exposure and one is the full local answer. The default is 0.7. This page
uses 1.
localExposure: LocalExposureSettings(
enabled: local,
strength: strength,
shadowStops: shadowStops,
highlightStops: highlightStops,
),
Switch Local exposure off and you get one exposure for the whole frame: the crate is lost in the dark. Switch it on and the dark half of the room lifts while the window comes down. Raise Shadow stops to lift the dark parts further, and Highlight stops to bring more back from the window.
Step 4: Check that it ran #
The weights are measured at an eighth of the frame's size, in three small
passes, and the composite applies them. The frame lists them as one pass
called local exposure.
if (local && strength > 0) {
expectPassOrDecline(frame, 'local exposure');
} else if (passRan(frame, 'local exposure')) {
throw StateError('local exposure is off and its pass ran');
}