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 / configureDepthSort
are defined purely in terms of injectable getters. 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.
Limits (same as LuxarApp, and for the same reasons)
One layer per page, and never alongside a LuxarApp. The scene-loader
manager, dimension manager, material manager, and worker pool are process
singletons; two owners would share and then corrupt each other's state.
The layer has no notifier UI; archive failures are exposed through
LuxarLayer.onDatasetFault and LuxarLayer.getDatasetFault so
the host can surface them.
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/configureDepthSortare defined purely in terms of injectable getters. 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.Limits (same as
LuxarApp, and for the same reasons)One layer per page, and never alongside a
LuxarApp. The scene-loader manager, dimension manager, material manager, and worker pool are process singletons; two owners would share and then corrupt each other's state. The layer has no notifier UI; archive failures are exposed through LuxarLayer.onDatasetFault and LuxarLayer.getDatasetFault so the host can surface them.