@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.
Files changed (72) hide show
  1. package/AGENTS.md +1479 -678
  2. package/README.md +4 -2
  3. package/examples/README.md +56 -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 +129 -0
  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 +187 -22
  51. package/src/geometry.ts +1718 -203
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +457 -154
  54. package/src/index.ts +32 -21
  55. package/src/input.ts +11 -2
  56. package/src/light.ts +7 -8
  57. package/src/lod.ts +148 -0
  58. package/src/material.ts +322 -190
  59. package/src/mesh.ts +492 -259
  60. package/src/mixer.ts +27 -14
  61. package/src/model-file.ts +54 -15
  62. package/src/model.ts +151 -54
  63. package/src/motion.ts +17 -0
  64. package/src/node.ts +423 -23
  65. package/src/orbit.ts +378 -116
  66. package/src/profile.ts +4 -25
  67. package/src/resolve.ts +181 -0
  68. package/src/scene-shadows.ts +94 -93
  69. package/src/scene.ts +608 -185
  70. package/src/skeleton.ts +211 -0
  71. package/src/sweep.ts +27 -8
  72. 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, VertexAttribute } from "@solidrt/core/gpu"
8
- import { 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 & {
@@ -24,7 +25,8 @@ export type Mesh = SceneNode & {
24
25
  renderOrder: number
25
26
  /** Draw into the scene's shadow map (default false, Three's default).
26
27
  * Set with setCastShadow. A casting instanced mesh is skipped (the
27
- * depth pass cannot know its record layout). */
28
+ * depth pass cannot know its record layout), and so is any geometry
29
+ * that is not a triangle list: lines and points cast nothing. */
28
30
  castShadow: boolean
29
31
  /** Layer membership bitmask (default 1, Three's `object.layers`): a
30
32
  * target draws the mesh when its mask intersects this. Not inherited
@@ -44,6 +46,9 @@ export type Mesh = SceneNode & {
44
46
  * null for an unskinned mesh. */
45
47
  _cullJoints: SceneNode[] | null
46
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
47
52
  /** The geometry-buffer reference the entry was built from, acquired at
48
53
  * attach and what _detach releases - like _transparent, a snapshot,
49
54
  * because setGeometry swaps mesh.geometry before the rebuild. */
@@ -63,6 +68,12 @@ export type Mesh = SceneNode & {
63
68
  * variant declares uBones) - any other override validates its bindings
64
69
  * against a program that may not declare these names. */
65
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
66
77
  /** Instance state when the mesh was made by createInstancedMesh; null on
67
78
  * an ordinary mesh. */
68
79
  _instances: MeshInstances | null
@@ -71,20 +82,25 @@ export type Mesh = SceneNode & {
71
82
  _sprite: boolean
72
83
  }
73
84
 
74
- /** The per-mesh half of instancing: the record buffer and its bookkeeping.
75
- * Read the public fields freely; write through the population's own
76
- * functions (addInstance/removeInstance, setRecords/setRecordCount) so
77
- * 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. */
78
91
  export type MeshInstances = {
79
- /** The GPU record buffer (instance slot 0), owned by the mesh
80
- * (disposeInstances frees it): the core-written matrices on an
81
- * instanced mesh, the app's records on a record mesh. */
82
- buffer: BufferId
83
- /** Floats per record: INSTANCE_FLOATS on an instanced mesh, the
84
- * material's slot-0 instanceAttributes summed on a record mesh. */
85
- stride: number
86
- /** Records the buffer has room for; doubles on growth (a replacement
87
- * 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). */
88
104
  capacity: number
89
105
  /** The buffer label, carried to replacement buffers on growth. */
90
106
  label: string | undefined
@@ -102,47 +118,69 @@ export type MeshInstances = {
102
118
  /** The node-backed population (createInstancedMesh): handles by slot
103
119
  * (null = free) and the free list; null on a record mesh. */
104
120
  nodes: InstanceSlots | null
105
- /** The app-written style records (instance slot 1) of an instanced mesh
106
- * whose material declares slot-1 attributes; null otherwise (and on a
107
- * record mesh, whose one buffer is the app's already). */
108
- 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
109
135
  }
110
136
 
111
137
  export type InstanceSlots = { slots: (InstanceNode | null)[]; free: number[] }
112
138
 
113
139
  /**
114
- * The style half of an instanced mesh: one record per instance slot of
115
- * the material's slot-1 instanceAttributes (a tint, a frame index, any
116
- * per-copy floats the vertex stage reads), owned by the app and written
117
- * per instance with setInstanceStyle - the JS twin of the core's matrix
118
- * records in slot 0, @solidrt/2d's pose/style split one dimension up.
119
- * Writes land in `data` and publish as ONE coalesced buffer write per
120
- * mesh at the scene's next sync, whatever the number of instances
121
- * 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).
122
152
  */
123
- export type InstanceStyle = {
124
- /** The GPU style buffer (instance slot 1), mesh-owned like `buffer`. */
125
- buffer: BufferId
126
- /** 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. */
127
156
  stride: number
128
- /** The JS mirror, capacity * stride floats; the buffer is published
129
- * from it. Read freely, write through setInstanceStyle. */
130
- data: Float32Array
131
- /** The record a fresh (or recycled) slot starts with: the material's
132
- * instanceStyle, zeros without one. */
133
- blank: Float32Array
134
- /** 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,
135
164
  * or null. */
136
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[]
137
175
  }
138
176
 
139
177
  /** A mesh from createInstancedMesh: one draw entry drawing the geometry
140
178
  * once per instance node (addInstance). */
141
- export type InstancedMesh = Mesh & { _instances: MeshInstances & { nodes: InstanceSlots } }
179
+ export type InstancedMesh = Mesh & { _instances: MeshInstances & { matrix: BufferId; nodes: InstanceSlots } }
142
180
 
143
181
  /** A mesh from createRecordMesh: one draw entry drawing the geometry once
144
- * per record of a JS-written buffer (setRecords). */
145
- 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 } }
146
184
 
147
185
  /**
148
186
  * One instance of an InstancedMesh: a scene node like any other
@@ -152,7 +190,7 @@ export type RecordMesh = Mesh & { _instances: MeshInstances & { nodes: null } }
152
190
  * coalesced buffer write per flush however many instances moved. A
153
191
  * hidden instance draws nothing (its record collapses to zero scale).
154
192
  * Slot-bound to its mesh: created by addInstance, destroyed by
155
- * removeInstance; the generic add/remove reject it.
193
+ * destroy; the generic add/remove reject it.
156
194
  */
157
195
  export type InstanceNode = SceneNode & {
158
196
  kind: "instance"
@@ -181,11 +219,13 @@ export function createMesh(geometry: Geometry, material: Material): Mesh {
181
219
  mesh.cullMargin = 0
182
220
  mesh._cullJoints = null
183
221
  mesh._entry = null
222
+ mesh._range = null
184
223
  mesh._buffers = null
185
224
  mesh._transparent = false
186
225
  mesh._center = [0, 0, 0]
187
226
  mesh._params = null
188
227
  mesh._textures = null
228
+ mesh._morphOwner = geometry.morphs === undefined ? null : mesh
189
229
  mesh._instances = null
190
230
  mesh._sprite = false
191
231
  return mesh
@@ -225,32 +265,124 @@ export function localBounds(mesh: Mesh): Float32Array | null {
225
265
  return mesh._sprite ? SPRITE_BOUNDS : geometryBounds(mesh.geometry)
226
266
  }
227
267
 
228
- const ATTRIBUTE_FLOATS: Record<VertexAttribute["format"], number> = { f32: 1, vec2: 2, vec3: 3, vec4: 4 }
229
-
230
- /** Floats per record of one instance slot of an attribute list (slot 0
231
- * by default; the list's `slot` keys pick the others). */
232
- export function instanceStride(attributes: InstanceAttribute[], slot = 0): number {
233
- let stride = 0
234
- for (let a of attributes) if ((a.slot ?? 0) === slot) stride += ATTRIBUTE_FLOATS[a.format]
235
- return stride
236
- }
237
-
238
- // The highest instance slot an attribute list names.
239
- function instanceSlots(attributes: InstanceAttribute[]): number {
240
- let top = 0
241
- for (let a of attributes) top = Math.max(top, a.slot ?? 0)
242
- return top + 1
243
- }
244
-
245
268
  /** Floats per instance record on an instanced mesh: one column-major
246
269
  * mat4, the instance's placement inside the mesh (the material reads it
247
270
  * through INSTANCE_MATRIX_ATTRIBUTES / INSTANCE_MATRIX in ./glsl). */
248
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)
249
276
  // Instance slots an instanced mesh reserves without a capacity; growth
250
277
  // doubles past it.
251
278
  const DEFAULT_CAPACITY = 64
252
- // The instance buffers a mesh binds: the matrix slot and the style slot.
253
- 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
+ }
254
386
 
255
387
  /** What both population kinds take. */
256
388
  type PopulationOptions = {
@@ -286,51 +418,45 @@ function copyBounds(bounds: ArrayLike<number> | undefined, site: string): Float3
286
418
  return out
287
419
  }
288
420
 
289
- function instancedAttributes(material: Material, site: string): InstanceAttribute[] {
290
- let attributes = material.instanceAttributes
291
- if (attributes === undefined) {
292
- 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("; ") + ")")
293
440
  }
294
- return attributes
441
+ return stream.buffer
295
442
  }
296
443
 
297
444
  /**
298
- * Whether a material's instance attributes fit a mesh's population: the
299
- * slot-0 stride must be the mesh's record stride, a slot-1 layout must
300
- * match the mesh's style stride (a record mesh has no slot 1), and no
301
- * slot beyond. Throws otherwise - at creation for the mesh's own
302
- * material, at add() for a swapped one, at a shadow view's attach for a
303
- * 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.
304
450
  */
305
451
  export function checkInstancePairing(material: Material, inst: MeshInstances, site: string): void {
306
- let attrs = instancedAttributes(material, site)
307
- let slots = instanceSlots(attrs)
308
- if (slots > MESH_INSTANCE_SLOTS) {
309
- throw new Error(site + ": instance attributes name slot " + (slots - 1) + "; a mesh binds slots 0 (records) and 1 (style) only")
310
- }
311
- let stride = instanceStride(attrs, 0)
312
- if (stride !== inst.stride) {
313
- throw new Error(site + ": the material's slot-0 instance attributes take " + stride + " floats but the mesh's records are " + inst.stride)
314
- }
315
- let styleStride = instanceStride(attrs, 1)
316
- if (styleStride > 0) {
317
- if (inst.style === null) {
318
- 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)" : ""))
319
- }
320
- if (styleStride !== inst.style.stride) {
321
- throw new Error(site + ": the material's slot-1 (style) attributes take " + styleStride + " floats but the mesh's style records are " + inst.style.stride)
322
- }
323
- }
452
+ for (let layout of instancedBuffers(material, site)) streamBuffer(inst, layout, site)
324
453
  }
325
454
 
326
- /** The instance-buffer binding of a draw entry for a material over a
327
- * population: the plain slot-0 key, or both slots when the material's
328
- * pipeline reads the style records too (the two spellings are exclusive
329
- * and must match the pipeline's slot count exactly). */
330
- export function instanceBinding(material: Material, inst: MeshInstances): { instanceBuffer?: BufferId; instanceBuffers?: BufferId[] } {
331
- let attrs = material.instanceAttributes
332
- if (attrs !== undefined && inst.style !== null && instanceSlots(attrs) > 1) return { instanceBuffers: [inst.buffer, inst.style.buffer] }
333
- 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"))
334
460
  }
335
461
 
336
462
  /**
@@ -356,93 +482,159 @@ export function instanceBinding(material: Material, inst: MeshInstances): { inst
356
482
  * frees the record buffers when done for good.
357
483
  */
358
484
  export function createInstancedMesh(geometry: Geometry, material: Material, opts?: InstancedMeshOptions): InstancedMesh {
359
- let attrs = instancedAttributes(material, "createInstancedMesh")
360
- let stride = instanceStride(attrs, 0)
361
- if (stride !== INSTANCE_FLOATS) {
485
+ let buffers = instancedBuffers(material, "createInstancedMesh")
486
+ let first = buffers[0]
487
+ if (first === undefined || layoutKey(first.attributes) !== MATRIX_KEY) {
362
488
  throw new Error(
363
- "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)),
364
490
  )
365
491
  }
366
492
  let capacity = opts?.capacity ?? DEFAULT_CAPACITY
367
493
  if (!(Number.isInteger(capacity) && capacity > 0)) {
368
494
  throw new Error("createInstancedMesh: capacity must be a positive integer, got " + capacity)
369
495
  }
370
- let styleStride = instanceStride(attrs, 1)
371
- let style: InstanceStyle | null = null
372
- if (styleStride > 0) {
373
- let blank = new Float32Array(styleStride)
374
- if (material.instanceStyle !== undefined) {
375
- if (material.instanceStyle.length !== styleStride) {
376
- throw new Error("createInstancedMesh: the material's instanceStyle has " + material.instanceStyle.length + " floats but its slot-1 attributes take " + styleStride)
377
- }
378
- blank.set(material.instanceStyle)
379
- }
380
- style = {
381
- buffer: createBuffer(capacity * styleStride * 4, { autoFree: false, label: opts?.label === undefined ? undefined : opts.label + "-style" }),
382
- stride: styleStride,
383
- data: new Float32Array(capacity * styleStride),
384
- blank,
385
- dirty: null,
386
- }
387
- }
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
+ })
388
500
  let mesh = createMesh(geometry, material) as InstancedMesh
389
501
  mesh._instances = {
390
- buffer: createBuffer(capacity * INSTANCE_FLOATS * 4, { autoFree: false, label: opts?.label }),
391
- stride: INSTANCE_FLOATS,
502
+ matrix: createBuffer(capacity * INSTANCE_BYTES, { autoFree: false, label: opts?.label }),
503
+ streams,
392
504
  capacity,
393
505
  label: opts?.label,
394
506
  count: 0,
395
507
  bounds: copyBounds(opts?.bounds, "createInstancedMesh"),
396
508
  nodes: { slots: [], free: [] },
397
- style,
509
+ levels: null,
510
+ morph: populationMorph(geometry, capacity, opts?.label),
398
511
  }
399
- checkInstancePairing(material, mesh._instances, "createInstancedMesh")
400
512
  return mesh
401
513
  }
402
514
 
403
- // Extend a style's dirty range over [lo, hi) floats of the mirror and
404
- // ask the scene to publish it at the next sync.
405
- function markStyle(mesh: InstancedMesh, style: InstanceStyle, lo: number, hi: number): void {
406
- if (style.dirty === null) style.dirty = [lo, hi]
407
- else {
408
- if (lo < style.dirty[0]) style.dirty[0] = lo
409
- 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
+ }
410
541
  }
411
- 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]
412
550
  }
413
551
 
414
552
  /**
415
- * Write an instance's style record - the floats of the material's
416
- * slot-1 instance attributes, in order (`[r, g, b, a]` for a stock
417
- * material's instanceColors): the app's per-copy data beside the matrix
418
- * the core writes. Any number of instances styled between two frames
419
- * cost one coalesced buffer write at the scene's sync, so this is a
420
- * frame-rate path like setTransform. Throws on a material without
421
- * slot-1 attributes or a record of the wrong length; a no-op on a
422
- * 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.
423
564
  */
424
565
  export function setInstanceStyle(instance: InstanceNode, values: ArrayLike<number>): void {
425
566
  let mesh = instance.mesh
426
567
  if (mesh === null) return
427
- let style = mesh._instances.style
428
- if (style === null) throw new Error("setInstanceStyle: the mesh's material declares no slot-1 (style) instance attributes")
429
- if (values.length !== style.stride) {
430
- 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))
431
618
  }
432
- let at = instance._slot * style.stride
433
- style.data.set(values, at)
434
- 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)
435
625
  }
436
626
 
437
- /** Publish an instanced mesh's pending style writes as one buffer write
438
- * (the scene calls it from its sync; nothing to do without a dirty
439
- * range). */
440
- export function publishInstanceStyle(mesh: InstancedMesh): void {
441
- let style = mesh._instances.style
442
- if (style === null || style.dirty === null) return
443
- let [lo, hi] = style.dirty
444
- style.dirty = null
445
- 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
+ }
446
638
  }
447
639
 
448
640
  /**
@@ -452,10 +644,12 @@ export function publishInstanceStyle(mesh: InstancedMesh): void {
452
644
  * mesh's subtree (a squad group within a fleet: the record stays
453
645
  * mesh-relative through it). Its record slot is fixed for its life (a
454
646
  * removed instance's slot recycles to the next add); past the
455
- * 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
456
648
  * other: setTransform/setTransition/setVisible, lookAt, worldPosition,
457
649
  * pointer handlers, children of its own (a headlight mesh under a car
458
- * 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.
459
653
  */
460
654
  export function addInstance(mesh: InstancedMesh, update?: TransformUpdate, parent: SceneNode = mesh): InstanceNode {
461
655
  let inst: (MeshInstances & { nodes: InstanceSlots }) | null = mesh._instances
@@ -464,7 +658,10 @@ export function addInstance(mesh: InstancedMesh, update?: TransformUpdate, paren
464
658
  if (p === null) throw new Error("addInstance: parent must be the mesh or a node inside its subtree")
465
659
  }
466
660
  let slot = inst.nodes.free.pop() ?? inst.nodes.slots.length
467
- 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)
468
665
  let instance = makeNode("instance") as InstanceNode
469
666
  instance.mesh = mesh
470
667
  ;(instance as { _slot: number })._slot = slot
@@ -472,71 +669,51 @@ export function addInstance(mesh: InstancedMesh, update?: TransformUpdate, paren
472
669
  inst.nodes.slots[slot] = instance
473
670
  instance.parent = parent
474
671
  parent.children.push(instance)
475
- if (slot >= inst.count) {
476
- inst.count = slot + 1
477
- mesh._scene?._setCount(mesh)
478
- }
479
- // A recycled slot must not wear its last occupant's style.
480
- let style = inst.style
481
- if (style !== null) {
482
- let at = slot * style.stride
483
- style.data.set(style.blank, at)
484
- 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
+ }
485
683
  }
486
684
  if (parent._scene) enterScene(instance, parent._scene)
487
685
  return instance
488
686
  }
489
687
 
490
- // Double the record buffers: replacements (never a resize) the live
491
- // instances' record sinks move to in one core call, republished whole at
492
- // the next flush, and the style mirror re-published whole from JS; the
493
- // entry re-points, then the old buffers, which the entry held alive until
494
- // now, are freed.
495
- function growInstances(mesh: InstancedMesh, next: number): void {
496
- let inst = mesh._instances
497
- let previous = inst.buffer
498
- 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
499
697
  inst.capacity = next
500
- // Out of a scene nothing is bound, and retargeting an empty source throws.
501
- if (inst.nodes.slots.some(n => n !== null && n._node !== null)) spatial.retargetRecords(previous, inst.buffer)
502
- let style = inst.style
503
- let previousStyle: BufferId | null = null
504
- if (style !== null) {
505
- previousStyle = style.buffer
506
- style.buffer = createBuffer(next * style.stride * 4, { autoFree: false, label: inst.label === undefined ? undefined : inst.label + "-style" })
507
- let grown = new Float32Array(next * style.stride)
508
- grown.set(style.data)
509
- style.data = grown
510
- style.dirty = null
511
- 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)
512
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
+ })
513
714
  mesh._scene?._setBuffer(mesh)
514
- destroyBuffer(previous)
515
- if (previousStyle !== null) destroyBuffer(previousStyle)
516
- }
517
-
518
- /**
519
- * Destroy an instance: its slot hides at the next flush and recycles to
520
- * the next addInstance, and the handle goes inert (`mesh` null; later
521
- * writes are no-ops). Its children leave the scene with it and stay
522
- * parented to the dead node - remove() one to re-add it elsewhere. Remove
523
- * means destroy here, as for a 2d sprite: an instance cannot exist
524
- * outside its mesh.
525
- */
526
- export function removeInstance(instance: InstanceNode): void {
527
- let mesh = instance.mesh
528
- if (mesh === null) return
529
- if (instance._scene) leaveScene(instance)
530
- let parent = instance.parent
531
- if (parent !== null) {
532
- let i = parent.children.indexOf(instance)
533
- if (i >= 0) parent.children.splice(i, 1)
534
- instance.parent = null
535
- }
536
- let nodes = mesh._instances.nodes
537
- nodes.slots[instance._slot] = null
538
- nodes.free.push(instance._slot)
539
- 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)
540
717
  }
541
718
 
542
719
  /**
@@ -545,7 +722,7 @@ export function removeInstance(instance: InstanceNode): void {
545
722
  * floats - the raw escape hatch for motion only JS can compute at scale
546
723
  * (a particle sim, a crowd stepped in a worker), Three's InstancedMesh
547
724
  * and Godot's MultiMesh as plain data. The material must declare
548
- * `instanceAttributes` (shaderMaterialClass); its vertex stage reads each
725
+ * `instanceBuffers` (shaderMaterialClass); its vertex stage reads each
549
726
  * record through those `in` variables. `records` is the interleaved
550
727
  * attribute data (stride = the attributes' floats summed) and is uploaded
551
728
  * here; its length is the buffer's initial capacity, which setRecords
@@ -557,56 +734,57 @@ export function removeInstance(instance: InstanceNode): void {
557
734
  export function createRecordMesh(
558
735
  geometry: Geometry,
559
736
  material: Material,
560
- records: Float32Array,
737
+ records: ArrayBufferView,
561
738
  count?: number,
562
739
  opts?: RecordMeshOptions,
563
740
  ): RecordMesh {
564
- let attrs = instancedAttributes(material, "createRecordMesh")
565
- let stride = instanceStride(attrs, 0)
566
- if (records.length % stride !== 0) {
567
- throw new Error("createRecordMesh: " + records.length + " floats is not a whole number of " + stride + "-float records")
568
- }
569
- let capacity = records.length / stride
741
+ let buffers = instancedBuffers(material, "createRecordMesh")
742
+ let capacity = recordCount(records, buffers[0]!.attributes, "createRecordMesh")
570
743
  let mesh = createMesh(geometry, material) as RecordMesh
571
744
  mesh._instances = {
572
- buffer: createBuffer(records, { autoFree: false, label: opts?.label }),
573
- stride,
745
+ matrix: null,
746
+ streams: buffers.map((b, i) => makeStream(b.attributes, capacity, null, streamLabel(opts?.label, i))),
574
747
  capacity,
575
748
  label: opts?.label,
576
749
  count: Math.max(0, Math.min(Math.floor(count ?? capacity), capacity)),
577
750
  bounds: copyBounds(opts?.bounds, "createRecordMesh"),
578
751
  nodes: null,
579
- style: null,
752
+ levels: null,
753
+ morph: null,
580
754
  }
581
- checkInstancePairing(material, mesh._instances, "createRecordMesh")
755
+ copyRecords(mesh, records, capacity)
582
756
  return mesh
583
757
  }
584
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
+
585
767
  /**
586
- * Overwrite a record mesh's records from the start of its buffer and (by
587
- * default) draw exactly the records written - pass `count` to draw fewer,
588
- * or to keep more previously written ones alive past a partial rewrite.
589
- * More records than the buffer holds grow it: capacity doubles (or jumps
590
- * to the records written when that is more), a new buffer is created and
591
- * written, the mesh's entry is re-pointed at it and the old buffer freed
592
- * - so a population grows without a new mesh, with the copies amortized
593
- * like any dynamic array (size the initial records to skip them).
594
- * 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.
595
782
  */
596
- export function setRecords(mesh: RecordMesh, records: Float32Array, count?: number): void {
783
+ export function setRecords(mesh: RecordMesh, records: ArrayBufferView, count?: number): void {
597
784
  let inst = mesh._instances
598
- if (records.length % inst.stride !== 0) {
599
- throw new Error("setRecords: " + records.length + " floats is not a whole number of " + inst.stride + "-float records")
600
- }
601
- let written = records.length / inst.stride
602
- if (written > inst.capacity) {
603
- let previous = inst.buffer
604
- inst.capacity = Math.max(written, inst.capacity * 2)
605
- inst.buffer = createBuffer(inst.capacity * inst.stride * 4, { autoFree: false, label: inst.label })
606
- mesh._scene?._setBuffer(mesh)
607
- destroyBuffer(previous)
608
- }
609
- 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)
610
788
  setRecordCount(mesh, count ?? written)
611
789
  }
612
790
 
@@ -621,15 +799,32 @@ export function setRecordCount(mesh: RecordMesh, count: number): void {
621
799
  }
622
800
 
623
801
  /**
624
- * Detach the mesh (if attached) and free its record buffers (the style
625
- * 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
626
804
  * reference count (unlike geometry buffers they are never shared), so
627
805
  * this is the one explicit free; the mesh cannot be re-added afterwards.
628
- * 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.
629
811
  */
630
812
  export function disposeInstances(mesh: InstancedMesh | RecordMesh): void {
631
813
  let inst: MeshInstances | null = mesh._instances
632
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
633
828
  if (mesh._scene) remove(mesh)
634
829
  if (inst.nodes !== null) {
635
830
  for (let n of inst.nodes.slots) if (n !== null) n.mesh = null
@@ -637,8 +832,9 @@ export function disposeInstances(mesh: InstancedMesh | RecordMesh): void {
637
832
  // buffer still exists (a write into a freed buffer warns).
638
833
  spatial.flush()
639
834
  }
640
- destroyBuffer(inst.buffer)
641
- 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)
642
838
  ;(mesh as Mesh)._instances = null
643
839
  }
644
840
 
@@ -664,6 +860,8 @@ export function setLayers(mesh: Mesh, layers: number): void {
664
860
  if (mesh.layers === layers) return
665
861
  mesh.layers = layers
666
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)
667
865
  }
668
866
 
669
867
  /** Draw the mesh into the scene's shadow map, or stop (see Mesh.castShadow). */
@@ -671,6 +869,7 @@ export function setCastShadow(mesh: Mesh, cast: boolean): void {
671
869
  if (mesh.castShadow === cast) return
672
870
  mesh.castShadow = cast
673
871
  mesh._scene?._setCast(mesh)
872
+ for (let l of mesh._instances?.levels ?? []) if (l !== mesh) setCastShadow(l, cast)
674
873
  }
675
874
 
676
875
  /** Frustum culling per mesh: `frustumCulled` (default true) switches the
@@ -694,9 +893,43 @@ export function setGeometry(mesh: Mesh, geometry: Geometry): void {
694
893
  if (mesh._sprite) throw new Error("setGeometry: a sprite draws the shared unit quad and takes no geometry")
695
894
  if (mesh.geometry === geometry) return
696
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
+ }
697
909
  rebuildEntry(mesh)
698
910
  }
699
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
+
700
933
  /** Swap a mesh's material: its draw entry is rebuilt. */
701
934
  export function setMaterial(mesh: Mesh, material: Material): void {
702
935
  if (mesh.material === material) return