Clustered lights
Open the live demo · Read the source · View on GitHub
Each draw gets a short list of lights: eight slots, plus a tail of twenty-four more, ranked against the draw's bounding sphere. For a vase that is plenty. A floor that spans the whole map is a single draw, though, and every lamp on this page stands inside its sphere, so every lamp scores the same. Ties go by scene order: the floor gets the first thirty-two lamps added, one half of the grid, and every other lamp over it does nothing.
Clustered lights hand the list out per pixel instead. Each frame, the view is cut into 16 by 9 tiles and 24 depth slices, and each of those cells gets the lights whose range reaches it. A fragment reads the lights of the cell it lands in.
Step 1: One big floor #
A white slab twelve metres square, drawn in one call. The scene's ambient light is turned down to 0.03 so that only the lamps light it.
final Scene scene = Scene()
..ambientIntensity = 0.03
..add(
MeshNode(
DeviceMesh.upload(
context.device,
CuboidShape(size: Vector3(12.0, 0.1, 12.0)).build(),
),
Material(name: 'floor', lighting: LightingModel.lambert),
name: 'floor',
)..setPosition(0.0, -0.05, 0.0),
);
Step 2: Sixty-four lamps #
An eight by eight grid of point lights, a metre apart and thirty centimetres above the floor, each a different hue. None of them casts a shadow. With a range of 0.8 metres each lamp makes its own spot on the floor, so you can count which ones reach it.
for (var i = 0; i < 8; i++) {
for (var j = 0; j < 8; j++) {
final LightNode light = LightNode(
name: 'lamp $i $j',
type: LightType.point,
intensity: 1.5,
range: range,
color: _hue((i * 8 + j) / 64.0),
)..setPosition(i - 3.5, 0.3, j - 3.5);
_lights.add(light);
scene.add(light);
}
}
Step 3: Turn the cells on #
clusteredLights is a single flag on RenderSettings, off by default. Switch
Clustered lights off and the floor falls back to the per-draw list: thirty-two of
the sixty-four spots stay dark. Switch it on and all of them light up.
Drag Light range up and the spots grow into each other. Each cell then holds more lights, and each pixel does more work. A fragment reads at most twenty-four lights from its cell, and past that the cell keeps the first twenty-four in scene order and drops the rest. At 0.8 metres the fullest cell here holds twelve; at the slider's 3 metres it holds fifty-six, so the far end of the slider is already past the cap.
clusteredLights: clustered,
Note. The cells only take over when a scene has more lights than the eight slots and no light channels are in use. The eight slots stay either way, and they are still the only lights that can cast a shadow.