@solidrt/3d 0.0.57 → 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1479 -678
- package/README.md +4 -2
- package/examples/README.md +56 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +129 -0
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +187 -22
- package/src/geometry.ts +1718 -203
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +457 -154
- package/src/index.ts +32 -21
- package/src/input.ts +11 -2
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +322 -190
- package/src/mesh.ts +492 -259
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +151 -54
- package/src/motion.ts +17 -0
- package/src/node.ts +423 -23
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +608 -185
- package/src/skeleton.ts +211 -0
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/src/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 { 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 {
|
|
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
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*
|
|
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
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
|
|
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
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
blank:
|
|
134
|
-
/** 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,
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
253
|
-
|
|
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
|
|
290
|
-
let
|
|
291
|
-
if (
|
|
292
|
-
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("; ") + ")")
|
|
293
440
|
}
|
|
294
|
-
return
|
|
441
|
+
return stream.buffer
|
|
295
442
|
}
|
|
296
443
|
|
|
297
444
|
/**
|
|
298
|
-
* Whether a material's instance
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
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
|
|
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
|
|
327
|
-
* population
|
|
328
|
-
*
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
360
|
-
let
|
|
361
|
-
if (
|
|
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
|
|
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
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
404
|
-
//
|
|
405
|
-
function
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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?.
|
|
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
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
438
|
-
* (the scene calls it from its sync
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
let
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
let
|
|
498
|
-
|
|
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
|
-
|
|
501
|
-
if (inst.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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(
|
|
515
|
-
if (
|
|
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
|
-
* `
|
|
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:
|
|
737
|
+
records: ArrayBufferView,
|
|
561
738
|
count?: number,
|
|
562
739
|
opts?: RecordMeshOptions,
|
|
563
740
|
): RecordMesh {
|
|
564
|
-
let
|
|
565
|
-
let
|
|
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
|
-
|
|
573
|
-
|
|
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
|
-
|
|
752
|
+
levels: null,
|
|
753
|
+
morph: null,
|
|
580
754
|
}
|
|
581
|
-
|
|
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
|
|
587
|
-
* default) draw exactly the records written - pass `count` to
|
|
588
|
-
* or to keep more previously written ones alive past a
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
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:
|
|
783
|
+
export function setRecords(mesh: RecordMesh, records: ArrayBufferView, count?: number): void {
|
|
597
784
|
let inst = mesh._instances
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
625
|
-
* 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
|
|
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.
|
|
641
|
-
|
|
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
|