Tracks every lod_group scene-graph node currently loaded. For each
one, every frame:
Fold each child's raw nD positionBounds into a cached per-entry
local-space :type:BoundingBox, mapping nD axes onto X/Y/Z through
the current displayDims; when any child publishes robust
lodBounds, fold a second (metric) box the same way, falling back per
child to positionBounds (outliers stay visible and resident).
Lift the complete-geometry box to world space through the group's
matrixWorld (:func:transformBoundingBox). Off-screen gate: a
world box entirely outside the camera frustum holds the group at its
coarsest READY level. Eviction uses the same box.
Otherwise measure the metric box ON SCREEN: its LOCAL corners go
through projView × matrixWorld (so a rotated group is sized by its
own box, not by its inflated world AABB), in the units the entry's
selector names:
'screen-area' (what every derived ladder stamps): the viewport
fraction covered by the box's INSCRIBED ellipsoid, projected exactly
and sized at its near depth (projectBoxAreaFraction).
Viewport-clipped, tops out at 1.0;
'coverage' (legacy): the pixel diagonal of the projected corners'
AABB ÷ FILL_FACTOR × fittedAxisPx (min(viewport.width, viewport.height), the extent calculateCameraDistance fits).
Both saturate to +Infinity (finest) with the camera INSIDE the box.
The session lodBias then scales the metric in area units.
For a screen-area ladder whose every level carries a
median_footprint stamp, the projected-FOOTPRINT pick decides
instead: the coarsest level whose median splat stays under
config.lod.maxMedianFootprintPx — except while the metric is
saturated, where the finest level stands.
Otherwise pick the finest child whose coverage_fraction
threshold the metric satisfies, with 10% asymmetric hysteresis on the
downgrade direction (and, during playback, capped at the finest level
whose measured load time fits the period).
If the desired child differs from the current active one, swap
visibility — a dissolve for blendable groups — gated by the
never-downgrade display gate: a fresh aspiration whose additive
ladder is still streaming is not shown while the previously-displayed
level looks strictly better (shouldHoldPreviousDisplay in
lod-display-gate.ts).
The atomic-swap invariant on initial load is realized by
loadLodGroupNode (sequential awaits + visible=false after
attach + a single register() call at the end) — no per-child
ready gate is needed in the registry.
Partition frustum gating (registerPartition): a kind=partition
group's parts are frustum- and slice-culled every frame, and a part that
re-enters the frustum is resynced — see partition-gate.ts, which the
registry drives before and after its LOD pass.
Collaborators. The registry is the orchestrator; the stateful pieces
live in their own modules, each stating until when it needs the loop to keep
ticking (tick-demand.ts), which evaluatePerFrame folds into one
requestTick:
lod-dissolve.ts — the time-driven level dissolve (state machine);
playback-aspiration.ts — reload timing and the playback level cap;
partition-gate.ts — partition culling, resync and lazy activation;
capture-quiescence.ts — the offline-capture "frame is final" test;
lod-selector-math.ts / lod-display-gate.ts / lod-fade.ts /
lod-eviction.ts — the pure metric, display, opacity and eviction
policies.
The bbox infrastructure is shared with the scene-bounds cache and
camera framing — projectBoundsToDisplayDims /
transformBoundingBox live in
scene-manager/clipping/bounds-math.ts and are reused here.
Wiring: the SceneLoader instantiates one registry per scene; the
pipeline hooks evaluatePerFrame into AnimationController
alongside the dynamic-clipping callback. Manual override
(setSelectorMode(path, { lockLevel: i })) bypasses the auto
selector — driven by the layers-panel dropdown.
Per-frame LOD-group selector.
Tracks every
lod_groupscene-graph node currently loaded. For each one, every frame:positionBoundsinto a cached per-entry local-space :type:BoundingBox, mapping nD axes onto X/Y/Z through the currentdisplayDims; when any child publishes robustlodBounds, fold a second (metric) box the same way, falling back per child topositionBounds(outliers stay visible and resident).matrixWorld(:func:transformBoundingBox). Off-screen gate: a world box entirely outside the camera frustum holds the group at its coarsest READY level. Eviction uses the same box.matrixWorld(so a rotated group is sized by its own box, not by its inflated world AABB), in the units the entry'sselectornames:'screen-area'(what every derived ladder stamps): the viewport fraction covered by the box's INSCRIBED ellipsoid, projected exactly and sized at its near depth (projectBoxAreaFraction). Viewport-clipped, tops out at 1.0;'coverage'(legacy): the pixel diagonal of the projected corners' AABB ÷FILL_FACTOR × fittedAxisPx(min(viewport.width, viewport.height), the extentcalculateCameraDistancefits). Both saturate to+Infinity(finest) with the camera INSIDE the box. The sessionlodBiasthen scales the metric in area units.screen-arealadder whose every level carries amedian_footprintstamp, the projected-FOOTPRINT pick decides instead: the coarsest level whose median splat stays underconfig.lod.maxMedianFootprintPx— except while the metric is saturated, where the finest level stands.coverage_fractionthreshold the metric satisfies, with 10% asymmetric hysteresis on the downgrade direction (and, during playback, capped at the finest level whose measured load time fits the period).shouldHoldPreviousDisplayinlod-display-gate.ts).The atomic-swap invariant on initial load is realized by
loadLodGroupNode(sequential awaits +visible=falseafter attach + a singleregister()call at the end) — no per-childreadygate is needed in the registry.Partition frustum gating (
registerPartition): akind=partitiongroup's parts are frustum- and slice-culled every frame, and a part that re-enters the frustum is resynced — seepartition-gate.ts, which the registry drives before and after its LOD pass.Collaborators. The registry is the orchestrator; the stateful pieces live in their own modules, each stating until when it needs the loop to keep ticking (
tick-demand.ts), whichevaluatePerFramefolds into onerequestTick:lod-dissolve.ts— the time-driven level dissolve (state machine);playback-aspiration.ts— reload timing and the playback level cap;partition-gate.ts— partition culling, resync and lazy activation;capture-quiescence.ts— the offline-capture "frame is final" test;lod-selector-math.ts/lod-display-gate.ts/lod-fade.ts/lod-eviction.ts— the pure metric, display, opacity and eviction policies.The bbox infrastructure is shared with the scene-bounds cache and camera framing —
projectBoundsToDisplayDims/transformBoundingBoxlive inscene-manager/clipping/bounds-math.tsand are reused here.Wiring: the SceneLoader instantiates one registry per scene; the pipeline hooks
evaluatePerFrameintoAnimationControlleralongside the dynamic-clipping callback. Manual override (setSelectorMode(path, { lockLevel: i })) bypasses the auto selector — driven by the layers-panel dropdown.