@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.
Files changed (70) hide show
  1. package/AGENTS.md +734 -264
  2. package/README.md +4 -2
  3. package/examples/README.md +50 -4
  4. package/examples/bloom.tsx +113 -0
  5. package/examples/cascades.tsx +4 -4
  6. package/examples/collision.tsx +6 -6
  7. package/examples/exits.tsx +96 -0
  8. package/examples/first-person.tsx +5 -5
  9. package/examples/fleet.tsx +4 -4
  10. package/examples/fog.tsx +5 -5
  11. package/examples/instanced.tsx +70 -50
  12. package/examples/lamps.tsx +4 -4
  13. package/examples/lod.tsx +125 -0
  14. package/examples/materials.tsx +9 -9
  15. package/examples/morph.tsx +174 -0
  16. package/examples/normals.tsx +118 -0
  17. package/examples/{lit.tsx → phong.tsx} +9 -9
  18. package/examples/polyhedra.tsx +61 -0
  19. package/examples/scene-atlas.tsx +70 -0
  20. package/examples/scene-views.tsx +5 -5
  21. package/examples/shadows.tsx +9 -9
  22. package/examples/sky-lit.tsx +3 -3
  23. package/examples/skybox.tsx +5 -5
  24. package/examples/standard.tsx +2 -2
  25. package/examples/streams.tsx +122 -0
  26. package/examples/sweep-paths.tsx +5 -5
  27. package/examples/wireframe.tsx +24 -2
  28. package/package.json +3 -3
  29. package/src/clip.ts +18 -5
  30. package/src/color.ts +11 -2
  31. package/src/components/context.tsx +11 -2
  32. package/src/components/directional-light.tsx +2 -2
  33. package/src/components/first-person-camera.tsx +4 -4
  34. package/src/components/group.tsx +9 -5
  35. package/src/components/hemisphere-light.tsx +2 -2
  36. package/src/components/index.ts +4 -1
  37. package/src/components/instanced-mesh.tsx +28 -10
  38. package/src/components/lod.tsx +102 -0
  39. package/src/components/mesh.tsx +24 -6
  40. package/src/components/node-props.ts +35 -5
  41. package/src/components/orbit-camera.tsx +15 -12
  42. package/src/components/point-light.tsx +2 -2
  43. package/src/components/record-mesh.tsx +12 -7
  44. package/src/components/scene.tsx +55 -2
  45. package/src/components/spot-light.tsx +2 -2
  46. package/src/components/sprite.tsx +2 -2
  47. package/src/components/view3d.tsx +18 -1
  48. package/src/environment.ts +9 -8
  49. package/src/first-person.ts +125 -38
  50. package/src/geometry-gpu.ts +172 -39
  51. package/src/geometry.ts +1573 -239
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +363 -141
  54. package/src/index.ts +31 -21
  55. package/src/light.ts +7 -8
  56. package/src/lod.ts +148 -0
  57. package/src/material.ts +316 -202
  58. package/src/mesh.ts +490 -256
  59. package/src/mixer.ts +27 -14
  60. package/src/model-file.ts +54 -15
  61. package/src/model.ts +86 -21
  62. package/src/motion.ts +17 -0
  63. package/src/node.ts +421 -22
  64. package/src/orbit.ts +378 -116
  65. package/src/profile.ts +4 -25
  66. package/src/resolve.ts +181 -0
  67. package/src/scene-shadows.ts +94 -93
  68. package/src/scene.ts +605 -185
  69. package/src/sweep.ts +27 -8
  70. 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 lit()/unlit()
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; sparse accessors and morph
26
- // targets (the "weights" channel path) are skipped
27
- // or ignored; vertex colors, tangents and further UV sets are dropped
28
- // (the standard layout has no slot for them yet). Skins ARE parsed:
29
- // joints, inverse binds, and the "skinned" vertex layout.
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 { layoutStride, packGeometry, packIndices, STANDARD_FLOATS } from "./geometry.ts"
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 lit()/unlit() take from a glTF material. */
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 lit({ color }) takes. */
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 lit({ emissive }) takes. */
67
+ * sRGB, what phong({ emissive }) takes. */
62
68
  emissive: [number, number, number]
63
- /** KHR_materials_emissive_strength (default 1): lit's emissiveIntensity. */
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 standard prefix). */
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
- path: "position" | "rotation" | "scale"
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. "cubic" stores THREE elements per key - in-tangent,
141
- * value, out-tangent, in that order. */
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, so
178
- * it is conservative (not vertex-tight) for parts under rotated nodes. */
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 "skinned" layout's stride (standard prefix + aJoints + aWeights).
227
- const SKINNED_FLOATS = layoutStride("skinned")
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
- const IDENTITY = mat4()
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
- for (let i = 0; i < acc.count; i++) {
397
- let at = base + i * stride
398
- for (let e = 0; e < elements; e++) out[i * elements + e] = read(dv, at + e * compBytes)
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
- return { data: out, elements, count: acc.count }
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").data : null
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.data
444
- weights = w.data
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
- let stride = skinned ? SKINNED_FLOATS : STANDARD_FLOATS
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
- let vertices: Float32Array
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 Float32Array(count * stride)
509
- for (let i = 0; i < count; i++) writeVertex(vertices, i, pos.data, nrm, uv, i, stride, joints, weights)
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 Float32Array(triangles * 3 * stride)
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(vertices, out, pos.data, null, uv, a, stride, joints, weights)
533
- writeVertex(vertices, out + 1, pos.data, null, uv, b, stride, joints, weights)
534
- writeVertex(vertices, out + 2, pos.data, null, uv, c, stride, joints, weights)
535
- faceNormal(face, vertices, out, stride)
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 at = (out + k) * stride + 3
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). Skinned vertices are
555
- // already model-space, so their box goes through identity.
556
- let bw = skinned ? IDENTITY : world
557
- let lo: Vec3 = [Infinity, Infinity, Infinity]
558
- let hi: Vec3 = [-Infinity, -Infinity, -Infinity]
559
- for (let i = 0; i < count; i++) {
560
- let at = i * stride
561
- for (let k = 0; k < 3; k++) {
562
- let v = vertices[at + k]!
563
- if (v < lo[k]!) lo[k] = v
564
- if (v > hi[k]!) hi[k] = v
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
- for (let corner = 0; corner < 8; corner++) {
568
- let x = corner & 1 ? hi[0] : lo[0], y = corner & 2 ? hi[1] : lo[1], z = corner & 4 ? hi[2] : lo[2]
569
- let wx = bw[0] * x + bw[4] * y + bw[8] * z + bw[12]
570
- let wy = bw[1] * x + bw[5] * y + bw[9] * z + bw[13]
571
- let wz = bw[2] * x + bw[6] * y + bw[10] * z + bw[14]
572
- if (wx < bounds[0]!) bounds[0] = wx
573
- if (wy < bounds[1]!) bounds[1] = wy
574
- if (wz < bounds[2]!) bounds[2] = wz
575
- if (wx > bounds[3]!) bounds[3] = wx
576
- if (wy > bounds[4]!) bounds[4] = wy
577
- if (wz > bounds[5]!) bounds[5] = wz
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 = skinned
589
- ? { vertices, indices: packIndices(packedIndices, count), layout: "skinned", label: name }
590
- : packGeometry(vertices, packedIndices, { label: name })
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
- for (let k = 0; k < prims.length; k++) emit(prims[k], prims.length > 1 ? pending.name + "#" + k : pending.name, slot, world, skin)
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
- // Outside the subset: "weights" (morph targets) and extension paths.
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 (output.count !== input.count * perKey) {
733
- throw new Error("parseGltf: " + what + " " + path + " has " + output.count + " values for " + input.count + " keys")
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
- if (parts.length === 0) bounds.fill(0)
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 the skinned layout the joint indices plus their weights
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
- function writeVertex(
800
- out: Float32Array,
801
- slot: number,
802
- pos: Float32Array,
803
- nrm: Float32Array | null,
804
- uv: Float32Array | null,
805
- src: number,
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
- out[at + 6] = uv[src * 2]!
821
- out[at + 7] = uv[src * 2 + 1]!
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 (joints !== null && weights !== null) {
824
- let j = src * 4
825
- out[at + 8] = joints[j]!
826
- out[at + 9] = joints[j + 1]!
827
- out[at + 10] = joints[j + 2]!
828
- out[at + 11] = joints[j + 3]!
829
- let sum = weights[j]! + weights[j + 1]! + weights[j + 2]! + weights[j + 3]!
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
- out[at + 12] = weights[j]! * inv
832
- out[at + 13] = weights[j + 1]! * inv
833
- out[at + 14] = weights[j + 2]! * inv
834
- out[at + 15] = weights[j + 3]! * inv
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, v: Float32Array, first: number, stride: number): void {
840
- let a = first * stride, b = (first + 1) * stride, c = (first + 2) * stride
841
- let abx = v[b]! - v[a]!, aby = v[b + 1]! - v[a + 1]!, abz = v[b + 2]! - v[a + 2]!
842
- let acx = v[c]! - v[a]!, acy = v[c + 1]! - v[a + 1]!, acz = v[c + 2]! - v[a + 2]!
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