Thirty-two lights and the fade band
Open the live demo · Read the source · View on GitHub
Eight is the cap on one draw, not on a scene. Past that, the renderer picks the eight lights that matter most to each object and hands the rest, up to twenty-four more, through a texture-backed light list. Eight slots plus a twenty-four light tail is where "thirty-two lights on one draw" in the CHANGELOG comes from, and only a torch past that thirty-second one is actually left unlit.
Step 1: A tiled floor and a ring of torches #
Forty point lights arranged in a ring, over a floor built as a grid of tiles rather than one giant plane. A single draw's bounding sphere wide enough to reach every torch would score them all alike — a light inside an object's sphere scores that object's ceiling no matter where inside it sits, so nothing would tell one torch from another. A tile small enough that a torch's own range can fall outside it makes "which torches reach this one" a real question with a real answer, tile by tile.
// A grid of tiles, not one plane. A single 20x20 draw's own bounding
// sphere (radius about 14) reaches every torch on the ring below, six
// units out — every one of them lands *inside* it, and `_relevanceIn`
// scores a light inside an object's sphere at that object's ceiling
// regardless of exactly where inside it sits. Selection then cannot
// tell one torch from another, and `lightFadeBand` has nothing to
// soften: every candidate ties, so the torch that finally overflows the
// list is exactly as strong as the thirty-two that did not. Twenty-five
// four-by-four tiles each carry a bounding sphere small enough, next to
// a torch's own wide range, that how far a tile sits from the ring is a
// real question with a real answer — the tile at the very centre is
// furthest from every torch at once, and is where the band has
// something to soften.
final DeviceMesh tile = DeviceMesh.upload(
context.device,
PlaneShape(width: _tileSize, depth: _tileSize).build(),
);
final Material floorMaterial = Material(
baseColor: Vector4(0.5, 0.5, 0.55, 1.0),
roughness: 0.85,
doubleSided: true,
);
for (var row = 0; row < _tilesPerSide; row++) {
for (var col = 0; col < _tilesPerSide; col++) {
final double x = (col - (_tilesPerSide - 1) / 2) * _tileSize;
final double z = (row - (_tilesPerSide - 1) / 2) * _tileSize;
scene.add(
MeshNode(tile, floorMaterial, name: 'floor $row,$col')
..setPosition(x, 0.0, z),
);
}
}
for (var i = 0; i < _torchCount; i++) {
final double angle = i / _torchCount * 2.0 * math.pi;
scene.add(
LightNode(
name: 'torch $i',
type: LightType.point,
intensity: 2.5,
range: _torchRange,
)..setPosition(math.cos(angle) * 6.0, 0.6, math.sin(angle) * 6.0),
);
}
Step 2: The fade band #
Every tile has more than thirty-two torches within reach, so every one of
them turns some away — the tile at the very centre of the floor turns away
the most, being the single farthest point from every torch on the ring at
once. lightFadeBand turns the cliff where a turned-away torch's
contribution ends into a ramp: drag it up and the centre tile — lit by
nothing but the weakest of its list — dims towards black, while the ring
itself, whose nearest torches sit nowhere near that cliff, stays exactly
as bright.
lightFadeBand: fadeBand,
Step 3: What the frame reports #
FrameResult.lightsDropped is how many lights a scene actually loses: past
the eight direct slots and the twenty-four-light tail, not past the eight
alone. With forty torches and a cap of thirty-two, eight of them are
genuinely unlit, and this page's test reads that number back off the
frame.
if (scene.lights.length != _torchCount) {
throw StateError(
'expected $_torchCount torches, found '
'${scene.lights.length}',
);
}
final int expectedDropped = math.max(
_torchCount - LightBuffer.maxLights - LightBuffer.maxExtraLights,
0,
);
if (expectedDropped == 0) {
throw StateError(
'this scene needs more torches than the cap to prove '
'anything is actually dropped',
);
}
if (frame.lightsDropped != expectedDropped) {
throw StateError(
'expected $expectedDropped lights past the eight slots and the '
'twenty-four-light tail, the frame reports ${frame.lightsDropped}',
);
}
final int tileCount = _tilesPerSide * _tilesPerSide;
if (scene.meshes.length != tileCount) {
throw StateError(
'expected $tileCount floor tiles, found ${scene.meshes.length} — a '
'floor back to one plane gives every torch the same bounding-sphere '
'ceiling and the fade band nothing to soften',
);
}
if (frame.drawCalls < tileCount) {
throw StateError('fewer than $tileCount tiles were drawn');
}