@solidrt/3d 0.0.58 → 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +24 -2
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +605 -185
- package/src/sweep.ts +27 -8
- 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,
|
|
8
|
-
import {
|
|
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 {
|
|
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
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*
|
|
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
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
*
|
|
134
|
-
blank:
|
|
135
|
-
/** The
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
252
|
-
|
|
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
|
|
289
|
-
let
|
|
290
|
-
if (
|
|
291
|
-
throw new Error(site + ": the material declares no
|
|
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
|
|
441
|
+
return stream.buffer
|
|
294
442
|
}
|
|
295
443
|
|
|
296
444
|
/**
|
|
297
|
-
* Whether a material's instance
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
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
|
|
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
|
|
326
|
-
* population
|
|
327
|
-
*
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
359
|
-
let
|
|
360
|
-
if (
|
|
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
|
|
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
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
403
|
-
//
|
|
404
|
-
function
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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?.
|
|
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
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
437
|
-
* (the scene calls it from its sync
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
let
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
let
|
|
497
|
-
|
|
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
|
-
|
|
500
|
-
if (inst.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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(
|
|
514
|
-
if (
|
|
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
|
-
* `
|
|
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:
|
|
737
|
+
records: ArrayBufferView,
|
|
560
738
|
count?: number,
|
|
561
739
|
opts?: RecordMeshOptions,
|
|
562
740
|
): RecordMesh {
|
|
563
|
-
let
|
|
564
|
-
let
|
|
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
|
-
|
|
572
|
-
|
|
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
|
-
|
|
752
|
+
levels: null,
|
|
753
|
+
morph: null,
|
|
579
754
|
}
|
|
580
|
-
|
|
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
|
|
586
|
-
* default) draw exactly the records written - pass `count` to
|
|
587
|
-
* or to keep more previously written ones alive past a
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
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:
|
|
783
|
+
export function setRecords(mesh: RecordMesh, records: ArrayBufferView, count?: number): void {
|
|
596
784
|
let inst = mesh._instances
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
624
|
-
* buffer
|
|
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.
|
|
640
|
-
|
|
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
|