5.3 KiB
Clustered forward lighting
Renderer defaults to clustered local lights. Geometry submission, static slabs,
instancing, and GPU skinning are unchanged: one forward material pass, with
separate sun-shadow passes. No G-buffer, geometry prepass, compute shader, depth
readback, or per-object light selection is required by clustering.
This follows the CPU-assigned clustered-forward approach described in Filament's rendering documentation. It is a first lighting increment, not Filament feature or visual parity.
Lighting and shadow policy
| Local lighting | Sun mode | World lighting work |
|---|---|---|
| Clustered (default) | Realtime | CSM; no world atlas or CPU probe bake |
| Clustered | OnChange | Sun-only atlas and legacy CPU probes |
Legacy (MAKEPAD_CLUSTERED=off) |
Either | Original eight-light selection and lamp atlas |
Baked per-model AO is retained in every mode. Clustered lights reach primitives, terrain, diffuse/PBR/custom models, and skinned characters per fragment. PBR materials evaluate local GGX specular as well as diffuse. Explicitly prelit materials retain their existing unlit/baked-color semantics.
Local lights can request budgeted realtime shadows; see Entity lights
for Splash headlights, point/spot fixtures and shadow budgets. Generic lights
default to unshadowed and can shine through walls. The sun retains its own tier.
Engine-authored lights use (1 - distance / radius)^2; imported glTF punctual
lights use their separate photometric mode. Full linear HDR, exposure/tone
mapping, IBL and unified PBR materials remain follow-ups. Clustering does not
itself add MSAA.
Budgets and stereo
Renderer::set_cluster_config(ClusterConfig { ... }) controls XY tiles, depth
slices, lights per cluster, and total uploaded lights. Defaults are 16 × 9 × 24,
32 lights/cluster, and 1024 total. Hard limits are 32 × 24 × 32, 64 lights/cluster,
and 4096 total. set_clustered_lighting(bool) switches the complete legacy/new
lighting policy and invalidates old baked state. Policy survives realm changes.
Flat views use frustum tiles with logarithmic depth. The backend currently late-latches XR eye matrices without exposing them to this host. XR therefore uses a conservative world-space 3D grid over light influence, shared by both eyes—not clusters derived from an unrelated flat camera. Large, dispersed worlds make that fallback less selective. Exact stereo/per-eye frustum clustering needs an eye-view snapshot API before it can replace this fallback.
Smaller lights_per_cluster bounds fragment cost but may visibly omit lights;
finer grids cost CPU/memory but can reduce false-positive light references. Start
device tuning with the counters, not a claimed universal Quest preset. This path
has not yet been benchmarked on a headset.
RenderStats::clustered / Renderer::cluster_stats() report build/upload-queue
CPU microseconds, uploaded bytes, occupancy, references, rejected lights, and
overflow. They do not measure GPU time. Per-cluster overflow retains lights
with the strongest conservative contribution estimate; equal scores retain the
earlier input. The global cap keeps the first valid input lights. Counts are
explicit because neither cap promises all lights survive. Overflow logs are
throttled; MAKEPAD_CLUSTER_STATS=1 also logs frame counters periodically.
Texture ABI
One nearest-sampled RGBA32F texture contains cluster headers, packed light
indices (four scalar indices per texel), then three texels per light:
position/radius, RGB/angular falloff, and normalized emission direction/mode. Headers
store scalar index offset/count. CPU list storage and texture upload vectors are
reused. Integer-valued floats are exactly representable within the hard caps.
The shader mixin and CPU packing live together in src/clustered.rs.
Local shadows use a separate nearest-sampled metadata texture (one light header, four camera rows per face) and a depth atlas, preserving the light-data stride. Inherited texture bindings are resolved by name for derived PBR materials.
Reproduce
Build the sandbox in its own workspace, then launch from the checkout root:
# In apps/commercial/sandbox:
cargo build --release -p makepad-sandbox
# From the checkout root:
MAKEPAD_CLUSTER_STATS=1 ./apps/commercial/sandbox/target/release/makepad-sandbox --remote # with Sandbox's world setting set to `clustered`
The fixture supplies 256 moving lights and a single large floor, independent of
downloaded models. MAKEPAD_CLUSTER_DEMO_LIGHTS=N changes the count;
MAKEPAD_CLUSTER_DEMO_FREEZE=1 fixes light motion for A/B images. Add
MAKEPAD_CLUSTERED=off for the old path. That comparison deliberately shows
missing lights in the old eight-slot path; it is not equal-quality GPU work.
F8 switches sun modes. Finish owned test instances via the printed remote port's
/gq endpoint.
cargo test --release -p makepad-render
cargo test --release -p makepad-render clustered::tests::benchmark_build_and_pack -- --ignored --nocapture
Coverage includes more than eight overlapping lights, depth/tile boundaries, rotated and orthographic cameras, XR/world-grid lookup, empty/invalid inputs, bounded overflow, texture packing, material shader compilation/binding order, and mode/realm lifecycle. The ignored benchmark measures CPU build+pack only.