@solidrt/3d 0.0.57 → 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1479 -678
- package/README.md +4 -2
- package/examples/README.md +56 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +129 -0
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +187 -22
- package/src/geometry.ts +1718 -203
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +457 -154
- package/src/index.ts +32 -21
- package/src/input.ts +11 -2
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +322 -190
- package/src/mesh.ts +492 -259
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +151 -54
- package/src/motion.ts +17 -0
- package/src/node.ts +423 -23
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +608 -185
- package/src/skeleton.ts +211 -0
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/src/node.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// types only).
|
|
8
8
|
|
|
9
9
|
import * as spatial from "flux:spatial"
|
|
10
|
-
import type { NodeId, NodeTransition } from "flux:spatial"
|
|
10
|
+
import type { NodeId, NodeMotionSpec, NodeTransition } from "flux:spatial"
|
|
11
11
|
import { on } from "srt:events"
|
|
12
|
+
import { createMutableTexture, destroyTexture } from "@solidrt/core/gpu"
|
|
12
13
|
import type { ShaderParams, TextureId } from "@solidrt/core/gpu"
|
|
13
14
|
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
14
15
|
// The scene's lookAt() aims a node; math's builds a camera's view matrix -
|
|
@@ -46,6 +47,69 @@ let scaleScratch: Vec3 = [1, 1, 1]
|
|
|
46
47
|
let declared = new Map<NodeId, SceneNode>()
|
|
47
48
|
let subscribed = false
|
|
48
49
|
|
|
50
|
+
// Free routing: destroy() leaves a node whose declaration carries an
|
|
51
|
+
// `exit` in the core as a LEAVING one (drawn, picked by nothing) until its
|
|
52
|
+
// exits settle, and the core's "spatialNodeFreed" event says when it is
|
|
53
|
+
// gone - the cue to detach its entries and recycle what was kept for it
|
|
54
|
+
// (an instance's record slot), then to run what was deferred to that
|
|
55
|
+
// moment (`then`: a disposer that reached the node mid-exit). Keyed by
|
|
56
|
+
// core id, with the scene the node was in (the handle's own `_scene` is
|
|
57
|
+
// already null: a destroyed handle routes no writes). One lazy
|
|
58
|
+
// subscription.
|
|
59
|
+
type Leaving = { node: SceneNode; scene: SceneHooks; then: (() => void)[] }
|
|
60
|
+
let leaving = new Map<NodeId, Leaving>()
|
|
61
|
+
let freeingSubscribed = false
|
|
62
|
+
|
|
63
|
+
function awaitFree(id: NodeId, node: SceneNode, scene: SceneHooks): void {
|
|
64
|
+
leaving.set(id, { node, scene, then: [] })
|
|
65
|
+
if (freeingSubscribed) return
|
|
66
|
+
freeingSubscribed = true
|
|
67
|
+
on("spatialNodeFreed", (event: { node: NodeId }) => {
|
|
68
|
+
let entry = leaving.get(event.node)
|
|
69
|
+
if (!entry) return
|
|
70
|
+
leaving.delete(event.node)
|
|
71
|
+
finishLeaving(entry)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function finishLeaving(entry: Leaving): void {
|
|
76
|
+
finishLeave(entry.node, entry.scene)
|
|
77
|
+
for (let fn of entry.then) fn()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Run `fn` once `node` has finished leaving, if it is on its way out
|
|
82
|
+
* (destroyed, its exit still playing): returns true and holds `fn` for
|
|
83
|
+
* the free; false when the node is not leaving, and the caller does its
|
|
84
|
+
* work now. What a disposer uses when destroy may just have let the node
|
|
85
|
+
* go: `disposeInstances` and `model.dispose` free buffers and textures
|
|
86
|
+
* the corpse still draws with, so on a leaving node they wait for it
|
|
87
|
+
* instead of cutting the exit short. A corpse freed early (`freeLeaving`,
|
|
88
|
+
* a parent's `destroy`) runs the held work at that point.
|
|
89
|
+
*/
|
|
90
|
+
export function afterFree(node: SceneNode, fn: () => void): boolean {
|
|
91
|
+
if (node._node === null) return false
|
|
92
|
+
let entry = leaving.get(node._node)
|
|
93
|
+
if (!entry) return false
|
|
94
|
+
entry.then.push(fn)
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Free every leaving node `where` admits NOW (its exit cut short where it
|
|
100
|
+
* stands) and finish its leave, held work included: the teardown paths
|
|
101
|
+
* call this before they free what a corpse still draws with - a scene's
|
|
102
|
+
* targets, a layer's buffers.
|
|
103
|
+
*/
|
|
104
|
+
export function freeLeaving(where: (node: SceneNode, scene: SceneHooks) => boolean): void {
|
|
105
|
+
for (let [id, entry] of [...leaving]) {
|
|
106
|
+
if (!where(entry.node, entry.scene)) continue
|
|
107
|
+
leaving.delete(id)
|
|
108
|
+
spatial.destroyNode(id)
|
|
109
|
+
finishLeaving(entry)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
49
113
|
function declareTransition(id: NodeId, node: SceneNode): void {
|
|
50
114
|
declared.set(id, node)
|
|
51
115
|
if (subscribed) return
|
|
@@ -74,6 +138,10 @@ export type SceneHooks = {
|
|
|
74
138
|
* and record binding. */
|
|
75
139
|
_attachInstance(instance: InstanceNode): void
|
|
76
140
|
_detachInstance(instance: InstanceNode): void
|
|
141
|
+
/** A mesh is on its way out (destroy with an `exit`): drop it from pick
|
|
142
|
+
* routing now - the core skips a leaving node anyway - while its entries
|
|
143
|
+
* keep drawing until `_detach` at the free. */
|
|
144
|
+
_exiting(mesh: Mesh): void
|
|
77
145
|
_attachLight(light: Light): void
|
|
78
146
|
_detachLight(light: Light): void
|
|
79
147
|
_lightChanged(): void
|
|
@@ -81,9 +149,14 @@ export type SceneHooks = {
|
|
|
81
149
|
_setCount(mesh: Mesh): void
|
|
82
150
|
/** Re-point the mesh's entries at its (replaced) instance buffers. */
|
|
83
151
|
_setBuffer(mesh: Mesh): void
|
|
84
|
-
/**
|
|
152
|
+
/** Re-point the mesh's morphing entries at its population's (replaced)
|
|
153
|
+
* weights texture. */
|
|
154
|
+
_setMorphTexture(mesh: Mesh): void
|
|
155
|
+
/** The mesh's draw range changed (setDrawRange): apply it to its entries. */
|
|
156
|
+
_setRange(mesh: Mesh): void
|
|
157
|
+
/** One of the mesh's record streams has a dirty range: publish it at
|
|
85
158
|
* the next sync. */
|
|
86
|
-
|
|
159
|
+
_setRecords(mesh: Mesh): void
|
|
87
160
|
/** The mesh's castShadow flag changed: re-evaluate the filtered views. */
|
|
88
161
|
_setCast(mesh: Mesh): void
|
|
89
162
|
/** The mesh's layers bitmask changed: re-evaluate every target. */
|
|
@@ -93,8 +166,17 @@ export type SceneHooks = {
|
|
|
93
166
|
_reorder(): void
|
|
94
167
|
/** The node's transform changed (for the sort and light bookkeeping). */
|
|
95
168
|
_moved(node: SceneNode): void
|
|
169
|
+
/** Push the node's LOD declaration (`_lod`) to the core: at scene enter
|
|
170
|
+
* once its level children are in, and on every setLod. */
|
|
171
|
+
_bindLod(node: SceneNode): void
|
|
96
172
|
}
|
|
97
173
|
|
|
174
|
+
/** A node's LOD declaration (createLod/setLod, createInstancedLod): the
|
|
175
|
+
* levels nearest first - each a child node, or none for a population's
|
|
176
|
+
* levels - with the projected size below which each hands over, and the
|
|
177
|
+
* cross-fade band. Re-applied whenever the node enters a scene. */
|
|
178
|
+
export type LodConfig = { levels: { node: SceneNode | null; size: number }[]; fade: number }
|
|
179
|
+
|
|
98
180
|
export type SceneNode = {
|
|
99
181
|
kind: "group" | "mesh" | "light" | "instance"
|
|
100
182
|
parent: SceneNode | null
|
|
@@ -127,10 +209,16 @@ export type SceneNode = {
|
|
|
127
209
|
* press (NodeTapEvent: `tapCount`); bubbles like down/move/up. */
|
|
128
210
|
onTap?: (event: NodeTapEvent) => void
|
|
129
211
|
/** A declared transition (setTransition) settled naturally on one
|
|
130
|
-
* component; a cancel, snap
|
|
212
|
+
* component; a cancel, snap, scene leave or exit never fires. */
|
|
131
213
|
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
132
|
-
/** The core node while in a scene (created at add, freed at remove
|
|
214
|
+
/** The core node while in a scene (created at add, freed at remove or
|
|
215
|
+
* at destroy's free - a destroyed node with an `exit` keeps it while
|
|
216
|
+
* it animates out). */
|
|
133
217
|
_node: NodeId | null
|
|
218
|
+
/** Gone for good (destroy): every write is a no-op, add/remove throw or
|
|
219
|
+
* skip, and the core node - if one is still animating out - is nobody's
|
|
220
|
+
* to write. */
|
|
221
|
+
_destroyed: boolean
|
|
134
222
|
_moved: boolean
|
|
135
223
|
/** The core writes this node's TRS itself (a clip player's target, a
|
|
136
224
|
* root-motion anchor), so the JS mirror above is stale: setTransform
|
|
@@ -143,17 +231,28 @@ export type SceneNode = {
|
|
|
143
231
|
* skin): bound to the core at every scene enter, so the flush writes
|
|
144
232
|
* `inverse(anchorWorld) * world * post` - the model-local bone matrix -
|
|
145
233
|
* to the palette texture's row whenever the node moves. `anchor` is the
|
|
146
|
-
* model root
|
|
234
|
+
* model root - the body's, for a piece bound onto its skeleton - and
|
|
235
|
+
* null for non-joints (the common case pays one null check). */
|
|
147
236
|
_palettes: { texture: TextureId; row: number; post: Float32Array; anchor: SceneNode }[] | null
|
|
237
|
+
/** The morph weights this node owns (see Mesh._morphOwner): the target
|
|
238
|
+
* names, the weights texture its core register is published to, and
|
|
239
|
+
* the JS-side weights a scene enter restores. null for the common case;
|
|
240
|
+
* created on demand for a mesh whose geometry carries targets, by
|
|
241
|
+
* createModel for a glTF node with them. */
|
|
242
|
+
_morph: MorphState | null
|
|
148
243
|
/** A culling-only local box (a model joint's influence region, in joint
|
|
149
244
|
* space): bound at every scene enter so the joint's world box follows
|
|
150
245
|
* the pose without joining the picking index. null for the common case. */
|
|
151
246
|
_cullBounds: Float32Array | null
|
|
247
|
+
/** The LOD group this node heads (see LodConfig); null for the common
|
|
248
|
+
* case. */
|
|
249
|
+
_lod: LodConfig | null
|
|
152
250
|
}
|
|
153
251
|
|
|
154
|
-
/** The settled component of a node transition
|
|
252
|
+
/** The settled component of a node transition: a transform component,
|
|
253
|
+
* or "weights" for the morph target weights lane. */
|
|
155
254
|
export type TransitionEndEvent = {
|
|
156
|
-
component: "position" | "rotation" | "scale"
|
|
255
|
+
component: "position" | "rotation" | "scale" | "weights"
|
|
157
256
|
}
|
|
158
257
|
|
|
159
258
|
/** What every scene pointer event carries, whichever handler sees it:
|
|
@@ -249,6 +348,31 @@ export type ScenePointerListener = {
|
|
|
249
348
|
onTap?: (event: SceneTapEvent) => void
|
|
250
349
|
}
|
|
251
350
|
|
|
351
|
+
/** A node's morph weights (internal; see SceneNode._morph). */
|
|
352
|
+
export type MorphState = {
|
|
353
|
+
/** One name per target, in the geometry's order. */
|
|
354
|
+
names: string[]
|
|
355
|
+
/** The weights texture: rgba32f, ceil(n / 4) texels wide - the node's
|
|
356
|
+
* core register published into `row` of it (bindWeightsSlot), what the
|
|
357
|
+
* entries bind as uMorphWeights. A plain owner's own one-row texture;
|
|
358
|
+
* an instance's is its population's (one row per record slot, the
|
|
359
|
+
* shader reads row gl_InstanceID), shared and owned by the mesh. */
|
|
360
|
+
texture: TextureId
|
|
361
|
+
row: number
|
|
362
|
+
/** The texture is the population's, not this node's to free. */
|
|
363
|
+
shared: boolean
|
|
364
|
+
/** The last JS write: what a scene enter pushes to the core. In a
|
|
365
|
+
* scene the core register is authoritative (getMorphWeights reads it). */
|
|
366
|
+
weights: Float32Array
|
|
367
|
+
/** The core node the texture's row is bound to (activateMorph), null
|
|
368
|
+
* when none is - the guard against rebinding at every entry build. */
|
|
369
|
+
bound: NodeId | null
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The per-target weights of a setMorphWeights write: by name (a partial
|
|
373
|
+
* write, the other targets untouched) or every weight in target order. */
|
|
374
|
+
export type MorphWeights = Record<string, number> | ArrayLike<number>
|
|
375
|
+
|
|
252
376
|
/** A bare node record (internal: the mesh and light constructors build on it). */
|
|
253
377
|
export function makeNode(kind: SceneNode["kind"]): SceneNode {
|
|
254
378
|
return {
|
|
@@ -260,12 +384,15 @@ export function makeNode(kind: SceneNode["kind"]): SceneNode {
|
|
|
260
384
|
scale: [1, 1, 1],
|
|
261
385
|
visible: true,
|
|
262
386
|
_node: null,
|
|
387
|
+
_destroyed: false,
|
|
263
388
|
_native: false,
|
|
264
389
|
_moved: false,
|
|
265
390
|
_scene: null,
|
|
266
391
|
_transition: null,
|
|
267
392
|
_palettes: null,
|
|
393
|
+
_morph: null,
|
|
268
394
|
_cullBounds: null,
|
|
395
|
+
_lod: null,
|
|
269
396
|
}
|
|
270
397
|
}
|
|
271
398
|
|
|
@@ -274,25 +401,125 @@ export function createGroup(): SceneNode {
|
|
|
274
401
|
}
|
|
275
402
|
|
|
276
403
|
/** Attach `child` under `parent` (re-parenting detaches it first). An
|
|
277
|
-
* instance is slot-bound to its mesh: addInstance places it.
|
|
404
|
+
* instance is slot-bound to its mesh: addInstance places it. A destroyed
|
|
405
|
+
* node, on either side, is gone for good and throws. */
|
|
278
406
|
export function add(parent: SceneNode, child: SceneNode): void {
|
|
279
|
-
if (child.kind === "instance") throw new Error("add: an instance is slot-bound to its mesh - addInstance places it,
|
|
407
|
+
if (child.kind === "instance") throw new Error("add: an instance is slot-bound to its mesh - addInstance places it, destroy destroys it")
|
|
408
|
+
if (child._destroyed) throw new Error("add: the child was destroyed")
|
|
409
|
+
if (parent._destroyed) throw new Error("add: the parent was destroyed")
|
|
280
410
|
if (child.parent !== null) remove(child)
|
|
281
411
|
child.parent = parent
|
|
282
412
|
parent.children.push(child)
|
|
283
413
|
if (parent._scene) enterScene(child, parent._scene)
|
|
284
414
|
}
|
|
285
415
|
|
|
286
|
-
/**
|
|
287
|
-
*
|
|
416
|
+
/**
|
|
417
|
+
* Detach `child` from its parent (and its meshes from the scene) - Three's
|
|
418
|
+
* `parent.remove(child)`, Godot's remove_child: the subtree stays intact
|
|
419
|
+
* and re-adds cleanly, so this is the verb for a node that is coming back
|
|
420
|
+
* (or about to be disposed). It SNAPS: a detached node plays no exit,
|
|
421
|
+
* that is `destroy`'s. An instance is slot-bound to its mesh and cannot
|
|
422
|
+
* be detached (destroy it); a destroyed node is already gone (no-op).
|
|
423
|
+
*/
|
|
288
424
|
export function remove(child: SceneNode): void {
|
|
289
|
-
if (child.kind === "instance") throw new Error("remove: an instance is slot-bound to its mesh -
|
|
425
|
+
if (child.kind === "instance") throw new Error("remove: an instance is slot-bound to its mesh - destroy destroys it")
|
|
426
|
+
if (child._destroyed) return
|
|
290
427
|
if (child._scene) leaveScene(child)
|
|
291
|
-
|
|
428
|
+
unlink(child)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Destroy a node and everything under it - Unity's Destroy, Godot's
|
|
433
|
+
* queue_free, @solidrt/2d's destroySprite/destroyGroup one dimension up:
|
|
434
|
+
* the subtree is gone for good and every handle in it goes inert (writes
|
|
435
|
+
* are no-ops, add throws, remove skips). This is the removal an `exit`
|
|
436
|
+
* rides on: a node whose transition declares one animates each such
|
|
437
|
+
* component to its exit value first and stays drawn meanwhile, a ghost
|
|
438
|
+
* that no pick, raycast, overlap or sweep sees, then frees when the last
|
|
439
|
+
* settles (no onTransitionEnd fires for an exit: the app already let go).
|
|
440
|
+
* Children go first, each animating its own exit, and their parent - an
|
|
441
|
+
* exit of its own or not - stays in the core as their frame until the
|
|
442
|
+
* last of them has settled, so a dying character's parts leave in its
|
|
443
|
+
* frame to the end; a mixer targeting a joint of the subtree keeps
|
|
444
|
+
* driving it while it goes. An instance is destroyed like any node here
|
|
445
|
+
* (its record slot recycles at the free). GPU resources stay on their own
|
|
446
|
+
* disposers (disposeGeometry, disposeInstances, model.dispose), as a
|
|
447
|
+
* sprite's atlas does; a disposer reaching a node still animating out
|
|
448
|
+
* frees it on the spot. Outside a scene there is nothing to animate: the
|
|
449
|
+
* subtree just goes inert.
|
|
450
|
+
*/
|
|
451
|
+
export function destroy(node: SceneNode): void {
|
|
452
|
+
if (node._destroyed) return
|
|
453
|
+
for (let c of node.children.slice()) destroy(c)
|
|
454
|
+
letGo(node)
|
|
455
|
+
unlink(node)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function unlink(node: SceneNode): void {
|
|
459
|
+
let parent = node.parent
|
|
292
460
|
if (parent !== null) {
|
|
293
|
-
let i = parent.children.indexOf(
|
|
461
|
+
let i = parent.children.indexOf(node)
|
|
294
462
|
if (i >= 0) parent.children.splice(i, 1)
|
|
295
|
-
|
|
463
|
+
node.parent = null
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The per-node half of destroy: mark the handle inert and let the core
|
|
469
|
+
* node go through its declaration's exits (flux:spatial exitNode). A node
|
|
470
|
+
* kept leaving finishes at the free (awaitFree); one freed on the spot,
|
|
471
|
+
* or outside a scene, finishes now. The handle's scene reference goes
|
|
472
|
+
* here so every scene-routed setter is a no-op from this point; the entry
|
|
473
|
+
* on the leaving map carries the scene to the finish.
|
|
474
|
+
*/
|
|
475
|
+
function letGo(node: SceneNode): void {
|
|
476
|
+
node._destroyed = true
|
|
477
|
+
let scene = node._scene
|
|
478
|
+
let id = node._node
|
|
479
|
+
node._scene = null
|
|
480
|
+
if (scene === null || id === null) {
|
|
481
|
+
finishLeave(node, null)
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
declared.delete(id)
|
|
485
|
+
if (node.kind === "mesh") scene._exiting(node as Mesh)
|
|
486
|
+
else if (node.kind === "instance") scene._detachInstance(node as InstanceNode)
|
|
487
|
+
if (spatial.exitNode(id)) awaitFree(id, node, scene)
|
|
488
|
+
else finishLeave(node, scene)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* The end of a destroyed node's leave, once its core node is gone (or
|
|
493
|
+
* never was): its entries come off the scene, an instance's slot recycles
|
|
494
|
+
* and the handle drops its core id. `scene` is null for a node that was
|
|
495
|
+
* not in one.
|
|
496
|
+
*/
|
|
497
|
+
function finishLeave(node: SceneNode, scene: SceneHooks | null): void {
|
|
498
|
+
node._node = null
|
|
499
|
+
if (node.kind === "instance") {
|
|
500
|
+
let instance = node as InstanceNode
|
|
501
|
+
let mesh = instance.mesh
|
|
502
|
+
if (mesh !== null) {
|
|
503
|
+
let nodes = mesh._instances?.nodes
|
|
504
|
+
if (nodes) {
|
|
505
|
+
nodes.slots[instance._slot] = null
|
|
506
|
+
nodes.free.push(instance._slot)
|
|
507
|
+
}
|
|
508
|
+
instance.mesh = null
|
|
509
|
+
}
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
if (scene !== null) {
|
|
513
|
+
if (node.kind === "mesh") scene._detach(node as Mesh)
|
|
514
|
+
else if (node.kind === "light") scene._detachLight(node as Light)
|
|
515
|
+
}
|
|
516
|
+
// A destroyed owner's weights texture goes with it: its entries (and
|
|
517
|
+
// those of the parts under a group owner, destroyed before it) are
|
|
518
|
+
// off the scene by now. An instance's row lives in its population's
|
|
519
|
+
// texture, which stays.
|
|
520
|
+
if (node._morph !== null) {
|
|
521
|
+
if (!node._morph.shared) destroyTexture(node._morph.texture)
|
|
522
|
+
node._morph = null
|
|
296
523
|
}
|
|
297
524
|
}
|
|
298
525
|
|
|
@@ -314,6 +541,10 @@ export function enterScene(node: SceneNode, scene: SceneHooks): void {
|
|
|
314
541
|
if (p.anchor._node !== null) spatial.bindTextureSlot(node._node, p.texture, p.row, p.post, p.anchor._node)
|
|
315
542
|
}
|
|
316
543
|
}
|
|
544
|
+
// A morph owner with weights already (a model's node, a mesh written
|
|
545
|
+
// before its first enter) binds them now; a mesh owner nothing wrote
|
|
546
|
+
// gets its state when a morphing entry first needs it (activateMorph).
|
|
547
|
+
if (node._morph !== null) activateMorph(node)
|
|
317
548
|
if (node._cullBounds !== null) spatial.setCullBounds(node._node, node._cullBounds)
|
|
318
549
|
if (node.kind === "mesh") {
|
|
319
550
|
spatial.setLayers(node._node, (node as Mesh).layers)
|
|
@@ -322,6 +553,8 @@ export function enterScene(node: SceneNode, scene: SceneHooks): void {
|
|
|
322
553
|
else if (node.kind === "light") scene._attachLight(node as Light)
|
|
323
554
|
else if (node.kind === "instance") scene._attachInstance(node as InstanceNode)
|
|
324
555
|
for (let c of node.children) enterScene(c, scene)
|
|
556
|
+
// After the children: node levels must be live to be named.
|
|
557
|
+
if (node._lod !== null) scene._bindLod(node)
|
|
325
558
|
scene._schedule()
|
|
326
559
|
}
|
|
327
560
|
|
|
@@ -339,6 +572,160 @@ export function leaveScene(node: SceneNode): void {
|
|
|
339
572
|
}
|
|
340
573
|
}
|
|
341
574
|
|
|
575
|
+
/** A weights texture over `targets` targets and `rows` owners: rgba32f,
|
|
576
|
+
* four weights per texel, zeroed (the core writes the rows). */
|
|
577
|
+
export function createWeightsTexture(targets: number, rows: number, label?: string): TextureId {
|
|
578
|
+
let width = Math.max(1, Math.ceil(targets / 4))
|
|
579
|
+
return createMutableTexture(new Float32Array(width * 4 * rows), width, rows, { format: "rgba32f", filter: "nearest", autoFree: false, label: label ?? "morph-weights" })
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Build a morph state for `names` seeded with `initial` (zeros without):
|
|
583
|
+
* the one-row weights texture is created here, freed when the owner is
|
|
584
|
+
* destroyed (finishLeave) or its geometry changes (resetMorph). */
|
|
585
|
+
export function createMorphState(names: string[], initial: ArrayLike<number> | null, label?: string): MorphState {
|
|
586
|
+
let weights = new Float32Array(names.length)
|
|
587
|
+
if (initial !== null) {
|
|
588
|
+
if (initial.length !== names.length) throw new Error("createMorphState: " + initial.length + " weights for " + names.length + " targets")
|
|
589
|
+
weights.set(initial)
|
|
590
|
+
}
|
|
591
|
+
return { names, texture: createWeightsTexture(names.length, 1, label), row: 0, shared: false, weights, bound: null }
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** The owner's morph state with its register bound to the weights
|
|
595
|
+
* texture and seeded, for a node in a scene (a scene enter, a morphing
|
|
596
|
+
* entry's build, an instance's attach): idempotent per core node. An
|
|
597
|
+
* instance binds its slot's row on every level's population texture
|
|
598
|
+
* (an instanced LOD's levels share the register). Outside a scene it
|
|
599
|
+
* only makes the state. */
|
|
600
|
+
export function activateMorph(node: SceneNode): MorphState {
|
|
601
|
+
let m = ensureMorph(node)
|
|
602
|
+
if (node._node !== null && m.bound !== node._node) {
|
|
603
|
+
for (let texture of morphTextures(node, m)) spatial.bindWeightsSlot(node._node, texture, m.row)
|
|
604
|
+
spatial.setWeights(node._node, m.weights)
|
|
605
|
+
m.bound = node._node
|
|
606
|
+
}
|
|
607
|
+
return m
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Every weights texture the node's register publishes into: its own,
|
|
611
|
+
* plus for an instance the other levels' population textures. */
|
|
612
|
+
function morphTextures(node: SceneNode, m: MorphState): TextureId[] {
|
|
613
|
+
let textures = [m.texture]
|
|
614
|
+
let mesh = node.kind === "instance" ? (node as InstanceNode).mesh : null
|
|
615
|
+
for (let level of mesh?._instances.levels ?? []) {
|
|
616
|
+
let texture = level._instances?.morph?.texture
|
|
617
|
+
if (texture !== undefined && !textures.includes(texture)) textures.push(texture)
|
|
618
|
+
}
|
|
619
|
+
return textures
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** The node's morph state, made on first use for a mesh that owns its
|
|
623
|
+
* own targets or an instance of a population with targets; throws for a
|
|
624
|
+
* node without targets. */
|
|
625
|
+
export function ensureMorph(node: SceneNode): MorphState {
|
|
626
|
+
if (node._morph !== null) return node._morph
|
|
627
|
+
if (node.kind === "instance") {
|
|
628
|
+
let instance = node as InstanceNode
|
|
629
|
+
let population = instance.mesh?._instances.morph ?? null
|
|
630
|
+
if (instance.mesh === null || population === null) throw new Error("The instance's mesh has no morph targets (geometry with `morphs`)")
|
|
631
|
+
let names = instance.mesh.geometry.morphs!.names
|
|
632
|
+
node._morph = { names, texture: population.texture, row: instance._slot, shared: true, weights: new Float32Array(names.length), bound: null }
|
|
633
|
+
return node._morph
|
|
634
|
+
}
|
|
635
|
+
let mesh = node.kind === "mesh" ? (node as Mesh) : null
|
|
636
|
+
if (mesh === null || mesh._morphOwner !== mesh || mesh.geometry.morphs === undefined) {
|
|
637
|
+
throw new Error("The node has no morph targets (a mesh over geometry with `morphs`, or a model node with targets)")
|
|
638
|
+
}
|
|
639
|
+
node._morph = createMorphState(mesh.geometry.morphs.names, null, mesh.geometry.label ? mesh.geometry.label + "-weights" : undefined)
|
|
640
|
+
return node._morph
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Move an instance's morph state to its population's replacement
|
|
644
|
+
* texture (growth: same targets, same row): the core register keeps its
|
|
645
|
+
* weights and any running track, only the slot moves - unbound from the
|
|
646
|
+
* old texture, bound to the new, staged at the next flush. */
|
|
647
|
+
export function rebindMorph(node: SceneNode, texture: TextureId): void {
|
|
648
|
+
let m = node._morph
|
|
649
|
+
if (m === null) return
|
|
650
|
+
let previous = m.texture
|
|
651
|
+
m.texture = texture
|
|
652
|
+
if (node._node !== null && m.bound === node._node) {
|
|
653
|
+
spatial.unbindWeightsSlot(node._node, previous)
|
|
654
|
+
spatial.bindWeightsSlot(node._node, texture, m.row)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Drop a node's morph state (setGeometry, a population's texture
|
|
659
|
+
* replaced): the next enter, or the next setMorphWeights, makes a fresh
|
|
660
|
+
* one. In a scene the core slots are unbound first; the entries rebuild
|
|
661
|
+
* after. An owner's own texture is freed, a shared one is the mesh's. */
|
|
662
|
+
export function resetMorph(node: SceneNode): void {
|
|
663
|
+
if (node._morph === null) return
|
|
664
|
+
if (node._node !== null) spatial.unbindWeightsSlot(node._node)
|
|
665
|
+
if (!node._morph.shared) destroyTexture(node._morph.texture)
|
|
666
|
+
node._morph = null
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Write morph target weights on a node that owns them - a mesh whose
|
|
671
|
+
* geometry carries `morphs`, an instance of an instanced mesh over such
|
|
672
|
+
* geometry (each copy morphs by its own weights), or a model's node for
|
|
673
|
+
* a glTF mesh with targets (`model.nodes`; every part of that mesh
|
|
674
|
+
* follows it). By name,
|
|
675
|
+
* `{ smile: 0.7 }` leaves the other targets at their last written
|
|
676
|
+
* value; an array writes them all in target order. Weights are
|
|
677
|
+
* unbounded (glTF's model: 0..1 is the authored range, past it
|
|
678
|
+
* extrapolates). The write lands in the spatial core's register and
|
|
679
|
+
* reaches the vertex stage at the next flush. It is a TARGET like a
|
|
680
|
+
* setTransform write: a `weights` entry in the node's transition
|
|
681
|
+
* declaration (setTransition, `{ weights: { duration: 300 } }`) makes
|
|
682
|
+
* every write animate, and `transition` here animates this one write
|
|
683
|
+
* on a motion of its own (`{ duration, bounce? | curve, delay? }` or a
|
|
684
|
+
* shorthand string), declaration or not - a smile springs in; a settle
|
|
685
|
+
* calls `onTransitionEnd` with component "weights". A clip's weights
|
|
686
|
+
* track writes the same register, last write wins each frame (the
|
|
687
|
+
* players advance first, so a write from onFrame wins). Three's
|
|
688
|
+
* `morphTargetInfluences`, Godot's `set_blend_shape_value`, Unity's
|
|
689
|
+
* `SetBlendShapeWeight` (Unity's scale is 0..100).
|
|
690
|
+
*/
|
|
691
|
+
export function setMorphWeights(node: SceneNode, weights: MorphWeights, transition?: NodeMotionSpec): void {
|
|
692
|
+
let m = ensureMorph(node)
|
|
693
|
+
if (typeof (weights as ArrayLike<number>).length === "number") {
|
|
694
|
+
let all = weights as ArrayLike<number>
|
|
695
|
+
if (all.length !== m.names.length) {
|
|
696
|
+
throw new Error("setMorphWeights: " + all.length + " weights for " + m.names.length + " targets (" + m.names.join(", ") + ")")
|
|
697
|
+
}
|
|
698
|
+
m.weights.set(all)
|
|
699
|
+
} else {
|
|
700
|
+
for (let [name, value] of Object.entries(weights as Record<string, number>)) {
|
|
701
|
+
let i = m.names.indexOf(name)
|
|
702
|
+
if (i < 0) throw new Error("setMorphWeights: no morph target '" + name + "' (targets: " + m.names.join(", ") + ")")
|
|
703
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error("setMorphWeights: weight '" + name + "' must be a finite number, got " + value)
|
|
704
|
+
m.weights[i] = value
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (node._node !== null) spatial.writeWeights(node._node, m.weights, transition)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** The node's morph weights, one per target in target order - a copy,
|
|
711
|
+
* read from the spatial core while the node is in a scene (a playing
|
|
712
|
+
* weights track shows here), from the last JS write otherwise. */
|
|
713
|
+
export function getMorphWeights(node: SceneNode): Float32Array {
|
|
714
|
+
let m = ensureMorph(node)
|
|
715
|
+
let out = new Float32Array(m.names.length)
|
|
716
|
+
if (node._node !== null) spatial.readWeights(node._node, out)
|
|
717
|
+
else out.set(m.weights)
|
|
718
|
+
return out
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** The node's morph target names, in target order (the index of each
|
|
722
|
+
* weight); empty for a node without targets. */
|
|
723
|
+
export function getMorphNames(node: SceneNode): string[] {
|
|
724
|
+
if (node._morph !== null) return node._morph.names.slice()
|
|
725
|
+
let mesh = node.kind === "mesh" ? (node as Mesh) : node.kind === "instance" ? (node as InstanceNode).mesh : null
|
|
726
|
+
return mesh !== null && (mesh._morphOwner === mesh || node.kind === "instance") && mesh.geometry.morphs !== undefined ? mesh.geometry.morphs.names.slice() : []
|
|
727
|
+
}
|
|
728
|
+
|
|
342
729
|
/** The node's local transform in the FFI carrier. */
|
|
343
730
|
export function fillTransform(node: SceneNode): Float32Array {
|
|
344
731
|
let t = transformScratch
|
|
@@ -364,17 +751,28 @@ export type { TransformUpdate } from "./math.ts"
|
|
|
364
751
|
* (position/scale per lane, rotation along the quaternion geodesic - a
|
|
365
752
|
* spring keeps its velocity through retargets, the pursuit-safe shape),
|
|
366
753
|
* so JS writes once per target change instead of once per frame. A spec
|
|
367
|
-
* per component (position, rotation, scale
|
|
754
|
+
* per component (position, rotation, scale, and `weights` for a morph
|
|
755
|
+
* owner's setMorphWeights writes, one lane per target) plus `all`; each
|
|
368
756
|
* `{ duration, bounce? }` (a spring, the default) / `{ duration, curve }`
|
|
369
757
|
* (a tween) / a shorthand string like "300ms ease-out". The declaration
|
|
370
758
|
* lives on the node and re-applies whenever it enters a scene; the pose
|
|
371
|
-
* it enters with
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
759
|
+
* it enters with snaps, unless a component's `from` (its lanes: `[x, y,
|
|
760
|
+
* z]`, a quaternion for rotation) animates it in from there at every
|
|
761
|
+
* scene enter (a weights `from` is one number per target); a
|
|
762
|
+
* component's `exit` is where it animates to when `destroy` lets go of
|
|
763
|
+
* the node (see there). Either endpoint takes the
|
|
764
|
+
* object form `{ value, duration?, curve?, bounce?, delay? }` to own its
|
|
765
|
+
* direction's motion, and `delay` on an entry holds its writes; `stagger`
|
|
766
|
+
* (ms) on a node spaces the enters and exits of its descendants that
|
|
767
|
+
* begin in one frame (a Group's declaration, the element rule: the
|
|
768
|
+
* orchestrator is always an ancestor). Clearing cancels running tracks in
|
|
769
|
+
* place (the node keeps its mid-flight transform) and later writes snap. Each natural settle calls the node's
|
|
770
|
+
* `onTransitionEnd` with the component (the raw "spatialTransitionEnd"
|
|
771
|
+
* engine event on srt:events stays for flux:spatial consumers; it carries
|
|
772
|
+
* the core id, `_node`).
|
|
376
773
|
*/
|
|
377
774
|
export function setTransition(node: SceneNode, transition: NodeTransition | string | null): void {
|
|
775
|
+
if (node._destroyed) return
|
|
378
776
|
node._transition = transition
|
|
379
777
|
if (node._node !== null) {
|
|
380
778
|
spatial.setTransition(node._node, transition)
|
|
@@ -395,6 +793,7 @@ export function setTransition(node: SceneNode, transition: NodeTransition | stri
|
|
|
395
793
|
* equal to the node's current quaternion is also a no-op.
|
|
396
794
|
*/
|
|
397
795
|
export function setTransform(node: SceneNode, update: TransformUpdate): void {
|
|
796
|
+
if (node._destroyed) return
|
|
398
797
|
// A no-op write costs nothing: driving every node from onFrame is the
|
|
399
798
|
// intended shape, and most nodes did not move. Exact compares, like
|
|
400
799
|
// setVisible - a value that survives a float round trip unchanged is the
|
|
@@ -456,6 +855,7 @@ export function setTransform(node: SceneNode, update: TransformUpdate): void {
|
|
|
456
855
|
* if it were a rotation).
|
|
457
856
|
*/
|
|
458
857
|
export function lookAt(node: SceneNode, target: Vec3, up: Vec3 = WORLD_UP): void {
|
|
858
|
+
if (node._destroyed) return
|
|
459
859
|
let parent = node.parent
|
|
460
860
|
if (parent === null) {
|
|
461
861
|
// No ancestors: parent space IS world space, aim straight from the
|
|
@@ -570,7 +970,7 @@ function unrotate(out: Vec3, m: Mat4, v: Vec3): Vec3 {
|
|
|
570
970
|
/** Show or hide a node and its whole subtree (a hidden mesh costs one
|
|
571
971
|
* `instanceCount: 0` draw range - the entry stays, drawing nothing). */
|
|
572
972
|
export function setVisible(node: SceneNode, visible: boolean): void {
|
|
573
|
-
if (node.visible === visible) return
|
|
973
|
+
if (node._destroyed || node.visible === visible) return
|
|
574
974
|
node.visible = visible
|
|
575
975
|
if (node._node !== null) {
|
|
576
976
|
spatial.setVisible(node._node, visible)
|