@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/gltf.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// glTF 2.0, the subset an app needs to show authored models: the scene's
|
|
2
2
|
// node tree RETAINED - a node table of local TRS with parent links (matrix
|
|
3
3
|
// nodes decomposed; shear dropped), vertices in NODE-LOCAL space, one part
|
|
4
|
-
// per mesh primitive referencing its node -
|
|
5
|
-
//
|
|
6
|
-
// indices
|
|
4
|
+
// per mesh primitive referencing its node - positions, normals (flat ones
|
|
5
|
+
// generated when absent, per the spec, for triangles), one UV set and
|
|
6
|
+
// indices in the primitive's topology (strips and fans unrolled to
|
|
7
|
+
// triangle lists, a line loop closed into a strip; lines and points keep
|
|
8
|
+
// theirs on the geometry), and materials reduced to what phong()/unlit()
|
|
9
|
+
// draw - base color
|
|
7
10
|
// factor and texture, normal map (with scale), emissive factor and map
|
|
8
11
|
// (KHR_materials_emissive_strength as the intensity), double-sidedness, alpha
|
|
9
12
|
// blending and masking - plus the file's animations as baked clips
|
|
@@ -19,23 +22,30 @@
|
|
|
19
22
|
// bytes in `images`, and uploading is the engine side's job.
|
|
20
23
|
//
|
|
21
24
|
// Outside the subset: Draco/meshopt-compressed meshes and any other
|
|
22
|
-
// required extension throw naming it;
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
25
|
+
// required extension throw naming it; tangents (the base channel and
|
|
26
|
+
// the morph target one) and further UV sets are dropped. Sparse
|
|
27
|
+
// accessors are read (what exporters write morph targets as), and
|
|
28
|
+
// morph targets land packed on the part's geometry with the mesh's
|
|
29
|
+
// weights on its node and the "weights" channel path kept. COLOR_0 IS
|
|
30
|
+
// parsed, linear and premultiplied as glTF stores it, into the aColor
|
|
31
|
+
// channel the stock materials read under vertexColors: the "colored"
|
|
32
|
+
// layout for a static primitive, the skinned list plus aColor for a
|
|
33
|
+
// rigged one. Skins ARE parsed: joints, inverse binds, and the "skinned"
|
|
34
|
+
// vertex layout.
|
|
27
35
|
|
|
28
36
|
import { compose, decompose, det3, mat4, multiply } from "./math.ts"
|
|
29
37
|
import { linearToSrgb } from "./color.ts"
|
|
30
38
|
import type { Mat4, Quat, Vec3 } from "./math.ts"
|
|
31
|
-
import {
|
|
39
|
+
import { attributeAccess, layoutKey, layoutStride, packIndices, packMorphTargets, vertexView } from "./geometry.ts"
|
|
40
|
+
import type { AttributeAccess, MorphTarget, VertexLayout } from "./geometry.ts"
|
|
32
41
|
import type { Geometry } from "./geometry.ts"
|
|
42
|
+
import type { Topology, VertexAttribute, VertexFormat } from "@solidrt/core/gpu"
|
|
33
43
|
|
|
34
|
-
/** What
|
|
44
|
+
/** What phong()/unlit() take from a glTF material. */
|
|
35
45
|
export type ModelMaterial = {
|
|
36
46
|
name: string
|
|
37
47
|
/** Straight [r, g, b, a] 0..1: glTF's LINEAR baseColorFactor encoded
|
|
38
|
-
* to sRGB, what
|
|
48
|
+
* to sRGB, what phong({ color }) takes. */
|
|
39
49
|
color: [number, number, number, number]
|
|
40
50
|
/** Index into ModelData.images (the base color texture), or null. */
|
|
41
51
|
map: number | null
|
|
@@ -54,9 +64,9 @@ export type ModelMaterial = {
|
|
|
54
64
|
/** glTF normalTexture.scale (default 1); meaningful with normalMap. */
|
|
55
65
|
normalScale: number
|
|
56
66
|
/** glTF's linear emissiveFactor (default [0, 0, 0] = off) encoded to
|
|
57
|
-
* sRGB, what
|
|
67
|
+
* sRGB, what phong({ emissive }) takes. */
|
|
58
68
|
emissive: [number, number, number]
|
|
59
|
-
/** KHR_materials_emissive_strength (default 1):
|
|
69
|
+
/** KHR_materials_emissive_strength (default 1): phong's emissiveIntensity. */
|
|
60
70
|
emissiveIntensity: number
|
|
61
71
|
/** Index into ModelData.images (the emissive map), or null. */
|
|
62
72
|
emissiveMap: number | null
|
|
@@ -85,6 +95,11 @@ export type ModelNode = {
|
|
|
85
95
|
/** Unit quaternion [x, y, z, w]. */
|
|
86
96
|
rotation: Quat
|
|
87
97
|
scale: Vec3
|
|
98
|
+
/** Present on a node whose mesh carries morph targets: the initial
|
|
99
|
+
* weight per target (the node's own `weights`, else the mesh's, else
|
|
100
|
+
* zeros), what the node's parts draw with until a clip or
|
|
101
|
+
* setMorphWeights writes them. */
|
|
102
|
+
weights?: number[]
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
/** One drawable: a mesh node's primitive. Vertices are LOCAL to its
|
|
@@ -99,8 +114,17 @@ export type ModelPart = {
|
|
|
99
114
|
* this part (unused for placement when `skin` is set). */
|
|
100
115
|
node: number
|
|
101
116
|
/** Index into ModelData.skins, or null. A skinned part's geometry has
|
|
102
|
-
* the "skinned" layout (aJoints/aWeights after the
|
|
117
|
+
* the "skinned" layout (aJoints/aWeights after the base prefix). */
|
|
103
118
|
skin: number | null
|
|
119
|
+
/** The part's vertices and indices. The layout is the base prefix,
|
|
120
|
+
* aJoints/aWeights for a rigged primitive, aColor for one with COLOR_0
|
|
121
|
+
* (premultiplied linear), each channel in the file's own format when
|
|
122
|
+
* the vertex vocabulary has it (a quantized uv, a byte color, u8/u16
|
|
123
|
+
* joints) and float otherwise - except joints, an integer channel:
|
|
124
|
+
* an off-spec float JOINTS_0 lands in uint16x4. Positions and normals
|
|
125
|
+
* are always float.
|
|
126
|
+
* The preset name ("base", "colored", "skinned") is used when the
|
|
127
|
+
* list equals it. */
|
|
104
128
|
geometry: Geometry
|
|
105
129
|
/** Index into ModelData.materials. */
|
|
106
130
|
material: number
|
|
@@ -126,15 +150,18 @@ export type ModelSkin = {
|
|
|
126
150
|
export type ModelChannel = {
|
|
127
151
|
/** Index into ModelData.nodes. */
|
|
128
152
|
node: number
|
|
129
|
-
|
|
153
|
+
/** The node's local TRS, or "weights": the morph target weights of the
|
|
154
|
+
* node's mesh (glTF's weights path). */
|
|
155
|
+
path: "position" | "rotation" | "scale" | "weights"
|
|
130
156
|
/** glTF's three: "step" holds each key, "linear" lerps (a rotation
|
|
131
157
|
* slerps), "cubic" is CUBICSPLINE - a Hermite with per-key tangents. */
|
|
132
158
|
interpolation: "step" | "linear" | "cubic"
|
|
133
159
|
/** Key times in seconds, ascending. */
|
|
134
160
|
times: Float32Array
|
|
135
161
|
/** One element per key: 3 floats for position/scale, 4 (a quaternion)
|
|
136
|
-
* for rotation
|
|
137
|
-
*
|
|
162
|
+
* for rotation, one per morph target for weights (`channelElements`
|
|
163
|
+
* reads it off the data). "cubic" stores THREE elements per key -
|
|
164
|
+
* in-tangent, value, out-tangent, in that order. */
|
|
138
165
|
values: Float32Array
|
|
139
166
|
}
|
|
140
167
|
|
|
@@ -170,8 +197,11 @@ export type ModelData = {
|
|
|
170
197
|
/** Encoded image files (PNG/JPEG bytes) the materials' `map` index. */
|
|
171
198
|
images: Uint8Array[]
|
|
172
199
|
/** World-space rest-pose [minX, minY, minZ, maxX, maxY, maxZ] over every
|
|
173
|
-
* part - each part's local box through its node's composed transform,
|
|
174
|
-
*
|
|
200
|
+
* part - each part's local box through its node's composed transform, and
|
|
201
|
+
* a skinned part's per-joint boxes through the joints' rest transforms
|
|
202
|
+
* (where the skin places its vertices, armature scale included) - so it
|
|
203
|
+
* is conservative (not vertex-tight) for parts under rotated nodes and
|
|
204
|
+
* for skinned parts. */
|
|
175
205
|
bounds: Float32Array
|
|
176
206
|
}
|
|
177
207
|
|
|
@@ -182,15 +212,82 @@ export type UriResolver = (uri: string) => Uint8Array
|
|
|
182
212
|
const GLB_MAGIC = 0x46546c67
|
|
183
213
|
const CHUNK_JSON = 0x4e4f534a
|
|
184
214
|
const CHUNK_BIN = 0x004e4942
|
|
215
|
+
// glTF primitive modes (the GL enum values).
|
|
216
|
+
const MODE_POINTS = 0
|
|
217
|
+
const MODE_LINES = 1
|
|
218
|
+
const MODE_LINE_LOOP = 2
|
|
219
|
+
const MODE_LINE_STRIP = 3
|
|
185
220
|
const MODE_TRIANGLES = 4
|
|
221
|
+
const MODE_TRIANGLE_STRIP = 5
|
|
222
|
+
const MODE_TRIANGLE_FAN = 6
|
|
223
|
+
|
|
224
|
+
/** A triangle strip unrolled to a list: triangle t is (t, t+1, t+2),
|
|
225
|
+
* every odd one with its last two swapped so the winding stays put. */
|
|
226
|
+
function stripTriangles(strip: ArrayLike<number>): number[] {
|
|
227
|
+
let out: number[] = []
|
|
228
|
+
for (let t = 0; t + 2 < strip.length; t++) {
|
|
229
|
+
if (t % 2 === 0) out.push(strip[t]!, strip[t + 1]!, strip[t + 2]!)
|
|
230
|
+
else out.push(strip[t]!, strip[t + 2]!, strip[t + 1]!)
|
|
231
|
+
}
|
|
232
|
+
return out
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A triangle fan unrolled to a list: every triangle shares the hub. */
|
|
236
|
+
function fanTriangles(fan: ArrayLike<number>): number[] {
|
|
237
|
+
let out: number[] = []
|
|
238
|
+
for (let t = 1; t + 1 < fan.length; t++) out.push(fan[0]!, fan[t]!, fan[t + 1]!)
|
|
239
|
+
return out
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** A line loop as a strip that returns to its first vertex. */
|
|
243
|
+
function closeLoop(loop: ArrayLike<number>): number[] {
|
|
244
|
+
let out = Array.from(loop)
|
|
245
|
+
if (loop.length > 0) out.push(loop[0]!)
|
|
246
|
+
return out
|
|
247
|
+
}
|
|
186
248
|
|
|
187
249
|
// The spec's alphaCutoff when a MASK material leaves it out.
|
|
188
250
|
const GLTF_ALPHA_CUTOFF = 0.5
|
|
189
251
|
|
|
190
|
-
// The
|
|
191
|
-
|
|
252
|
+
// The vertex format an accessor's (componentType, normalized, element
|
|
253
|
+
// count) spells when the table has that row, keyed "<type>[n]x<count>".
|
|
254
|
+
// These are exactly the quantized forms KHR_mesh_quantization allows for
|
|
255
|
+
// uvs, colors, joints and weights, so a quantized export keeps its bytes
|
|
256
|
+
// in the vertex buffer; a channel with no row (a byte-pair uv, a short
|
|
257
|
+
// normal, any position) widens to float32xN.
|
|
258
|
+
const ACCESSOR_FORMATS: Record<string, VertexFormat> = {
|
|
259
|
+
"5126x1": "float32",
|
|
260
|
+
"5126x2": "float32x2",
|
|
261
|
+
"5126x3": "float32x3",
|
|
262
|
+
"5126x4": "float32x4",
|
|
263
|
+
"5121nx4": "unorm8x4",
|
|
264
|
+
"5120nx4": "snorm8x4",
|
|
265
|
+
"5123nx2": "unorm16x2",
|
|
266
|
+
"5123nx4": "unorm16x4",
|
|
267
|
+
"5122nx2": "snorm16x2",
|
|
268
|
+
"5122nx4": "snorm16x4",
|
|
269
|
+
"5121x4": "uint8x4",
|
|
270
|
+
"5123x2": "uint16x2",
|
|
271
|
+
"5123x4": "uint16x4",
|
|
272
|
+
}
|
|
273
|
+
const FLOAT_FORMATS: Record<number, VertexFormat> = { 1: "float32", 2: "float32x2", 3: "float32x3", 4: "float32x4" }
|
|
274
|
+
|
|
275
|
+
// The vertex format a channel of `elements` components lands in: the
|
|
276
|
+
// accessor's own when it has a row, float otherwise.
|
|
277
|
+
function channelFormat(format: VertexFormat | null, elements: number): VertexFormat {
|
|
278
|
+
return format ?? FLOAT_FORMATS[elements]!
|
|
279
|
+
}
|
|
192
280
|
|
|
193
|
-
|
|
281
|
+
// A part's layout as the preset it equals, else its list: what the
|
|
282
|
+
// container writes and what the presets' docs promise ("base",
|
|
283
|
+
// "colored", "skinned" when the file carried floats).
|
|
284
|
+
function presetOrList(attrs: VertexAttribute[]): VertexLayout | undefined {
|
|
285
|
+
let key = layoutKey(attrs)
|
|
286
|
+
if (key === layoutKey("base")) return undefined
|
|
287
|
+
if (key === layoutKey("colored")) return "colored"
|
|
288
|
+
if (key === layoutKey("skinned")) return "skinned"
|
|
289
|
+
return attrs
|
|
290
|
+
}
|
|
194
291
|
|
|
195
292
|
const COMPONENT_BYTES: Record<number, number> = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 }
|
|
196
293
|
const TYPE_ELEMENTS: Record<string, number> = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 }
|
|
@@ -341,34 +438,58 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
341
438
|
// only when something uses it.
|
|
342
439
|
let defaultMaterial = -1
|
|
343
440
|
|
|
344
|
-
let accessorFloats = (index: number, what: string): { data: Float32Array; elements: number; count: number } => {
|
|
441
|
+
let accessorFloats = (index: number, what: string): { data: Float32Array; elements: number; count: number; format: VertexFormat | null } => {
|
|
345
442
|
let acc = gltf.accessors[index]
|
|
346
443
|
if (acc === undefined) throw new Error("parseGltf: " + what + " names a missing accessor " + index)
|
|
347
|
-
if (acc.sparse !== undefined) throw new Error("parseGltf: " + what + " uses a sparse accessor, which is not supported")
|
|
348
444
|
let elements = TYPE_ELEMENTS[acc.type]
|
|
349
445
|
let compBytes = COMPONENT_BYTES[acc.componentType]
|
|
350
446
|
if (elements === undefined || compBytes === undefined) throw new Error("parseGltf: " + what + " has an unknown accessor type")
|
|
351
447
|
let out = new Float32Array(acc.count * elements)
|
|
352
|
-
if (acc.bufferView === undefined) return { data: out, elements, count: acc.count }
|
|
353
|
-
let view = gltf.bufferViews[acc.bufferView]
|
|
354
|
-
let bytes = bufferViewBytes(acc.bufferView)
|
|
355
|
-
let stride = view.byteStride ?? compBytes * elements
|
|
356
|
-
let dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
357
|
-
let base = acc.byteOffset ?? 0
|
|
358
448
|
let normalized = acc.normalized === true
|
|
449
|
+
let format = ACCESSOR_FORMATS[acc.componentType + (normalized ? "n" : "") + "x" + elements] ?? null
|
|
359
450
|
let read = readerFor(acc.componentType, normalized)
|
|
360
|
-
|
|
361
|
-
let
|
|
362
|
-
|
|
451
|
+
if (acc.bufferView !== undefined) {
|
|
452
|
+
let view = gltf.bufferViews[acc.bufferView]
|
|
453
|
+
let bytes = bufferViewBytes(acc.bufferView)
|
|
454
|
+
let stride = view.byteStride ?? compBytes * elements
|
|
455
|
+
let dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
456
|
+
let base = acc.byteOffset ?? 0
|
|
457
|
+
for (let i = 0; i < acc.count; i++) {
|
|
458
|
+
let at = base + i * stride
|
|
459
|
+
for (let e = 0; e < elements; e++) out[i * elements + e] = read(dv, at + e * compBytes)
|
|
460
|
+
}
|
|
363
461
|
}
|
|
364
|
-
|
|
462
|
+
// A sparse accessor overrides `sparse.count` elements of the base
|
|
463
|
+
// (the bufferView, or zeros without one): tightly packed indices in
|
|
464
|
+
// their own component type and values in the accessor's - the form
|
|
465
|
+
// exporters write morph targets in.
|
|
466
|
+
let sparse = acc.sparse
|
|
467
|
+
if (sparse !== undefined) {
|
|
468
|
+
let indexBytes = COMPONENT_BYTES[sparse.indices?.componentType]
|
|
469
|
+
if (indexBytes === undefined || sparse.indices.bufferView === undefined || sparse.values?.bufferView === undefined) {
|
|
470
|
+
throw new Error("parseGltf: " + what + " has a malformed sparse accessor")
|
|
471
|
+
}
|
|
472
|
+
let ib = bufferViewBytes(sparse.indices.bufferView)
|
|
473
|
+
let idv = new DataView(ib.buffer, ib.byteOffset, ib.byteLength)
|
|
474
|
+
let ibase = sparse.indices.byteOffset ?? 0
|
|
475
|
+
let readIndex = readerFor(sparse.indices.componentType, false)
|
|
476
|
+
let vb = bufferViewBytes(sparse.values.bufferView)
|
|
477
|
+
let vdv = new DataView(vb.buffer, vb.byteOffset, vb.byteLength)
|
|
478
|
+
let vbase = sparse.values.byteOffset ?? 0
|
|
479
|
+
for (let i = 0; i < sparse.count; i++) {
|
|
480
|
+
let target = readIndex(idv, ibase + i * indexBytes)
|
|
481
|
+
if (target >= acc.count) throw new Error("parseGltf: " + what + " sparse index " + target + " is past the accessor's " + acc.count + " elements")
|
|
482
|
+
for (let e = 0; e < elements; e++) out[target * elements + e] = read(vdv, vbase + (i * elements + e) * compBytes)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return { data: out, elements, count: acc.count, format }
|
|
365
486
|
}
|
|
366
487
|
|
|
367
488
|
// The node table is built lazily: a glTF node gets a slot only when a
|
|
368
489
|
// part somewhere below it needs the chain (materialize walks ancestors
|
|
369
490
|
// first, so the table stays in pre-order), which is what prunes cameras,
|
|
370
491
|
// lights and unused empties.
|
|
371
|
-
type PendingNode = { name: string; parent: PendingNode | null; position: Vec3; rotation: Quat; scale: Vec3; index: number | null }
|
|
492
|
+
type PendingNode = { name: string; parent: PendingNode | null; position: Vec3; rotation: Quat; scale: Vec3; world: Mat4; index: number | null }
|
|
372
493
|
let nodes: ModelNode[] = []
|
|
373
494
|
let materialize = (p: PendingNode): number => {
|
|
374
495
|
if (p.index !== null) return p.index
|
|
@@ -379,24 +500,44 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
379
500
|
}
|
|
380
501
|
|
|
381
502
|
let parts: ModelPart[] = []
|
|
382
|
-
let pendingJointBounds: { skin: number; positions: Float32Array; count: number; joints: Float32Array; weights: Float32Array }[] = []
|
|
503
|
+
let pendingJointBounds: { skin: number; positions: Float32Array; count: number; joints: Float32Array; weights: Float32Array; slack: number }[] = []
|
|
383
504
|
let bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
|
|
505
|
+
// Grow a bounds box by a local box through a transform: its 8 corners,
|
|
506
|
+
// conservative under rotation, exact under translation and axis-aligned
|
|
507
|
+
// scale. An empty box (no vertex grew it) contributes nothing.
|
|
508
|
+
let growBounds = (into: Float32Array, box: ArrayLike<number>, m: Mat4): void => {
|
|
509
|
+
if (!(box[0]! <= box[3]!)) return
|
|
510
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
511
|
+
let x = corner & 1 ? box[3]! : box[0]!, y = corner & 2 ? box[4]! : box[1]!, z = corner & 4 ? box[5]! : box[2]!
|
|
512
|
+
let wx = m[0] * x + m[4] * y + m[8] * z + m[12]
|
|
513
|
+
let wy = m[1] * x + m[5] * y + m[9] * z + m[13]
|
|
514
|
+
let wz = m[2] * x + m[6] * y + m[10] * z + m[14]
|
|
515
|
+
if (wx < into[0]!) into[0] = wx
|
|
516
|
+
if (wy < into[1]!) into[1] = wy
|
|
517
|
+
if (wz < into[2]!) into[2] = wz
|
|
518
|
+
if (wx > into[3]!) into[3] = wx
|
|
519
|
+
if (wy > into[4]!) into[4] = wy
|
|
520
|
+
if (wz > into[5]!) into[5] = wz
|
|
521
|
+
}
|
|
522
|
+
}
|
|
384
523
|
|
|
385
|
-
let emit = (prim: any, name: string, node: number, world: Mat4, skin: number | null): void => {
|
|
386
|
-
if ((prim.mode ?? MODE_TRIANGLES) !== MODE_TRIANGLES) return
|
|
524
|
+
let emit = (prim: any, name: string, node: number, world: Mat4, skin: number | null, targetNames: string[] | null): void => {
|
|
387
525
|
if (prim.attributes?.POSITION === undefined) return
|
|
526
|
+
let mode: number = prim.mode ?? MODE_TRIANGLES
|
|
388
527
|
let pos = accessorFloats(prim.attributes.POSITION, name + " POSITION")
|
|
389
528
|
if (pos.elements !== 3) throw new Error("parseGltf: " + name + " POSITION is not VEC3")
|
|
390
529
|
let count = pos.count
|
|
391
530
|
let nrm = prim.attributes.NORMAL !== undefined ? accessorFloats(prim.attributes.NORMAL, name + " NORMAL").data : null
|
|
392
|
-
let uv = prim.attributes.TEXCOORD_0 !== undefined ? accessorFloats(prim.attributes.TEXCOORD_0, name + " TEXCOORD_0")
|
|
531
|
+
let uv = prim.attributes.TEXCOORD_0 !== undefined ? accessorFloats(prim.attributes.TEXCOORD_0, name + " TEXCOORD_0") : null
|
|
532
|
+
let color = prim.attributes.COLOR_0 !== undefined ? accessorFloats(prim.attributes.COLOR_0, name + " COLOR_0") : null
|
|
533
|
+
if (color !== null && color.elements !== 3 && color.elements !== 4) throw new Error("parseGltf: " + name + " COLOR_0 is not VEC3 or VEC4")
|
|
393
534
|
// A skinned primitive: joints and weights become the "skinned"
|
|
394
535
|
// layout's extra channels; its vertices are ALREADY model-space bind
|
|
395
536
|
// pose (the node transform is ignored, per spec), so no flip and
|
|
396
537
|
// identity bounds.
|
|
397
538
|
let skinned = skin !== null
|
|
398
|
-
let joints: Float32Array | null = null
|
|
399
|
-
let weights: Float32Array | null = null
|
|
539
|
+
let joints: { data: Float32Array; format: VertexFormat | null } | null = null
|
|
540
|
+
let weights: { data: Float32Array; format: VertexFormat | null } | null = null
|
|
400
541
|
if (skinned) {
|
|
401
542
|
if (prim.attributes.JOINTS_0 === undefined || prim.attributes.WEIGHTS_0 === undefined) {
|
|
402
543
|
throw new Error("parseGltf: " + name + " is skinned but lacks JOINTS_0/WEIGHTS_0")
|
|
@@ -404,36 +545,100 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
404
545
|
let j = accessorFloats(prim.attributes.JOINTS_0, name + " JOINTS_0")
|
|
405
546
|
let w = accessorFloats(prim.attributes.WEIGHTS_0, name + " WEIGHTS_0")
|
|
406
547
|
if (j.elements !== 4 || w.elements !== 4) throw new Error("parseGltf: " + name + " JOINTS_0/WEIGHTS_0 are not VEC4")
|
|
407
|
-
joints = j
|
|
408
|
-
weights = w
|
|
548
|
+
joints = j
|
|
549
|
+
weights = w
|
|
550
|
+
}
|
|
551
|
+
// The layout the vertices are written in: the base prefix (a
|
|
552
|
+
// position is always float; a uv keeps a quantized form), the skin
|
|
553
|
+
// channels for a skinned primitive in the file's own integer forms
|
|
554
|
+
// (joints feed an integer `in`, so a float JOINTS_0 narrows to u16),
|
|
555
|
+
// aColor after them for one with COLOR_0 (a VEC3 color widens to four
|
|
556
|
+
// floats: it gains an alpha).
|
|
557
|
+
let attrs: VertexAttribute[] = [
|
|
558
|
+
{ name: "aPos", format: "float32x3" },
|
|
559
|
+
{ name: "aNormal", format: "float32x3" },
|
|
560
|
+
{ name: "aUV", format: channelFormat(uv?.format ?? null, 2) },
|
|
561
|
+
]
|
|
562
|
+
if (joints !== null && weights !== null) {
|
|
563
|
+
attrs.push({ name: "aJoints", format: joints.format ?? "uint16x4" }, { name: "aWeights", format: channelFormat(weights.format, 4) })
|
|
409
564
|
}
|
|
410
|
-
|
|
411
|
-
let
|
|
565
|
+
if (color !== null) attrs.push({ name: "aColor", format: channelFormat(color.elements === 4 ? color.format : null, 4) })
|
|
566
|
+
let layout = presetOrList(attrs)
|
|
567
|
+
let raw: ArrayLike<number> =
|
|
412
568
|
prim.indices !== undefined ? accessorFloats(prim.indices, name + " indices").data : Array.from({ length: count }, (_, i) => i)
|
|
413
|
-
|
|
569
|
+
// The primitive's topology, carried on the geometry. Triangle strips
|
|
570
|
+
// and fans are unrolled to lists (what the winding flip, the
|
|
571
|
+
// flat-shading un-index and the picking shape expect) and a line
|
|
572
|
+
// loop, which has no GPU topology, is closed into a strip.
|
|
573
|
+
let topology: Topology
|
|
574
|
+
let indices: ArrayLike<number>
|
|
575
|
+
switch (mode) {
|
|
576
|
+
case MODE_TRIANGLES:
|
|
577
|
+
topology = "triangles"
|
|
578
|
+
indices = raw
|
|
579
|
+
break
|
|
580
|
+
case MODE_TRIANGLE_STRIP:
|
|
581
|
+
topology = "triangles"
|
|
582
|
+
indices = stripTriangles(raw)
|
|
583
|
+
break
|
|
584
|
+
case MODE_TRIANGLE_FAN:
|
|
585
|
+
topology = "triangles"
|
|
586
|
+
indices = fanTriangles(raw)
|
|
587
|
+
break
|
|
588
|
+
case MODE_LINES:
|
|
589
|
+
topology = "lines"
|
|
590
|
+
indices = raw
|
|
591
|
+
break
|
|
592
|
+
case MODE_LINE_STRIP:
|
|
593
|
+
topology = "line-strip"
|
|
594
|
+
indices = raw
|
|
595
|
+
break
|
|
596
|
+
case MODE_LINE_LOOP:
|
|
597
|
+
topology = "line-strip"
|
|
598
|
+
indices = closeLoop(raw)
|
|
599
|
+
break
|
|
600
|
+
case MODE_POINTS:
|
|
601
|
+
topology = "points"
|
|
602
|
+
indices = raw
|
|
603
|
+
break
|
|
604
|
+
default:
|
|
605
|
+
throw new Error("parseGltf: " + name + " has an unknown primitive mode " + mode)
|
|
606
|
+
}
|
|
607
|
+
let triangles = topology === "triangles"
|
|
608
|
+
if (triangles && indices.length % 3 !== 0) throw new Error("parseGltf: " + name + " index count is not a multiple of 3")
|
|
414
609
|
|
|
415
610
|
// A mirroring chain (negative world determinant at the rest pose)
|
|
416
611
|
// flips the displayed winding, so the index order is flipped here to
|
|
417
612
|
// compensate - cull: "back" keeps the outside. Stored normals stay the
|
|
418
613
|
// authored ones: the runtime's inverse-transpose maps them outward for
|
|
419
614
|
// the flipped winding. Baked from the REST pose; a scale animated
|
|
420
|
-
// across zero would unbake it, which is pathological.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
let vertices:
|
|
615
|
+
// across zero would unbake it, which is pathological. Lines and
|
|
616
|
+
// points have no winding.
|
|
617
|
+
let flip = triangles && !skinned && det3(world) < 0
|
|
618
|
+
|
|
619
|
+
// The channels of the part's buffer, written through the accessors:
|
|
620
|
+
// whatever a channel's format, the writer takes floats.
|
|
621
|
+
let source: VertexSource = { pos: pos.data, nrm, uv: uv?.data ?? null, color: color === null ? null : { data: color.data, elements: color.elements }, joints: joints?.data ?? null, weights: weights?.data ?? null }
|
|
622
|
+
let vertices: ArrayBufferView
|
|
623
|
+
let writer: VertexWriter
|
|
428
624
|
let packedIndices: number[]
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
625
|
+
// Output slot -> source vertex, for the channels written after the
|
|
626
|
+
// interleave (the morph targets): null while the slots are the
|
|
627
|
+
// source order, the un-index's corner table otherwise.
|
|
628
|
+
let remap: number[] | null = null
|
|
629
|
+
if (nrm !== null || !triangles) {
|
|
630
|
+
// Indexed as authored. Lines and points without normals get zero
|
|
631
|
+
// ones (there is no face to take one from; nothing lights them).
|
|
632
|
+
vertices = vertexView(layout, new ArrayBuffer(count * layoutStride(layout)))
|
|
633
|
+
writer = vertexWriter(vertices, layout)
|
|
634
|
+
for (let i = 0; i < count; i++) writeVertex(writer, i, source, i)
|
|
635
|
+
packedIndices = Array.from(indices)
|
|
636
|
+
if (flip) {
|
|
637
|
+
for (let i = 0; i < packedIndices.length; i += 3) {
|
|
638
|
+
let b = packedIndices[i + 1]!
|
|
639
|
+
packedIndices[i + 1] = packedIndices[i + 2]!
|
|
640
|
+
packedIndices[i + 2] = b
|
|
641
|
+
}
|
|
437
642
|
}
|
|
438
643
|
} else {
|
|
439
644
|
// No normals: the spec asks for flat shading, which needs one vertex
|
|
@@ -442,59 +647,93 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
442
647
|
// after a flip), the direction authored normals would have, so the
|
|
443
648
|
// runtime maps both the same way.
|
|
444
649
|
let triangles = indices.length / 3
|
|
445
|
-
vertices = new
|
|
650
|
+
vertices = vertexView(layout, new ArrayBuffer(triangles * 3 * layoutStride(layout)))
|
|
651
|
+
writer = vertexWriter(vertices, layout)
|
|
446
652
|
packedIndices = new Array(triangles * 3)
|
|
653
|
+
remap = new Array(triangles * 3)
|
|
447
654
|
let face: Vec3 = [0, 0, 0]
|
|
448
655
|
for (let t = 0; t < triangles; t++) {
|
|
449
656
|
let a = indices[t * 3]!, b = indices[t * 3 + 1]!, c = indices[t * 3 + 2]!
|
|
450
657
|
if (flip) [b, c] = [c, b]
|
|
451
658
|
let out = t * 3
|
|
452
|
-
writeVertex(
|
|
453
|
-
writeVertex(
|
|
454
|
-
writeVertex(
|
|
455
|
-
|
|
659
|
+
writeVertex(writer, out, source, a)
|
|
660
|
+
writeVertex(writer, out + 1, source, b)
|
|
661
|
+
writeVertex(writer, out + 2, source, c)
|
|
662
|
+
remap[out] = a
|
|
663
|
+
remap[out + 1] = b
|
|
664
|
+
remap[out + 2] = c
|
|
665
|
+
faceNormal(face, writer.pos, out)
|
|
456
666
|
if (flip) {
|
|
457
667
|
face[0] = -face[0]
|
|
458
668
|
face[1] = -face[1]
|
|
459
669
|
face[2] = -face[2]
|
|
460
670
|
}
|
|
461
671
|
for (let k = 0; k < 3; k++) {
|
|
462
|
-
let
|
|
463
|
-
vertices[at] = face[0]
|
|
464
|
-
vertices[at + 1] = face[1]
|
|
465
|
-
vertices[at + 2] = face[2]
|
|
672
|
+
for (let c = 0; c < 3; c++) writer.nrm.set(out + k, c, face[c]!)
|
|
466
673
|
packedIndices[out + k] = out + k
|
|
467
674
|
}
|
|
468
675
|
}
|
|
469
676
|
count = triangles * 3
|
|
470
677
|
}
|
|
471
678
|
|
|
679
|
+
// Morph targets: each target's POSITION and NORMAL deltas (a target
|
|
680
|
+
// may carry either; TANGENT is dropped with the base tangents), read
|
|
681
|
+
// dense through the accessors (sparse ones expand there), remapped
|
|
682
|
+
// through the un-index when the primitive was flattened - normal
|
|
683
|
+
// deltas are dropped then, as flat normals are generated - and packed
|
|
684
|
+
// sparse by vertex onto the geometry. Names come from the mesh's
|
|
685
|
+
// extras.targetNames (what Blender writes), else "target<t>".
|
|
686
|
+
let targets: any[] = prim.targets ?? []
|
|
687
|
+
let morphs = targets.length === 0 ? undefined : packMorphTargets(count, targets.map((target: any, t: number): MorphTarget => {
|
|
688
|
+
let what = name + " target " + t
|
|
689
|
+
let deltas = (attribute: string): Float32Array | null => {
|
|
690
|
+
if (target?.[attribute] === undefined) return null
|
|
691
|
+
let acc = accessorFloats(target[attribute], what + " " + attribute)
|
|
692
|
+
if (acc.elements !== 3 || acc.count !== pos.count) throw new Error("parseGltf: " + what + " " + attribute + " is not VEC3 over the primitive's vertices")
|
|
693
|
+
if (remap === null) return acc.data
|
|
694
|
+
let out = new Float32Array(count * 3)
|
|
695
|
+
for (let slot = 0; slot < count; slot++) {
|
|
696
|
+
let from = remap[slot]! * 3
|
|
697
|
+
out[slot * 3] = acc.data[from]!
|
|
698
|
+
out[slot * 3 + 1] = acc.data[from + 1]!
|
|
699
|
+
out[slot * 3 + 2] = acc.data[from + 2]!
|
|
700
|
+
}
|
|
701
|
+
return out
|
|
702
|
+
}
|
|
703
|
+
return { name: targetNames?.[t] ?? "target" + t, position: deltas("POSITION") ?? new Float32Array(count * 3), normal: remap === null ? deltas("NORMAL") : null }
|
|
704
|
+
}))
|
|
705
|
+
|
|
472
706
|
// Model bounds: the part's local box through the node's rest-pose
|
|
473
707
|
// world transform (8 corners - conservative under rotation, exact
|
|
474
|
-
// under translation and axis-aligned scale)
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
let
|
|
481
|
-
for (let
|
|
482
|
-
let
|
|
483
|
-
|
|
484
|
-
|
|
708
|
+
// under translation and axis-aligned scale), grown by the morph
|
|
709
|
+
// extent first so a morphed shape stays inside. A skinned part's
|
|
710
|
+
// vertices are placed by its joints, not its node, so its box is
|
|
711
|
+
// folded in after the skins exist (growBounds over the joint boxes).
|
|
712
|
+
if (!skinned) {
|
|
713
|
+
let lo: Vec3 = [Infinity, Infinity, Infinity]
|
|
714
|
+
let hi: Vec3 = [-Infinity, -Infinity, -Infinity]
|
|
715
|
+
for (let i = 0; i < count; i++) {
|
|
716
|
+
for (let k = 0; k < 3; k++) {
|
|
717
|
+
let v = writer.pos.get(i, k)
|
|
718
|
+
if (v < lo[k]!) lo[k] = v
|
|
719
|
+
if (v > hi[k]!) hi[k] = v
|
|
720
|
+
}
|
|
485
721
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
722
|
+
if (morphs !== undefined) {
|
|
723
|
+
for (let k = 0; k < 3; k++) {
|
|
724
|
+
lo[k] = lo[k]! + morphs.extent[k]!
|
|
725
|
+
hi[k] = hi[k]! + morphs.extent[3 + k]!
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
growBounds(bounds, [lo[0], lo[1], lo[2], hi[0], hi[1], hi[2]], world)
|
|
729
|
+
} else {
|
|
730
|
+
// Joint boxes need the skin's inverse binds, and skins are built
|
|
731
|
+
// after the walk (their joints are ordinary nodes the walk
|
|
732
|
+
// registers), so the arrays are parked under the FILE's skin index
|
|
733
|
+
// and grown then - each box padded by the largest morph delta, as
|
|
734
|
+
// a morph moves a vertex in whichever joint's space.
|
|
735
|
+
let slack = morphs === undefined ? 0 : Math.max(...Array.from(morphs.extent, Math.abs))
|
|
736
|
+
pendingJointBounds.push({ skin: skin!, positions: pos.data, count: pos.count, joints: joints!.data, weights: weights!.data, slack })
|
|
498
737
|
}
|
|
499
738
|
|
|
500
739
|
let material = prim.material
|
|
@@ -505,9 +744,10 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
505
744
|
}
|
|
506
745
|
material = defaultMaterial
|
|
507
746
|
}
|
|
508
|
-
let geometry: Geometry =
|
|
509
|
-
|
|
510
|
-
|
|
747
|
+
let geometry: Geometry = { vertices, indices: packIndices(packedIndices, count), label: name }
|
|
748
|
+
if (layout !== undefined) geometry.layout = layout
|
|
749
|
+
if (morphs !== undefined) geometry.morphs = morphs
|
|
750
|
+
if (!triangles) geometry.topology = topology
|
|
511
751
|
parts.push({ name, node, skin, geometry, material })
|
|
512
752
|
}
|
|
513
753
|
|
|
@@ -531,7 +771,7 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
531
771
|
// winding and bounds match what the runtime will render.
|
|
532
772
|
let world = mat4()
|
|
533
773
|
multiply(world, parentWorld, compose(local, position, rotation, scale))
|
|
534
|
-
let pending: PendingNode = { name: node.name ?? "node" + index, parent, position, rotation, scale, index: null }
|
|
774
|
+
let pending: PendingNode = { name: node.name ?? "node" + index, parent, position, rotation, scale, world, index: null }
|
|
535
775
|
pendingByIndex.set(index, pending)
|
|
536
776
|
if (node.mesh !== undefined) {
|
|
537
777
|
let mesh = gltf.meshes[node.mesh]
|
|
@@ -540,7 +780,20 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
540
780
|
let slot = materialize(pending)
|
|
541
781
|
let skin = typeof node.skin === "number" ? node.skin : null
|
|
542
782
|
let prims: any[] = mesh.primitives ?? []
|
|
543
|
-
|
|
783
|
+
// Morph target names and the initial weights are the MESH's (a
|
|
784
|
+
// node may override the weights); the weights land on the node,
|
|
785
|
+
// which is what a weights channel and setMorphWeights address.
|
|
786
|
+
let targetCount = Math.max(0, ...prims.map((p: any): number => p?.targets?.length ?? 0))
|
|
787
|
+
let targetNames: string[] | null = Array.isArray(mesh.extras?.targetNames) ? mesh.extras.targetNames.map(String) : null
|
|
788
|
+
if (targetCount > 0) {
|
|
789
|
+
let weights: number[] = Array.isArray(node.weights) ? node.weights : Array.isArray(mesh.weights) ? mesh.weights : []
|
|
790
|
+
if (weights.length !== targetCount) {
|
|
791
|
+
if (weights.length !== 0) throw new Error("parseGltf: node " + pending.name + " has " + weights.length + " weights for " + targetCount + " morph targets")
|
|
792
|
+
weights = new Array(targetCount).fill(0)
|
|
793
|
+
}
|
|
794
|
+
nodes[slot]!.weights = weights.map(Number)
|
|
795
|
+
}
|
|
796
|
+
for (let k = 0; k < prims.length; k++) emit(prims[k], prims.length > 1 ? pending.name + "#" + k : pending.name, slot, world, skin, targetNames)
|
|
544
797
|
}
|
|
545
798
|
for (let child of node.children ?? []) walk(child, pending, world)
|
|
546
799
|
}
|
|
@@ -558,7 +811,7 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
558
811
|
// compact skins exist: each influenced vertex (weight above zero) goes
|
|
559
812
|
// through the joint's inverse bind into joint space and grows that
|
|
560
813
|
// joint's box.
|
|
561
|
-
let growJointBounds = (skin: ModelSkin, positions: Float32Array, count: number, joints: Float32Array, weights: Float32Array): void => {
|
|
814
|
+
let growJointBounds = (skin: ModelSkin, positions: Float32Array, count: number, joints: Float32Array, weights: Float32Array, slack: number): void => {
|
|
562
815
|
let jb = skin.jointBounds
|
|
563
816
|
let ib = skin.inverseBind
|
|
564
817
|
for (let i = 0; i < count; i++) {
|
|
@@ -572,25 +825,29 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
572
825
|
let py = ib[m + 1]! * x + ib[m + 5]! * y + ib[m + 9]! * z + ib[m + 13]!
|
|
573
826
|
let pz = ib[m + 2]! * x + ib[m + 6]! * y + ib[m + 10]! * z + ib[m + 14]!
|
|
574
827
|
let b = j * 6
|
|
575
|
-
if (px < jb[b]!) jb[b] = px
|
|
576
|
-
if (py < jb[b + 1]!) jb[b + 1] = py
|
|
577
|
-
if (pz < jb[b + 2]!) jb[b + 2] = pz
|
|
578
|
-
if (px > jb[b + 3]!) jb[b + 3] = px
|
|
579
|
-
if (py > jb[b + 4]!) jb[b + 4] = py
|
|
580
|
-
if (pz > jb[b + 5]!) jb[b + 5] = pz
|
|
828
|
+
if (px - slack < jb[b]!) jb[b] = px - slack
|
|
829
|
+
if (py - slack < jb[b + 1]!) jb[b + 1] = py - slack
|
|
830
|
+
if (pz - slack < jb[b + 2]!) jb[b + 2] = pz - slack
|
|
831
|
+
if (px + slack > jb[b + 3]!) jb[b + 3] = px + slack
|
|
832
|
+
if (py + slack > jb[b + 4]!) jb[b + 4] = py + slack
|
|
833
|
+
if (pz + slack > jb[b + 5]!) jb[b + 5] = pz + slack
|
|
581
834
|
}
|
|
582
835
|
}
|
|
583
836
|
}
|
|
584
837
|
let skinSlots = new Map<number, number>()
|
|
838
|
+
// Each compact skin's joints' rest-pose world transforms, for the bounds.
|
|
839
|
+
let skinJointWorlds: Mat4[][] = []
|
|
585
840
|
for (let part of parts) {
|
|
586
841
|
if (part.skin === null) continue
|
|
587
842
|
let slot = skinSlots.get(part.skin)
|
|
588
843
|
if (slot === undefined) {
|
|
589
844
|
let sk = gltf.skins?.[part.skin]
|
|
590
845
|
if (sk === undefined) throw new Error("parseGltf: part '" + part.name + "' names a missing skin " + part.skin)
|
|
846
|
+
let jointWorlds: Mat4[] = []
|
|
591
847
|
let jointIndices: number[] = (sk.joints ?? []).map((j: number): number => {
|
|
592
848
|
let pending = pendingByIndex.get(j)
|
|
593
849
|
if (pending === undefined) throw new Error("parseGltf: skin joint node " + j + " is not in the scene")
|
|
850
|
+
jointWorlds.push(pending.world)
|
|
594
851
|
return materialize(pending)
|
|
595
852
|
})
|
|
596
853
|
if (jointIndices.length === 0) throw new Error("parseGltf: skin " + part.skin + " has no joints")
|
|
@@ -615,29 +872,43 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
615
872
|
let jointBounds = new Float32Array(jointIndices.length * 6)
|
|
616
873
|
for (let j = 0; j < jointIndices.length; j++) jointBounds.set([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], j * 6)
|
|
617
874
|
skins.push({ joints: jointIndices, inverseBind, jointBounds })
|
|
875
|
+
skinJointWorlds.push(jointWorlds)
|
|
618
876
|
skinSlots.set(part.skin, slot)
|
|
619
877
|
}
|
|
620
878
|
part.skin = slot
|
|
621
879
|
}
|
|
622
880
|
for (let parked of pendingJointBounds) {
|
|
623
881
|
let slot = skinSlots.get(parked.skin)
|
|
624
|
-
if (slot !== undefined) growJointBounds(skins[slot]!, parked.positions, parked.count, parked.joints, parked.weights)
|
|
882
|
+
if (slot !== undefined) growJointBounds(skins[slot]!, parked.positions, parked.count, parked.joints, parked.weights, parked.slack)
|
|
625
883
|
}
|
|
884
|
+
// Skinned parts' share of the model bounds: each joint's box (joint
|
|
885
|
+
// space) through that joint's rest-pose world transform, which is where
|
|
886
|
+
// the skin places the vertices it influences at rest (a vertex blended
|
|
887
|
+
// between joints lands inside the union's box). The bind-pose vertex
|
|
888
|
+
// box itself is not where the part renders once an armature carries a
|
|
889
|
+
// rotation or a scale.
|
|
890
|
+
skins.forEach((skin, s) => {
|
|
891
|
+
let worlds = skinJointWorlds[s]!
|
|
892
|
+
for (let j = 0; j < skin.joints.length; j++) growBounds(bounds, skin.jointBounds.subarray(j * 6, j * 6 + 6), worlds[j]!)
|
|
893
|
+
})
|
|
626
894
|
|
|
627
895
|
// Animations, after the walk so channels can materialize their target
|
|
628
896
|
// nodes (a channel may target a meshless node - a joint, a rig pivot).
|
|
629
|
-
const CHANNEL_PATHS: Record<string, ModelChannel["path"]> = { translation: "position", rotation: "rotation", scale: "scale" }
|
|
897
|
+
const CHANNEL_PATHS: Record<string, ModelChannel["path"]> = { translation: "position", rotation: "rotation", scale: "scale", weights: "weights" }
|
|
630
898
|
let clips: ModelClip[] = (gltf.animations ?? []).map((anim: any, ai: number): ModelClip => {
|
|
631
899
|
let clipName = anim.name ?? "clip" + ai
|
|
632
900
|
let channels: ModelChannel[] = []
|
|
633
901
|
let duration = 0
|
|
634
902
|
for (let ch of anim.channels ?? []) {
|
|
635
|
-
//
|
|
903
|
+
// Extension paths (KHR_animation_pointer) are outside the subset.
|
|
636
904
|
let path = CHANNEL_PATHS[ch.target?.path]
|
|
637
905
|
if (path === undefined) continue
|
|
638
|
-
// A target outside the walked scene has nothing to move
|
|
906
|
+
// A target outside the walked scene has nothing to move, and a
|
|
907
|
+
// weights channel on a node without morph targets nothing to weigh.
|
|
639
908
|
let pending = pendingByIndex.get(ch.target.node)
|
|
640
909
|
if (pending === undefined) continue
|
|
910
|
+
let targetCount = pending.index === null ? undefined : nodes[pending.index]!.weights?.length
|
|
911
|
+
if (path === "weights" && targetCount === undefined) continue
|
|
641
912
|
let sampler = anim.samplers?.[ch.sampler]
|
|
642
913
|
if (sampler === undefined) throw new Error("parseGltf: animation " + clipName + " channel names a missing sampler " + ch.sampler)
|
|
643
914
|
let what = "animation " + clipName
|
|
@@ -645,11 +916,20 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
645
916
|
let output = accessorFloats(sampler.output, what + " output")
|
|
646
917
|
let interpolation: ModelChannel["interpolation"] =
|
|
647
918
|
sampler.interpolation === "STEP" ? "step" : sampler.interpolation === "CUBICSPLINE" ? "cubic" : "linear"
|
|
648
|
-
let elements = path === "rotation" ? 4 : 3
|
|
649
|
-
if (output.elements !== elements) throw new Error("parseGltf: " + what + " " + path + " output is not " + (elements === 4 ? "VEC4" : "VEC3"))
|
|
650
919
|
let perKey = interpolation === "cubic" ? 3 : 1
|
|
651
|
-
if (
|
|
652
|
-
|
|
920
|
+
if (path === "weights") {
|
|
921
|
+
// Scalars, one per target per key: the output is as long as the
|
|
922
|
+
// key count times the node's target count.
|
|
923
|
+
if (output.elements !== 1) throw new Error("parseGltf: " + what + " weights output is not SCALAR")
|
|
924
|
+
if (output.count !== input.count * perKey * targetCount!) {
|
|
925
|
+
throw new Error("parseGltf: " + what + " weights has " + output.count + " values for " + input.count + " keys of " + targetCount + " targets")
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
let elements = path === "rotation" ? 4 : 3
|
|
929
|
+
if (output.elements !== elements) throw new Error("parseGltf: " + what + " " + path + " output is not " + (elements === 4 ? "VEC4" : "VEC3"))
|
|
930
|
+
if (output.count !== input.count * perKey) {
|
|
931
|
+
throw new Error("parseGltf: " + what + " " + path + " has " + output.count + " values for " + input.count + " keys")
|
|
932
|
+
}
|
|
653
933
|
}
|
|
654
934
|
if (input.count > 0) duration = Math.max(duration, input.data[input.count - 1]!)
|
|
655
935
|
channels.push({ node: materialize(pending), path, interpolation, times: input.data, values: output.data })
|
|
@@ -657,7 +937,9 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
657
937
|
return { name: clipName, duration, channels }
|
|
658
938
|
})
|
|
659
939
|
|
|
660
|
-
|
|
940
|
+
// Nothing grew the box (no parts, or skinned parts whose weights reach
|
|
941
|
+
// no joint): an empty model sits at the origin.
|
|
942
|
+
if (!(bounds[0]! <= bounds[3]!)) bounds.fill(0)
|
|
661
943
|
return { nodes, parts, skins, clips, materials, images, bounds }
|
|
662
944
|
}
|
|
663
945
|
|
|
@@ -710,55 +992,76 @@ function readerFor(componentType: number, normalized: boolean): (dv: DataView, a
|
|
|
710
992
|
}
|
|
711
993
|
}
|
|
712
994
|
|
|
995
|
+
// The source channels of a primitive as the accessors decoded them
|
|
996
|
+
// (floats, whatever the file's component type) and the writers over the
|
|
997
|
+
// part's buffer, one per channel the layout carries.
|
|
998
|
+
type VertexSource = {
|
|
999
|
+
pos: Float32Array
|
|
1000
|
+
nrm: Float32Array | null
|
|
1001
|
+
uv: Float32Array | null
|
|
1002
|
+
color: { data: Float32Array; elements: number } | null
|
|
1003
|
+
joints: Float32Array | null
|
|
1004
|
+
weights: Float32Array | null
|
|
1005
|
+
}
|
|
1006
|
+
type VertexWriter = {
|
|
1007
|
+
pos: AttributeAccess
|
|
1008
|
+
nrm: AttributeAccess
|
|
1009
|
+
uv: AttributeAccess
|
|
1010
|
+
color: AttributeAccess | null
|
|
1011
|
+
joints: AttributeAccess | null
|
|
1012
|
+
weights: AttributeAccess | null
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function vertexWriter(vertices: ArrayBufferView, layout: VertexLayout | undefined): VertexWriter {
|
|
1016
|
+
return {
|
|
1017
|
+
pos: attributeAccess(vertices, layout, "aPos")!,
|
|
1018
|
+
nrm: attributeAccess(vertices, layout, "aNormal")!,
|
|
1019
|
+
uv: attributeAccess(vertices, layout, "aUV")!,
|
|
1020
|
+
color: attributeAccess(vertices, layout, "aColor"),
|
|
1021
|
+
joints: attributeAccess(vertices, layout, "aJoints"),
|
|
1022
|
+
weights: attributeAccess(vertices, layout, "aWeights"),
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
713
1026
|
// One interleaved vertex, node-local: position and normal copied as
|
|
714
1027
|
// authored (the runtime's uModel/uNormal do the placing), uv copied or
|
|
715
|
-
// zero, and for
|
|
1028
|
+
// zero, and for a skinned layout the joint indices plus their weights
|
|
716
1029
|
// RENORMALIZED to sum 1 (quantized exports drift a little; the spec asks
|
|
717
|
-
// for normalized weights).
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
let at = slot * stride
|
|
730
|
-
out[at] = pos[src * 3]!
|
|
731
|
-
out[at + 1] = pos[src * 3 + 1]!
|
|
732
|
-
out[at + 2] = pos[src * 3 + 2]!
|
|
733
|
-
if (nrm !== null) {
|
|
734
|
-
out[at + 3] = nrm[src * 3]!
|
|
735
|
-
out[at + 4] = nrm[src * 3 + 1]!
|
|
736
|
-
out[at + 5] = nrm[src * 3 + 2]!
|
|
1030
|
+
// for normalized weights). A COLOR_0 (VEC3 or VEC4, already 0..1 - the
|
|
1031
|
+
// normalized-integer path unpacks quantized exports) lands at the
|
|
1032
|
+
// layout's aColor slot premultiplied, alpha 1 for VEC3. Every write goes
|
|
1033
|
+
// through the channel's accessor, which re-encodes a packed format.
|
|
1034
|
+
function writeVertex(w: VertexWriter, slot: number, src: VertexSource, i: number): void {
|
|
1035
|
+
for (let k = 0; k < 3; k++) w.pos.set(slot, k, src.pos[i * 3 + k]!)
|
|
1036
|
+
if (src.nrm !== null) {
|
|
1037
|
+
for (let k = 0; k < 3; k++) w.nrm.set(slot, k, src.nrm[i * 3 + k]!)
|
|
1038
|
+
}
|
|
1039
|
+
if (src.uv !== null) {
|
|
1040
|
+
w.uv.set(slot, 0, src.uv[i * 2]!)
|
|
1041
|
+
w.uv.set(slot, 1, src.uv[i * 2 + 1]!)
|
|
737
1042
|
}
|
|
738
|
-
if (
|
|
739
|
-
|
|
740
|
-
|
|
1043
|
+
if (src.color !== null && w.color !== null) {
|
|
1044
|
+
let c = i * src.color.elements
|
|
1045
|
+
let a = src.color.elements === 4 ? src.color.data[c + 3]! : 1
|
|
1046
|
+
for (let k = 0; k < 3; k++) w.color.set(slot, k, src.color.data[c + k]! * a)
|
|
1047
|
+
w.color.set(slot, 3, a)
|
|
741
1048
|
}
|
|
742
|
-
if (joints !== null && weights !== null) {
|
|
743
|
-
let j =
|
|
744
|
-
|
|
745
|
-
out[at + 9] = joints[j + 1]!
|
|
746
|
-
out[at + 10] = joints[j + 2]!
|
|
747
|
-
out[at + 11] = joints[j + 3]!
|
|
748
|
-
let sum = weights[j]! + weights[j + 1]! + weights[j + 2]! + weights[j + 3]!
|
|
1049
|
+
if (src.joints !== null && src.weights !== null && w.joints !== null && w.weights !== null) {
|
|
1050
|
+
let j = i * 4
|
|
1051
|
+
let sum = src.weights[j]! + src.weights[j + 1]! + src.weights[j + 2]! + src.weights[j + 3]!
|
|
749
1052
|
let inv = sum > 1e-8 ? 1 / sum : 0
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1053
|
+
for (let k = 0; k < 4; k++) {
|
|
1054
|
+
w.joints.set(slot, k, src.joints[j + k]!)
|
|
1055
|
+
w.weights.set(slot, k, src.weights[j + k]! * inv)
|
|
1056
|
+
}
|
|
754
1057
|
}
|
|
755
1058
|
}
|
|
756
1059
|
|
|
757
1060
|
// The unit normal of the triangle at three consecutive vertex slots.
|
|
758
|
-
function faceNormal(out: Vec3,
|
|
759
|
-
let a = first
|
|
760
|
-
let abx =
|
|
761
|
-
let acx =
|
|
1061
|
+
function faceNormal(out: Vec3, pos: AttributeAccess, first: number): void {
|
|
1062
|
+
let a = first, b = first + 1, c = first + 2
|
|
1063
|
+
let abx = pos.get(b, 0) - pos.get(a, 0), aby = pos.get(b, 1) - pos.get(a, 1), abz = pos.get(b, 2) - pos.get(a, 2)
|
|
1064
|
+
let acx = pos.get(c, 0) - pos.get(a, 0), acy = pos.get(c, 1) - pos.get(a, 1), acz = pos.get(c, 2) - pos.get(a, 2)
|
|
762
1065
|
let nx = aby * acz - abz * acy
|
|
763
1066
|
let ny = abz * acx - abx * acz
|
|
764
1067
|
let nz = abx * acy - aby * acx
|