@solidrt/3d 0.0.58 → 0.0.60

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.
Files changed (70) hide show
  1. package/AGENTS.md +734 -264
  2. package/README.md +4 -2
  3. package/examples/README.md +50 -4
  4. package/examples/bloom.tsx +113 -0
  5. package/examples/cascades.tsx +4 -4
  6. package/examples/collision.tsx +6 -6
  7. package/examples/exits.tsx +96 -0
  8. package/examples/first-person.tsx +5 -5
  9. package/examples/fleet.tsx +4 -4
  10. package/examples/fog.tsx +5 -5
  11. package/examples/instanced.tsx +70 -50
  12. package/examples/lamps.tsx +4 -4
  13. package/examples/lod.tsx +125 -0
  14. package/examples/materials.tsx +9 -9
  15. package/examples/morph.tsx +174 -0
  16. package/examples/normals.tsx +118 -0
  17. package/examples/{lit.tsx → phong.tsx} +9 -9
  18. package/examples/polyhedra.tsx +61 -0
  19. package/examples/scene-atlas.tsx +70 -0
  20. package/examples/scene-views.tsx +5 -5
  21. package/examples/shadows.tsx +9 -9
  22. package/examples/sky-lit.tsx +3 -3
  23. package/examples/skybox.tsx +5 -5
  24. package/examples/standard.tsx +2 -2
  25. package/examples/streams.tsx +122 -0
  26. package/examples/sweep-paths.tsx +5 -5
  27. package/examples/wireframe.tsx +24 -2
  28. package/package.json +3 -3
  29. package/src/clip.ts +18 -5
  30. package/src/color.ts +11 -2
  31. package/src/components/context.tsx +11 -2
  32. package/src/components/directional-light.tsx +2 -2
  33. package/src/components/first-person-camera.tsx +4 -4
  34. package/src/components/group.tsx +9 -5
  35. package/src/components/hemisphere-light.tsx +2 -2
  36. package/src/components/index.ts +4 -1
  37. package/src/components/instanced-mesh.tsx +28 -10
  38. package/src/components/lod.tsx +102 -0
  39. package/src/components/mesh.tsx +24 -6
  40. package/src/components/node-props.ts +35 -5
  41. package/src/components/orbit-camera.tsx +15 -12
  42. package/src/components/point-light.tsx +2 -2
  43. package/src/components/record-mesh.tsx +12 -7
  44. package/src/components/scene.tsx +55 -2
  45. package/src/components/spot-light.tsx +2 -2
  46. package/src/components/sprite.tsx +2 -2
  47. package/src/components/view3d.tsx +18 -1
  48. package/src/environment.ts +9 -8
  49. package/src/first-person.ts +125 -38
  50. package/src/geometry-gpu.ts +172 -39
  51. package/src/geometry.ts +1573 -239
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +363 -141
  54. package/src/index.ts +31 -21
  55. package/src/light.ts +7 -8
  56. package/src/lod.ts +148 -0
  57. package/src/material.ts +316 -202
  58. package/src/mesh.ts +490 -256
  59. package/src/mixer.ts +27 -14
  60. package/src/model-file.ts +54 -15
  61. package/src/model.ts +86 -21
  62. package/src/motion.ts +17 -0
  63. package/src/node.ts +421 -22
  64. package/src/orbit.ts +378 -116
  65. package/src/profile.ts +4 -25
  66. package/src/resolve.ts +181 -0
  67. package/src/scene-shadows.ts +94 -93
  68. package/src/scene.ts +605 -185
  69. package/src/sweep.ts +27 -8
  70. package/tools/model.ts +3 -1
package/src/mesh.ts CHANGED
@@ -3,15 +3,16 @@
3
3
  // setter write paths that keep a live scene's draw entries in step. The
4
4
  // scene side is reached through the node's SceneHooks (node.ts).
5
5
 
6
- import { createBuffer, destroyBuffer, writeBuffer } from "@solidrt/core/gpu"
7
- import type { BufferId, DrawId, InstanceAttribute, ShaderParams, TextureBindings } from "@solidrt/core/gpu"
8
- import { FORMAT_FLOATS, geometryBounds, plane } from "./geometry.ts"
9
- import type { Geometry } from "./geometry.ts"
6
+ import { createBuffer, destroyBuffer, destroyTexture, writeBuffer } from "@solidrt/core/gpu"
7
+ import type { BufferId, DrawId, ShaderParams, TextureBindings, TextureId, VertexAttribute, VertexBufferLayout } from "@solidrt/core/gpu"
8
+ import { geometryBounds, isFloatLayout, layoutKey, layoutStride, plane, vertexBytes, vertexView, VERTEX_FORMATS } from "./geometry.ts"
9
+ import type { AttributeAccess, Geometry } from "./geometry.ts"
10
+ import { INSTANCE_MATRIX_ATTRIBUTES } from "./glsl.ts"
10
11
  import type { GeometryBuffers } from "./geometry-gpu.ts"
11
12
  import type { Material } from "./material.ts"
12
13
  import type { TransformUpdate, Vec3 } from "./math.ts"
13
14
  import * as spatial from "flux:spatial"
14
- import { enterScene, leaveScene, makeNode, remove, setTransform } from "./node.ts"
15
+ import { activateMorph, afterFree, createWeightsTexture, enterScene, makeNode, rebindMorph, remove, resetMorph, setTransform } from "./node.ts"
15
16
  import type { SceneNode } from "./node.ts"
16
17
 
17
18
  export type Mesh = SceneNode & {
@@ -45,6 +46,9 @@ export type Mesh = SceneNode & {
45
46
  * null for an unskinned mesh. */
46
47
  _cullJoints: SceneNode[] | null
47
48
  _entry: DrawId | null
49
+ /** The index range the entries draw, or null for the whole index list.
50
+ * Set with setDrawRange; reset by setGeometry. */
51
+ _range: { first: number; count: number } | null
48
52
  /** The geometry-buffer reference the entry was built from, acquired at
49
53
  * attach and what _detach releases - like _transparent, a snapshot,
50
54
  * because setGeometry swaps mesh.geometry before the rebuild. */
@@ -64,6 +68,12 @@ export type Mesh = SceneNode & {
64
68
  * variant declares uBones) - any other override validates its bindings
65
69
  * against a program that may not declare these names. */
66
70
  _textures: TextureBindings | null
71
+ /** The node whose weights register drives this mesh's morph targets
72
+ * (its weights texture is bound as uMorphWeights): the mesh itself for
73
+ * a standalone mesh whose geometry carries targets, the glTF node's
74
+ * group for a model part - one write and one clip track for every part
75
+ * of that mesh. null for geometry without targets. */
76
+ _morphOwner: SceneNode | null
67
77
  /** Instance state when the mesh was made by createInstancedMesh; null on
68
78
  * an ordinary mesh. */
69
79
  _instances: MeshInstances | null
@@ -72,20 +82,25 @@ export type Mesh = SceneNode & {
72
82
  _sprite: boolean
73
83
  }
74
84
 
75
- /** The per-mesh half of instancing: the record buffer and its bookkeeping.
76
- * Read the public fields freely; write through the population's own
77
- * functions (addInstance/removeInstance, setRecords/setRecordCount) so
78
- * the draw range follows. */
85
+ /** The per-mesh half of instancing: the population's bookkeeping, the
86
+ * core-written matrix buffer of an instanced mesh and the app-owned
87
+ * record streams. Read the public fields freely; write through the
88
+ * population's own functions (addInstance/destroy, setRecords/
89
+ * setRecordCount, instanceAttribute + updateRecords) so the draw range
90
+ * and the publishes follow. */
79
91
  export type MeshInstances = {
80
- /** The GPU record buffer (instance slot 0), owned by the mesh
81
- * (disposeInstances frees it): the core-written matrices on an
82
- * instanced mesh, the app's records on a record mesh. */
83
- buffer: BufferId
84
- /** Floats per record: INSTANCE_FLOATS on an instanced mesh, the
85
- * material's slot-0 instanceAttributes summed on a record mesh. */
86
- stride: number
87
- /** Records the buffer has room for; doubles on growth (a replacement
88
- * buffer, never a resize). */
92
+ /** The GPU matrix buffer of an instanced mesh - the material's first
93
+ * instance buffer, INSTANCE_MATRIX_ATTRIBUTES - which the core writes
94
+ * each instance node's placement into (JS never does; disposeInstances
95
+ * frees it). null on a record mesh, whose every buffer is a stream. */
96
+ matrix: BufferId | null
97
+ /** The app-owned record streams: one per instance buffer the mesh's
98
+ * material declared at creation, in declaration order, the matrix
99
+ * buffer excluded (so an instanced mesh's style record is streams[0],
100
+ * a record mesh's records are). */
101
+ streams: InstanceStream[]
102
+ /** Records every buffer has room for; doubles on growth (replacements,
103
+ * never a resize). */
89
104
  capacity: number
90
105
  /** The buffer label, carried to replacement buffers on growth. */
91
106
  label: string | undefined
@@ -103,47 +118,69 @@ export type MeshInstances = {
103
118
  /** The node-backed population (createInstancedMesh): handles by slot
104
119
  * (null = free) and the free list; null on a record mesh. */
105
120
  nodes: InstanceSlots | null
106
- /** The app-written style records (instance slot 1) of an instanced mesh
107
- * whose material declares slot-1 attributes; null otherwise (and on a
108
- * record mesh, whose one buffer is the app's already). */
109
- style: InstanceStyle | null
121
+ /** The level meshes of an instanced LOD (createInstancedLod), nearest
122
+ * first - the first is the population itself, the rest its children -
123
+ * sharing this population's instance slots, each with a matrix buffer
124
+ * of its own the core stages an instance into by its projected size.
125
+ * null on a plain population. */
126
+ levels: InstancedMesh[] | null
127
+ /** The population's morph weights texture when the geometry carries
128
+ * targets (an instanced mesh; a record mesh has no nodes to own
129
+ * weights): one row per record slot, `capacity` rows, each instance
130
+ * node's register published into its slot's row (setMorphWeights on
131
+ * the instance), what a morphing entry binds as uMorphWeights and the
132
+ * shader reads at row gl_InstanceID. Replaced on growth (the entries
133
+ * re-point), freed by disposeInstances. */
134
+ morph: { texture: TextureId; rows: number } | null
110
135
  }
111
136
 
112
137
  export type InstanceSlots = { slots: (InstanceNode | null)[]; free: number[] }
113
138
 
114
139
  /**
115
- * The style half of an instanced mesh: one record per instance slot of
116
- * the material's slot-1 instanceAttributes (a tint, a frame index, any
117
- * per-copy floats the vertex stage reads), owned by the app and written
118
- * per instance with setInstanceStyle - the JS twin of the core's matrix
119
- * records in slot 0, @solidrt/2d's pose/style split one dimension up.
120
- * Writes land in `data` and publish as ONE coalesced buffer write per
121
- * mesh at the scene's next sync, whatever the number of instances
122
- * styled; `buffer` grows with the matrix buffer.
140
+ * One app-owned per-instance buffer of a populated mesh: a vertex stream
141
+ * stepped per instance, Geometry.streams one step up. `layout` is the
142
+ * attribute list the material declared for it (any vertex format,
143
+ * tightly packed), `data` the JS mirror of `capacity` records - a
144
+ * Float32Array over an all-float layout, a Uint8Array otherwise
145
+ * (vertexView's rule) - and `buffer` the mesh-owned GPU copy. Writes go
146
+ * through instanceAttribute's accessor (or setInstanceStyle / setRecords,
147
+ * which encode for you), land in the mirror, and publish as ONE
148
+ * coalesced buffer write per stream at the scene's next sync over the
149
+ * `dirty` byte range - a frame-rate path, like setTransform. Growth
150
+ * replaces `data` and `buffer` (the accessor follows; a held `data`
151
+ * view does not).
123
152
  */
124
- export type InstanceStyle = {
125
- /** The GPU style buffer (instance slot 1), mesh-owned like `buffer`. */
126
- buffer: BufferId
127
- /** Floats per style record: the material's slot-1 attributes summed. */
153
+ export type InstanceStream = {
154
+ layout: VertexAttribute[]
155
+ /** Bytes per record: the layout's stride. */
128
156
  stride: number
129
- /** The JS mirror, capacity * stride floats; the buffer is published
130
- * from it. Read freely, write through setInstanceStyle. */
131
- data: Float32Array
132
- /** The record a fresh (or recycled) slot starts with: the material's
133
- * instanceStyle, zeros without one. */
134
- blank: Float32Array
135
- /** The float range [lo, hi) of `data` written since the last publish,
157
+ buffer: BufferId
158
+ data: ArrayBufferView
159
+ /** The record a fresh (or recycled) instance slot starts with: the
160
+ * material's instanceStyle encoded, on the first stream of an
161
+ * instanced mesh; zeros otherwise. */
162
+ blank: Uint8Array
163
+ /** The byte range [lo, hi) of `data` written since the last publish,
136
164
  * or null. */
137
165
  dirty: [number, number] | null
166
+ /** The DataView the accessors read through; follows `data`. */
167
+ _view: DataView
168
+ /** All-float32 layout: `data` is a Float32Array and a record write is
169
+ * plain indexed stores instead of a codec pass per component. */
170
+ _floats: boolean
171
+ /** Values per record: the layout's components summed. */
172
+ _components: number
173
+ /** One accessor per attribute, in layout order. */
174
+ _fields: AttributeAccess[]
138
175
  }
139
176
 
140
177
  /** A mesh from createInstancedMesh: one draw entry drawing the geometry
141
178
  * once per instance node (addInstance). */
142
- export type InstancedMesh = Mesh & { _instances: MeshInstances & { nodes: InstanceSlots } }
179
+ export type InstancedMesh = Mesh & { _instances: MeshInstances & { matrix: BufferId; nodes: InstanceSlots } }
143
180
 
144
181
  /** A mesh from createRecordMesh: one draw entry drawing the geometry once
145
- * per record of a JS-written buffer (setRecords). */
146
- export type RecordMesh = Mesh & { _instances: MeshInstances & { nodes: null } }
182
+ * per record of a JS-written stream (setRecords). */
183
+ export type RecordMesh = Mesh & { _instances: MeshInstances & { matrix: null; nodes: null } }
147
184
 
148
185
  /**
149
186
  * One instance of an InstancedMesh: a scene node like any other
@@ -153,7 +190,7 @@ export type RecordMesh = Mesh & { _instances: MeshInstances & { nodes: null } }
153
190
  * coalesced buffer write per flush however many instances moved. A
154
191
  * hidden instance draws nothing (its record collapses to zero scale).
155
192
  * Slot-bound to its mesh: created by addInstance, destroyed by
156
- * removeInstance; the generic add/remove reject it.
193
+ * destroy; the generic add/remove reject it.
157
194
  */
158
195
  export type InstanceNode = SceneNode & {
159
196
  kind: "instance"
@@ -182,11 +219,13 @@ export function createMesh(geometry: Geometry, material: Material): Mesh {
182
219
  mesh.cullMargin = 0
183
220
  mesh._cullJoints = null
184
221
  mesh._entry = null
222
+ mesh._range = null
185
223
  mesh._buffers = null
186
224
  mesh._transparent = false
187
225
  mesh._center = [0, 0, 0]
188
226
  mesh._params = null
189
227
  mesh._textures = null
228
+ mesh._morphOwner = geometry.morphs === undefined ? null : mesh
190
229
  mesh._instances = null
191
230
  mesh._sprite = false
192
231
  return mesh
@@ -226,30 +265,124 @@ export function localBounds(mesh: Mesh): Float32Array | null {
226
265
  return mesh._sprite ? SPRITE_BOUNDS : geometryBounds(mesh.geometry)
227
266
  }
228
267
 
229
- /** Floats per record of one instance slot of an attribute list (slot 0
230
- * by default; the list's `slot` keys pick the others). */
231
- export function instanceStride(attributes: InstanceAttribute[], slot = 0): number {
232
- let stride = 0
233
- for (let a of attributes) if ((a.slot ?? 0) === slot) stride += FORMAT_FLOATS[a.format]
234
- return stride
235
- }
236
-
237
- // The highest instance slot an attribute list names.
238
- function instanceSlots(attributes: InstanceAttribute[]): number {
239
- let top = 0
240
- for (let a of attributes) top = Math.max(top, a.slot ?? 0)
241
- return top + 1
242
- }
243
-
244
268
  /** Floats per instance record on an instanced mesh: one column-major
245
269
  * mat4, the instance's placement inside the mesh (the material reads it
246
270
  * through INSTANCE_MATRIX_ATTRIBUTES / INSTANCE_MATRIX in ./glsl). */
247
271
  export const INSTANCE_FLOATS = 16
272
+ const INSTANCE_BYTES = INSTANCE_FLOATS * Float32Array.BYTES_PER_ELEMENT
273
+ // The matrix layout by key: what an instanced material's first buffer
274
+ // must be, and what binds the core's buffer in any material's list.
275
+ const MATRIX_KEY = layoutKey(INSTANCE_MATRIX_ATTRIBUTES)
248
276
  // Instance slots an instanced mesh reserves without a capacity; growth
249
277
  // doubles past it.
250
278
  const DEFAULT_CAPACITY = 64
251
- // The instance buffers a mesh binds: the matrix slot and the style slot.
252
- const MESH_INSTANCE_SLOTS = 2
279
+
280
+ // Values per record of a layout: its attributes' components summed - what
281
+ // setInstanceStyle and a material's instanceStyle count in.
282
+ function layoutComponents(layout: VertexAttribute[]): number {
283
+ let n = 0
284
+ for (let a of layout) n += VERTEX_FORMATS[a.format].components
285
+ return n
286
+ }
287
+
288
+ // Records in a byte view of `layout`: 4-aligned and a whole number of
289
+ // strides, or a throw naming `site`.
290
+ function recordCount(records: ArrayBufferView, layout: VertexAttribute[], site: string): number {
291
+ let stride = layoutStride(layout)
292
+ if (records.byteOffset % Float32Array.BYTES_PER_ELEMENT !== 0) throw new Error(site + ": records must start on a 4-byte boundary")
293
+ if (records.byteLength % stride !== 0) {
294
+ throw new Error(site + ": " + records.byteLength + " record bytes is not a whole number of " + stride + "-byte (" + layoutKey(layout) + ") records")
295
+ }
296
+ return records.byteLength / stride
297
+ }
298
+
299
+ // The accessors of a stream's attributes, reading through the stream's
300
+ // current view so they survive growth.
301
+ function streamFields(stream: InstanceStream): AttributeAccess[] {
302
+ let offset = 0
303
+ return stream.layout.map(attr => {
304
+ let codec = VERTEX_FORMATS[attr.format]
305
+ let at = offset
306
+ offset += codec.bytes
307
+ return {
308
+ format: attr.format,
309
+ components: codec.components,
310
+ get: (i, k) => codec.get(stream._view, i * stream.stride + at, k),
311
+ set: (i, k, v) => codec.set(stream._view, i * stream.stride + at, k, v),
312
+ }
313
+ })
314
+ }
315
+
316
+ // Point a stream's mirror at `bytes` (a fresh ArrayBuffer of whole
317
+ // records): the handed-out view and the accessors' view.
318
+ function viewStream(stream: InstanceStream, bytes: ArrayBuffer): void {
319
+ stream.data = vertexView(stream.layout, bytes)
320
+ stream._view = new DataView(bytes)
321
+ }
322
+
323
+ function streamLabel(label: string | undefined, index: number): string | undefined {
324
+ return label === undefined ? undefined : label + "-" + index
325
+ }
326
+
327
+ // A stream of `capacity` zeroed records over `layout`; its GPU buffer is
328
+ // sized and unwritten (the mirror is the truth, publishes follow dirty).
329
+ function makeStream(layout: VertexAttribute[], capacity: number, blank: Uint8Array | null, label: string | undefined): InstanceStream {
330
+ let stride = layoutStride(layout)
331
+ let stream: InstanceStream = {
332
+ layout,
333
+ stride,
334
+ buffer: createBuffer(capacity * stride, { autoFree: false, label }),
335
+ data: new Uint8Array(0),
336
+ blank: blank ?? new Uint8Array(stride),
337
+ dirty: null,
338
+ _view: new DataView(new ArrayBuffer(0)),
339
+ _floats: isFloatLayout(layout),
340
+ _components: layoutComponents(layout),
341
+ _fields: [],
342
+ }
343
+ viewStream(stream, new ArrayBuffer(capacity * stride))
344
+ stream._fields = streamFields(stream)
345
+ return stream
346
+ }
347
+
348
+ // Encode `values` (one per component, in layout order, as the shader
349
+ // sees them) into record `i` of a stream: one typed-array set over an
350
+ // all-float layout (the values ARE the bytes), the codecs otherwise.
351
+ function writeRecord(stream: InstanceStream, i: number, values: ArrayLike<number>, site: string): void {
352
+ let n = stream._components
353
+ if (values.length !== n) throw new Error(site + ": a " + layoutKey(stream.layout) + " record is " + n + " values, got " + values.length)
354
+ if (stream._floats) {
355
+ // Indexed stores beat TypedArray.set over a plain array under QuickJS,
356
+ // which walks the array-like generically.
357
+ let data = stream.data as Float32Array
358
+ let at = i * n
359
+ for (let k = 0; k < n; k++) data[at + k] = values[k]!
360
+ return
361
+ }
362
+ let j = 0
363
+ for (let f of stream._fields) for (let k = 0; k < f.components; k++) f.set(i, k, values[j++]!)
364
+ }
365
+
366
+ // One record of `layout` encoded from `values`, the bytes a blank slot copies.
367
+ function encodeRecord(layout: VertexAttribute[], values: ArrayLike<number>, site: string): Uint8Array {
368
+ let stride = layoutStride(layout)
369
+ let one: InstanceStream = { layout, stride, buffer: 0 as BufferId, data: new Uint8Array(0), blank: new Uint8Array(0), dirty: null, _view: new DataView(new ArrayBuffer(0)), _floats: isFloatLayout(layout), _components: layoutComponents(layout), _fields: [] }
370
+ viewStream(one, new ArrayBuffer(stride))
371
+ one._fields = streamFields(one)
372
+ writeRecord(one, 0, values, site)
373
+ return vertexBytes(one.data)
374
+ }
375
+
376
+ // Extend a stream's dirty range over [lo, hi) bytes of the mirror and ask
377
+ // the scene to publish it at the next sync.
378
+ function markRecords(mesh: Mesh, stream: InstanceStream, lo: number, hi: number): void {
379
+ if (stream.dirty === null) stream.dirty = [lo, hi]
380
+ else {
381
+ if (lo < stream.dirty[0]) stream.dirty[0] = lo
382
+ if (hi > stream.dirty[1]) stream.dirty[1] = hi
383
+ }
384
+ mesh._scene?._setRecords(mesh)
385
+ }
253
386
 
254
387
  /** What both population kinds take. */
255
388
  type PopulationOptions = {
@@ -285,51 +418,45 @@ function copyBounds(bounds: ArrayLike<number> | undefined, site: string): Float3
285
418
  return out
286
419
  }
287
420
 
288
- function instancedAttributes(material: Material, site: string): InstanceAttribute[] {
289
- let attributes = material.instanceAttributes
290
- if (attributes === undefined) {
291
- throw new Error(site + ": the material declares no instanceAttributes - build it with shaderMaterialClass({ instanceAttributes: [...] }) or a stock material's `instanced`")
421
+ function instancedBuffers(material: Material, site: string): VertexBufferLayout[] {
422
+ let buffers = material.instanceBuffers
423
+ if (buffers === undefined) {
424
+ throw new Error(site + ": the material declares no instanceBuffers - build it with shaderMaterialClass({ instanceBuffers: [...] }) or a stock material's `instanced`")
425
+ }
426
+ return buffers
427
+ }
428
+
429
+ // The mesh buffer a declared instance layout binds to: the core's matrix
430
+ // buffer for the matrix layout, else the stream with the same attribute
431
+ // list (names and formats - a byte-equal layout with other formats would
432
+ // decode differently). Throws naming `site` when the mesh has none.
433
+ function streamBuffer(inst: MeshInstances, layout: VertexBufferLayout, site: string): BufferId {
434
+ let key = layoutKey(layout.attributes)
435
+ if (inst.matrix !== null && key === MATRIX_KEY) return inst.matrix
436
+ let stream = inst.streams.find(s => layoutKey(s.layout) === key)
437
+ if (stream === undefined) {
438
+ let carried = [...(inst.matrix !== null ? [MATRIX_KEY] : []), ...inst.streams.map(s => layoutKey(s.layout))]
439
+ throw new Error(site + ": the material's instance buffer " + key + " is none of the mesh's record layouts (" + carried.join("; ") + ")")
292
440
  }
293
- return attributes
441
+ return stream.buffer
294
442
  }
295
443
 
296
444
  /**
297
- * Whether a material's instance attributes fit a mesh's population: the
298
- * slot-0 stride must be the mesh's record stride, a slot-1 layout must
299
- * match the mesh's style stride (a record mesh has no slot 1), and no
300
- * slot beyond. Throws otherwise - at creation for the mesh's own
301
- * material, at add() for a swapped one, at a shadow view's attach for a
302
- * shadow variant.
445
+ * Whether a material's instance buffers fit a mesh's population: every
446
+ * layout it declares must be one the mesh carries (the matrix on an
447
+ * instanced mesh, else a stream with the same attribute list). Throws
448
+ * otherwise - at creation for the mesh's own material, at add() for a
449
+ * swapped one, at a shadow view's attach for a shadow variant.
303
450
  */
304
451
  export function checkInstancePairing(material: Material, inst: MeshInstances, site: string): void {
305
- let attrs = instancedAttributes(material, site)
306
- let slots = instanceSlots(attrs)
307
- if (slots > MESH_INSTANCE_SLOTS) {
308
- throw new Error(site + ": instance attributes name slot " + (slots - 1) + "; a mesh binds slots 0 (records) and 1 (style) only")
309
- }
310
- let stride = instanceStride(attrs, 0)
311
- if (stride !== inst.stride) {
312
- throw new Error(site + ": the material's slot-0 instance attributes take " + stride + " floats but the mesh's records are " + inst.stride)
313
- }
314
- let styleStride = instanceStride(attrs, 1)
315
- if (styleStride > 0) {
316
- if (inst.style === null) {
317
- throw new Error(site + ": the material declares slot-1 (style) instance attributes but the mesh has no style records" + (inst.nodes === null ? " (a record mesh is slot 0 only)" : ""))
318
- }
319
- if (styleStride !== inst.style.stride) {
320
- throw new Error(site + ": the material's slot-1 (style) attributes take " + styleStride + " floats but the mesh's style records are " + inst.style.stride)
321
- }
322
- }
452
+ for (let layout of instancedBuffers(material, site)) streamBuffer(inst, layout, site)
323
453
  }
324
454
 
325
- /** The instance-buffer binding of a draw entry for a material over a
326
- * population: the plain slot-0 key, or both slots when the material's
327
- * pipeline reads the style records too (the two spellings are exclusive
328
- * and must match the pipeline's slot count exactly). */
329
- export function instanceBinding(material: Material, inst: MeshInstances): { instanceBuffer?: BufferId; instanceBuffers?: BufferId[] } {
330
- let attrs = material.instanceAttributes
331
- if (attrs !== undefined && inst.style !== null && instanceSlots(attrs) > 1) return { instanceBuffers: [inst.buffer, inst.style.buffer] }
332
- return { instanceBuffer: inst.buffer }
455
+ /** The instance buffers a draw entry binds for a material over a
456
+ * population, one per declared layout in the pipeline's order after the
457
+ * geometry's: the matrix buffer or the stream with that layout. */
458
+ export function instanceBinding(material: Material, inst: MeshInstances): BufferId[] {
459
+ return (material.instanceBuffers ?? []).map(b => streamBuffer(inst, b, "instanceBinding"))
333
460
  }
334
461
 
335
462
  /**
@@ -355,93 +482,159 @@ export function instanceBinding(material: Material, inst: MeshInstances): { inst
355
482
  * frees the record buffers when done for good.
356
483
  */
357
484
  export function createInstancedMesh(geometry: Geometry, material: Material, opts?: InstancedMeshOptions): InstancedMesh {
358
- let attrs = instancedAttributes(material, "createInstancedMesh")
359
- let stride = instanceStride(attrs, 0)
360
- if (stride !== INSTANCE_FLOATS) {
485
+ let buffers = instancedBuffers(material, "createInstancedMesh")
486
+ let first = buffers[0]
487
+ if (first === undefined || layoutKey(first.attributes) !== MATRIX_KEY) {
361
488
  throw new Error(
362
- "createInstancedMesh: the material's slot-0 instanceAttributes take " + stride + " floats per record; an instance record is the " + INSTANCE_FLOATS + "-float matrix of INSTANCE_MATRIX_ATTRIBUTES",
489
+ "createInstancedMesh: the material's first instance buffer must be the instance matrix the core writes (INSTANCE_MATRIX_ATTRIBUTES); got " + (first === undefined ? "none" : layoutKey(first.attributes)),
363
490
  )
364
491
  }
365
492
  let capacity = opts?.capacity ?? DEFAULT_CAPACITY
366
493
  if (!(Number.isInteger(capacity) && capacity > 0)) {
367
494
  throw new Error("createInstancedMesh: capacity must be a positive integer, got " + capacity)
368
495
  }
369
- let styleStride = instanceStride(attrs, 1)
370
- let style: InstanceStyle | null = null
371
- if (styleStride > 0) {
372
- let blank = new Float32Array(styleStride)
373
- if (material.instanceStyle !== undefined) {
374
- if (material.instanceStyle.length !== styleStride) {
375
- throw new Error("createInstancedMesh: the material's instanceStyle has " + material.instanceStyle.length + " floats but its slot-1 attributes take " + styleStride)
376
- }
377
- blank.set(material.instanceStyle)
378
- }
379
- style = {
380
- buffer: createBuffer(capacity * styleStride * 4, { autoFree: false, label: opts?.label === undefined ? undefined : opts.label + "-style" }),
381
- stride: styleStride,
382
- data: new Float32Array(capacity * styleStride),
383
- blank,
384
- dirty: null,
385
- }
386
- }
496
+ let streams = buffers.slice(1).map((b, i) => {
497
+ let blank = i === 0 && material.instanceStyle !== undefined ? encodeRecord(b.attributes, material.instanceStyle, "createInstancedMesh: instanceStyle") : null
498
+ return makeStream(b.attributes, capacity, blank, streamLabel(opts?.label, i))
499
+ })
387
500
  let mesh = createMesh(geometry, material) as InstancedMesh
388
501
  mesh._instances = {
389
- buffer: createBuffer(capacity * INSTANCE_FLOATS * 4, { autoFree: false, label: opts?.label }),
390
- stride: INSTANCE_FLOATS,
502
+ matrix: createBuffer(capacity * INSTANCE_BYTES, { autoFree: false, label: opts?.label }),
503
+ streams,
391
504
  capacity,
392
505
  label: opts?.label,
393
506
  count: 0,
394
507
  bounds: copyBounds(opts?.bounds, "createInstancedMesh"),
395
508
  nodes: { slots: [], free: [] },
396
- style,
509
+ levels: null,
510
+ morph: populationMorph(geometry, capacity, opts?.label),
397
511
  }
398
- checkInstancePairing(material, mesh._instances, "createInstancedMesh")
399
512
  return mesh
400
513
  }
401
514
 
402
- // Extend a style's dirty range over [lo, hi) floats of the mirror and
403
- // ask the scene to publish it at the next sync.
404
- function markStyle(mesh: InstancedMesh, style: InstanceStyle, lo: number, hi: number): void {
405
- if (style.dirty === null) style.dirty = [lo, hi]
406
- else {
407
- if (lo < style.dirty[0]) style.dirty[0] = lo
408
- if (hi > style.dirty[1]) style.dirty[1] = hi
515
+ // The population's weights texture over `geometry`'s targets, `rows`
516
+ // slots deep; null for geometry without targets.
517
+ function populationMorph(geometry: Geometry, rows: number, label: string | undefined): { texture: TextureId; rows: number } | null {
518
+ if (geometry.morphs === undefined) return null
519
+ return { texture: createWeightsTexture(geometry.morphs.names.length, rows, label === undefined ? undefined : label + "-weights"), rows }
520
+ }
521
+
522
+ // Replace the population's weights texture: grown (`sameTargets`), every
523
+ // live instance's register moves to the new texture's row, its weights
524
+ // and any running track intact; the geometry changed, every instance
525
+ // drops its state (a fresh, zeroed one binds the new row at its next
526
+ // activation - now, for one in a scene). The entries re-point, then the
527
+ // old texture goes.
528
+ function replacePopulationMorph(mesh: InstancedMesh | RecordMesh, next: { texture: TextureId; rows: number } | null, sameTargets: boolean): void {
529
+ let inst: MeshInstances = mesh._instances
530
+ let previous = inst.morph
531
+ inst.morph = next
532
+ if (inst.nodes !== null) {
533
+ for (let n of inst.nodes.slots) {
534
+ if (n === null) continue
535
+ if (sameTargets && next !== null) rebindMorph(n, next.texture)
536
+ else {
537
+ resetMorph(n)
538
+ if (next !== null && n._scene !== null) activateMorph(n)
539
+ }
540
+ }
409
541
  }
410
- mesh._scene?._setStyle(mesh)
542
+ mesh._scene?._setMorphTexture(mesh)
543
+ if (previous !== null) destroyTexture(previous.texture)
544
+ }
545
+
546
+ /** The meshes sharing a population's instance slots: the LOD levels of an
547
+ * instanced LOD, else the mesh alone. */
548
+ function populationLevels(mesh: InstancedMesh): InstancedMesh[] {
549
+ return mesh._instances.levels ?? [mesh]
411
550
  }
412
551
 
413
552
  /**
414
- * Write an instance's style record - the floats of the material's
415
- * slot-1 instance attributes, in order (`[r, g, b, a]` for a stock
416
- * material's instanceColors): the app's per-copy data beside the matrix
417
- * the core writes. Any number of instances styled between two frames
418
- * cost one coalesced buffer write at the scene's sync, so this is a
419
- * frame-rate path like setTransform. Throws on a material without
420
- * slot-1 attributes or a record of the wrong length; a no-op on a
421
- * removed instance.
553
+ * Write an instance's style record: the values of the material's second
554
+ * instance buffer (the first stream), one per attribute component in
555
+ * order, as the shader sees them - `[r, g, b, a]` for a stock material's
556
+ * instanceColors, whatever the format stores them as. The app's per-copy
557
+ * data beside the matrix the core writes; any number of instances styled
558
+ * between two frames cost one coalesced buffer write at the scene's
559
+ * sync, so this is a frame-rate path like setTransform. Throws on a
560
+ * material without a second instance buffer or a record of the wrong
561
+ * length; a no-op on a removed instance. instanceAttribute + updateRecords
562
+ * is the same write by attribute name, and the only way into a third
563
+ * stream.
422
564
  */
423
565
  export function setInstanceStyle(instance: InstanceNode, values: ArrayLike<number>): void {
424
566
  let mesh = instance.mesh
425
567
  if (mesh === null) return
426
- let style = mesh._instances.style
427
- if (style === null) throw new Error("setInstanceStyle: the mesh's material declares no slot-1 (style) instance attributes")
428
- if (values.length !== style.stride) {
429
- throw new Error("setInstanceStyle: a style record is " + style.stride + " floats, got " + values.length)
568
+ if (mesh._instances.streams[0] === undefined) throw new Error("setInstanceStyle: the mesh's material declares no instance buffer beyond the matrix (no style record)")
569
+ // Every level of an instanced LOD takes the style; a level whose
570
+ // material carries no style record is left alone.
571
+ for (let l of populationLevels(mesh)) {
572
+ let stream = l._instances.streams[0]
573
+ if (stream === undefined) continue
574
+ writeRecord(stream, instance._slot, values, "setInstanceStyle")
575
+ markRecords(l, stream, instance._slot * stream.stride, (instance._slot + 1) * stream.stride)
576
+ }
577
+ }
578
+
579
+ /**
580
+ * The accessor for per-instance attribute `name` over the mesh's record
581
+ * streams - geometryAttribute one step up: record index in, component
582
+ * values as the shader sees them (a unorm8x4 tint reads and writes as
583
+ * 0..1), encoded through the format's codec. Write through it, then
584
+ * updateRecords the range; the accessor stays valid across growth. null
585
+ * when no stream carries the name (the matrix columns are the core's,
586
+ * not reachable here); throws on a disposed mesh.
587
+ */
588
+ export function instanceAttribute(mesh: InstancedMesh | RecordMesh, name: string): AttributeAccess | null {
589
+ let inst: MeshInstances | null = mesh._instances
590
+ if (inst === null) throw new Error("instanceAttribute: the mesh's instances are disposed")
591
+ for (let s of inst.streams) {
592
+ let i = s.layout.findIndex(a => a.name === name)
593
+ if (i >= 0) return s._fields[i]!
594
+ }
595
+ return null
596
+ }
597
+
598
+ /** Options of `updateRecords`: the stream (default 0) and the record
599
+ * range (default the whole stream, capacity wide). */
600
+ export type UpdateRecordsOptions = { stream?: number; first?: number; count?: number }
601
+
602
+ /**
603
+ * Publish records `[first, first + count)` of one record stream from its
604
+ * mirror at the scene's next sync: updateVertices for instance data, and
605
+ * the partial rewrite a population stepped in JS wants (ten moved records
606
+ * of ten thousand cost ten). Write the mirror first, through
607
+ * instanceAttribute, then call this. The range is against the stream's
608
+ * capacity, not the drawn count: a record mesh writes ahead and dials
609
+ * setRecordCount after.
610
+ */
611
+ export function updateRecords(mesh: InstancedMesh | RecordMesh, options: UpdateRecordsOptions = {}): void {
612
+ let inst: MeshInstances | null = mesh._instances
613
+ if (inst === null) throw new Error("updateRecords: the mesh's instances are disposed")
614
+ let index = options.stream ?? 0
615
+ let stream = inst.streams[index]
616
+ if (!Number.isInteger(index) || stream === undefined) {
617
+ throw new Error("updateRecords: stream " + index + " is out of range; the mesh has record streams 0.." + (inst.streams.length - 1))
430
618
  }
431
- let at = instance._slot * style.stride
432
- style.data.set(values, at)
433
- markStyle(mesh, style, at, at + style.stride)
619
+ let first = options.first ?? 0
620
+ let count = options.count ?? inst.capacity - first
621
+ if (!Number.isInteger(first) || !Number.isInteger(count) || first < 0 || count < 0 || first + count > inst.capacity) {
622
+ throw new Error("updateRecords: range [" + first + ", " + (first + count) + ") is outside the mesh's " + inst.capacity + " records")
623
+ }
624
+ if (count > 0) markRecords(mesh, stream, first * stream.stride, (first + count) * stream.stride)
434
625
  }
435
626
 
436
- /** Publish an instanced mesh's pending style writes as one buffer write
437
- * (the scene calls it from its sync; nothing to do without a dirty
438
- * range). */
439
- export function publishInstanceStyle(mesh: InstancedMesh): void {
440
- let style = mesh._instances.style
441
- if (style === null || style.dirty === null) return
442
- let [lo, hi] = style.dirty
443
- style.dirty = null
444
- writeBuffer(style.buffer, style.data.subarray(lo, hi), lo * 4)
627
+ /** Publish a mesh's pending record writes, one buffer write per dirty
628
+ * stream (the scene calls it from its sync). */
629
+ export function publishRecords(mesh: Mesh): void {
630
+ let inst = mesh._instances
631
+ if (inst === null) return
632
+ for (let s of inst.streams) {
633
+ if (s.dirty === null) continue
634
+ let [lo, hi] = s.dirty
635
+ s.dirty = null
636
+ writeBuffer(s.buffer, vertexBytes(s.data).subarray(lo, hi), lo)
637
+ }
445
638
  }
446
639
 
447
640
  /**
@@ -451,10 +644,12 @@ export function publishInstanceStyle(mesh: InstancedMesh): void {
451
644
  * mesh's subtree (a squad group within a fleet: the record stays
452
645
  * mesh-relative through it). Its record slot is fixed for its life (a
453
646
  * removed instance's slot recycles to the next add); past the
454
- * reservation the buffer doubles. From here on it is a node like any
647
+ * reservation the buffers double. From here on it is a node like any
455
648
  * other: setTransform/setTransition/setVisible, lookAt, worldPosition,
456
649
  * pointer handlers, children of its own (a headlight mesh under a car
457
- * instance) - and removeInstance, never remove.
650
+ * instance) - and `destroy` (its slot hides at the next flush and recycles
651
+ * to the next addInstance; with an `exit` it animates out first), never
652
+ * remove: an instance cannot exist outside its mesh.
458
653
  */
459
654
  export function addInstance(mesh: InstancedMesh, update?: TransformUpdate, parent: SceneNode = mesh): InstanceNode {
460
655
  let inst: (MeshInstances & { nodes: InstanceSlots }) | null = mesh._instances
@@ -463,7 +658,10 @@ export function addInstance(mesh: InstancedMesh, update?: TransformUpdate, paren
463
658
  if (p === null) throw new Error("addInstance: parent must be the mesh or a node inside its subtree")
464
659
  }
465
660
  let slot = inst.nodes.free.pop() ?? inst.nodes.slots.length
466
- if (slot >= inst.capacity) growInstances(mesh, inst.capacity * 2)
661
+ // The levels of an instanced LOD grow, count and blank together: one
662
+ // slot, one record per level.
663
+ let levels = populationLevels(mesh)
664
+ if (slot >= inst.capacity) for (let l of levels) growInstances(l, inst.capacity * 2)
467
665
  let instance = makeNode("instance") as InstanceNode
468
666
  instance.mesh = mesh
469
667
  ;(instance as { _slot: number })._slot = slot
@@ -471,71 +669,51 @@ export function addInstance(mesh: InstancedMesh, update?: TransformUpdate, paren
471
669
  inst.nodes.slots[slot] = instance
472
670
  instance.parent = parent
473
671
  parent.children.push(instance)
474
- if (slot >= inst.count) {
475
- inst.count = slot + 1
476
- mesh._scene?._setCount(mesh)
477
- }
478
- // A recycled slot must not wear its last occupant's style.
479
- let style = inst.style
480
- if (style !== null) {
481
- let at = slot * style.stride
482
- style.data.set(style.blank, at)
483
- markStyle(mesh, style, at, at + style.stride)
672
+ for (let l of levels) {
673
+ let li = l._instances
674
+ if (slot >= li.count) {
675
+ li.count = slot + 1
676
+ l._scene?._setCount(l)
677
+ }
678
+ // A recycled slot must not wear its last occupant's records.
679
+ for (let s of li.streams) {
680
+ vertexBytes(s.data).set(s.blank, slot * s.stride)
681
+ markRecords(l, s, slot * s.stride, (slot + 1) * s.stride)
682
+ }
484
683
  }
485
684
  if (parent._scene) enterScene(instance, parent._scene)
486
685
  return instance
487
686
  }
488
687
 
489
- // Double the record buffers: replacements (never a resize) the live
490
- // instances' record sinks move to in one core call, republished whole at
491
- // the next flush, and the style mirror re-published whole from JS; the
492
- // entry re-points, then the old buffers, which the entry held alive until
493
- // now, are freed.
494
- function growInstances(mesh: InstancedMesh, next: number): void {
495
- let inst = mesh._instances
496
- let previous = inst.buffer
497
- inst.buffer = createBuffer(next * INSTANCE_FLOATS * 4, { autoFree: false, label: inst.label })
688
+ // Grow the population's buffers to `next` records: replacements (never a
689
+ // resize). An instanced mesh's live matrix records move to the new buffer
690
+ // in one core call and republish at the next flush; every stream's mirror
691
+ // is copied over and marked whole, so the scene republishes it; the entry
692
+ // re-points, then the old buffers, which the entry held alive until now,
693
+ // are freed.
694
+ function growInstances(mesh: InstancedMesh | RecordMesh, next: number): void {
695
+ let inst: MeshInstances = mesh._instances
696
+ let previous = inst.capacity
498
697
  inst.capacity = next
499
- // Out of a scene nothing is bound, and retargeting an empty source throws.
500
- if (inst.nodes.slots.some(n => n !== null && n._node !== null)) spatial.retargetRecords(previous, inst.buffer)
501
- let style = inst.style
502
- let previousStyle: BufferId | null = null
503
- if (style !== null) {
504
- previousStyle = style.buffer
505
- style.buffer = createBuffer(next * style.stride * 4, { autoFree: false, label: inst.label === undefined ? undefined : inst.label + "-style" })
506
- let grown = new Float32Array(next * style.stride)
507
- grown.set(style.data)
508
- style.data = grown
509
- style.dirty = null
510
- markStyle(mesh, style, 0, inst.count * style.stride)
698
+ let freed: BufferId[] = []
699
+ if (inst.matrix !== null) {
700
+ freed.push(inst.matrix)
701
+ inst.matrix = createBuffer(next * INSTANCE_BYTES, { autoFree: false, label: inst.label })
702
+ // Out of a scene nothing is bound, and retargeting an empty source throws.
703
+ if (inst.nodes !== null && inst.nodes.slots.some(n => n !== null && n._node !== null)) spatial.retargetRecords(freed[0]!, inst.matrix)
511
704
  }
705
+ inst.streams.forEach((s, i) => {
706
+ freed.push(s.buffer)
707
+ s.buffer = createBuffer(next * s.stride, { autoFree: false, label: streamLabel(inst.label, i) })
708
+ let held = vertexBytes(s.data)
709
+ viewStream(s, new ArrayBuffer(next * s.stride))
710
+ vertexBytes(s.data).set(held)
711
+ s.dirty = null
712
+ markRecords(mesh, s, 0, previous * s.stride)
713
+ })
512
714
  mesh._scene?._setBuffer(mesh)
513
- destroyBuffer(previous)
514
- if (previousStyle !== null) destroyBuffer(previousStyle)
515
- }
516
-
517
- /**
518
- * Destroy an instance: its slot hides at the next flush and recycles to
519
- * the next addInstance, and the handle goes inert (`mesh` null; later
520
- * writes are no-ops). Its children leave the scene with it and stay
521
- * parented to the dead node - remove() one to re-add it elsewhere. Remove
522
- * means destroy here, as for a 2d sprite: an instance cannot exist
523
- * outside its mesh.
524
- */
525
- export function removeInstance(instance: InstanceNode): void {
526
- let mesh = instance.mesh
527
- if (mesh === null) return
528
- if (instance._scene) leaveScene(instance)
529
- let parent = instance.parent
530
- if (parent !== null) {
531
- let i = parent.children.indexOf(instance)
532
- if (i >= 0) parent.children.splice(i, 1)
533
- instance.parent = null
534
- }
535
- let nodes = mesh._instances.nodes
536
- nodes.slots[instance._slot] = null
537
- nodes.free.push(instance._slot)
538
- instance.mesh = null
715
+ for (let b of freed) destroyBuffer(b)
716
+ if (inst.morph !== null) replacePopulationMorph(mesh, populationMorph(mesh.geometry, next, inst.label), true)
539
717
  }
540
718
 
541
719
  /**
@@ -544,7 +722,7 @@ export function removeInstance(instance: InstanceNode): void {
544
722
  * floats - the raw escape hatch for motion only JS can compute at scale
545
723
  * (a particle sim, a crowd stepped in a worker), Three's InstancedMesh
546
724
  * and Godot's MultiMesh as plain data. The material must declare
547
- * `instanceAttributes` (shaderMaterialClass); its vertex stage reads each
725
+ * `instanceBuffers` (shaderMaterialClass); its vertex stage reads each
548
726
  * record through those `in` variables. `records` is the interleaved
549
727
  * attribute data (stride = the attributes' floats summed) and is uploaded
550
728
  * here; its length is the buffer's initial capacity, which setRecords
@@ -556,56 +734,57 @@ export function removeInstance(instance: InstanceNode): void {
556
734
  export function createRecordMesh(
557
735
  geometry: Geometry,
558
736
  material: Material,
559
- records: Float32Array,
737
+ records: ArrayBufferView,
560
738
  count?: number,
561
739
  opts?: RecordMeshOptions,
562
740
  ): RecordMesh {
563
- let attrs = instancedAttributes(material, "createRecordMesh")
564
- let stride = instanceStride(attrs, 0)
565
- if (records.length % stride !== 0) {
566
- throw new Error("createRecordMesh: " + records.length + " floats is not a whole number of " + stride + "-float records")
567
- }
568
- let capacity = records.length / stride
741
+ let buffers = instancedBuffers(material, "createRecordMesh")
742
+ let capacity = recordCount(records, buffers[0]!.attributes, "createRecordMesh")
569
743
  let mesh = createMesh(geometry, material) as RecordMesh
570
744
  mesh._instances = {
571
- buffer: createBuffer(records, { autoFree: false, label: opts?.label }),
572
- stride,
745
+ matrix: null,
746
+ streams: buffers.map((b, i) => makeStream(b.attributes, capacity, null, streamLabel(opts?.label, i))),
573
747
  capacity,
574
748
  label: opts?.label,
575
749
  count: Math.max(0, Math.min(Math.floor(count ?? capacity), capacity)),
576
750
  bounds: copyBounds(opts?.bounds, "createRecordMesh"),
577
751
  nodes: null,
578
- style: null,
752
+ levels: null,
753
+ morph: null,
579
754
  }
580
- checkInstancePairing(material, mesh._instances, "createRecordMesh")
755
+ copyRecords(mesh, records, capacity)
581
756
  return mesh
582
757
  }
583
758
 
759
+ // Overwrite the first `written` records of a record mesh's first stream
760
+ // from `records` and mark them for publish.
761
+ function copyRecords(mesh: RecordMesh, records: ArrayBufferView, written: number): void {
762
+ let stream = mesh._instances.streams[0]!
763
+ vertexBytes(stream.data).set(vertexBytes(records))
764
+ if (written > 0) markRecords(mesh, stream, 0, written * stream.stride)
765
+ }
766
+
584
767
  /**
585
- * Overwrite a record mesh's records from the start of its buffer and (by
586
- * default) draw exactly the records written - pass `count` to draw fewer,
587
- * or to keep more previously written ones alive past a partial rewrite.
588
- * More records than the buffer holds grow it: capacity doubles (or jumps
589
- * to the records written when that is more), a new buffer is created and
590
- * written, the mesh's entry is re-pointed at it and the old buffer freed
591
- * - so a population grows without a new mesh, with the copies amortized
592
- * like any dynamic array (size the initial records to skip them).
593
- * Frame-rate-safe like setMeshParams when no growth happens.
768
+ * Overwrite a record mesh's records (its first stream) from the start
769
+ * and (by default) draw exactly the records written - pass `count` to
770
+ * draw fewer, or to keep more previously written ones alive past a
771
+ * partial rewrite. `records` is laid out in the material's first
772
+ * instance layout (a Float32Array over an all-float layout, bytes
773
+ * through instanceAttribute's codecs otherwise); a length that is not
774
+ * whole records throws. More records than the buffer holds grow it:
775
+ * capacity doubles (or jumps to the records written when that is more)
776
+ * into replacement buffers, the entry is re-pointed and the old ones
777
+ * freed - so a population grows without a new mesh, with the copies
778
+ * amortized like any dynamic array (size the initial records to skip
779
+ * them). The write publishes at the scene's next sync, one buffer write;
780
+ * for a few records of many, write the mirror through instanceAttribute
781
+ * and updateRecords the range instead.
594
782
  */
595
- export function setRecords(mesh: RecordMesh, records: Float32Array, count?: number): void {
783
+ export function setRecords(mesh: RecordMesh, records: ArrayBufferView, count?: number): void {
596
784
  let inst = mesh._instances
597
- if (records.length % inst.stride !== 0) {
598
- throw new Error("setRecords: " + records.length + " floats is not a whole number of " + inst.stride + "-float records")
599
- }
600
- let written = records.length / inst.stride
601
- if (written > inst.capacity) {
602
- let previous = inst.buffer
603
- inst.capacity = Math.max(written, inst.capacity * 2)
604
- inst.buffer = createBuffer(inst.capacity * inst.stride * 4, { autoFree: false, label: inst.label })
605
- mesh._scene?._setBuffer(mesh)
606
- destroyBuffer(previous)
607
- }
608
- writeBuffer(inst.buffer, records)
785
+ let written = recordCount(records, inst.streams[0]!.layout, "setRecords")
786
+ if (written > inst.capacity) growInstances(mesh, Math.max(written, inst.capacity * 2))
787
+ copyRecords(mesh, records, written)
609
788
  setRecordCount(mesh, count ?? written)
610
789
  }
611
790
 
@@ -620,15 +799,32 @@ export function setRecordCount(mesh: RecordMesh, count: number): void {
620
799
  }
621
800
 
622
801
  /**
623
- * Detach the mesh (if attached) and free its record buffers (the style
624
- * buffer with the matrix one). The buffers are mesh-owned with no
802
+ * Detach the mesh (if attached) and free its record buffers (the matrix
803
+ * buffer and every stream). The buffers are mesh-owned with no
625
804
  * reference count (unlike geometry buffers they are never shared), so
626
805
  * this is the one explicit free; the mesh cannot be re-added afterwards.
627
- * An instanced mesh's instances go inert with it.
806
+ * An instanced mesh's instances go inert with it. On a mesh `destroy`
807
+ * just let go of, with an exit still playing (its own or an instance's),
808
+ * the free waits for the last of them - the components unmount as
809
+ * `destroy(mesh)` then `disposeInstances(mesh)`, so an `<Instance>`'s
810
+ * exit plays through its mesh's unmount.
628
811
  */
629
812
  export function disposeInstances(mesh: InstancedMesh | RecordMesh): void {
630
813
  let inst: MeshInstances | null = mesh._instances
631
814
  if (inst === null) return
815
+ // An instanced LOD frees every level's buffers; the levels are untied
816
+ // first so each disposes as a plain population.
817
+ if (inst.levels !== null) {
818
+ let levels = inst.levels
819
+ for (let l of levels) if (l._instances !== null) l._instances.levels = null
820
+ for (let l of levels) disposeInstances(l)
821
+ return
822
+ }
823
+ // A destroyed mesh still animating out (its own exit, or its instances'
824
+ // - it waits for them) keeps the buffers until it is gone: the
825
+ // component unmount is destroy then dispose, and an exit that dispose
826
+ // cut short would be no exit at all.
827
+ if (afterFree(mesh, () => disposeInstances(mesh))) return
632
828
  if (mesh._scene) remove(mesh)
633
829
  if (inst.nodes !== null) {
634
830
  for (let n of inst.nodes.slots) if (n !== null) n.mesh = null
@@ -636,8 +832,9 @@ export function disposeInstances(mesh: InstancedMesh | RecordMesh): void {
636
832
  // buffer still exists (a write into a freed buffer warns).
637
833
  spatial.flush()
638
834
  }
639
- destroyBuffer(inst.buffer)
640
- if (inst.style !== null) destroyBuffer(inst.style.buffer)
835
+ if (inst.matrix !== null) destroyBuffer(inst.matrix)
836
+ for (let s of inst.streams) destroyBuffer(s.buffer)
837
+ if (inst.morph !== null) destroyTexture(inst.morph.texture)
641
838
  ;(mesh as Mesh)._instances = null
642
839
  }
643
840
 
@@ -663,6 +860,8 @@ export function setLayers(mesh: Mesh, layers: number): void {
663
860
  if (mesh.layers === layers) return
664
861
  mesh.layers = layers
665
862
  mesh._scene?._setLayers(mesh)
863
+ // An instanced LOD's levels draw where the population draws.
864
+ for (let l of mesh._instances?.levels ?? []) if (l !== mesh) setLayers(l, layers)
666
865
  }
667
866
 
668
867
  /** Draw the mesh into the scene's shadow map, or stop (see Mesh.castShadow). */
@@ -670,6 +869,7 @@ export function setCastShadow(mesh: Mesh, cast: boolean): void {
670
869
  if (mesh.castShadow === cast) return
671
870
  mesh.castShadow = cast
672
871
  mesh._scene?._setCast(mesh)
872
+ for (let l of mesh._instances?.levels ?? []) if (l !== mesh) setCastShadow(l, cast)
673
873
  }
674
874
 
675
875
  /** Frustum culling per mesh: `frustumCulled` (default true) switches the
@@ -693,9 +893,43 @@ export function setGeometry(mesh: Mesh, geometry: Geometry): void {
693
893
  if (mesh._sprite) throw new Error("setGeometry: a sprite draws the shared unit quad and takes no geometry")
694
894
  if (mesh.geometry === geometry) return
695
895
  mesh.geometry = geometry
896
+ mesh._range = null
897
+ // A standalone mesh owns its own weights: a geometry with different
898
+ // targets (or none) replaces or drops them; a model part keeps its
899
+ // node as the owner, whatever the geometry.
900
+ if (mesh._morphOwner === mesh || mesh._morphOwner === null) {
901
+ mesh._morphOwner = geometry.morphs === undefined ? null : mesh
902
+ resetMorph(mesh)
903
+ }
904
+ // A population's weights texture follows its geometry's target list.
905
+ let inst = mesh._instances
906
+ if (inst !== null && inst.nodes !== null && (inst.morph !== null || geometry.morphs !== undefined)) {
907
+ replacePopulationMorph(mesh as InstancedMesh, populationMorph(geometry, inst.capacity, inst.label), false)
908
+ }
696
909
  rebuildEntry(mesh)
697
910
  }
698
911
 
912
+ /**
913
+ * Draw indices `[first, first + count)` of the mesh's geometry instead of
914
+ * the whole list (Three's `geometry.setDrawRange`; on the mesh here
915
+ * because the scene owns the entries): the reveal-and-hide dial for
916
+ * geometry built once - a trail growing along its buffer, a level-of-
917
+ * detail cut, a strip drawn up to a moving end. `count` absent draws to
918
+ * the end. Applied to the mesh's entry and every view's; setGeometry
919
+ * resets it to the whole list. Throws on a range outside the index list;
920
+ * a partial primitive at the cut draws nothing, like GL.
921
+ */
922
+ export function setDrawRange(mesh: Mesh, first: number, count?: number): void {
923
+ if (mesh._sprite) throw new Error("setDrawRange: a sprite draws the shared unit quad")
924
+ let total = mesh.geometry.indices.length
925
+ let n = count ?? total - first
926
+ if (!Number.isInteger(first) || !Number.isInteger(n) || first < 0 || n < 0 || first + n > total) {
927
+ throw new Error("setDrawRange: range [" + first + ", " + (first + n) + ") is outside the geometry's " + total + " indices")
928
+ }
929
+ mesh._range = first === 0 && n === total ? null : { first, count: n }
930
+ mesh._scene?._setRange(mesh)
931
+ }
932
+
699
933
  /** Swap a mesh's material: its draw entry is rebuilt. */
700
934
  export function setMaterial(mesh: Mesh, material: Material): void {
701
935
  if (mesh.material === material) return