@solidrt/3d 0.0.59 → 0.0.61

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/src/gltf.ts CHANGED
@@ -32,8 +32,19 @@
32
32
  // layout for a static primitive, the skinned list plus aColor for a
33
33
  // rigged one. Skins ARE parsed: joints, inverse binds, and the "skinned"
34
34
  // vertex layout.
35
+ //
36
+ // Reuse is kept, not unrolled: a mesh several nodes reference becomes
37
+ // ONE part per primitive whose `placements` list the further nodes
38
+ // (Three shares the geometry, Unity and Godot the mesh asset; here the
39
+ // runtime draws the part instanced, one entry for the population), and
40
+ // EXT_mesh_gpu_instancing lands the same way, its per-instance TRS as
41
+ // synthesized child nodes of the referencing node (Three's
42
+ // InstancedMesh, Godot's MultiMesh). The file's `extras` ride through
43
+ // where glTF puts them - root, node, mesh (onto the part), material -
44
+ // the way Three fills userData, so app data authored as custom
45
+ // properties arrives with the model.
35
46
 
36
- import { compose, decompose, det3, mat4, multiply } from "./math.ts"
47
+ import { compose, decompose, det3, mat4, multiply, quatNormalize } from "./math.ts"
37
48
  import { linearToSrgb } from "./color.ts"
38
49
  import type { Mat4, Quat, Vec3 } from "./math.ts"
39
50
  import { attributeAccess, layoutKey, layoutStride, packIndices, packMorphTargets, vertexView } from "./geometry.ts"
@@ -79,8 +90,16 @@ export type ModelMaterial = {
79
90
  * null; createModel hands it to `material` as both maps.metalnessMap
80
91
  * and maps.roughnessMap, standard's two channel-select options. */
81
92
  metalnessRoughnessMap: number | null
93
+ /** The glTF material's `extras`, when it has any. */
94
+ extras?: ModelExtras
82
95
  }
83
96
 
97
+ /** Application data carried beside the model: a JSON object, glTF's
98
+ * `extras` shape (Blender's custom properties export as it), what Three
99
+ * surfaces as `userData` and Godot as node metadata. Present only when
100
+ * non-empty. */
101
+ export type ModelExtras = Record<string, unknown>
102
+
84
103
  /** One node of the model's retained hierarchy: a local TRS under `parent`
85
104
  * (an earlier index into ModelData.nodes, or null for a root). Parts and
86
105
  * - in rigged models - skin joints and animation tracks reference nodes
@@ -100,6 +119,8 @@ export type ModelNode = {
100
119
  * zeros), what the node's parts draw with until a clip or
101
120
  * setMorphWeights writes them. */
102
121
  weights?: number[]
122
+ /** The glTF node's `extras`, when it has any. */
123
+ extras?: ModelExtras
103
124
  }
104
125
 
105
126
  /** One drawable: a mesh node's primitive. Vertices are LOCAL to its
@@ -111,8 +132,19 @@ export type ModelPart = {
111
132
  * numbers them `name#<k>`. */
112
133
  name: string
113
134
  /** Index into ModelData.nodes - the node whose world transform places
114
- * this part (unused for placement when `skin` is set). */
135
+ * this part (unused for placement when `skin` is set). With
136
+ * `placements` it is the FIRST placement. */
115
137
  node: number
138
+ /** The further nodes (indices into ModelData.nodes) placing the SAME
139
+ * geometry - a mesh several glTF nodes reference, or the instances of
140
+ * EXT_mesh_gpu_instancing as synthesized children of the referencing
141
+ * node. Present only when there is more than one placement; then the
142
+ * part is a population (createModel draws it as one InstancedMesh, an
143
+ * instance under every placement node) rather than a mesh per node.
144
+ * Never on a skinned or morphed part, and a placement whose world
145
+ * mirrors the first's (opposite determinant sign) is its own part,
146
+ * since the baked winding cannot serve both. */
147
+ placements?: number[]
116
148
  /** Index into ModelData.skins, or null. A skinned part's geometry has
117
149
  * the "skinned" layout (aJoints/aWeights after the base prefix). */
118
150
  skin: number | null
@@ -128,6 +160,9 @@ export type ModelPart = {
128
160
  geometry: Geometry
129
161
  /** Index into ModelData.materials. */
130
162
  material: number
163
+ /** The glTF MESH's `extras`, when it has any (every primitive of the
164
+ * mesh carries the same object). */
165
+ extras?: ModelExtras
131
166
  }
132
167
 
133
168
  /** One skin: the joints (as node-table indices) and each joint's inverse
@@ -203,6 +238,15 @@ export type ModelData = {
203
238
  * is conservative (not vertex-tight) for parts under rotated nodes and
204
239
  * for skinned parts. */
205
240
  bounds: Float32Array
241
+ /** The document's root `extras`, when it has any: app data authored
242
+ * beside the model, or written by a bake of the app's own geometry. */
243
+ extras?: ModelExtras
244
+ /** Named binary sections for app data that does not belong in JSON (an
245
+ * occupancy grid, a nav mesh, a collider set): a bake writes them, the
246
+ * container stores each as its own 4-aligned block, and decodeModel
247
+ * hands them back as views onto the file's bytes. glTF has no
248
+ * counterpart; parseGltf never fills it. */
249
+ blobs?: Record<string, Uint8Array>
206
250
  }
207
251
 
208
252
  /** Resolves a relative uri of a .gltf (its .bin buffers, image files) to
@@ -315,15 +359,45 @@ export function isGlb(bytes: Uint8Array): boolean {
315
359
  return bytes.length >= 12 && new DataView(bytes.buffer, bytes.byteOffset, 12).getUint32(0, true) === GLB_MAGIC
316
360
  }
317
361
 
318
- /** The external uris a document references (buffers and images), so an
319
- * async caller can fetch them before parseGltf. Usually empty for .glb -
320
- * but a .glb MAY reference external files (some exporters write image
321
- * uris), so this reads its JSON chunk rather than assuming. data: uris
322
- * are never listed. */
362
+ // The texture references the material map (parseGltf's `materials`)
363
+ // samples, and so the only images the parser ever opens: the prefetch
364
+ // list (gltfExternalUris) and the parser's demand set are both derived
365
+ // from this one table, so a channel added to the map is added here too or
366
+ // checks/gltf-check.ts fails on its all-channels fixture.
367
+ const SAMPLED_TEXTURES: ((material: any) => any)[] = [
368
+ (m) => m.pbrMetallicRoughness?.baseColorTexture,
369
+ (m) => m.normalTexture,
370
+ (m) => m.emissiveTexture,
371
+ (m) => m.pbrMetallicRoughness?.metallicRoughnessTexture,
372
+ ]
373
+
374
+ // The image indices a document's materials sample (SAMPLED_TEXTURES
375
+ // through textures[].source), the demand set imageSlot will meet.
376
+ function sampledImages(gltf: any): Set<number> {
377
+ let images = new Set<number>()
378
+ for (let m of gltf.materials ?? []) {
379
+ for (let channel of SAMPLED_TEXTURES) {
380
+ let ref = channel(m)
381
+ let source = ref === undefined ? undefined : gltf.textures?.[ref.index]?.source
382
+ if (source !== undefined) images.add(source)
383
+ }
384
+ }
385
+ return images
386
+ }
387
+
388
+ /** The external uris parseGltf will open on a document: every buffer,
389
+ * and the images a material samples (an image nothing samples is never
390
+ * read, so it is not listed), so an async caller can fetch exactly that
391
+ * set before the synchronous parse. Usually empty for .glb - but a .glb
392
+ * MAY reference external files (some exporters write image uris), so
393
+ * this reads its JSON chunk rather than assuming. data: uris are never
394
+ * listed. */
323
395
  export function gltfExternalUris(bytes: Uint8Array): string[] {
324
396
  let gltf = isGlb(bytes) ? readGlb(bytes).json : JSON.parse(new TextDecoder().decode(bytes))
397
+ let sampled = sampledImages(gltf)
398
+ let items: any[] = [...(gltf.buffers ?? []), ...(gltf.images ?? []).filter((_: any, i: number) => sampled.has(i))]
325
399
  let uris: string[] = []
326
- for (let item of [...(gltf.buffers ?? []), ...(gltf.images ?? [])]) {
400
+ for (let item of items) {
327
401
  if (typeof item.uri === "string" && !item.uri.startsWith("data:")) uris.push(item.uri)
328
402
  }
329
403
  return uris
@@ -354,9 +428,10 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
354
428
  throw new Error("parseGltf: the file's meshes are compressed (" + ext + "), which is not supported: re-export without mesh compression")
355
429
  }
356
430
  // Quantized attributes read through the normalized-integer path,
357
- // emissive strength is the emissive intensity; every other required
358
- // extension changes what the file means.
359
- if (ext !== "KHR_mesh_quantization" && ext !== "KHR_materials_emissive_strength") {
431
+ // emissive strength is the emissive intensity, GPU instancing is
432
+ // read as placements; every other required extension changes what
433
+ // the file means.
434
+ if (ext !== "KHR_mesh_quantization" && ext !== "KHR_materials_emissive_strength" && ext !== "EXT_mesh_gpu_instancing") {
360
435
  throw new Error("parseGltf: the file requires the " + ext + " extension, which is not supported")
361
436
  }
362
437
  }
@@ -405,6 +480,8 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
405
480
  // A texture reference's image slot, or null (the reference absent, or
406
481
  // its texture imageless). Further UV sets are outside the subset, so a
407
482
  // non-zero texCoord is ignored and the map samples the one UV set.
483
+ // Every reference the map below passes here is one of SAMPLED_TEXTURES
484
+ // (the prefetch list): a new channel goes in the table first.
408
485
  let textureSlot = (ref: any): number | null => {
409
486
  if (ref === undefined) return null
410
487
  let texture = gltf.textures?.[ref.index]
@@ -416,7 +493,7 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
416
493
  let emissiveFactor: number[] = m.emissiveFactor ?? [0, 0, 0]
417
494
  let strength = m.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1
418
495
  // glTF factors are linear; the material options are sRGB.
419
- return {
496
+ let material: ModelMaterial = {
420
497
  name: m.name ?? "material" + i,
421
498
  color: [linearToSrgb(factor[0]), linearToSrgb(factor[1]), linearToSrgb(factor[2]), factor[3] ?? 1],
422
499
  map: textureSlot(pbr.baseColorTexture),
@@ -433,6 +510,9 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
433
510
  roughness: typeof pbr.roughnessFactor === "number" ? pbr.roughnessFactor : 1,
434
511
  metalnessRoughnessMap: textureSlot(pbr.metallicRoughnessTexture),
435
512
  }
513
+ let extras = extrasOf(m)
514
+ if (extras !== undefined) material.extras = extras
515
+ return material
436
516
  })
437
517
  // Primitives without a material draw the spec's default; it is appended
438
518
  // only when something uses it.
@@ -489,16 +569,46 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
489
569
  // part somewhere below it needs the chain (materialize walks ancestors
490
570
  // first, so the table stays in pre-order), which is what prunes cameras,
491
571
  // lights and unused empties.
492
- type PendingNode = { name: string; parent: PendingNode | null; position: Vec3; rotation: Quat; scale: Vec3; world: Mat4; index: number | null }
572
+ type PendingNode = { name: string; parent: PendingNode | null; position: Vec3; rotation: Quat; scale: Vec3; world: Mat4; index: number | null; extras: ModelExtras | undefined }
493
573
  let nodes: ModelNode[] = []
494
574
  let materialize = (p: PendingNode): number => {
495
575
  if (p.index !== null) return p.index
496
576
  let parent = p.parent === null ? null : materialize(p.parent)
497
577
  p.index = nodes.length
498
- nodes.push({ name: p.name, parent, position: p.position, rotation: p.rotation, scale: p.scale })
578
+ let node: ModelNode = { name: p.name, parent, position: p.position, rotation: p.rotation, scale: p.scale }
579
+ if (p.extras !== undefined) node.extras = p.extras
580
+ nodes.push(node)
499
581
  return p.index
500
582
  }
501
583
 
584
+ /** One node placing a mesh: its table slot, name, rest-pose world, the
585
+ * file's skin index it draws with (null for a rigid placement) and its
586
+ * position in the walk, which orders the parts. */
587
+ type Placement = { slot: number; name: string; world: Mat4; skin: number | null; seq: number }
588
+ // A mesh's placements collected over the walk, keyed by the file's
589
+ // mesh index in first-use order, folded into parts after it.
590
+ let meshUses = new Map<number, { mesh: any; uses: Placement[] }>()
591
+ let sequence = 0
592
+ let place = (meshIndex: number, mesh: any, pending: PendingNode, skin: number | null, nodeWeights: unknown): void => {
593
+ let slot = materialize(pending)
594
+ let prims: any[] = mesh.primitives ?? []
595
+ // Morph target names and the initial weights are the MESH's (a node
596
+ // may override the weights); the weights land on the placing node,
597
+ // which is what a weights channel and setMorphWeights address.
598
+ let targetCount = Math.max(0, ...prims.map((p: any): number => p?.targets?.length ?? 0))
599
+ if (targetCount > 0) {
600
+ let weights: number[] = Array.isArray(nodeWeights) ? nodeWeights : Array.isArray(mesh.weights) ? mesh.weights : []
601
+ if (weights.length !== targetCount) {
602
+ if (weights.length !== 0) throw new Error("parseGltf: node " + pending.name + " has " + weights.length + " weights for " + targetCount + " morph targets")
603
+ weights = new Array(targetCount).fill(0)
604
+ }
605
+ nodes[slot]!.weights = weights.map(Number)
606
+ }
607
+ let use: Placement = { slot, name: pending.name, world: pending.world, skin, seq: sequence++ }
608
+ let uses = meshUses.get(meshIndex)
609
+ if (uses === undefined) meshUses.set(meshIndex, { mesh, uses: [use] })
610
+ else uses.uses.push(use)
611
+ }
502
612
  let parts: ModelPart[] = []
503
613
  let pendingJointBounds: { skin: number; positions: Float32Array; count: number; joints: Float32Array; weights: Float32Array; slack: number }[] = []
504
614
  let bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
@@ -521,8 +631,27 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
521
631
  }
522
632
  }
523
633
 
524
- let emit = (prim: any, name: string, node: number, world: Mat4, skin: number | null, targetNames: string[] | null): void => {
634
+ /** A primitive's built geometry per variant ("rigid"/"skinned", with or
635
+ * without the winding flip), shared by every part emitted from it: a
636
+ * skinned mesh two nodes place, a morphed one, a mirrored copy - each
637
+ * its own part, one set of bytes. `box` is the local rest box (morph
638
+ * extent included) for the bounds, null for a skinned build (its box
639
+ * comes from the joints); `indexed` says the vertices are the authored
640
+ * ones (not the flat-shading un-index), so a twin under the other
641
+ * winding sign can share them and differ in index order alone. */
642
+ type Built = { geometry: Geometry; box: number[] | null; indexed: boolean }
643
+ type PrimitiveCache = Map<string, Built>
644
+
645
+ // One part from one primitive under its placements: the first names and
646
+ // owns it (`node`), the rest are `placements`. Every placement shares
647
+ // the skin (null, for more than one) and the winding sign, which the
648
+ // fold below guarantees.
649
+ let emit = (prim: any, name: string, placed: Placement[], targetNames: string[] | null, extras: ModelExtras | undefined, shared: PrimitiveCache): void => {
525
650
  if (prim.attributes?.POSITION === undefined) return
651
+ let first = placed[0]!
652
+ let node = first.slot
653
+ let world = first.world
654
+ let skin = first.skin
526
655
  let mode: number = prim.mode ?? MODE_TRIANGLES
527
656
  let pos = accessorFloats(prim.attributes.POSITION, name + " POSITION")
528
657
  if (pos.elements !== 3) throw new Error("parseGltf: " + name + " POSITION is not VEC3")
@@ -616,6 +745,49 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
616
745
  // points have no winding.
617
746
  let flip = triangles && !skinned && det3(world) < 0
618
747
 
748
+ // The part from a built geometry: the bounds through every placement
749
+ // (a skinned build parks its joint boxes instead), the material, the
750
+ // record. Shared by the first build and every later part over it.
751
+ let finish = (built: Built): void => {
752
+ if (built.box !== null) {
753
+ for (let p of placed) growBounds(bounds, built.box, p.world)
754
+ } else {
755
+ // Joint boxes need the skin's inverse binds, and skins are built
756
+ // after the walk (their joints are ordinary nodes the walk
757
+ // registers), so the arrays are parked under the FILE's skin index
758
+ // and grown then - each box padded by the largest morph delta, as
759
+ // a morph moves a vertex in whichever joint's space.
760
+ let m = built.geometry.morphs
761
+ let slack = m === undefined ? 0 : Math.max(...Array.from(m.extent, Math.abs))
762
+ pendingJointBounds.push({ skin: skin!, positions: pos.data, count: pos.count, joints: joints!.data, weights: weights!.data, slack })
763
+ }
764
+ let material = prim.material
765
+ if (material === undefined) {
766
+ if (defaultMaterial < 0) {
767
+ defaultMaterial = materials.length
768
+ materials.push({ ...DEFAULT_MATERIAL })
769
+ }
770
+ material = defaultMaterial
771
+ }
772
+ let part: ModelPart = { name, node, skin, geometry: built.geometry, material }
773
+ if (placed.length > 1) part.placements = placed.slice(1).map((p) => p.slot)
774
+ if (extras !== undefined) part.extras = extras
775
+ parts.push(part)
776
+ }
777
+ // The same primitive built before for this variant: one geometry for
778
+ // every part over it.
779
+ let variant = skinned ? "skinned" : "rigid"
780
+ let key = variant + (flip ? "|flipped" : "")
781
+ let built = shared.get(key)
782
+ if (built !== undefined) {
783
+ finish(built)
784
+ return
785
+ }
786
+ // The other winding sign's build, when it exists: an indexed one holds
787
+ // the very vertices this variant needs.
788
+ let twin = shared.get(variant + (flip ? "" : "|flipped"))
789
+ if (twin !== undefined && !twin.indexed) twin = undefined
790
+
619
791
  // The channels of the part's buffer, written through the accessors:
620
792
  // whatever a channel's format, the writer takes floats.
621
793
  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 }
@@ -626,12 +798,20 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
626
798
  // interleave (the morph targets): null while the slots are the
627
799
  // source order, the un-index's corner table otherwise.
628
800
  let remap: number[] | null = null
629
- if (nrm !== null || !triangles) {
801
+ let indexed = nrm !== null || !triangles
802
+ if (indexed) {
630
803
  // Indexed as authored. Lines and points without normals get zero
631
- // ones (there is no face to take one from; nothing lights them).
632
- vertices = vertexView(layout, new ArrayBuffer(count * layoutStride(layout)))
633
- writer = vertexWriter(vertices, layout)
634
- for (let i = 0; i < count; i++) writeVertex(writer, i, source, i)
804
+ // ones (there is no face to take one from; nothing lights them). A
805
+ // twin under the other winding sign already holds these vertices:
806
+ // share its buffer, only the index order differs.
807
+ if (twin !== undefined) {
808
+ vertices = twin.geometry.vertices
809
+ writer = vertexWriter(vertices, layout)
810
+ } else {
811
+ vertices = vertexView(layout, new ArrayBuffer(count * layoutStride(layout)))
812
+ writer = vertexWriter(vertices, layout)
813
+ for (let i = 0; i < count; i++) writeVertex(writer, i, source, i)
814
+ }
635
815
  packedIndices = Array.from(indices)
636
816
  if (flip) {
637
817
  for (let i = 0; i < packedIndices.length; i += 3) {
@@ -684,7 +864,8 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
684
864
  // sparse by vertex onto the geometry. Names come from the mesh's
685
865
  // extras.targetNames (what Blender writes), else "target<t>".
686
866
  let targets: any[] = prim.targets ?? []
687
- let morphs = targets.length === 0 ? undefined : packMorphTargets(count, targets.map((target: any, t: number): MorphTarget => {
867
+ // A twin's packed targets are these exactly (both indexed, no remap).
868
+ let morphs = twin !== undefined ? twin.geometry.morphs : targets.length === 0 ? undefined : packMorphTargets(count, targets.map((target: any, t: number): MorphTarget => {
688
869
  let what = name + " target " + t
689
870
  let deltas = (attribute: string): Float32Array | null => {
690
871
  if (target?.[attribute] === undefined) return null
@@ -703,12 +884,13 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
703
884
  return { name: targetNames?.[t] ?? "target" + t, position: deltas("POSITION") ?? new Float32Array(count * 3), normal: remap === null ? deltas("NORMAL") : null }
704
885
  }))
705
886
 
706
- // Model bounds: the part's local box through the node's rest-pose
707
- // world transform (8 corners - conservative under rotation, exact
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).
887
+ // The local rest box (8 corners through each placement's world in
888
+ // finish - conservative under rotation, exact under translation and
889
+ // axis-aligned scale), grown by the morph extent first so a morphed
890
+ // shape stays inside. A skinned build's vertices are placed by its
891
+ // joints, not its node, so it has none here: its box is folded in
892
+ // after the skins exist (growBounds over the joint boxes).
893
+ let box: number[] | null = null
712
894
  if (!skinned) {
713
895
  let lo: Vec3 = [Infinity, Infinity, Infinity]
714
896
  let hi: Vec3 = [-Infinity, -Infinity, -Infinity]
@@ -725,30 +907,16 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
725
907
  hi[k] = hi[k]! + morphs.extent[3 + k]!
726
908
  }
727
909
  }
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 })
910
+ box = [lo[0], lo[1], lo[2], hi[0], hi[1], hi[2]]
737
911
  }
738
912
 
739
- let material = prim.material
740
- if (material === undefined) {
741
- if (defaultMaterial < 0) {
742
- defaultMaterial = materials.length
743
- materials.push({ ...DEFAULT_MATERIAL })
744
- }
745
- material = defaultMaterial
746
- }
747
913
  let geometry: Geometry = { vertices, indices: packIndices(packedIndices, count), label: name }
748
914
  if (layout !== undefined) geometry.layout = layout
749
915
  if (morphs !== undefined) geometry.morphs = morphs
750
916
  if (!triangles) geometry.topology = topology
751
- parts.push({ name, node, skin, geometry, material })
917
+ built = { geometry, box, indexed }
918
+ shared.set(key, built)
919
+ finish(built)
752
920
  }
753
921
 
754
922
  let local = mat4()
@@ -756,6 +924,10 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
756
924
  let walk = (index: number, parent: PendingNode | null, parentWorld: Mat4): void => {
757
925
  let node = gltf.nodes[index]
758
926
  if (node === undefined) throw new Error("parseGltf: scene names a missing node " + index)
927
+ // glTF's hierarchy is a strict tree: a node reached twice has two
928
+ // parents, or the children form a cycle - which would otherwise
929
+ // recurse until the stack goes.
930
+ if (pendingByIndex.has(index)) throw new Error("parseGltf: node " + index + " appears twice in the hierarchy (two parents, or a cycle in children)")
759
931
  let position: Vec3 = [0, 0, 0]
760
932
  let rotation: Quat = [0, 0, 0, 1]
761
933
  let scale: Vec3 = [1, 1, 1]
@@ -771,38 +943,108 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
771
943
  // winding and bounds match what the runtime will render.
772
944
  let world = mat4()
773
945
  multiply(world, parentWorld, compose(local, position, rotation, scale))
774
- let pending: PendingNode = { name: node.name ?? "node" + index, parent, position, rotation, scale, world, index: null }
946
+ let pending: PendingNode = { name: node.name ?? "node" + index, parent, position, rotation, scale, world, index: null, extras: extrasOf(node) }
775
947
  pendingByIndex.set(index, pending)
776
948
  if (node.mesh !== undefined) {
777
949
  let mesh = gltf.meshes[node.mesh]
778
950
  if (mesh === undefined) throw new Error("parseGltf: node " + index + " names a missing mesh " + node.mesh)
779
951
  if (node.name === undefined && mesh.name !== undefined) pending.name = mesh.name
780
- let slot = materialize(pending)
781
952
  let skin = typeof node.skin === "number" ? node.skin : null
782
- let prims: any[] = mesh.primitives ?? []
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)
953
+ let instancing = node.extensions?.EXT_mesh_gpu_instancing
954
+ if (instancing === undefined) place(node.mesh, mesh, pending, skin, node.weights)
955
+ else {
956
+ // EXT_mesh_gpu_instancing: the node's mesh is drawn once per
957
+ // instance, each a TRS in the node's local space, and not at
958
+ // the node itself. Every instance becomes a synthesized child
959
+ // node ("<name>[i]"), so the hierarchy stays one table and an
960
+ // instance moves like any node; the per-instance custom
961
+ // attributes the extension allows (underscore-prefixed) are
962
+ // dropped, as Three drops them.
963
+ let what = "node " + pending.name + " EXT_mesh_gpu_instancing"
964
+ let attrs = instancing.attributes ?? {}
965
+ let read = (attribute: string, elements: number): Float32Array | null => {
966
+ if (attrs[attribute] === undefined) return null
967
+ let acc = accessorFloats(attrs[attribute], what + " " + attribute)
968
+ if (acc.elements !== elements) throw new Error("parseGltf: " + what + " " + attribute + " is not VEC" + elements)
969
+ return acc.data
970
+ }
971
+ let t = read("TRANSLATION", 3)
972
+ let r = read("ROTATION", 4)
973
+ let s = read("SCALE", 3)
974
+ let counts = [t === null ? null : t.length / 3, r === null ? null : r.length / 4, s === null ? null : s.length / 3].filter((c): c is number => c !== null)
975
+ if (counts.length === 0) throw new Error("parseGltf: " + what + " has no TRANSLATION, ROTATION or SCALE attribute")
976
+ if (counts.some((c) => c !== counts[0])) throw new Error("parseGltf: " + what + " attributes disagree on the instance count")
977
+ for (let i = 0; i < counts[0]!; i++) {
978
+ let p: Vec3 = t === null ? [0, 0, 0] : [t[i * 3]!, t[i * 3 + 1]!, t[i * 3 + 2]!]
979
+ // A quantized rotation (normalized int16, which the extension
980
+ // allows) is unit only to its precision; renormalized so the
981
+ // node stores a unit quaternion like every other.
982
+ let q: Quat = [0, 0, 0, 1]
983
+ if (r !== null) quatNormalize(q, [r[i * 4]!, r[i * 4 + 1]!, r[i * 4 + 2]!, r[i * 4 + 3]!])
984
+ let sc: Vec3 = s === null ? [1, 1, 1] : [s[i * 3]!, s[i * 3 + 1]!, s[i * 3 + 2]!]
985
+ let instanceWorld = mat4()
986
+ multiply(instanceWorld, world, compose(local, p, q, sc))
987
+ place(node.mesh, mesh, { name: pending.name + "[" + i + "]", parent: pending, position: p, rotation: q, scale: sc, world: instanceWorld, index: null, extras: undefined }, skin, node.weights)
793
988
  }
794
- nodes[slot]!.weights = weights.map(Number)
795
989
  }
796
- for (let k = 0; k < prims.length; k++) emit(prims[k], prims.length > 1 ? pending.name + "#" + k : pending.name, slot, world, skin, targetNames)
797
990
  }
798
991
  for (let child of node.children ?? []) walk(child, pending, world)
799
992
  }
800
993
 
801
994
  let scene = gltf.scenes?.[gltf.scene ?? 0]
802
- let roots: number[] = scene?.nodes ?? (gltf.nodes ?? []).map((_: unknown, i: number) => i)
995
+ let allNodes: any[] = gltf.nodes ?? []
996
+ // No scene (both `scene` and `scenes` are optional): draw everything,
997
+ // from the true roots - the nodes no other node lists as a child - so a
998
+ // nested node is visited once under its parent, never a second time as
999
+ // a root against the identity.
1000
+ let listedAsChild = new Set<number>()
1001
+ if (scene === undefined) for (let n of allNodes) for (let c of n?.children ?? []) listedAsChild.add(c)
1002
+ let roots: number[] = scene?.nodes ?? allNodes.map((_: unknown, i: number) => i).filter((i) => !listedAsChild.has(i))
803
1003
  let root = mat4()
804
1004
  for (let index of roots) walk(index, null, root)
805
1005
 
1006
+ // Parts, after the walk: one primitive under its placements. A mesh
1007
+ // several nodes use folds into one part per primitive with the further
1008
+ // nodes as placements (the runtime draws it instanced) - except what
1009
+ // cannot share one draw, which stays a part of its own per use: a
1010
+ // skinned use (bind-pose model space, placed by its skin; a second
1011
+ // reference draws the same thing per spec), a morphed one (its weights
1012
+ // are the node's), and a use whose rest-pose winding sign differs from
1013
+ // the bucket's (the flip is baked into the indices). Those parts still
1014
+ // share the primitive's built geometry (one cache per primitive, see
1015
+ // emit), so the bytes exist once. Parts are emitted in WALK order -
1016
+ // each bucket where its first placement was visited, a node's
1017
+ // primitives in order - so a file's part order does not depend on how
1018
+ // its meshes interleave.
1019
+ let queue: { seq: number; k: number; run: () => void }[] = []
1020
+ for (let { mesh, uses } of meshUses.values()) {
1021
+ let prims: any[] = mesh.primitives ?? []
1022
+ let targetNames: string[] | null = Array.isArray(mesh.extras?.targetNames) ? mesh.extras.targetNames.map(String) : null
1023
+ let extras = extrasOf(mesh)
1024
+ for (let k = 0; k < prims.length; k++) {
1025
+ let prim = prims[k]
1026
+ let morphed = (prim?.targets?.length ?? 0) > 0
1027
+ let buckets: { alone: boolean; mirrored: boolean; placed: Placement[] }[] = []
1028
+ for (let use of uses) {
1029
+ let alone = use.skin !== null || morphed
1030
+ let mirrored = det3(use.world) < 0
1031
+ let bucket = alone ? undefined : buckets.find((b) => !b.alone && b.mirrored === mirrored)
1032
+ if (bucket === undefined) {
1033
+ bucket = { alone, mirrored, placed: [] }
1034
+ buckets.push(bucket)
1035
+ }
1036
+ bucket.placed.push(use)
1037
+ }
1038
+ let shared: PrimitiveCache = new Map()
1039
+ for (let b of buckets) {
1040
+ let name = b.placed[0]!.name
1041
+ queue.push({ seq: b.placed[0]!.seq, k, run: () => emit(prim, prims.length > 1 ? name + "#" + k : name, b.placed, targetNames, extras, shared) })
1042
+ }
1043
+ }
1044
+ }
1045
+ queue.sort((a, b) => a.seq - b.seq || a.k - b.k)
1046
+ for (let item of queue) item.run()
1047
+
806
1048
  // Skins, after the walk: joints are ordinary nodes (usually meshless),
807
1049
  // materialized here so the retained table carries them; part.skin
808
1050
  // remaps from the file's skin index to the compact list.
@@ -940,7 +1182,19 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
940
1182
  // Nothing grew the box (no parts, or skinned parts whose weights reach
941
1183
  // no joint): an empty model sits at the origin.
942
1184
  if (!(bounds[0]! <= bounds[3]!)) bounds.fill(0)
943
- return { nodes, parts, skins, clips, materials, images, bounds }
1185
+ let data: ModelData = { nodes, parts, skins, clips, materials, images, bounds }
1186
+ let extras = extrasOf(gltf)
1187
+ if (extras !== undefined) data.extras = extras
1188
+ return data
1189
+ }
1190
+
1191
+ /** An object's glTF `extras` as a JSON object, or undefined when absent,
1192
+ * not an object, or empty - so the field appears only where there is
1193
+ * data. */
1194
+ function extrasOf(owner: any): ModelExtras | undefined {
1195
+ let extras = owner?.extras
1196
+ if (typeof extras !== "object" || extras === null || Array.isArray(extras)) return undefined
1197
+ return Object.keys(extras).length > 0 ? (extras as ModelExtras) : undefined
944
1198
  }
945
1199
 
946
1200
  function readGlb(bytes: Uint8Array): { json: any; bin: Uint8Array | null } {
package/src/index.ts CHANGED
@@ -49,7 +49,7 @@ export { extrude, lathe, pathFrames, polygon, sweep, tube } from "./sweep.ts"
49
49
  export type { ExtrudeOptions, LatheOptions, PathFrames, PathPoint, SweepPath, TubeOptions } from "./sweep.ts"
50
50
  export { phong, shaderMaterial, shaderMaterialClass, sprite, standard, unlit } from "./material.ts"
51
51
  export type { LitOptions, Material, PhongOptions, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, SpriteOptions, StandardOptions, UnlitOptions } from "./material.ts"
52
- export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedLod, InstancedMesh, Lod, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SpotLight, Sprite, View3d, useScene } from "./components/index.ts"
52
+ export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedLod, InstancedMesh, Lod, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SpotLight, Sprite, View3d, useScene, Shot, Shots } from "./components/index.ts"
53
53
  export type {
54
54
  CameraTarget,
55
55
  DirectionalLightProps,
@@ -73,13 +73,16 @@ export type {
73
73
  SpriteProps,
74
74
  TransformProps,
75
75
  View3dProps,
76
+ ShotProps,
77
+ ShotsProps,
76
78
  } from "./components/index.ts"
77
79
  export { gltfExternalUris, isGlb, parseGltf } from "./gltf.ts"
78
- export type { ModelChannel, ModelClip, ModelData, ModelMaterial, ModelNode, ModelPart, ModelSkin, UriResolver } from "./gltf.ts"
80
+ export type { ModelChannel, ModelClip, ModelData, ModelExtras, ModelMaterial, ModelNode, ModelPart, ModelSkin, UriResolver } from "./gltf.ts"
79
81
  export { decodeModel, encodeModel } from "./model-file.ts"
80
82
  export { createModel, loadGltf, loadModel } from "./model.ts"
81
83
  export type { Model, ModelMaps, ModelOptions } from "./model.ts"
82
84
  export { bindSkeleton } from "./skeleton.ts"
85
+ export type { BindSkeletonOptions } from "./skeleton.ts"
83
86
  export { createMixer } from "./mixer.ts"
84
87
  export type { Mixer, MixerOptions, MixerPlayOptions } from "./mixer.ts"
85
88
  export { channelElements, sampleChannel } from "./clip.ts"
@@ -87,7 +90,9 @@ export { createOrbitCamera } from "./orbit.ts"
87
90
  export type { OrbitAxes, OrbitCamera as OrbitCameraHandle, OrbitCameraOptions, OrbitPose, OrbitPoseState, OrbitTarget } from "./orbit.ts"
88
91
  export { createFirstPersonCamera } from "./first-person.ts"
89
92
  export type { FirstPersonAxes, FirstPersonCamera as FirstPersonCameraHandle, FirstPersonCameraOptions, FirstPersonPose, FirstPersonPoseState, FirstPersonTarget } from "./first-person.ts"
90
- export { firstPersonActions, firstPersonBindings, orbitActions, orbitBindings } from "./input.ts"
93
+ export { createShots, mixCamera } from "./shots.ts"
94
+ export type { ShotsHandle, ShotsOwner, ShotTarget } from "./shots.ts"
95
+ export { firstPersonActions, firstPersonBindings, mapBindings, orbitActions, orbitBindings } from "./input.ts"
91
96
  export type { CameraDevices } from "./input.ts"
92
97
  // math's lookAt (the camera view matrix) stays on the /math subpath: the
93
98
  // root's lookAt is the scene verb, the same split as `add`.