@solidrt/3d 0.0.60 → 0.0.62
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 +436 -139
- package/examples/cascades.tsx +10 -4
- package/examples/pick.tsx +6 -4
- package/examples/sprites.tsx +7 -1
- package/package.json +4 -3
- package/src/components/context.tsx +13 -2
- package/src/components/first-person-camera.tsx +1 -1
- package/src/components/index.ts +2 -0
- package/src/components/instanced-mesh.tsx +25 -6
- package/src/components/lod.tsx +3 -1
- package/src/components/orbit-camera.tsx +105 -20
- package/src/components/shots.tsx +93 -0
- package/src/first-person.ts +48 -16
- package/src/gltf.ts +316 -62
- package/src/index.ts +8 -3
- package/src/input.ts +61 -17
- package/src/light.ts +25 -2
- package/src/material.ts +101 -25
- package/src/math.ts +11 -0
- package/src/mesh.ts +68 -21
- package/src/model-data.ts +27 -0
- package/src/model-file.ts +112 -37
- package/src/model.ts +90 -29
- package/src/node.ts +1 -1
- package/src/orbit.ts +526 -108
- package/src/scene-shadows.ts +4 -3
- package/src/scene.ts +201 -189
- package/src/shots.ts +63 -0
- package/src/skeleton.ts +39 -7
- package/tools/environment.ts +1 -0
- package/tools/model.ts +4 -3
- package/tools/node-env.d.ts +25 -0
- package/src/motion.ts +0 -17
- package/src/order.ts +0 -58
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @solidrt/3d/model - the model DATA side of the package without the
|
|
2
|
+
// runtime: the glTF parser, the .srtm container and the pure geometry kit,
|
|
3
|
+
// for a script that bakes an app's own geometry under bun (or any host
|
|
4
|
+
// with no flux). The package root imports `flux:*` and so loads only on
|
|
5
|
+
// the runtime; this entry imports nothing that does. tools/model.ts (the
|
|
6
|
+
// glTF bake) runs on exactly this surface, so an app's bake script and
|
|
7
|
+
// the tool share one path:
|
|
8
|
+
//
|
|
9
|
+
// import { box, encodeModel, transformGeometry } from "@solidrt/3d/model"
|
|
10
|
+
// writeFileSync("assets/terrain.srtm", encodeModel({ nodes, parts, ... }))
|
|
11
|
+
//
|
|
12
|
+
// createModel / loadModel on the runtime read what it writes. Keep this
|
|
13
|
+
// entry pure: tests/model-data.test.ts imports it under bun and fails the
|
|
14
|
+
// moment a runtime import creeps into the chain.
|
|
15
|
+
|
|
16
|
+
export { gltfExternalUris, isGlb, parseGltf } from "./gltf.ts"
|
|
17
|
+
export type { ModelChannel, ModelClip, ModelData, ModelExtras, ModelMaterial, ModelNode, ModelPart, ModelSkin, UriResolver } from "./gltf.ts"
|
|
18
|
+
export { decodeModel, encodeModel } from "./model-file.ts"
|
|
19
|
+
export { arrowHelper, axesHelper, box, box3Helper, capsule, capsuleHelper, circle, cone, cylinder, dodecahedron, fillAttribute, fillColors, edgesGeometry, mergeVertices, normalsHelper, toNonIndexed, withNormals, geometryBounds, geometryTopology, gridHelper, icosahedron, attributeAccess, geometryAttribute, geometryKey, geometryLayouts, geometrySlot, geometryStreams, geometryVertexCount, isFloatFormat, isFloatLayout, layoutAttributes, layoutKey, layoutSlot, layoutStride, mergeGeometries, octahedron, packGeometry, packMorphTargets, plane, planeHelper, polyhedron, ring, sphere, tetrahedron, torus, torusKnot, transformGeometry, validateGeometry, wireframeGeometry, vertexBytes, vertexCount, vertexView, withAttribute, withColors, withMorphTargets, MORPH_ENTRY_TEXELS, MORPH_TEXEL_FLOATS, BASE_FLOATS, VERTEX_FORMATS, VERTEX_LAYOUTS } from "./geometry.ts"
|
|
20
|
+
export type { ArrowHelperOptions, AttributeAccess, AttributeFill, VertexStream, WithAttributeOptions, AxesHelperOptions, BoxOptions, CapsuleHelperOptions, CapsuleOptions, CircleOptions, ColorFill, ConeOptions, CylinderOptions, FormatCodec, Geometry, GeometryOptions, GridHelperOptions, MorphTarget, MorphTargets, NormalsHelperOptions, PlaneHelperOptions, PlaneOptions, PolyhedronOptions, RingOptions, SphereOptions, TorusKnotOptions, TorusOptions, VertexLayout } from "./geometry.ts"
|
|
21
|
+
export { fillet, roundRect, triangulate } from "./profile.ts"
|
|
22
|
+
export type { Profile, ProfilePoint } from "./profile.ts"
|
|
23
|
+
export { extrude, lathe, pathFrames, polygon, sweep, tube } from "./sweep.ts"
|
|
24
|
+
export type { ExtrudeOptions, LatheOptions, PathFrames, PathPoint, SweepPath, TubeOptions } from "./sweep.ts"
|
|
25
|
+
export { linearColor, linearToSrgb, premultipliedColor, srgbToLinear } from "./color.ts"
|
|
26
|
+
export { rayBoxDistance, compose, copy, eulerFromQuat, identity, lookAt, mat4, multiply, normalMatrix, orthographic, perspective, quat, quatFromAxisAngle, quatFromEuler, quatFromFrame, quatFromTo, quatMultiply, quatNormalize, quatSlerp } from "./math.ts"
|
|
27
|
+
export type { Mat4, Quat, Vec2, Vec3 } from "./math.ts"
|
package/src/model-file.ts
CHANGED
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
// "SRTM" u32 | version u32 | jsonLength u32 | json (padded to 4) | payload
|
|
8
8
|
// The JSON header describes each block's byte range into the payload; every
|
|
9
9
|
// block starts 4-aligned so Float32Array/Uint32Array views sit on it
|
|
10
|
-
// directly. Images travel as their encoded files (PNG/JPEG bytes)
|
|
10
|
+
// directly. Images travel as their encoded files (PNG/JPEG bytes), app
|
|
11
|
+
// data as the header's `extras` (JSON) and named `blobs` (aligned blocks).
|
|
12
|
+
// Geometry is a table the parts index, and a buffer view is written once
|
|
13
|
+
// however many geometries read it: parts sharing a Geometry object (a
|
|
14
|
+
// skinned or morphed mesh several nodes place) share one entry, and two
|
|
15
|
+
// geometries over one vertex buffer (a mirrored copy: same vertices, the
|
|
16
|
+
// flipped index order) share the vertex block - decodeModel restores the
|
|
17
|
+
// same identities, so the runtime uploads shared bytes once.
|
|
11
18
|
|
|
12
|
-
import type { ModelChannel, ModelData, ModelMaterial, ModelNode, ModelSkin } from "./gltf.ts"
|
|
19
|
+
import type { ModelChannel, ModelData, ModelExtras, ModelMaterial, ModelNode, ModelSkin } from "./gltf.ts"
|
|
13
20
|
import { layoutStride, vertexView, VERTEX_FORMATS } from "./geometry.ts"
|
|
14
21
|
import type { VertexAttribute } from "@solidrt/core/gpu"
|
|
15
|
-
import type { VertexLayout } from "./geometry.ts"
|
|
22
|
+
import type { Geometry, VertexLayout } from "./geometry.ts"
|
|
16
23
|
|
|
17
24
|
/** "SRTM" read as a little-endian u32. */
|
|
18
25
|
const MAGIC = 0x4d545253
|
|
@@ -39,7 +46,15 @@ const MAGIC = 0x4d545253
|
|
|
39
46
|
// Version 9 names the plain layout "base" (position, normal, uv - the
|
|
40
47
|
// prefix "colored" and "skinned" extend) where version 8 wrote
|
|
41
48
|
// "standard"; a version-8 file's layout word does not parse.
|
|
42
|
-
|
|
49
|
+
// Version 10 carries app data and reuse: `extras` (JSON) on the root, the
|
|
50
|
+
// nodes, the parts and the materials, named binary `blobs` as payload
|
|
51
|
+
// blocks, a part's `placements` (the further nodes drawing the same
|
|
52
|
+
// geometry), and geometry as a table the parts index (shared entries and
|
|
53
|
+
// shared blocks, see the header). A version-9 file has none of these and is rejected like
|
|
54
|
+
// every earlier version rather than read as a file without them, so a
|
|
55
|
+
// stale bake never silently drops the data an app relies on. Re-bake
|
|
56
|
+
// with `srt tool 3d/model`.
|
|
57
|
+
const VERSION = 10
|
|
43
58
|
|
|
44
59
|
// The named layouts the container writes by name; a custom attribute-list
|
|
45
60
|
// layout (a skinned primitive with COLOR_0, a withAttribute channel) is
|
|
@@ -61,17 +76,29 @@ function decodeLayout(layout: string | VertexAttribute[], name: string): VertexL
|
|
|
61
76
|
|
|
62
77
|
type Block = { offset: number; bytes: number }
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
material: number
|
|
79
|
+
/** One geometry of the table: its interleaved vertex block, index block
|
|
80
|
+
* and packed morph targets. Blocks may be shared between entries. */
|
|
81
|
+
type GeometryHeader = {
|
|
82
|
+
vertices: Block
|
|
69
83
|
layout: string | VertexAttribute[]
|
|
70
84
|
vertexCount: number
|
|
71
85
|
indexBits: 16 | 32
|
|
72
86
|
index: Block
|
|
73
|
-
/** The
|
|
87
|
+
/** The packed morph targets, when it has any. */
|
|
74
88
|
morphs?: { names: string[]; texels: Block; extent: number[] }
|
|
89
|
+
label?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type PartHeader = {
|
|
93
|
+
name: string
|
|
94
|
+
node: number
|
|
95
|
+
skin: number | null
|
|
96
|
+
material: number
|
|
97
|
+
/** Index into the header's geometry table. */
|
|
98
|
+
geometry: number
|
|
99
|
+
/** The further nodes placing the part's geometry (ModelPart.placements). */
|
|
100
|
+
placements?: number[]
|
|
101
|
+
extras?: ModelExtras
|
|
75
102
|
}
|
|
76
103
|
|
|
77
104
|
type SkinHeader = { joints: number[]; inverseBind: Block; jointBounds: Block }
|
|
@@ -90,10 +117,16 @@ type Header = {
|
|
|
90
117
|
nodes: ModelNode[]
|
|
91
118
|
materials: ModelMaterial[]
|
|
92
119
|
images: Block[]
|
|
120
|
+
geometries: GeometryHeader[]
|
|
93
121
|
parts: PartHeader[]
|
|
94
122
|
skins: SkinHeader[]
|
|
95
123
|
clips: ClipHeader[]
|
|
96
124
|
bounds: number[]
|
|
125
|
+
/** The root extras (ModelData.extras); node and material extras ride
|
|
126
|
+
* inside their own records. */
|
|
127
|
+
extras?: ModelExtras
|
|
128
|
+
/** The named binary sections (ModelData.blobs), each its own block. */
|
|
129
|
+
blobs?: Record<string, Block>
|
|
97
130
|
}
|
|
98
131
|
|
|
99
132
|
/** Serialize a model into the .srtm container. */
|
|
@@ -112,29 +145,49 @@ export function encodeModel(data: ModelData): Uint8Array {
|
|
|
112
145
|
return block
|
|
113
146
|
}
|
|
114
147
|
|
|
115
|
-
|
|
116
|
-
|
|
148
|
+
// A buffer view is written once however many geometries read it, and a
|
|
149
|
+
// Geometry object gets one table entry however many parts draw it.
|
|
150
|
+
let views = new Map<ArrayBufferView, Block>()
|
|
151
|
+
let pushOnce = (view: ArrayBufferView): Block => {
|
|
152
|
+
let have = views.get(view)
|
|
153
|
+
if (have !== undefined) return have
|
|
154
|
+
let block = push(new Uint8Array(view.buffer, view.byteOffset, view.byteLength))
|
|
155
|
+
views.set(view, block)
|
|
156
|
+
return block
|
|
157
|
+
}
|
|
158
|
+
let geometries: GeometryHeader[] = []
|
|
159
|
+
let geometryIds = new Map<Geometry, number>()
|
|
160
|
+
let geometryId = (g: Geometry, name: string): number => {
|
|
161
|
+
let id = geometryIds.get(g)
|
|
162
|
+
if (id !== undefined) return id
|
|
117
163
|
if (g.streams !== undefined && g.streams.length > 0) {
|
|
118
|
-
throw new Error("encodeModel: part '" +
|
|
164
|
+
throw new Error("encodeModel: part '" + name + "' carries extra vertex streams; the container writes one interleaved buffer per geometry")
|
|
119
165
|
}
|
|
120
166
|
let layout = g.layout === undefined ? "base" : g.layout
|
|
121
|
-
let
|
|
122
|
-
|
|
167
|
+
let header: GeometryHeader = {
|
|
168
|
+
vertices: pushOnce(g.vertices),
|
|
169
|
+
layout,
|
|
170
|
+
vertexCount: g.vertices.byteLength / layoutStride(layout),
|
|
171
|
+
indexBits: g.indices instanceof Uint32Array ? 32 : 16,
|
|
172
|
+
index: pushOnce(g.indices),
|
|
173
|
+
}
|
|
174
|
+
if (g.morphs !== undefined) header.morphs = { names: g.morphs.names, texels: pushOnce(g.morphs.texels), extent: Array.from(g.morphs.extent) }
|
|
175
|
+
if (g.label !== undefined) header.label = g.label
|
|
176
|
+
id = geometries.length
|
|
177
|
+
geometries.push(header)
|
|
178
|
+
geometryIds.set(g, id)
|
|
179
|
+
return id
|
|
180
|
+
}
|
|
181
|
+
let parts: PartHeader[] = data.parts.map((part) => {
|
|
123
182
|
let header: PartHeader = {
|
|
124
|
-
...vertices,
|
|
125
183
|
name: part.name,
|
|
126
184
|
node: part.node,
|
|
127
185
|
skin: part.skin,
|
|
128
186
|
material: part.material,
|
|
129
|
-
|
|
130
|
-
vertexCount: g.vertices.byteLength / layoutStride(layout),
|
|
131
|
-
indexBits: g.indices instanceof Uint32Array ? 32 : 16,
|
|
132
|
-
index,
|
|
133
|
-
}
|
|
134
|
-
if (g.morphs !== undefined) {
|
|
135
|
-
let texels = push(new Uint8Array(g.morphs.texels.buffer, g.morphs.texels.byteOffset, g.morphs.texels.byteLength))
|
|
136
|
-
header.morphs = { names: g.morphs.names, texels, extent: Array.from(g.morphs.extent) }
|
|
187
|
+
geometry: geometryId(part.geometry, part.name),
|
|
137
188
|
}
|
|
189
|
+
if (part.placements !== undefined && part.placements.length > 0) header.placements = part.placements
|
|
190
|
+
if (part.extras !== undefined) header.extras = part.extras
|
|
138
191
|
return header
|
|
139
192
|
})
|
|
140
193
|
let images = data.images.map((image) => push(image))
|
|
@@ -151,7 +204,12 @@ export function encodeModel(data: ModelData): Uint8Array {
|
|
|
151
204
|
values: floats(c.values),
|
|
152
205
|
})),
|
|
153
206
|
}))
|
|
154
|
-
let header: Header = { nodes: data.nodes, materials: data.materials, images, parts, skins, clips, bounds: Array.from(data.bounds) }
|
|
207
|
+
let header: Header = { nodes: data.nodes, materials: data.materials, images, geometries, parts, skins, clips, bounds: Array.from(data.bounds) }
|
|
208
|
+
if (data.extras !== undefined) header.extras = data.extras
|
|
209
|
+
if (data.blobs !== undefined) {
|
|
210
|
+
header.blobs = {}
|
|
211
|
+
for (let [name, bytes] of Object.entries(data.blobs)) header.blobs[name] = push(bytes)
|
|
212
|
+
}
|
|
155
213
|
|
|
156
214
|
let json = new TextEncoder().encode(JSON.stringify(header))
|
|
157
215
|
let jsonPadded = json.byteLength + ((4 - (json.byteLength % 4)) % 4)
|
|
@@ -187,26 +245,37 @@ export function decodeModel(bytes: Uint8Array): ModelData {
|
|
|
187
245
|
let header: Header = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, base + 12, jsonLength)))
|
|
188
246
|
let payload = base + 12 + jsonLength + ((4 - (jsonLength % 4)) % 4)
|
|
189
247
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
let
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
248
|
+
// The geometry table first: one Geometry object per entry, so parts
|
|
249
|
+
// naming the same entry share it (and the runtime shares its buffers).
|
|
250
|
+
let geometries: Geometry[] = header.geometries.map((g, i) => {
|
|
251
|
+
let name = g.label ?? "geometry " + i
|
|
252
|
+
let layout = decodeLayout(g.layout, name)
|
|
253
|
+
let indexCount = g.index.bytes / (g.indexBits / 8)
|
|
254
|
+
let geometry: Geometry = {
|
|
255
|
+
vertices: vertexView(layout, buffer, payload + g.vertices.offset, g.vertexCount * layoutStride(layout)),
|
|
256
|
+
indices: g.indexBits === 32 ? new Uint32Array(buffer, payload + g.index.offset, indexCount) : new Uint16Array(buffer, payload + g.index.offset, indexCount),
|
|
197
257
|
}
|
|
198
|
-
if (
|
|
199
|
-
if (
|
|
200
|
-
|
|
258
|
+
if (g.label !== undefined) geometry.label = g.label
|
|
259
|
+
if (g.layout !== "base") geometry.layout = layout
|
|
260
|
+
if (g.morphs !== undefined) {
|
|
261
|
+
let m = g.morphs
|
|
201
262
|
geometry.morphs = { names: m.names, texels: new Float32Array(buffer, payload + m.texels.offset, m.texels.bytes / 4), extent: Float32Array.from(m.extent) }
|
|
202
263
|
}
|
|
203
|
-
return
|
|
264
|
+
return geometry
|
|
265
|
+
})
|
|
266
|
+
let parts = header.parts.map((part) => {
|
|
267
|
+
let geometry = geometries[part.geometry]
|
|
268
|
+
if (geometry === undefined) throw new Error("decodeModel: part '" + part.name + "' names a missing geometry " + part.geometry)
|
|
269
|
+
let out: ModelData["parts"][0] = {
|
|
204
270
|
name: part.name,
|
|
205
271
|
node: part.node,
|
|
206
272
|
skin: part.skin,
|
|
207
273
|
material: part.material,
|
|
208
274
|
geometry,
|
|
209
275
|
}
|
|
276
|
+
if (part.placements !== undefined) out.placements = part.placements
|
|
277
|
+
if (part.extras !== undefined) out.extras = part.extras
|
|
278
|
+
return out
|
|
210
279
|
})
|
|
211
280
|
let images = header.images.map((block) => new Uint8Array(buffer, payload + block.offset, block.bytes))
|
|
212
281
|
let floats = (block: Block): Float32Array => new Float32Array(buffer, payload + block.offset, block.bytes / 4)
|
|
@@ -222,5 +291,11 @@ export function decodeModel(bytes: Uint8Array): ModelData {
|
|
|
222
291
|
values: floats(c.values),
|
|
223
292
|
})),
|
|
224
293
|
}))
|
|
225
|
-
|
|
294
|
+
let data: ModelData = { nodes: header.nodes, parts, skins, clips, materials: header.materials, images, bounds: Float32Array.from(header.bounds) }
|
|
295
|
+
if (header.extras !== undefined) data.extras = header.extras
|
|
296
|
+
if (header.blobs !== undefined) {
|
|
297
|
+
data.blobs = {}
|
|
298
|
+
for (let [name, block] of Object.entries(header.blobs)) data.blobs[name] = new Uint8Array(buffer, payload + block.offset, block.bytes)
|
|
299
|
+
}
|
|
300
|
+
return data
|
|
226
301
|
}
|
package/src/model.ts
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
// local TRS, each part's mesh under its node - with the images uploaded as
|
|
4
4
|
// textures and a material per glTF material: Three's `gltf.scene`, an
|
|
5
5
|
// object you add to the scene and place with setTransform, whose named
|
|
6
|
-
// nodes (`model.nodes`) can be moved individually.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// nodes (`model.nodes`) can be moved individually. A part several nodes
|
|
7
|
+
// place (ModelPart.placements: a reused mesh, EXT_mesh_gpu_instancing) is
|
|
8
|
+
// ONE InstancedMesh at identity under the model root with an instance
|
|
9
|
+
// under every placement node, so the copies are one draw entry and still
|
|
10
|
+
// ride, animate, pick and cull as nodes of the hierarchy. The file's
|
|
11
|
+
// extras and blobs come along as plain data. The model owns what it
|
|
12
|
+
// created (geometry buffers, record buffers, textures): dispose() frees
|
|
13
|
+
// them and detaches the group. loadGltf / loadModel are the
|
|
14
|
+
// read-then-create conveniences over flux:fs; parseGltf / decodeModel +
|
|
15
|
+
// createModel are the primitives under them, for bytes obtained any other
|
|
16
|
+
// way (a binary import, a fetch).
|
|
11
17
|
|
|
12
18
|
import { file } from "flux:fs"
|
|
13
19
|
import * as spatial from "flux:spatial"
|
|
@@ -16,7 +22,7 @@ import type { Owner } from "@solidrt/core"
|
|
|
16
22
|
import { createMutableTexture, createTexture, destroyTexture } from "@solidrt/core/gpu"
|
|
17
23
|
import type { TextureId } from "@solidrt/core/gpu"
|
|
18
24
|
import { gltfExternalUris, parseGltf } from "./gltf.ts"
|
|
19
|
-
import type { ModelClip, ModelData, ModelMaterial } from "./gltf.ts"
|
|
25
|
+
import type { ModelClip, ModelData, ModelExtras, ModelMaterial } from "./gltf.ts"
|
|
20
26
|
import { decodeModel } from "./model-file.ts"
|
|
21
27
|
import { disposeGeometry } from "./geometry-gpu.ts"
|
|
22
28
|
import { layoutSlot } from "./geometry.ts"
|
|
@@ -24,8 +30,8 @@ import { standard } from "./material.ts"
|
|
|
24
30
|
import type { Material } from "./material.ts"
|
|
25
31
|
import { add, afterFree, createGroup, createMorphState, remove, setTransform } from "./node.ts"
|
|
26
32
|
import type { SceneNode } from "./node.ts"
|
|
27
|
-
import { createMesh } from "./mesh.ts"
|
|
28
|
-
import type { Mesh } from "./mesh.ts"
|
|
33
|
+
import { addInstance, createInstancedMesh, createMesh, disposeInstances } from "./mesh.ts"
|
|
34
|
+
import type { InstancedMesh, InstanceNode, Mesh } from "./mesh.ts"
|
|
29
35
|
import { refreshJointBounds, unbindSkeleton } from "./skeleton.ts"
|
|
30
36
|
import type { WornPiece } from "./skeleton.ts"
|
|
31
37
|
|
|
@@ -61,9 +67,13 @@ export type ModelOptions = {
|
|
|
61
67
|
* `vertexColors` is true when the part carries COLOR_0 in aColor (pass
|
|
62
68
|
* it through, or read aColor yourself); `morphed` is true when the part
|
|
63
69
|
* carries morph targets (pass it through as `morph`, or splice
|
|
64
|
-
* MORPH_DECLS/MORPH_APPLY yourself)
|
|
65
|
-
*
|
|
66
|
-
|
|
70
|
+
* MORPH_DECLS/MORPH_APPLY yourself); `instanced` is true when the part
|
|
71
|
+
* is a population (ModelPart.placements: drawn as one InstancedMesh),
|
|
72
|
+
* so the material must be instanced (pass it through as `instanced`,
|
|
73
|
+
* or declare INSTANCE_MATRIX_ATTRIBUTES and splice INSTANCE_MATRIX
|
|
74
|
+
* yourself). `data.materials` is in file order, so the calls arrive in
|
|
75
|
+
* file order too. */
|
|
76
|
+
material?: (material: ModelMaterial, maps: ModelMaps, skinned: boolean, vertexColors: boolean, morphed: boolean, instanced: boolean) => Material
|
|
67
77
|
/** Debug name for the textures. */
|
|
68
78
|
label?: string
|
|
69
79
|
/** Free the model with the owning reactive scope (default true, like
|
|
@@ -77,15 +87,31 @@ export type ModelOptions = {
|
|
|
77
87
|
* nested Groups, with each part's mesh a child of its node. */
|
|
78
88
|
export type Model = SceneNode & {
|
|
79
89
|
kind: "group"
|
|
80
|
-
/** The parts by name, in file order; each `mesh` sits under its node.
|
|
81
|
-
|
|
90
|
+
/** The parts by name, in file order; each `mesh` sits under its node.
|
|
91
|
+
* A part with placements (a mesh several nodes place) is instead ONE
|
|
92
|
+
* InstancedMesh at identity under the model root, with `instances` in
|
|
93
|
+
* placement order (the part's node first, then its placements), each
|
|
94
|
+
* an instance node at identity under its placement node in `nodes`:
|
|
95
|
+
* move the placement node and its copy follows; a pick on a copy
|
|
96
|
+
* reports `hit.mesh` = this mesh and `hit.instance` one of these
|
|
97
|
+
* (its `parent` is the placement node). A material swap on the mesh
|
|
98
|
+
* applies to every copy. `extras` is the glTF mesh's, when it has
|
|
99
|
+
* any. */
|
|
100
|
+
parts: { name: string; mesh: Mesh; instances?: InstanceNode[]; extras?: ModelExtras }[]
|
|
82
101
|
/** The file's retained nodes in table order (parents first), each an
|
|
83
102
|
* ordinary Group under the model: `setTransform` on one moves its
|
|
84
103
|
* subtree - a wheel spins relative to the axle it hangs from. Names
|
|
85
|
-
* repeat when the file's do; find yours with `.find()`/`.filter()`.
|
|
86
|
-
|
|
104
|
+
* repeat when the file's do; find yours with `.find()`/`.filter()`.
|
|
105
|
+
* `extras` is the glTF node's, when it has any. */
|
|
106
|
+
nodes: { name: string; node: SceneNode; extras?: ModelExtras }[]
|
|
87
107
|
/** One per glTF material, in file order. */
|
|
88
108
|
materials: Material[]
|
|
109
|
+
/** The file's root extras (glTF `extras`, or what a bake wrote), when
|
|
110
|
+
* it has any. */
|
|
111
|
+
extras?: ModelExtras
|
|
112
|
+
/** The file's named binary sections (a bake's), when it has any: views
|
|
113
|
+
* onto the loaded bytes. */
|
|
114
|
+
blobs?: Record<string, Uint8Array>
|
|
89
115
|
/** The file's animation clips (empty when it has none); createMixer
|
|
90
116
|
* plays them. Channel node indices resolve through `nodes`. */
|
|
91
117
|
clips: ModelClip[]
|
|
@@ -144,7 +170,7 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
144
170
|
label: label ? label + "-image" + i : undefined,
|
|
145
171
|
})
|
|
146
172
|
})
|
|
147
|
-
let make = opts.material ?? ((m: ModelMaterial, maps: ModelMaps, skinned: boolean, vertexColors: boolean, morphed: boolean): Material => {
|
|
173
|
+
let make = opts.material ?? ((m: ModelMaterial, maps: ModelMaps, skinned: boolean, vertexColors: boolean, morphed: boolean, instanced: boolean): Material => {
|
|
148
174
|
// An emissive factor of zero is emission OFF (the glTF product rule:
|
|
149
175
|
// factor times texture), so the map is skipped too - no sampler for
|
|
150
176
|
// a term that cannot show.
|
|
@@ -167,18 +193,20 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
167
193
|
skinned: skinned || undefined,
|
|
168
194
|
vertexColors: vertexColors || undefined,
|
|
169
195
|
morph: morphed || undefined,
|
|
196
|
+
instanced: instanced || undefined,
|
|
170
197
|
})
|
|
171
198
|
})
|
|
172
199
|
let slot = (index: number | null): TextureId | null => (index === null ? null : textures[index]!)
|
|
173
200
|
// One instance per glTF material as today, plus a skinned variant per
|
|
174
|
-
// material the skinned parts bring
|
|
175
|
-
// the COLOR_0 parts bring
|
|
176
|
-
//
|
|
201
|
+
// material the skinned parts bring, a vertex-colored one per material
|
|
202
|
+
// the COLOR_0 parts bring, a morphed and an instanced one likewise (a
|
|
203
|
+
// material shared by parts that differ in any needs two programs -
|
|
204
|
+
// different vertex stages).
|
|
177
205
|
let variants = new Map<string, Material>()
|
|
178
|
-
let materialFor = (index: number, skinned: boolean, vertexColors: boolean, morphed: boolean): Material => {
|
|
206
|
+
let materialFor = (index: number, skinned: boolean, vertexColors: boolean, morphed: boolean, instanced: boolean): Material => {
|
|
179
207
|
let m = data.materials[index]
|
|
180
208
|
if (m === undefined) throw new Error("createModel: a part names a missing material " + index)
|
|
181
|
-
let key = index + (skinned ? "|skinned" : "") + (vertexColors ? "|colored" : "") + (morphed ? "|morphed" : "")
|
|
209
|
+
let key = index + (skinned ? "|skinned" : "") + (vertexColors ? "|colored" : "") + (morphed ? "|morphed" : "") + (instanced ? "|instanced" : "")
|
|
182
210
|
let made = variants.get(key)
|
|
183
211
|
if (made === undefined) {
|
|
184
212
|
made = make(
|
|
@@ -193,12 +221,13 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
193
221
|
skinned,
|
|
194
222
|
vertexColors,
|
|
195
223
|
morphed,
|
|
224
|
+
instanced,
|
|
196
225
|
)
|
|
197
226
|
variants.set(key, made)
|
|
198
227
|
}
|
|
199
228
|
return made
|
|
200
229
|
}
|
|
201
|
-
let materials = data.materials.map((_, i) => materialFor(i, false, false, false))
|
|
230
|
+
let materials = data.materials.map((_, i) => materialFor(i, false, false, false, false))
|
|
202
231
|
|
|
203
232
|
let model = createGroup() as Model
|
|
204
233
|
model._skins = []
|
|
@@ -213,7 +242,7 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
213
242
|
return group
|
|
214
243
|
})
|
|
215
244
|
data.nodes.forEach((n, i) => add(n.parent === null ? model : groups[n.parent]!, groups[i]!))
|
|
216
|
-
model.nodes = data.nodes.map((n, i) => ({ name: n.name, node: groups[i]! }))
|
|
245
|
+
model.nodes = data.nodes.map((n, i) => (n.extras === undefined ? { name: n.name, node: groups[i]! } : { name: n.name, node: groups[i]!, extras: n.extras }))
|
|
217
246
|
// A glTF node with morph targets owns its parts' weights (the file's
|
|
218
247
|
// node.weights / mesh.weights seed them): one register, one weights
|
|
219
248
|
// texture, every part of the mesh reads it - so setMorphWeights on
|
|
@@ -268,10 +297,37 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
268
297
|
// Each joint's influence box, in joint space: culling-only bounds the
|
|
269
298
|
// flush carries through the pose, united over every skin reaching it.
|
|
270
299
|
refreshJointBounds(model)
|
|
300
|
+
let groupOf = (part: ModelData["parts"][0], index: number): SceneNode => {
|
|
301
|
+
let node = groups[index]
|
|
302
|
+
if (node === undefined) throw new Error("createModel: part '" + part.name + "' names a missing node " + index)
|
|
303
|
+
return node
|
|
304
|
+
}
|
|
271
305
|
model.parts = data.parts.map((part) => {
|
|
272
306
|
let skinned = part.skin !== null
|
|
273
307
|
let morphed = part.geometry.morphs !== undefined
|
|
274
|
-
let
|
|
308
|
+
let colored = layoutSlot(part.geometry.layout, "aColor") !== null
|
|
309
|
+
let extras = part.extras
|
|
310
|
+
let placements = part.placements ?? []
|
|
311
|
+
if (placements.length > 0) {
|
|
312
|
+
// A population: the geometry once, drawn per placement. The mesh
|
|
313
|
+
// sits at identity under the model root, which anchors its records,
|
|
314
|
+
// and each placement node - the first is `part.node` - carries an
|
|
315
|
+
// instance at identity, so the record the core writes is that
|
|
316
|
+
// node's model-relative world: moving the node (setTransform, a
|
|
317
|
+
// clip, a transition) moves the copy, hiding it hides the copy,
|
|
318
|
+
// and a pick lands on the instance. Skins place by palette and
|
|
319
|
+
// morph weights are per node, so neither can share one geometry;
|
|
320
|
+
// parseGltf never folds them, and a hand-built ModelData that does
|
|
321
|
+
// is refused rather than drawn wrong.
|
|
322
|
+
if (skinned) throw new Error("createModel: part '" + part.name + "' is skinned and has placements; a skinned part cannot be shared")
|
|
323
|
+
if (morphed) throw new Error("createModel: part '" + part.name + "' has morph targets and placements; a morphed part cannot be shared")
|
|
324
|
+
let material = materialFor(part.material, false, colored, false, true)
|
|
325
|
+
let mesh = createInstancedMesh(part.geometry, material, { capacity: placements.length + 1, anchor: model, label: label ? label + "-" + part.name : part.name })
|
|
326
|
+
add(model, mesh)
|
|
327
|
+
let instances = [part.node, ...placements].map((index) => addInstance(mesh, undefined, groupOf(part, index)))
|
|
328
|
+
return extras === undefined ? { name: part.name, mesh, instances } : { name: part.name, mesh, instances, extras }
|
|
329
|
+
}
|
|
330
|
+
let material = materialFor(part.material, skinned, colored, morphed, false)
|
|
275
331
|
let mesh = createMesh(part.geometry, material)
|
|
276
332
|
// The part's weights are its glTF node's (above), not its own.
|
|
277
333
|
if (morphed) mesh._morphOwner = groups[part.node] ?? mesh
|
|
@@ -287,15 +343,15 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
287
343
|
mesh._cullJoints = skin.joints.slice()
|
|
288
344
|
add(model, mesh)
|
|
289
345
|
} else {
|
|
290
|
-
|
|
291
|
-
if (node === undefined) throw new Error("createModel: part '" + part.name + "' names a missing node " + part.node)
|
|
292
|
-
add(node, mesh)
|
|
346
|
+
add(groupOf(part, part.node), mesh)
|
|
293
347
|
}
|
|
294
|
-
return { name: part.name, mesh }
|
|
348
|
+
return extras === undefined ? { name: part.name, mesh } : { name: part.name, mesh, extras }
|
|
295
349
|
})
|
|
296
350
|
model.materials = materials
|
|
297
351
|
model.clips = data.clips
|
|
298
352
|
model.bounds = data.bounds
|
|
353
|
+
if (data.extras !== undefined) model.extras = data.extras
|
|
354
|
+
if (data.blobs !== undefined) model.blobs = data.blobs
|
|
299
355
|
let disposed = false
|
|
300
356
|
model.dispose = () => {
|
|
301
357
|
if (disposed) return
|
|
@@ -311,7 +367,12 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
311
367
|
// destroyed texture.
|
|
312
368
|
if (afterFree(model, () => model.dispose())) return
|
|
313
369
|
disposed = true
|
|
314
|
-
for (let part of model.parts)
|
|
370
|
+
for (let part of model.parts) {
|
|
371
|
+
// A population's record buffers are mesh-owned (the one explicit
|
|
372
|
+
// free); its geometry buffers are shared like any part's.
|
|
373
|
+
if (part.instances !== undefined) disposeInstances(part.mesh as InstancedMesh)
|
|
374
|
+
disposeGeometry(part.mesh.geometry)
|
|
375
|
+
}
|
|
315
376
|
for (let id of textures) destroyTexture(id)
|
|
316
377
|
textures.length = 0
|
|
317
378
|
// The nodes' weights textures (the parts' entries are off the scene
|
package/src/node.ts
CHANGED
|
@@ -163,7 +163,7 @@ export type SceneHooks = {
|
|
|
163
163
|
_setLayers(mesh: Mesh): void
|
|
164
164
|
/** A light's castShadow/shadow options changed. */
|
|
165
165
|
_shadowChanged(light: CastingLight): void
|
|
166
|
-
_reorder(): void
|
|
166
|
+
_reorder(mesh: Mesh): void
|
|
167
167
|
/** The node's transform changed (for the sort and light bookkeeping). */
|
|
168
168
|
_moved(node: SceneNode): void
|
|
169
169
|
/** Push the node's LOD declaration (`_lod`) to the core: at scene enter
|