@solidrt/3d 0.0.60 → 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/AGENTS.md +436 -139
- package/examples/cascades.tsx +10 -4
- package/examples/pick.tsx +6 -4
- package/examples/sprites.tsx +7 -1
- package/package.json +4 -3
- package/src/components/context.tsx +13 -2
- package/src/components/first-person-camera.tsx +1 -1
- package/src/components/index.ts +2 -0
- package/src/components/instanced-mesh.tsx +25 -6
- package/src/components/lod.tsx +3 -1
- package/src/components/orbit-camera.tsx +105 -20
- package/src/components/shots.tsx +93 -0
- package/src/first-person.ts +48 -16
- package/src/gltf.ts +316 -62
- package/src/index.ts +8 -3
- package/src/input.ts +61 -17
- package/src/light.ts +25 -2
- package/src/material.ts +101 -25
- package/src/math.ts +11 -0
- package/src/mesh.ts +68 -21
- package/src/model-data.ts +27 -0
- package/src/model-file.ts +112 -37
- package/src/model.ts +90 -29
- package/src/node.ts +1 -1
- package/src/orbit.ts +526 -108
- package/src/scene-shadows.ts +4 -3
- package/src/scene.ts +201 -189
- package/src/shots.ts +63 -0
- package/src/skeleton.ts +39 -7
- package/tools/environment.ts +1 -0
- package/tools/model.ts +4 -3
- package/tools/node-env.d.ts +25 -0
- package/src/motion.ts +0 -17
- package/src/order.ts +0 -58
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
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
|
|
358
|
-
// extension changes what
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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`.
|