Create a new scene manager instance.
Sets up the EventDispatcher base class. Does not initialize Three.js components - call init() to set up renderer, scene, camera, and controls.
The graphics-API renderer. Holds the Renderer union honestly:
a THREE.WebGLRenderer on the default path, or a WebGPURenderer
when opted in via ?renderer=webgpu / VITE_LUXAR_USE_WEBGPU=1
(which itself may dispatch to a real WebGPU adapter or transparently
fall back to its internal WebGL2 backend depending on browser support).
Every method called on this field across the codebase
(PostProcessingManager, picking-system, BloomChain,
FxaaPass, UI panels) is part of the common Renderer surface
in Three r185 — no WebGLRenderer-only API is used
unconditionally. The discriminator for callers that genuinely
must branch is this.capabilities.apiSurface (see
RendererCapabilities).
The lazily built scene environment that lights material="physical" meshes;
null until init. Public so a debug surface or test can ask
isReady(); nothing else should need to touch it.
PrivateunsubscribeReadonlydepthThis app's depth-sort coordinator: the scene loader's commits report to it,
the post-processing glass split reads its node registry, and the dataset
switch releases its nodes. Owned here because the scene manager is the one
per-app object every one of those already reaches; the init pipeline wires
its camera and render loop (configure) and the app's dispose pipeline
disposes it.
Capabilities snapshot for the active renderer. Hides raw-GL queries behind a typed interface so the eventual WebGPU port has a single implementation seam.
Three.js scene graph - container for all 3D objects and lights
Camera for 3D viewing (perspective or orthographic)
ControlsManager - manages different camera control types (orbit, fly, ortho)
HDR post-processing manager for bloom and tone mapping effects
PrivatecanvasThe HTML canvas element where 3D rendering occurs
PrivateisTrack whether we're centered on bounding box or origin
PrivatelastStore the last calculated bounding box center
PrivatesceneThe scene's up vector — world +Y unless the zarr viewer_config
authored one (set in loadSceneData). Every camera fit/reset
(Home/F, center-on-origin, load-time auto-frame) squares to this,
so author-oriented scenes reset to THEIR horizon, not world Y.
PrivatesceneAxis-aligned opening view in force for this scene: the session override
(?viewAxis=) else the authored viewer_config.camera.view_axis, else
null (the historical look-along-−Z framing). Resolved in loadSceneData.
PrivateviewSession ?viewAxis= override; beats the scene's authored preset.
PrivateresizerResize debouncing with requestAnimationFrame for smooth resizing
PrivatecontextWebGL context-loss / restoration concern. Constructed lazily in setupContextLossHandling() once the canvas + renderer are wired up. The class owns the canvas listeners and the isContextLost flag — SceneManager just forwards events through it.
PrivatelastPerspective FOV in effect at the last perspective→ortho swap, restored on the inverse ortho→perspective swap so a control-mode round trip preserves the user's FOV instead of resetting it to the config default.
PrivatedynamicDynamic clipping planes state
Private ReadonlyboundsCached scene bounds + bounding sphere + near-cull margin.
Invalidated on scene load/clear; lazily recomputed by
boundsCache.ensure(scene).
Private Readonly_Reusable Vector2 for getDrawingBufferSize (avoids per-call allocation).
Ownership contract: This Vector2 instance is owned by SceneManager
and is reused across every camera-materials update — every resize
mutates it in place. Consumers reached via makeCameraMaterialsCtx
MUST treat the supplied bufferSize as read-only borrow scoped to
the current call. If a consumer needs to hold the value across
frames, it must .copy() the Vector2 into its own storage, not
retain the shared reference — otherwise the next resize will silently
corrupt the held value.
PrivatepixelExplicit DPR selected by adaptive/manual resolution control.
null means "track the live pixel-ratio ceiling".
Non-null values must survive ordinary window resizes; otherwise a
resize event immediately after a manual DPR change silently restores
ceiling resolution while the AdaptiveDPRManager/UI still reports the
reduced DPR.
PrivatedebugWhen true, additional hardware/runtime info is logged at startup.
Set via init's debug flag (forwarded from ?debug URL parameter).
PrivaterendererOptional per-instance backend override. When set, setupRenderer
uses it instead of consulting the env var. Threaded from the
?renderer=webgl|webgpu URL parameter through LuxarAppOptions.
PrivatewebgpuDiagnostic WebGPURenderer mode. When true and the selected renderer is
WebGPURenderer, pass { forceWebGL: true } so Three.js uses its
internal WebGL2 backend while Luxar still dispatches TSL materials.
PrivateperfOpt-in to WebGPURenderer({ trackTimestamp: true }) for the perf
bench. Off by default; flipped via ?perfTimestamp URL param.
PrivatehdrHDR output requested (?hdr) and its optional pinned headroom; WebGPU only.
PrivatehdrPrivateblendWebGL-only blend-variant warm-up (?noBlendWarmup disables).
When true, resize events are suppressed (used during recording to prevent resolution changes). Delegated to ResizeOrchestrator — exposed as a getter/setter so recording-panel.ts continues to read
sceneManager.resizeLocked directly.Current FOV in degrees. Perspective: the live camera FOV. Orthographic:
the stashed perspective FOV (lastPerspectiveFov) — the single source of
truth for the FOV that a future ortho→perspective swap will restore, so a
FOV set while in ortho (Reset-to-Defaults / zarr-authored) is reported and
honored rather than masked by the config default.
The pixel ratio the renderer is currently sized for: the explicit override if one is engaged, otherwise the live ceiling — and clamped to that ceiling either way.
Exposed so callers that need to re-apply the pixel ratio by hand
(the recording session, after a scaled capture) can ask for the
value the resize path would use instead of reaching for
window.devicePixelRatio, which respects neither the override nor
the pixel-ratio cap.
Set an absolute perspective FOV using rendering-setting validation semantics. Invalid values fall back to the configured default rather than clamping.
The one way to publish a camera change made outside the controls' own input handling.
Runs write (if given), brings the camera's world matrices up to date,
and makes sure exactly ONE change event reaches the controls manager.
That is where every consumer of a camera change listens: the scene
manager's relay (loop wake, picking dirty), the input handler, and the
embedder's camera-changed. If the write already made the controls fire
change (an orbit update() does when the pose moved), nothing more is
dispatched; if it fired none (fly controls never do for an outside write;
a fov or zoom change goes through no controls at all), one is dispatched.
Before this, writers each picked their own subset: the fov and clipping
setters dispatched on the scene manager only, so embedders never heard of
a fov change; the framing helpers relied on controls.update(), which in
fly mode fires nothing, so framing there neither woke an idle loop nor
told the embedder; setCameraZoom notified nobody.
Optionalwrite: () => voidApply an authored zoom after the camera has switched to ortho projection.
Initialize the renderer pipeline.
The HTMLCanvasElement to render into. Callers
resolve this themselves (document.getElementById(...) in the
standalone app's main.ts; arbitrary container child for embedders).
SceneManager performs no DOM lookups of its own.
Optionaldebug?: booleanVerbose hardware/runtime logging.
Optionalrenderer?: "webgl" | "webgpu"Optional backend override. When provided, wins over the
VITE_LUXAR_USE_WEBGPU / VITE_LUXAR_USE_LEGACY_WEBGL env vars
and the WebGL default. Threaded from LuxarAppOptions.renderer,
ultimately from the ?renderer=webgl|webgpu URL parameter.
OptionalwebgpuForceWebGL?: booleanDiagnostic flag for WebGPURenderer({ forceWebGL: true }).
Threaded from LuxarAppOptions.webgpuForceWebGL, ultimately from
the ?webgpuForceWebgl URL parameter.
OptionalperfTimestamp?: booleanOpt-in to GPU timestamp queries. Threaded from
LuxarAppOptions.perfTimestamp, ultimately from the
?perfTimestamp URL flag set by the perf bench.
Optionalhdr?: booleanHDR output from LuxarAppOptions.hdr / hdrHeadroom (?hdr[=n]). WebGPU only.
OptionalhdrHeadroom?: numberOptionalblendWarmup?: booleanWebGL-only blend warm-up. When true, classic THREE.WebGLRenderer
sessions pre-compile each DISTINCT blend-mode program variant a
material can reach, one compile per post-frame idle opportunity, so the first
Layers-panel blend switch does not pay SwiftShader's synchronous
link cost on the click path.
OptionalviewAxis?: ViewAxisName | nullSession axis-preset override (?viewAxis=xy|xz|yz), beating each
scene's authored viewer_config.camera.view_axis. Threaded from
LuxarAppOptions.viewAxis.
PrivatesetupInitialize Three.js WebGL renderer with optimal settings
This creates the WebGL rendering context that will handle all GPU operations. Key configurations:
PrivatesetupPrivatesetupOpt-in renderer setup — selected via ?renderer=webgpu URL flag
or VITE_LUXAR_USE_WEBGPU=1. Constructs a WebGPURenderer and
runs its async init(). On browsers with WebGPU support, the
renderer acquires a WebGPU adapter and dispatches TSL graphs
to WGSL. On browsers without WebGPU (Firefox today, older
Safari), Three's WebGPURenderer transparently falls back to a
WebGL2 backend — the TSL graphs target both from one source.
NOTE: WebGL is the production default; this method is
reached only when the user explicitly opts into WebGPU.
See BROWSER_SUPPORT_POLICY.md for the policy.
PrivatesetupConstruct the WebGLContextRecovery concern and attach its
canvas listeners (WebGL2 path), OR attach a device.lost
observer that surfaces the failure as an event (WebGPU path).
WebGL2 path. The recovery instance owns the loss/restored
handlers, the isContextLost flag, and the deterministic
rebuild order. Bound here because webglcontextlost /
webglcontextrestored canvas events fire only on WebGL
contexts.
WebGPU path. Three's WebGPURenderer recreates its own
GPU device internally when device.lost resolves, but it does
not rebuild Luxar-owned resources (post-processing render
targets, picking buffers, material caches, interleaved geometry
buffers). A full rebuild path mirroring WebGL2 is non-trivial
and untested in CI today, so for now we treat WebGPU device
loss as unrecoverable: log it loudly and dispatch a
webgpu-device-lost event so the host application can prompt
for a reload. The event payload carries the device-loss reason
for diagnostics.
Check if WebGL context is currently lost. Forwards to the
recovery instance; returns false when recovery hasn't been
wired up yet (pre-init / post-dispose).
PrivateconfigurePrivatesetupInitialize the Three.js scene
PrivatesetupWire the LAZY scene environment (rendering/environment/scene-environment.ts).
Nothing is built here. The environment — a prefiltered RoomEnvironment on
scene.environment, the one lighting input a physically based mesh material
needs — is constructed the first time the material manager creates a physical
mesh material and never otherwise, so a scene without one keeps
scene.environment === null and renders exactly as it did before the
environment existed. House materials never read it either way (spec
MESH_PHYSICAL_MATERIALS_SPEC.md §3.3).
PrivatesetupInitialize the perspective camera. Thin delegate over
createDefaultPerspectiveCamera in scene-manager/camera/camera-setup
so the FOV/clip/initial-position pose is unit-testable in isolation.
PrivatesetupInitialize ControlsManager for flexible camera control
ControlsManager supports multiple control types:
Features:
PrivateresetReset camera + controls to default state when loading a new
dataset. Camera-side reset is delegated to
resetCameraToInitialPosition; the controls-side state machine
(reset → update → saveState) stays here because it's the
controls manager's contract.
PrivatesetupInitialize the HDR post-processing pipeline. Wires the manager's resize callback to refresh material uniforms that cache the drawing-buffer size.
Load scene data from Zarr source.
Cache and prefetch flags propagate through loaderConfig from
LuxarApp (originally derived from ?noCache/?cacheDebug/etc URL
parameters in main.ts).
options.applyViewerConfigFov is the caller's localStorage-precedence
decision, not a feature switch. Returning visitors keep their stored FOV
for auto-framed scenes; an authored position instead carries the resolved
scene FOV with it as one framing contract. Under an orthographic camera the
FOV only stashes the next perspective value; framing uses camera zoom, and
the later projection swap preserves that frustum.
OptionalloaderConfig: LoaderConfigPrivateframeFrame the opening camera from the root's METADATA alone, before any node
loads (the loader's onSceneMetadata hook). Load-time decisions that read
the view — B4's partition gating ranks parts against the camera and the
displayed dims — then see the pose the scene opens on, rather than the
reset pose and the previous scene's (or no) dimensions. The root joins the
scene only for the synchronous framing call, so no frame draws it
half-built. loadSceneData frames again once the nodes are in, which
re-applies the same pose. A target_node names a node not built yet, so
such a scene is framed after the load only (as before).
PrivateframeThe opening-camera framing of root (already in this.scene): displayed
dims, scene-scale orbit limits, the authored camera (or an auto-frame from
the metadata bounds) and the clipping planes. Idempotent.
PrivateapplyThe scene FOV, applied unconditionally when the caller asked for it, else only if valid.
PrivateresolveThe session override, else the scene's authored preset, else none.
PrivateframingThe up vector and (for an axis preset) view direction every fit/reset squares to. Without a preset — or one whose letters cannot form a plane — this is the scene up alone, the historical framing.
Get the viewer config from the loaded scene's root group userData.
The loaded scene's content_hash (undefined for a store that records none).
The baked environment map the loader found in the store, if any (see loadBakedEnvironment).
Give the scene environment what a live scene capture needs (the init pipeline
calls this once the load-activity predicate exists). The capture pushes the cube
camera's params (a square drawing buffer, pixel ratio 1) to the material manager
so point and line footprints render at the right size in the six faces, and
restores the main camera's push afterwards through the ordinary path. The 90°
projection itself — its ortho test included — is read from the cube camera at
draw time, so the camera KIND needs no push: every GLSL program and TSL graph
reads it in shader from the matrix, except the TSL screen-space line quad,
whose per-draw hook re-points it at its cached perspective graph for the faces
(a lookup, no build; see selectProjectionVariant).
Arm WebGL blend warm-up after all scene-dependent dataset setup completes.
PrivateapplyApply viewer config from zarr (camera position/target/up, background
color). Thin delegate over applyZarrViewerConfig in
scene-manager/camera/camera-setup; forwards the helper's
positionApplied flag so loadSceneData can suppress auto-framing.
(Author target alone does NOT suppress auto-framing.)
PrivateclearClear all loaded content from the scene, keeping lights and background.
Thin delegate over clearLoadedSceneContent in
scene-manager/render-pipeline/scene-disposal.
Center camera on the bounding box of all visible objects in scene.
Computes the bounding box of all Points and InstancedMesh objects, positions camera to view entire scene, and updates controls target. Skips centering if no geometry is found in the scene.
Called automatically after scene loading if dataset has reasonable size. Can be called manually via F key to recenter after navigation.
PrivateframecenterCameraOnScene's write, published by the caller.
Frame the camera on one object subtree (the per-layer sibling of centerCameraOnScene). Returns false when the subtree holds no framable geometry (e.g. a partition whose parts haven't streamed yet).
Get current camera target center point.
Returns either the bounding box center (if auto-centered) or origin (0,0,0) if using default positioning. The returned vector is a clone, safe to modify.
Current center as Vector3 (bounding box center or origin)
Get controls manager for camera interaction.
Provides access to orbit, fly, and ortho controls for advanced camera manipulation.
ControlsManager instance managing camera controls
Toggle camera centering between origin and bounding box center.
Switches between two centering modes:
Useful when dataset is not centered at origin or when you want to return to default camera position.
Center camera and controls on the origin (0,0,0) at the current distance. Also used by the rail Home popover's "Center on origin" action. Clears the bounding-box-centered flag so a subsequent toggleCentering switches back to the bounding box.
Update canvas size and camera aspect ratio for window resize.
Handles window resize events with debouncing via requestAnimationFrame. Updates:
Called automatically on window resize. Debouncing ensures smooth resize without excessive recomputations.
PrivatemeasureMeasure the viewport the canvas should fill.
Fullscreen-first: while any fullscreen is active the canvas is styled
to fill the screen (100vw/100vh, see
window-event-handler.onFullscreenChange, which keys off the same
isDocumentFullscreen() check — standard + webkit), so its DOM parent — an embed
container — no longer reflects its displayed size. Measure the window.
Parent-first otherwise: Three.js stamps inline width/height px
styles onto the canvas on every setSize, so the canvas's own client box
reflects our last stamp, not the host's layout. The parent element (the
embedder's frame, or document.body in the standalone app — sized 100% by
base/layout.css) is the box that actually tracks layout changes. Falls
back to the canvas's own box, then the window, when the parent reports
zero (detached canvas, jsdom).
Resize to the canvas's container box rather than the window.
This is the embedding-safe resize path: an embedded canvas lives inside
a host container whose size can change without the window changing
(sidebars, splitters, flex/grid reflow), so a window.innerWidth-based
resize would stamp window-sized inline styles onto the canvas and
overflow the host frame. Sizing comes from measureViewport
(parent-first). Synchronous via resizeNow; callers that fire it from
a ResizeObserver already get browser-batched delivery (~once/frame).
PrivatemakeBuild the per-call ResizeCtx snapshot used by the resize orchestrator.
PrivatesetStore/clear the explicit DPR override and return the effective DPR. Delegates the math to dpr-policy.computePixelRatioOverride.
Update pixel ratio for adaptive performance optimization.
Uses setSize with updateStyle=false to keep canvas CSS size constant while reducing the internal buffer resolution for better performance.
This method is called by the AdaptiveDPRManager when FPS drops below acceptable thresholds, and by the manual DPR control when adaptive mode is disabled.
Unlike resizeToCanvas this does NOT dispatch change, although
the resize clears the canvas: the idle restore resizes right after the
loop stops and draws its own frame, and a change would wake the loop
again. A caller outside a tick (a UI control) requests the repaint.
The new device pixel ratio to use
Update perspective camera FOV with bounds checking. Returns true when applied. In orthographic mode there is no live FOV to change, but the request is applied to the stashed perspective FOV so a FOV set while in ortho is honored on the next ortho→perspective swap.
Update camera clipping planes with validation.
Auto-adjust clipping planes from scene bounds (metadata first, geometry fallback). Also feeds the bounding-box diagonal into the scale-aware controls.
PrivateautoAuto-frame the camera to fit the scene contents using metadata bounds. Delegates to the camera-framing helper. Updates the centering-state tracking fields when framing succeeds.
If true, keep the current controls target (set by zarr viewer_config) instead of overwriting it with the bounding box center.
PrivateinvalidateInvalidate cached scene bounds. Called on scene load / clear. Display dims are immutable per scene, so no invalidation is needed for dimension navigation.
Update dynamic clipping planes using cached bounding sphere projection.
Called each frame by AnimationController. Uses a cached bounding sphere
(invalidated on scene load/clear) for smooth near/far values with zero
object allocations and — once the metadata bounds are cached — zero
per-frame scene graph traversal. A metadata-less scene is the exception:
the cache has no negative caching, so the per-frame ensure() re-walks
the graph each frame (see clipping/scene-bounds-cache.ts).
true when near/far changed (the frame must be redrawn)
PrivatemakeBuild the narrow ctx that the clipping-policy helpers consume. Created on demand to keep helper signatures stable as subsystem fields evolve.
Set dynamic clipping enabled/disabled.
Get current dynamic clipping state.
PrivategetGet 3D bounding box from scene metadata, projecting nD bounds to display dimensions. Delegates to the bounds-cache helper.
3D bounding box or null if metadata bounds not available
Update global exposure (log2 stops). Applied in the vendored tone mapping shader before tone mapping.
Update global offset (additive brightness shift). Applied in the vendored tone mapping shader before tone mapping.
Update global gamma correction. Applied in the vendored tone mapping shader before tone mapping.
Clean up all Three.js resources to prevent memory leaks.
WebGL resources (textures, buffers, shaders) are not automatically garbage collected and must be explicitly disposed. This method ensures proper cleanup of all GPU resources:
After calling dispose(), the scene manager cannot be reused.
Enable or disable automatic camera rotation
Whether to enable auto-rotation
Set the turntable speed, in REVOLUTIONS PER MINUTE.
A full turn takes 60 / speed seconds — 1.0 is one turn a minute, the
0.25 default is one turn every four minutes. The unit is inherited from
three.js OrbitControls and is what auto_rotate_speed means in every
authored scene, so it is the stored unit; the Navigation popover shows the
equivalent PERIOD in seconds (see secondsPerTurnFromRpm).
Frame-rate independent: the update step scales by deltaTime, so the
turn takes the same wall-clock time at 30 fps and at 144.
Revolutions per minute (> 0).
Set the camera-frame or fixed scene axis the turntable revolves around; see AutoRotateAxis.
Enable/disable the auto-dolly: a sinusoidal in-and-out motion along the
view direction, the turntable's radial sibling. Live in orbit AND ortho
(in 2D it breathes camera.zoom).
Dolly amplitude as a percent of the viewing distance (15 → ±15%).
Dolly period in seconds (one full in-and-out oscillation).
Orbit wheel-zoom speed (live; shared with ortho — same control class).
Orbit damping factor (live; shared with ortho — same control class).
Fly-mode mouse-look sensitivity (live on the current fly controls).
Toggle "natural drag" — swap LEFT ↔ RIGHT mouse buttons in orbit mode so a one-finger touchpad drag rotates and right-drag pans. Applies to orbit (3D) only; ortho and fly modes ignore.
Get current auto-rotation state
Current auto-dolly state (see setAutoDolly).
Current turntable axis (see setAutoRotateAxis).
Switch camera control type. Handles camera swap for ortho mode and
dispatches camera-changed at this public call site.
Control type ('orbit', 'fly', or 'ortho')
Get current control type.
PrivatemakeBuild the narrow ctx that the camera-mode helpers consume.
Update all materials with current camera projection parameters. Perspective: FOV-based. Orthographic: frustum-based.
Called internally on resize and camera changes. Also called by RecordingPanel when the renderer is resized for offline capture.
PrivatemakeBuild the narrow ctx that the camera-materials helpers consume.
Set fly controls movement speed
Set fly controls rotation speed
Set fly controls inertial mode
Set fly controls damping
Set fly controls rotation damping
Get current scene scale (bounding box diagonal). Returns 0 if not yet set.
SceneManager orchestrates all Three.js components for 3D rendering
Responsibilities:
Technical Details: