PrivateregisteredPrivatecapsRenderer 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.
PrivatesubscribedMaterials 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.
PrivatetotalDiagnostic: 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().
PrivatecreatePrivatephysicalListeners 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.
PrivateownedMaterials 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.
PrivatestaticMaterials 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.
PrivatecurrentPrivatecurrentPrivatecurrentPrivatecurrentPrivatelifecycleBuild the lifecycle context handed to subscribeToDispose and
removeFromRegistries. Lazy getter — captures this references
once, reused across calls.
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.
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.
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.
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.
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.
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.
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.
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).
An unsubscribe function.
Create a per-mesh point picking material, dispatching on
caps.apiSurface. The returned material is NOT cached — picking
materials have per-mesh lifetimes; the NodeFactory disposes
them when the mesh disposes. Camera updates flow through
register() like any other camera-aware material.
Same shape as createPointPickingMaterial, for lines.
Same shape as createPointPickingMaterial, for gsplats.
Same shape as createPointPickingMaterial, for meshes.
Create the post-processing mega-shader material, dispatching on
caps.apiSurface. PostProcessingManager owns the single instance; this
is the seam between THREE.ShaderMaterial-derived
MegaShaderMaterial and the TSL MegaShaderTSLMaterial.
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.
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.
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.
Dispose all managed materials.
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.
Get cache statistics (delegates to stats.ts).
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.
Cumulative wall-clock ms spent inside new XMaterial(...)
calls. Excludes WebGL program compilation, which happens lazily
on first render.
Number of new XMaterial(...) calls.
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.