Luxar Viewer API Documentation - v2026.9.22
    Preparing search index...

    Manages all materials in the scene with lifecycle tracking and global camera-uniform updates. Supports points, lines, gsplats and mesh.

    ALL visual materials are PER NODE and are never cached. Point, line and gsplat materials each carry the node's own element texture (uPointTex / uLineTex / uSplatTex), so sharing one would rebind a node's texture onto another node's mesh at every commit; a mesh material carries no element texture but holds per-node shading state instead (see MaterialManager.getMeshMaterial). The historical line-material LRU was the last cached kind and died with the lines texture-storage migration.

    Index
    registeredMaterials: Set<Material<MaterialEventMap> & CameraAwareMaterial> = ...
    caps: RendererCapabilities | null = null

    Renderer capabilities — drives the GLSL vs. TSL dispatch in the four getXMaterial factories. SceneManager.setupRenderer calls setCaps once the renderer is alive; before that hook fires, the manager defaults to the WebGL2 path so unit tests that touch material creation don't need to know about caps.

    subscribedMaterials: WeakSet<Material<MaterialEventMap> & CameraAwareMaterial> = ...

    Materials whose dispose event we have already wired a listener for. Separate from registeredMaterials because register() / getXMaterial() can be called repeatedly with the same instance (a re-register(), or a clone re-registered explicitly), and a second addEventListener('dispose', ...) would silently stack listeners on THREE's EventDispatcher.

    totalCreateMs: number = 0

    Diagnostic: cumulative wall-clock time spent constructing materials (Point/Line/GSplat/Mesh). Useful as a proxy for "how much time does the user spend waiting for material-creation work" — first-use stutter shows up as a single large delta on the affected animation frame. Exposed in getCacheStats().

    createCount: number = 0
    physicalMaterialListeners: Set<() => void> = ...

    Listeners for onPhysicalMaterialCreated. The scene environment that lights physical meshes is built lazily, and this manager is the one place a physical material is born — so this hook is how the SceneManager learns it is time to build it, without the node factory knowing about renderers.

    ownedMaterials: Set<Material<MaterialEventMap>> = ...

    Materials that entered through register() rather than a manager factory (per-node point/line/gsplat/mesh materials live in registeredMaterials only).

    Examples: GPU-picking materials and colormap clones. These need manager-level disposal — and global camera uniforms when they are camera-aware — but are tracked separately for leak diagnostics.

    Typed as plain THREE.Material because register() accepts one: a non-camera-aware entry belongs in the leak diagnostic exactly as much as a camera-aware one, so this set must span both rather than silently omitting half of them.

    staticMaterials: Set<Material<MaterialEventMap>> = ...

    Materials that are tracked for disposal but take NO camera broadcast.

    The generic fallback for anything without updateCameraParams: register dispatches on isCameraAwareMaterial and lands the rest here. All four geometry types (mesh included, since #1431 gave it the near fade) are camera-aware today, so nothing from the getXMaterial factories reaches this set.

    It is NOT what keeps such a material from leaking: register() adds to ownedMaterials on the same path, and dispose unions that in. Two jobs are left, and both are real. It is the destination that is not the camera broadcast — updateCameraParams iterates registeredMaterials, so a material with no updateCameraParams has to land somewhere else or the broadcast would throw on it. And it is a term in the stats snapshot: totalRegistered is registeredMaterials.size + staticMaterials.size, so a non-camera-aware entry shows up in the leak diagnostic instead of vanishing from it. See LifecycleCtx.staticMaterials.

    currentResolution: Vector2 = ...
    currentIsOrtho: boolean = false
    currentNearCull: number | undefined = undefined
    currentPixelRatio: number = 1
    • Set the renderer capabilities. Called once by SceneManager after the renderer is alive. Determines which backend the dispatch in getPointMaterial (and Line/GSplat equivalents) picks.

      Materials already handed out keep their original class; switching caps only affects which class subsequent calls construct.

      Parameters

      Returns void

    • The two capabilities a mesh texture upload depends on.

      Exposed as a narrow accessor rather than the whole RendererCapabilities because the texture upload is the only consumer outside this class, and it needs exactly these two. Returns the conservative answer when caps have not been set (unit tests, pre-renderer): filterableFloatTextures: false selects a HalfFloat upload, which filters correctly on every backend — degrading HDR precision is recoverable, whereas a float32 texture the device cannot filter silently samples blocky.

      Returns { filterableFloatTextures: boolean; maxAnisotropy: number }

    • Create a point material — PER NODE, no LRU cache.

      Point data lives in a per-node texture (uPointTex), so two nodes can never share a point material: sharing would rebind one node's texture onto another's mesh at every commit. Every call creates a fresh material that the node owns for its lifetime (the node factory stamps _layerMaterialCloned: true, so LayersPanel / LOD-cross-fade mutate it directly instead of clone-on-first-use). There is no material cache: every material is per-node, so getCacheStats() reports only registry size and create-time, never a cache size. Mirrors getGSplatMaterial.

      Dispatches to PointTSLMaterial (NodeMaterial / TSL) when the active renderer reports caps.apiSurface === 'webgpu', otherwise to the GLSL PointMaterial. Both classes expose the same update surface (see LuxarPointMaterial), so callers in node-factory and the layers panel don't need to branch.

      Parameters

      Returns LuxarPointMaterial

    • Create a line material — PER NODE, no LRU cache.

      Segment data lives in a per-node texture (uLineTex, since the texture-backed storage migration), so two nodes can never share a line material: sharing would rebind one node's texture onto another's mesh at every commit. Every call creates a fresh material that the node owns for its lifetime (the node factory stamps _layerMaterialCloned: true, so LayersPanel / LOD-cross-fade mutate it directly instead of clone-on-first-use). There is no material cache: every material is per-node. Mirrors getPointMaterial / getGSplatMaterial.

      Dispatches to LineTSLMaterial (NodeMaterial / TSL) when the active renderer reports caps.apiSurface === 'webgpu', otherwise to the GLSL LineMaterial. Both classes expose the same update surface via LuxarLineMaterial.

      Parameters

      Returns LuxarLineMaterial

    • Create a gsplat material — PER NODE, no LRU cache.

      Splat data lives in a per-node texture (uSplatTex), so two nodes can never share a gsplat material: sharing would rebind one node's texture onto another's mesh at every commit. Every call creates a fresh material that the node owns for its lifetime (the node factory stamps _layerMaterialCloned: true, so LayersPanel / LOD-cross-fade mutate it directly instead of clone-on-first-use). There is no material cache: every material is per-node.

      Dispatches to GSplatTSLMaterial (NodeMaterial / TSL) when the active renderer reports caps.apiSurface === 'webgpu', otherwise to the GLSL GSplatMaterial. Both classes expose the same update surface via LuxarGSplatMaterial.

      Parameters

      Returns LuxarGSplatMaterial

    • Create a mesh material — PER NODE, no LRU cache.

      Per node for a different reason than its three siblings: they carry the node's own element texture, so sharing would rebind one node's data onto another's mesh. A mesh material holds no per-node texture at all — but it does hold two pieces of per-node state that make sharing wrong anyway: the shading compile-time variant (a function of that node's shading and its normals' validity for the active view) and side (re-applied per epoch by applyMeshSide). Sharing would let one node's shading model and face-sidedness follow another's.

      Enters registeredMaterials and takes the camera broadcast like its three siblings. It consumes only part of it — there is no screen-space size to recompute from the resolution — but the near-cull distance drives the shared near fade (#1431), and a mesh left out of the broadcast would fade against the constructor's 0.1 default instead of the scene's. There is no meshMaterialCache: no type has a material cache — every material is per-node, so getCacheStats() reports only registry size and create-time, never a cache size.

      Dispatches to MeshTSLMaterial when the active renderer reports caps.apiSurface === 'webgpu', otherwise the GLSL MeshMaterial.

      Parameters

      Returns LuxarMeshMaterial

    • Create a per-node PHYSICAL mesh material — three's MeshPhysicalMaterial (GLSL) or MeshPhysicalNodeMaterial (TSL) behind the Luxar leaf surface (MESH_PHYSICAL_MATERIALS_SPEC.md §3.2).

      Deliberately NOT getMeshMaterial with a flag: the two families share nothing but the geometry. This one takes no camera broadcast (it has no near fade — it enters through register, which files it as static), stamps no blending mode unless opaque, and is lit by the scene environment rather than a shader constant — which is why every creation notifies onPhysicalMaterialCreated: the environment is built lazily, on the first of these, and never for a scene that has none.

      Parameters

      Returns LuxarPhysicalMeshMaterial

    • Subscribe to physical-material creation. Fired synchronously inside getMeshPhysicalMaterial, after the material exists, on EVERY creation — the subscriber is expected to be idempotent (SceneEnvironment.ensure is).

      Parameters

      • listener: () => void

      Returns () => void

      An unsubscribe function.

    • Update camera parameters for all registered materials: viewport size, projection kind, near cull and pixel ratio. The projection terms themselves (FOV, ortho zoom, off-axis frustum) are read in shader from the projection matrix, so they need no push.

      Parameters

      • resolution: Vector2
      • isOrtho: boolean
      • nearCull: number | undefined
      • pixelRatio: number

      Returns void

    • Register a material the manager did not construct, for lifecycle tracking and — where applicable — global camera-parameter updates.

      Use this for non-cached materials such as per-node colormap clones and picking materials. Materials from the four getXMaterial factories register themselves.

      Takes a plain THREE.Material and DISPATCHES on the capability rather than demanding it, which is the same isCameraAwareMaterial pattern the picking system already uses. A camera-aware material joins the broadcast registry and receives the current camera state immediately; one that reads no camera uniform at all is tracked for disposal only.

      Dispatching here rather than at the call site is deliberate: it leaves ONE public entry point that cannot be called wrongly. Requiring & CameraAwareMaterial instead pushed the problem outward — the layers panel's LuxarMaterial had to claim a method it never calls just to satisfy this signature, which made a perfectly valid non-camera-aware leaf material unrepresentable in the panel.

      Parameters

      • material: Material

      Returns void

    • Drop a material from every registry without disposing the underlying GPU object. The picking-material classes use this in their custom dispose() paths so the manager stops broadcasting camera updates before the caller disposes it directly.

      Widened to THREE.Material alongside register, so anything that can be registered can be unregistered — the pair must accept the same set.

      Parameters

      • material: Material

      Returns void

    • Context-restore hook, called by webgl-context-recovery between the renderer rebuild and NodeFactory.rebuildAfterContextRestore.

      There is nothing to rebuild here: materials are per node, so the node factory reconstructs them along with their meshes and textures. This used to drop the per-type allocation caches, which no longer exist.

      It must stay a no-op rather than clearing the registries — registeredMaterials / ownedMaterials track materials attached to visible meshes, which must keep receiving updateCameraParams() across a restore. Kept as an explicit member so the recovery sequence stays readable and its ordering test keeps a real subject.

      Returns void

    • Get cache statistics (delegates to stats.ts).

      Returns {
          ownedMaterials: number;
          totalRegistered: number;
          totalCreateMs: number;
          createCount: number;
      }

      • ownedMaterials: number
      • totalRegistered: number

        Every material the manager is tracking, camera-aware or not — so a leak in a registry-only material is as visible here as one in the four geometry types. Materials in staticMaterials are counted but never receive updateCameraParams.

      • totalCreateMs: number

        Cumulative wall-clock ms spent inside new XMaterial(...) calls. Excludes WebGL program compilation, which happens lazily on first render.

      • createCount: number

        Number of new XMaterial(...) calls.