Layer mode — render a Luxar scene inside a host application's own
Three.js renderer, camera, and scene graph.
This is the headless sibling of LuxarApp. Where LuxarApp owns the
whole pipeline (renderer, camera, controls, post-processing, panels, input),
LuxarLayer owns none of it: the host keeps its renderer and camera, and
the layer contributes a THREE.Group plus the per-frame bookkeeping that
keeps Luxar's streaming, LOD selection, and depth sorting correct.
The seam this rests on is already in the architecture: SceneLoader.loadScene
returns a plain THREE.Group, and LODGroupRegistryDeps /
DepthSortCoordinator.configure take injectable getters. The layer owns
its own coordinator and shares buildLodRegistryDeps / wireDensityGuard
wiring with the app. Nothing in the data, cache, LOD, or material path
needs SceneManager.
Calling LuxarLayer.resize after a viewport or camera-projection
change (the layer cannot observe the host's canvas).
Draw order for its own geometry. Luxar stamps the configured
renderOrder onto every Group it owns; host transparent groups should use
explicit lower/higher values rather than rely on insertion order.
Scene environment sharing. If the host leaves scene.environment unset, the
first Luxar physical mesh installs a prefiltered RoomEnvironment there, which
can also affect the host's own lighting-model materials. A host-supplied
environment is preserved, and an environment owned by the layer is released
by LuxarLayer.dispose.
A scene's post/camera/UI config does NOT apply in layer mode
Everything a scene declares under viewer_config that is applied by
ui/rendering-controls.ts rather than by the node path is silently inert
here — tone_mapping, exposure, global_gamma, global_offset, the
bloom_* family, background_color, and the whole camera block. The layer
owns no post-processing, no camera, and no UI, so nothing consumes them. Only
per-node appearance (colormap, blending mode, opacity, absorption, the
intensity/offset window) travels with the geometry and takes effect.
Tone mapping is the one worth calling out, because it is invisible from the
scene file and changes what the data looks like. Luxar tone-maps in the
mega-shader, a post-processing pass — PostProcessingManager even forces
renderer.toneMapping = NoToneMapping because of it — so it is not a
per-material setting that could be pushed onto the nodes. Measured in a host application: a scene
authored with ACES rendered pixel-identical to one authored with None.
(Measured, not asserted — no test in this repo covers it.)
The consequence: emissive geometry in a host with no tone mapping clips
flat. additive blending sums contributions into a framebuffer that clamps
at 1.0, and normalising amplitudes fixes the per-splat scale, not the
accumulated one. Without a filmic rolloff, overlapping bright structure goes
to white with no gradient.
A host that wants the rolloff has to tone-map itself
(renderer.toneMapping = THREE.ACESFilmicToneMapping, which an OutputPass
will pick up) — noting that this applies to the host's own geometry too.
Otherwise the only exposure controls are the scene's authored opacity and
LuxarLayer.setExposure, and max blending is the one mode that
cannot saturate at all.
Several hosts on one page
A layer may share the page with a LuxarApp and with other layers: it owns
its own loader manager, dimension state, material manager, depth-sort
coordinator and blend warm-up, and holds a lease on the page's shared worker
pools. Two layers in ONE THREE.Scene draw as whole layers in renderOrder
order rather than interleaving by depth (the second one warns). Every host on
the page must render through the same backend. See the module README.
Layer mode — render a Luxar scene inside a host application's own Three.js renderer, camera, and scene graph.
This is the headless sibling of
LuxarApp. WhereLuxarAppowns the whole pipeline (renderer, camera, controls, post-processing, panels, input),LuxarLayerowns none of it: the host keeps its renderer and camera, and the layer contributes aTHREE.Groupplus the per-frame bookkeeping that keeps Luxar's streaming, LOD selection, and depth sorting correct.The seam this rests on is already in the architecture:
SceneLoader.loadScenereturns a plainTHREE.Group, andLODGroupRegistryDeps/DepthSortCoordinator.configuretake injectable getters. The layer owns its own coordinator and sharesbuildLodRegistryDeps/wireDensityGuardwiring with the app. Nothing in the data, cache, LOD, or material path needsSceneManager.The minimal embed shape:
What the host is responsible for
renderOrderonto every Group it owns; host transparent groups should use explicit lower/higher values rather than rely on insertion order.scene.environmentunset, the first Luxar physical mesh installs a prefilteredRoomEnvironmentthere, which can also affect the host's own lighting-model materials. A host-supplied environment is preserved, and an environment owned by the layer is released by LuxarLayer.dispose.A scene's post/camera/UI config does NOT apply in layer mode
Everything a scene declares under
viewer_configthat is applied byui/rendering-controls.tsrather than by the node path is silently inert here —tone_mapping,exposure,global_gamma,global_offset, thebloom_*family,background_color, and the wholecamerablock. The layer owns no post-processing, no camera, and no UI, so nothing consumes them. Only per-node appearance (colormap, blending mode, opacity, absorption, the intensity/offset window) travels with the geometry and takes effect.Tone mapping is the one worth calling out, because it is invisible from the scene file and changes what the data looks like. Luxar tone-maps in the mega-shader, a post-processing pass —
PostProcessingManagereven forcesrenderer.toneMapping = NoToneMappingbecause of it — so it is not a per-material setting that could be pushed onto the nodes. Measured in a host application: a scene authored with ACES rendered pixel-identical to one authored withNone. (Measured, not asserted — no test in this repo covers it.)The consequence: emissive geometry in a host with no tone mapping clips flat.
additiveblending sums contributions into a framebuffer that clamps at 1.0, and normalising amplitudes fixes the per-splat scale, not the accumulated one. Without a filmic rolloff, overlapping bright structure goes to white with no gradient.A host that wants the rolloff has to tone-map itself (
renderer.toneMapping = THREE.ACESFilmicToneMapping, which anOutputPasswill pick up) — noting that this applies to the host's own geometry too. Otherwise the only exposure controls are the scene's authoredopacityand LuxarLayer.setExposure, andmaxblending is the one mode that cannot saturate at all.Several hosts on one page
A layer may share the page with a
LuxarAppand with other layers: it owns its own loader manager, dimension state, material manager, depth-sort coordinator and blend warm-up, and holds a lease on the page's shared worker pools. Two layers in ONE THREE.Scene draw as whole layers inrenderOrderorder rather than interleaving by depth (the second one warns). Every host on the page must render through the same backend. See the module README.The layer has no notifier UI; archive failures are exposed through LuxarLayer.onDatasetFault and LuxarLayer.getDatasetFault so the host can surface them.