@solidrt/3d 0.0.58 → 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +24 -2
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +605 -185
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/src/gltf.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// generated when absent, per the spec, for triangles), one UV set and
|
|
6
6
|
// indices in the primitive's topology (strips and fans unrolled to
|
|
7
7
|
// triangle lists, a line loop closed into a strip; lines and points keep
|
|
8
|
-
// theirs on the geometry), and materials reduced to what
|
|
8
|
+
// theirs on the geometry), and materials reduced to what phong()/unlit()
|
|
9
9
|
// draw - base color
|
|
10
10
|
// factor and texture, normal map (with scale), emissive factor and map
|
|
11
11
|
// (KHR_materials_emissive_strength as the intensity), double-sidedness, alpha
|
|
@@ -22,24 +22,30 @@
|
|
|
22
22
|
// bytes in `images`, and uploading is the engine side's job.
|
|
23
23
|
//
|
|
24
24
|
// Outside the subset: Draco/meshopt-compressed meshes and any other
|
|
25
|
-
// required extension throw naming it;
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
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.
|
|
30
35
|
|
|
31
36
|
import { compose, decompose, det3, mat4, multiply } from "./math.ts"
|
|
32
37
|
import { linearToSrgb } from "./color.ts"
|
|
33
38
|
import type { Mat4, Quat, Vec3 } from "./math.ts"
|
|
34
|
-
import {
|
|
39
|
+
import { attributeAccess, layoutKey, layoutStride, packIndices, packMorphTargets, vertexView } from "./geometry.ts"
|
|
40
|
+
import type { AttributeAccess, MorphTarget, VertexLayout } from "./geometry.ts"
|
|
35
41
|
import type { Geometry } from "./geometry.ts"
|
|
36
|
-
import type { Topology } from "@solidrt/core/gpu"
|
|
42
|
+
import type { Topology, VertexAttribute, VertexFormat } from "@solidrt/core/gpu"
|
|
37
43
|
|
|
38
|
-
/** What
|
|
44
|
+
/** What phong()/unlit() take from a glTF material. */
|
|
39
45
|
export type ModelMaterial = {
|
|
40
46
|
name: string
|
|
41
47
|
/** Straight [r, g, b, a] 0..1: glTF's LINEAR baseColorFactor encoded
|
|
42
|
-
* to sRGB, what
|
|
48
|
+
* to sRGB, what phong({ color }) takes. */
|
|
43
49
|
color: [number, number, number, number]
|
|
44
50
|
/** Index into ModelData.images (the base color texture), or null. */
|
|
45
51
|
map: number | null
|
|
@@ -58,9 +64,9 @@ export type ModelMaterial = {
|
|
|
58
64
|
/** glTF normalTexture.scale (default 1); meaningful with normalMap. */
|
|
59
65
|
normalScale: number
|
|
60
66
|
/** glTF's linear emissiveFactor (default [0, 0, 0] = off) encoded to
|
|
61
|
-
* sRGB, what
|
|
67
|
+
* sRGB, what phong({ emissive }) takes. */
|
|
62
68
|
emissive: [number, number, number]
|
|
63
|
-
/** KHR_materials_emissive_strength (default 1):
|
|
69
|
+
/** KHR_materials_emissive_strength (default 1): phong's emissiveIntensity. */
|
|
64
70
|
emissiveIntensity: number
|
|
65
71
|
/** Index into ModelData.images (the emissive map), or null. */
|
|
66
72
|
emissiveMap: number | null
|
|
@@ -89,6 +95,11 @@ export type ModelNode = {
|
|
|
89
95
|
/** Unit quaternion [x, y, z, w]. */
|
|
90
96
|
rotation: Quat
|
|
91
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[]
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
/** One drawable: a mesh node's primitive. Vertices are LOCAL to its
|
|
@@ -103,8 +114,17 @@ export type ModelPart = {
|
|
|
103
114
|
* this part (unused for placement when `skin` is set). */
|
|
104
115
|
node: number
|
|
105
116
|
/** Index into ModelData.skins, or null. A skinned part's geometry has
|
|
106
|
-
* the "skinned" layout (aJoints/aWeights after the
|
|
117
|
+
* the "skinned" layout (aJoints/aWeights after the base prefix). */
|
|
107
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. */
|
|
108
128
|
geometry: Geometry
|
|
109
129
|
/** Index into ModelData.materials. */
|
|
110
130
|
material: number
|
|
@@ -130,15 +150,18 @@ export type ModelSkin = {
|
|
|
130
150
|
export type ModelChannel = {
|
|
131
151
|
/** Index into ModelData.nodes. */
|
|
132
152
|
node: number
|
|
133
|
-
|
|
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"
|
|
134
156
|
/** glTF's three: "step" holds each key, "linear" lerps (a rotation
|
|
135
157
|
* slerps), "cubic" is CUBICSPLINE - a Hermite with per-key tangents. */
|
|
136
158
|
interpolation: "step" | "linear" | "cubic"
|
|
137
159
|
/** Key times in seconds, ascending. */
|
|
138
160
|
times: Float32Array
|
|
139
161
|
/** One element per key: 3 floats for position/scale, 4 (a quaternion)
|
|
140
|
-
* for rotation
|
|
141
|
-
*
|
|
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. */
|
|
142
165
|
values: Float32Array
|
|
143
166
|
}
|
|
144
167
|
|
|
@@ -174,8 +197,11 @@ export type ModelData = {
|
|
|
174
197
|
/** Encoded image files (PNG/JPEG bytes) the materials' `map` index. */
|
|
175
198
|
images: Uint8Array[]
|
|
176
199
|
/** World-space rest-pose [minX, minY, minZ, maxX, maxY, maxZ] over every
|
|
177
|
-
* part - each part's local box through its node's composed transform,
|
|
178
|
-
*
|
|
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. */
|
|
179
205
|
bounds: Float32Array
|
|
180
206
|
}
|
|
181
207
|
|
|
@@ -223,10 +249,45 @@ function closeLoop(loop: ArrayLike<number>): number[] {
|
|
|
223
249
|
// The spec's alphaCutoff when a MASK material leaves it out.
|
|
224
250
|
const GLTF_ALPHA_CUTOFF = 0.5
|
|
225
251
|
|
|
226
|
-
// The
|
|
227
|
-
|
|
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" }
|
|
228
274
|
|
|
229
|
-
|
|
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
|
+
}
|
|
280
|
+
|
|
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
|
+
}
|
|
230
291
|
|
|
231
292
|
const COMPONENT_BYTES: Record<number, number> = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 }
|
|
232
293
|
const TYPE_ELEMENTS: Record<string, number> = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 }
|
|
@@ -377,34 +438,58 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
377
438
|
// only when something uses it.
|
|
378
439
|
let defaultMaterial = -1
|
|
379
440
|
|
|
380
|
-
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 } => {
|
|
381
442
|
let acc = gltf.accessors[index]
|
|
382
443
|
if (acc === undefined) throw new Error("parseGltf: " + what + " names a missing accessor " + index)
|
|
383
|
-
if (acc.sparse !== undefined) throw new Error("parseGltf: " + what + " uses a sparse accessor, which is not supported")
|
|
384
444
|
let elements = TYPE_ELEMENTS[acc.type]
|
|
385
445
|
let compBytes = COMPONENT_BYTES[acc.componentType]
|
|
386
446
|
if (elements === undefined || compBytes === undefined) throw new Error("parseGltf: " + what + " has an unknown accessor type")
|
|
387
447
|
let out = new Float32Array(acc.count * elements)
|
|
388
|
-
if (acc.bufferView === undefined) return { data: out, elements, count: acc.count }
|
|
389
|
-
let view = gltf.bufferViews[acc.bufferView]
|
|
390
|
-
let bytes = bufferViewBytes(acc.bufferView)
|
|
391
|
-
let stride = view.byteStride ?? compBytes * elements
|
|
392
|
-
let dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
393
|
-
let base = acc.byteOffset ?? 0
|
|
394
448
|
let normalized = acc.normalized === true
|
|
449
|
+
let format = ACCESSOR_FORMATS[acc.componentType + (normalized ? "n" : "") + "x" + elements] ?? null
|
|
395
450
|
let read = readerFor(acc.componentType, normalized)
|
|
396
|
-
|
|
397
|
-
let
|
|
398
|
-
|
|
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
|
+
}
|
|
399
461
|
}
|
|
400
|
-
|
|
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 }
|
|
401
486
|
}
|
|
402
487
|
|
|
403
488
|
// The node table is built lazily: a glTF node gets a slot only when a
|
|
404
489
|
// part somewhere below it needs the chain (materialize walks ancestors
|
|
405
490
|
// first, so the table stays in pre-order), which is what prunes cameras,
|
|
406
491
|
// lights and unused empties.
|
|
407
|
-
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 }
|
|
408
493
|
let nodes: ModelNode[] = []
|
|
409
494
|
let materialize = (p: PendingNode): number => {
|
|
410
495
|
if (p.index !== null) return p.index
|
|
@@ -415,24 +500,44 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
415
500
|
}
|
|
416
501
|
|
|
417
502
|
let parts: ModelPart[] = []
|
|
418
|
-
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 }[] = []
|
|
419
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
|
+
}
|
|
420
523
|
|
|
421
|
-
let emit = (prim: any, name: string, node: number, world: Mat4, skin: number | null): void => {
|
|
524
|
+
let emit = (prim: any, name: string, node: number, world: Mat4, skin: number | null, targetNames: string[] | null): void => {
|
|
422
525
|
if (prim.attributes?.POSITION === undefined) return
|
|
423
526
|
let mode: number = prim.mode ?? MODE_TRIANGLES
|
|
424
527
|
let pos = accessorFloats(prim.attributes.POSITION, name + " POSITION")
|
|
425
528
|
if (pos.elements !== 3) throw new Error("parseGltf: " + name + " POSITION is not VEC3")
|
|
426
529
|
let count = pos.count
|
|
427
530
|
let nrm = prim.attributes.NORMAL !== undefined ? accessorFloats(prim.attributes.NORMAL, name + " NORMAL").data : null
|
|
428
|
-
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")
|
|
429
534
|
// A skinned primitive: joints and weights become the "skinned"
|
|
430
535
|
// layout's extra channels; its vertices are ALREADY model-space bind
|
|
431
536
|
// pose (the node transform is ignored, per spec), so no flip and
|
|
432
537
|
// identity bounds.
|
|
433
538
|
let skinned = skin !== null
|
|
434
|
-
let joints: Float32Array | null = null
|
|
435
|
-
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
|
|
436
541
|
if (skinned) {
|
|
437
542
|
if (prim.attributes.JOINTS_0 === undefined || prim.attributes.WEIGHTS_0 === undefined) {
|
|
438
543
|
throw new Error("parseGltf: " + name + " is skinned but lacks JOINTS_0/WEIGHTS_0")
|
|
@@ -440,10 +545,25 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
440
545
|
let j = accessorFloats(prim.attributes.JOINTS_0, name + " JOINTS_0")
|
|
441
546
|
let w = accessorFloats(prim.attributes.WEIGHTS_0, name + " WEIGHTS_0")
|
|
442
547
|
if (j.elements !== 4 || w.elements !== 4) throw new Error("parseGltf: " + name + " JOINTS_0/WEIGHTS_0 are not VEC4")
|
|
443
|
-
joints = j
|
|
444
|
-
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) })
|
|
445
564
|
}
|
|
446
|
-
|
|
565
|
+
if (color !== null) attrs.push({ name: "aColor", format: channelFormat(color.elements === 4 ? color.format : null, 4) })
|
|
566
|
+
let layout = presetOrList(attrs)
|
|
447
567
|
let raw: ArrayLike<number> =
|
|
448
568
|
prim.indices !== undefined ? accessorFloats(prim.indices, name + " indices").data : Array.from({ length: count }, (_, i) => i)
|
|
449
569
|
// The primitive's topology, carried on the geometry. Triangle strips
|
|
@@ -495,18 +615,23 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
495
615
|
// across zero would unbake it, which is pathological. Lines and
|
|
496
616
|
// points have no winding.
|
|
497
617
|
let flip = triangles && !skinned && det3(world) < 0
|
|
498
|
-
// Joint boxes need the skin's inverse binds, and skins are built after
|
|
499
|
-
// the walk (their joints are ordinary nodes the walk registers), so
|
|
500
|
-
// the arrays are parked under the FILE's skin index and grown then.
|
|
501
|
-
if (skinned) pendingJointBounds.push({ skin: skin!, positions: pos.data, count: pos.count, joints: joints!, weights: weights! })
|
|
502
618
|
|
|
503
|
-
|
|
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
|
|
504
624
|
let packedIndices: number[]
|
|
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
|
|
505
629
|
if (nrm !== null || !triangles) {
|
|
506
630
|
// Indexed as authored. Lines and points without normals get zero
|
|
507
631
|
// ones (there is no face to take one from; nothing lights them).
|
|
508
|
-
vertices = new
|
|
509
|
-
|
|
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)
|
|
510
635
|
packedIndices = Array.from(indices)
|
|
511
636
|
if (flip) {
|
|
512
637
|
for (let i = 0; i < packedIndices.length; i += 3) {
|
|
@@ -522,59 +647,93 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
522
647
|
// after a flip), the direction authored normals would have, so the
|
|
523
648
|
// runtime maps both the same way.
|
|
524
649
|
let triangles = indices.length / 3
|
|
525
|
-
vertices = new
|
|
650
|
+
vertices = vertexView(layout, new ArrayBuffer(triangles * 3 * layoutStride(layout)))
|
|
651
|
+
writer = vertexWriter(vertices, layout)
|
|
526
652
|
packedIndices = new Array(triangles * 3)
|
|
653
|
+
remap = new Array(triangles * 3)
|
|
527
654
|
let face: Vec3 = [0, 0, 0]
|
|
528
655
|
for (let t = 0; t < triangles; t++) {
|
|
529
656
|
let a = indices[t * 3]!, b = indices[t * 3 + 1]!, c = indices[t * 3 + 2]!
|
|
530
657
|
if (flip) [b, c] = [c, b]
|
|
531
658
|
let out = t * 3
|
|
532
|
-
writeVertex(
|
|
533
|
-
writeVertex(
|
|
534
|
-
writeVertex(
|
|
535
|
-
|
|
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)
|
|
536
666
|
if (flip) {
|
|
537
667
|
face[0] = -face[0]
|
|
538
668
|
face[1] = -face[1]
|
|
539
669
|
face[2] = -face[2]
|
|
540
670
|
}
|
|
541
671
|
for (let k = 0; k < 3; k++) {
|
|
542
|
-
let
|
|
543
|
-
vertices[at] = face[0]
|
|
544
|
-
vertices[at + 1] = face[1]
|
|
545
|
-
vertices[at + 2] = face[2]
|
|
672
|
+
for (let c = 0; c < 3; c++) writer.nrm.set(out + k, c, face[c]!)
|
|
546
673
|
packedIndices[out + k] = out + k
|
|
547
674
|
}
|
|
548
675
|
}
|
|
549
676
|
count = triangles * 3
|
|
550
677
|
}
|
|
551
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
|
+
|
|
552
706
|
// Model bounds: the part's local box through the node's rest-pose
|
|
553
707
|
// world transform (8 corners - conservative under rotation, exact
|
|
554
|
-
// under translation and axis-aligned scale)
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
let
|
|
561
|
-
for (let
|
|
562
|
-
let
|
|
563
|
-
|
|
564
|
-
|
|
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
|
+
}
|
|
565
721
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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 })
|
|
578
737
|
}
|
|
579
738
|
|
|
580
739
|
let material = prim.material
|
|
@@ -585,9 +744,9 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
585
744
|
}
|
|
586
745
|
material = defaultMaterial
|
|
587
746
|
}
|
|
588
|
-
let geometry: Geometry =
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
591
750
|
if (!triangles) geometry.topology = topology
|
|
592
751
|
parts.push({ name, node, skin, geometry, material })
|
|
593
752
|
}
|
|
@@ -612,7 +771,7 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
612
771
|
// winding and bounds match what the runtime will render.
|
|
613
772
|
let world = mat4()
|
|
614
773
|
multiply(world, parentWorld, compose(local, position, rotation, scale))
|
|
615
|
-
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 }
|
|
616
775
|
pendingByIndex.set(index, pending)
|
|
617
776
|
if (node.mesh !== undefined) {
|
|
618
777
|
let mesh = gltf.meshes[node.mesh]
|
|
@@ -621,7 +780,20 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
621
780
|
let slot = materialize(pending)
|
|
622
781
|
let skin = typeof node.skin === "number" ? node.skin : null
|
|
623
782
|
let prims: any[] = mesh.primitives ?? []
|
|
624
|
-
|
|
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)
|
|
625
797
|
}
|
|
626
798
|
for (let child of node.children ?? []) walk(child, pending, world)
|
|
627
799
|
}
|
|
@@ -639,7 +811,7 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
639
811
|
// compact skins exist: each influenced vertex (weight above zero) goes
|
|
640
812
|
// through the joint's inverse bind into joint space and grows that
|
|
641
813
|
// joint's box.
|
|
642
|
-
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 => {
|
|
643
815
|
let jb = skin.jointBounds
|
|
644
816
|
let ib = skin.inverseBind
|
|
645
817
|
for (let i = 0; i < count; i++) {
|
|
@@ -653,25 +825,29 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
653
825
|
let py = ib[m + 1]! * x + ib[m + 5]! * y + ib[m + 9]! * z + ib[m + 13]!
|
|
654
826
|
let pz = ib[m + 2]! * x + ib[m + 6]! * y + ib[m + 10]! * z + ib[m + 14]!
|
|
655
827
|
let b = j * 6
|
|
656
|
-
if (px < jb[b]!) jb[b] = px
|
|
657
|
-
if (py < jb[b + 1]!) jb[b + 1] = py
|
|
658
|
-
if (pz < jb[b + 2]!) jb[b + 2] = pz
|
|
659
|
-
if (px > jb[b + 3]!) jb[b + 3] = px
|
|
660
|
-
if (py > jb[b + 4]!) jb[b + 4] = py
|
|
661
|
-
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
|
|
662
834
|
}
|
|
663
835
|
}
|
|
664
836
|
}
|
|
665
837
|
let skinSlots = new Map<number, number>()
|
|
838
|
+
// Each compact skin's joints' rest-pose world transforms, for the bounds.
|
|
839
|
+
let skinJointWorlds: Mat4[][] = []
|
|
666
840
|
for (let part of parts) {
|
|
667
841
|
if (part.skin === null) continue
|
|
668
842
|
let slot = skinSlots.get(part.skin)
|
|
669
843
|
if (slot === undefined) {
|
|
670
844
|
let sk = gltf.skins?.[part.skin]
|
|
671
845
|
if (sk === undefined) throw new Error("parseGltf: part '" + part.name + "' names a missing skin " + part.skin)
|
|
846
|
+
let jointWorlds: Mat4[] = []
|
|
672
847
|
let jointIndices: number[] = (sk.joints ?? []).map((j: number): number => {
|
|
673
848
|
let pending = pendingByIndex.get(j)
|
|
674
849
|
if (pending === undefined) throw new Error("parseGltf: skin joint node " + j + " is not in the scene")
|
|
850
|
+
jointWorlds.push(pending.world)
|
|
675
851
|
return materialize(pending)
|
|
676
852
|
})
|
|
677
853
|
if (jointIndices.length === 0) throw new Error("parseGltf: skin " + part.skin + " has no joints")
|
|
@@ -696,29 +872,43 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
696
872
|
let jointBounds = new Float32Array(jointIndices.length * 6)
|
|
697
873
|
for (let j = 0; j < jointIndices.length; j++) jointBounds.set([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], j * 6)
|
|
698
874
|
skins.push({ joints: jointIndices, inverseBind, jointBounds })
|
|
875
|
+
skinJointWorlds.push(jointWorlds)
|
|
699
876
|
skinSlots.set(part.skin, slot)
|
|
700
877
|
}
|
|
701
878
|
part.skin = slot
|
|
702
879
|
}
|
|
703
880
|
for (let parked of pendingJointBounds) {
|
|
704
881
|
let slot = skinSlots.get(parked.skin)
|
|
705
|
-
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)
|
|
706
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
|
+
})
|
|
707
894
|
|
|
708
895
|
// Animations, after the walk so channels can materialize their target
|
|
709
896
|
// nodes (a channel may target a meshless node - a joint, a rig pivot).
|
|
710
|
-
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" }
|
|
711
898
|
let clips: ModelClip[] = (gltf.animations ?? []).map((anim: any, ai: number): ModelClip => {
|
|
712
899
|
let clipName = anim.name ?? "clip" + ai
|
|
713
900
|
let channels: ModelChannel[] = []
|
|
714
901
|
let duration = 0
|
|
715
902
|
for (let ch of anim.channels ?? []) {
|
|
716
|
-
//
|
|
903
|
+
// Extension paths (KHR_animation_pointer) are outside the subset.
|
|
717
904
|
let path = CHANNEL_PATHS[ch.target?.path]
|
|
718
905
|
if (path === undefined) continue
|
|
719
|
-
// 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.
|
|
720
908
|
let pending = pendingByIndex.get(ch.target.node)
|
|
721
909
|
if (pending === undefined) continue
|
|
910
|
+
let targetCount = pending.index === null ? undefined : nodes[pending.index]!.weights?.length
|
|
911
|
+
if (path === "weights" && targetCount === undefined) continue
|
|
722
912
|
let sampler = anim.samplers?.[ch.sampler]
|
|
723
913
|
if (sampler === undefined) throw new Error("parseGltf: animation " + clipName + " channel names a missing sampler " + ch.sampler)
|
|
724
914
|
let what = "animation " + clipName
|
|
@@ -726,11 +916,20 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
726
916
|
let output = accessorFloats(sampler.output, what + " output")
|
|
727
917
|
let interpolation: ModelChannel["interpolation"] =
|
|
728
918
|
sampler.interpolation === "STEP" ? "step" : sampler.interpolation === "CUBICSPLINE" ? "cubic" : "linear"
|
|
729
|
-
let elements = path === "rotation" ? 4 : 3
|
|
730
|
-
if (output.elements !== elements) throw new Error("parseGltf: " + what + " " + path + " output is not " + (elements === 4 ? "VEC4" : "VEC3"))
|
|
731
919
|
let perKey = interpolation === "cubic" ? 3 : 1
|
|
732
|
-
if (
|
|
733
|
-
|
|
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
|
+
}
|
|
734
933
|
}
|
|
735
934
|
if (input.count > 0) duration = Math.max(duration, input.data[input.count - 1]!)
|
|
736
935
|
channels.push({ node: materialize(pending), path, interpolation, times: input.data, values: output.data })
|
|
@@ -738,7 +937,9 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
738
937
|
return { name: clipName, duration, channels }
|
|
739
938
|
})
|
|
740
939
|
|
|
741
|
-
|
|
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)
|
|
742
943
|
return { nodes, parts, skins, clips, materials, images, bounds }
|
|
743
944
|
}
|
|
744
945
|
|
|
@@ -791,55 +992,76 @@ function readerFor(componentType: number, normalized: boolean): (dv: DataView, a
|
|
|
791
992
|
}
|
|
792
993
|
}
|
|
793
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
|
+
|
|
794
1026
|
// One interleaved vertex, node-local: position and normal copied as
|
|
795
1027
|
// authored (the runtime's uModel/uNormal do the placing), uv copied or
|
|
796
|
-
// zero, and for
|
|
1028
|
+
// zero, and for a skinned layout the joint indices plus their weights
|
|
797
1029
|
// RENORMALIZED to sum 1 (quantized exports drift a little; the spec asks
|
|
798
|
-
// for normalized weights).
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
stride: number,
|
|
807
|
-
joints: Float32Array | null = null,
|
|
808
|
-
weights: Float32Array | null = null,
|
|
809
|
-
): void {
|
|
810
|
-
let at = slot * stride
|
|
811
|
-
out[at] = pos[src * 3]!
|
|
812
|
-
out[at + 1] = pos[src * 3 + 1]!
|
|
813
|
-
out[at + 2] = pos[src * 3 + 2]!
|
|
814
|
-
if (nrm !== null) {
|
|
815
|
-
out[at + 3] = nrm[src * 3]!
|
|
816
|
-
out[at + 4] = nrm[src * 3 + 1]!
|
|
817
|
-
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]!)
|
|
818
1038
|
}
|
|
819
|
-
if (uv !== null) {
|
|
820
|
-
|
|
821
|
-
|
|
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]!)
|
|
822
1042
|
}
|
|
823
|
-
if (
|
|
824
|
-
let
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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)
|
|
1048
|
+
}
|
|
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]!
|
|
830
1052
|
let inv = sum > 1e-8 ? 1 / sum : 0
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
+
}
|
|
835
1057
|
}
|
|
836
1058
|
}
|
|
837
1059
|
|
|
838
1060
|
// The unit normal of the triangle at three consecutive vertex slots.
|
|
839
|
-
function faceNormal(out: Vec3,
|
|
840
|
-
let a = first
|
|
841
|
-
let abx =
|
|
842
|
-
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)
|
|
843
1065
|
let nx = aby * acz - abz * acy
|
|
844
1066
|
let ny = abz * acx - abx * acz
|
|
845
1067
|
let nz = abx * acy - aby * acx
|