@solidrt/3d 0.0.54 → 0.0.56
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 +1121 -177
- package/README.md +15 -7
- package/examples/README.md +86 -5
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +20 -5
- package/examples/collision.tsx +250 -0
- package/examples/first-person.tsx +119 -0
- package/examples/fleet.tsx +194 -0
- package/examples/fog.tsx +173 -0
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +32 -6
- package/examples/pick.tsx +2 -4
- package/examples/probe.tsx +101 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +32 -3
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +86 -0
- package/examples/skybox.tsx +128 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +132 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +199 -0
- package/src/color.ts +32 -0
- package/src/components.tsx +773 -82
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +306 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1390 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +57 -9
- package/src/light.ts +383 -0
- package/src/material.ts +682 -216
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +211 -34
- package/src/node.ts +506 -0
- package/src/orbit.ts +97 -48
- package/src/scene-pointer.ts +166 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1304 -1532
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
package/src/gltf.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// glTF 2.0, the subset an app needs to show authored models: the scene's
|
|
2
|
-
// node tree
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// node tree RETAINED - a node table of local TRS with parent links (matrix
|
|
3
|
+
// nodes decomposed; shear dropped), vertices in NODE-LOCAL space, one part
|
|
4
|
+
// per mesh primitive referencing its node - triangles with positions,
|
|
5
|
+
// normals (flat ones generated when absent, per the spec), one UV set and
|
|
6
|
+
// indices, and materials reduced to what lit()/unlit() draw - base color
|
|
7
|
+
// factor and texture, normal map (with scale), emissive factor and map
|
|
8
|
+
// (KHR_materials_emissive_strength as the intensity), double-sidedness, alpha
|
|
9
|
+
// blending and masking - plus the file's animations as baked clips
|
|
10
|
+
// (times/values per channel, the mixer's food). Nodes with no part below
|
|
11
|
+
// them and no animation channel targeting them (cameras, lights, unused
|
|
12
|
+
// empties) are pruned. Both containers: .gltf JSON with external or
|
|
13
|
+
// data: buffers and images, and single-file .glb.
|
|
8
14
|
//
|
|
9
15
|
// Pure module by design - a parse is JSON plus typed-array views plus one
|
|
10
16
|
// interleave loop per primitive, so it runs the same under bun (the bake
|
|
@@ -14,19 +20,22 @@
|
|
|
14
20
|
//
|
|
15
21
|
// Outside the subset: Draco/meshopt-compressed meshes and any other
|
|
16
22
|
// required extension throw naming it; non-triangle primitives, sparse
|
|
17
|
-
// accessors and morph
|
|
18
|
-
// tangents and further UV sets are dropped
|
|
19
|
-
// slot for them yet).
|
|
23
|
+
// accessors and morph targets (the "weights" channel path) are skipped
|
|
24
|
+
// or ignored; vertex colors, tangents and further UV sets are dropped
|
|
25
|
+
// (the standard layout has no slot for them yet). Skins ARE parsed:
|
|
26
|
+
// joints, inverse binds, and the "skinned" vertex layout.
|
|
20
27
|
|
|
21
|
-
import { compose, mat4, multiply
|
|
28
|
+
import { compose, decompose, det3, mat4, multiply } from "./math.ts"
|
|
29
|
+
import { linearToSrgb } from "./color.ts"
|
|
22
30
|
import type { Mat4, Quat, Vec3 } from "./math.ts"
|
|
23
|
-
import { packGeometry, STANDARD_FLOATS } from "./geometry.ts"
|
|
31
|
+
import { layoutStride, packGeometry, packIndices, STANDARD_FLOATS } from "./geometry.ts"
|
|
24
32
|
import type { Geometry } from "./geometry.ts"
|
|
25
33
|
|
|
26
34
|
/** What lit()/unlit() take from a glTF material. */
|
|
27
35
|
export type ModelMaterial = {
|
|
28
36
|
name: string
|
|
29
|
-
/** Straight [r, g, b, a] 0..1
|
|
37
|
+
/** Straight [r, g, b, a] 0..1: glTF's LINEAR baseColorFactor encoded
|
|
38
|
+
* to sRGB, what lit({ color }) takes. */
|
|
30
39
|
color: [number, number, number, number]
|
|
31
40
|
/** Index into ModelData.images (the base color texture), or null. */
|
|
32
41
|
map: number | null
|
|
@@ -40,26 +49,129 @@ export type ModelMaterial = {
|
|
|
40
49
|
alphaMode: "OPAQUE" | "MASK" | "BLEND"
|
|
41
50
|
/** glTF alphaCutoff (default 0.5); meaningful for MASK only. */
|
|
42
51
|
alphaCutoff: number
|
|
52
|
+
/** Index into ModelData.images (the tangent-space normal map), or null. */
|
|
53
|
+
normalMap: number | null
|
|
54
|
+
/** glTF normalTexture.scale (default 1); meaningful with normalMap. */
|
|
55
|
+
normalScale: number
|
|
56
|
+
/** glTF's linear emissiveFactor (default [0, 0, 0] = off) encoded to
|
|
57
|
+
* sRGB, what lit({ emissive }) takes. */
|
|
58
|
+
emissive: [number, number, number]
|
|
59
|
+
/** KHR_materials_emissive_strength (default 1): lit's emissiveIntensity. */
|
|
60
|
+
emissiveIntensity: number
|
|
61
|
+
/** Index into ModelData.images (the emissive map), or null. */
|
|
62
|
+
emissiveMap: number | null
|
|
63
|
+
/** glTF metallicFactor (default 1): standard's metalness. */
|
|
64
|
+
metalness: number
|
|
65
|
+
/** glTF roughnessFactor (default 1): standard's roughness. */
|
|
66
|
+
roughness: number
|
|
67
|
+
/** Index into ModelData.images (glTF's ONE packed
|
|
68
|
+
* metallicRoughnessTexture: green = roughness, blue = metalness), or
|
|
69
|
+
* null; createModel hands it to `material` as both maps.metalnessMap
|
|
70
|
+
* and maps.roughnessMap, standard's two channel-select options. */
|
|
71
|
+
metalnessRoughnessMap: number | null
|
|
43
72
|
}
|
|
44
73
|
|
|
45
|
-
/** One
|
|
74
|
+
/** One node of the model's retained hierarchy: a local TRS under `parent`
|
|
75
|
+
* (an earlier index into ModelData.nodes, or null for a root). Parts and
|
|
76
|
+
* - in rigged models - skin joints and animation tracks reference nodes
|
|
77
|
+
* by index. */
|
|
78
|
+
export type ModelNode = {
|
|
79
|
+
/** The glTF node's name (or `node<i>`). */
|
|
80
|
+
name: string
|
|
81
|
+
/** Index of the parent in ModelData.nodes (always lower - the table is
|
|
82
|
+
* in pre-order), or null for a root node. */
|
|
83
|
+
parent: number | null
|
|
84
|
+
position: Vec3
|
|
85
|
+
/** Unit quaternion [x, y, z, w]. */
|
|
86
|
+
rotation: Quat
|
|
87
|
+
scale: Vec3
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One drawable: a mesh node's primitive. Vertices are LOCAL to its
|
|
91
|
+
* node - except a skinned part, whose vertices are in MODEL space at the
|
|
92
|
+
* bind pose (the spec ignores a skinned node's transform; the skin
|
|
93
|
+
* matrices place them). */
|
|
46
94
|
export type ModelPart = {
|
|
47
|
-
/** The
|
|
48
|
-
*
|
|
95
|
+
/** The owning node's name; a node whose mesh has several primitives
|
|
96
|
+
* numbers them `name#<k>`. */
|
|
49
97
|
name: string
|
|
98
|
+
/** Index into ModelData.nodes - the node whose world transform places
|
|
99
|
+
* this part (unused for placement when `skin` is set). */
|
|
100
|
+
node: number
|
|
101
|
+
/** Index into ModelData.skins, or null. A skinned part's geometry has
|
|
102
|
+
* the "skinned" layout (aJoints/aWeights after the standard prefix). */
|
|
103
|
+
skin: number | null
|
|
50
104
|
geometry: Geometry
|
|
51
105
|
/** Index into ModelData.materials. */
|
|
52
106
|
material: number
|
|
53
107
|
}
|
|
54
108
|
|
|
109
|
+
/** One skin: the joints (as node-table indices) and each joint's inverse
|
|
110
|
+
* bind matrix - model space to joint space at the bind pose. The palette
|
|
111
|
+
* a skinned draw needs is jointWorld x inverseBind per joint. */
|
|
112
|
+
export type ModelSkin = {
|
|
113
|
+
/** Indices into ModelData.nodes, one per joint. */
|
|
114
|
+
joints: number[]
|
|
115
|
+
/** 16 floats per joint, column-major, in joint order. */
|
|
116
|
+
inverseBind: Float32Array
|
|
117
|
+
/** 6 floats per joint: the box, in the JOINT's space, of every vertex it
|
|
118
|
+
* influences - the runtime culls a skinned part by the union of these
|
|
119
|
+
* carried through the posed joints, so the box follows the animation
|
|
120
|
+
* (Godot's and Unity's per-bone bounds). A joint influencing nothing
|
|
121
|
+
* holds an inverted box (min > max). */
|
|
122
|
+
jointBounds: Float32Array
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One animated property of one node: baked key times and values. */
|
|
126
|
+
export type ModelChannel = {
|
|
127
|
+
/** Index into ModelData.nodes. */
|
|
128
|
+
node: number
|
|
129
|
+
path: "position" | "rotation" | "scale"
|
|
130
|
+
/** glTF's three: "step" holds each key, "linear" lerps (a rotation
|
|
131
|
+
* slerps), "cubic" is CUBICSPLINE - a Hermite with per-key tangents. */
|
|
132
|
+
interpolation: "step" | "linear" | "cubic"
|
|
133
|
+
/** Key times in seconds, ascending. */
|
|
134
|
+
times: Float32Array
|
|
135
|
+
/** One element per key: 3 floats for position/scale, 4 (a quaternion)
|
|
136
|
+
* for rotation. "cubic" stores THREE elements per key - in-tangent,
|
|
137
|
+
* value, out-tangent, in that order. */
|
|
138
|
+
values: Float32Array
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** One named animation: baked channels over the model's nodes - what the
|
|
142
|
+
* mixer plays and what animation-core will consume. */
|
|
143
|
+
export type ModelClip = {
|
|
144
|
+
name: string
|
|
145
|
+
/** Seconds: the largest key time over the channels. */
|
|
146
|
+
duration: number
|
|
147
|
+
channels: ModelChannel[]
|
|
148
|
+
/** @internal The core clip id once a mixer registered it; freed by
|
|
149
|
+
* model.dispose. */
|
|
150
|
+
_core?: number
|
|
151
|
+
/** @internal Root-corrected core clips (travel pinned and/or height
|
|
152
|
+
* rebased) by variant key, registered on first such play; freed by
|
|
153
|
+
* model.dispose. */
|
|
154
|
+
_coreVariants?: Map<string, number>
|
|
155
|
+
}
|
|
156
|
+
|
|
55
157
|
/** A parsed model: plain data, no GPU resources. What parseGltf and
|
|
56
158
|
* decodeModel produce and createModel consumes. */
|
|
57
159
|
export type ModelData = {
|
|
160
|
+
/** The retained node hierarchy, pre-order (parents before children),
|
|
161
|
+
* pruned to nodes with a part somewhere below them or an animation
|
|
162
|
+
* channel targeting them. */
|
|
163
|
+
nodes: ModelNode[]
|
|
58
164
|
parts: ModelPart[]
|
|
165
|
+
/** The skins the parts reference (empty for an unrigged model). */
|
|
166
|
+
skins: ModelSkin[]
|
|
167
|
+
/** The file's animations (empty when it has none). */
|
|
168
|
+
clips: ModelClip[]
|
|
59
169
|
materials: ModelMaterial[]
|
|
60
170
|
/** Encoded image files (PNG/JPEG bytes) the materials' `map` index. */
|
|
61
171
|
images: Uint8Array[]
|
|
62
|
-
/** World-space [minX, minY, minZ, maxX, maxY, maxZ] over every
|
|
172
|
+
/** World-space rest-pose [minX, minY, minZ, maxX, maxY, maxZ] over every
|
|
173
|
+
* part - each part's local box through its node's composed transform, so
|
|
174
|
+
* it is conservative (not vertex-tight) for parts under rotated nodes. */
|
|
63
175
|
bounds: Float32Array
|
|
64
176
|
}
|
|
65
177
|
|
|
@@ -75,6 +187,11 @@ const MODE_TRIANGLES = 4
|
|
|
75
187
|
// The spec's alphaCutoff when a MASK material leaves it out.
|
|
76
188
|
const GLTF_ALPHA_CUTOFF = 0.5
|
|
77
189
|
|
|
190
|
+
// The "skinned" layout's stride (standard prefix + aJoints + aWeights).
|
|
191
|
+
const SKINNED_FLOATS = layoutStride("skinned")
|
|
192
|
+
|
|
193
|
+
const IDENTITY = mat4()
|
|
194
|
+
|
|
78
195
|
const COMPONENT_BYTES: Record<number, number> = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 }
|
|
79
196
|
const TYPE_ELEMENTS: Record<string, number> = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 }
|
|
80
197
|
|
|
@@ -86,6 +203,14 @@ const DEFAULT_MATERIAL: ModelMaterial = {
|
|
|
86
203
|
transparent: false,
|
|
87
204
|
alphaMode: "OPAQUE",
|
|
88
205
|
alphaCutoff: GLTF_ALPHA_CUTOFF,
|
|
206
|
+
normalMap: null,
|
|
207
|
+
normalScale: 1,
|
|
208
|
+
emissive: [0, 0, 0],
|
|
209
|
+
emissiveIntensity: 1,
|
|
210
|
+
emissiveMap: null,
|
|
211
|
+
metalness: 1,
|
|
212
|
+
roughness: 1,
|
|
213
|
+
metalnessRoughnessMap: null,
|
|
89
214
|
}
|
|
90
215
|
|
|
91
216
|
/** True when the bytes are a .glb container (the "glTF" magic). */
|
|
@@ -93,12 +218,13 @@ export function isGlb(bytes: Uint8Array): boolean {
|
|
|
93
218
|
return bytes.length >= 12 && new DataView(bytes.buffer, bytes.byteOffset, 12).getUint32(0, true) === GLB_MAGIC
|
|
94
219
|
}
|
|
95
220
|
|
|
96
|
-
/** The external uris a
|
|
97
|
-
*
|
|
98
|
-
*
|
|
221
|
+
/** The external uris a document references (buffers and images), so an
|
|
222
|
+
* async caller can fetch them before parseGltf. Usually empty for .glb -
|
|
223
|
+
* but a .glb MAY reference external files (some exporters write image
|
|
224
|
+
* uris), so this reads its JSON chunk rather than assuming. data: uris
|
|
225
|
+
* are never listed. */
|
|
99
226
|
export function gltfExternalUris(bytes: Uint8Array): string[] {
|
|
100
|
-
|
|
101
|
-
let gltf = JSON.parse(new TextDecoder().decode(bytes))
|
|
227
|
+
let gltf = isGlb(bytes) ? readGlb(bytes).json : JSON.parse(new TextDecoder().decode(bytes))
|
|
102
228
|
let uris: string[] = []
|
|
103
229
|
for (let item of [...(gltf.buffers ?? []), ...(gltf.images ?? [])]) {
|
|
104
230
|
if (typeof item.uri === "string" && !item.uri.startsWith("data:")) uris.push(item.uri)
|
|
@@ -107,9 +233,11 @@ export function gltfExternalUris(bytes: Uint8Array): string[] {
|
|
|
107
233
|
}
|
|
108
234
|
|
|
109
235
|
/**
|
|
110
|
-
* Parse a .glb or .gltf into ModelData. `resolve` supplies the bytes of
|
|
111
|
-
*
|
|
112
|
-
* percent-encoded);
|
|
236
|
+
* Parse a .glb or .gltf into ModelData. `resolve` supplies the bytes of
|
|
237
|
+
* external files by their uri as written in the document (still
|
|
238
|
+
* percent-encoded); a fully self-contained .glb needs none, but a .glb
|
|
239
|
+
* with external image uris (spec-legal, and real exporters write them)
|
|
240
|
+
* resolves the same way.
|
|
113
241
|
*/
|
|
114
242
|
export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
115
243
|
let gltf: any
|
|
@@ -128,13 +256,17 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
128
256
|
if (ext === "KHR_draco_mesh_compression" || ext === "EXT_meshopt_compression") {
|
|
129
257
|
throw new Error("parseGltf: the file's meshes are compressed (" + ext + "), which is not supported: re-export without mesh compression")
|
|
130
258
|
}
|
|
131
|
-
// Quantized attributes read through the normalized-integer path
|
|
132
|
-
//
|
|
133
|
-
|
|
259
|
+
// Quantized attributes read through the normalized-integer path,
|
|
260
|
+
// emissive strength is the emissive intensity; every other required
|
|
261
|
+
// extension changes what the file means.
|
|
262
|
+
if (ext !== "KHR_mesh_quantization" && ext !== "KHR_materials_emissive_strength") {
|
|
134
263
|
throw new Error("parseGltf: the file requires the " + ext + " extension, which is not supported")
|
|
135
264
|
}
|
|
136
265
|
}
|
|
137
266
|
|
|
267
|
+
// Also reachable from a .glb: the container usually embeds everything,
|
|
268
|
+
// but external uris are legal there too (real exporters use them for
|
|
269
|
+
// images), which is why loadGltf resolves for both containers.
|
|
138
270
|
let external = (uri: string, what: string): Uint8Array => {
|
|
139
271
|
if (uri.startsWith("data:")) return decodeDataUri(uri)
|
|
140
272
|
if (resolve === undefined) throw new Error("parseGltf: " + what + " references the external file " + uri + " and no resolver was given")
|
|
@@ -173,22 +305,36 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
173
305
|
return slot
|
|
174
306
|
}
|
|
175
307
|
|
|
308
|
+
// A texture reference's image slot, or null (the reference absent, or
|
|
309
|
+
// its texture imageless). Further UV sets are outside the subset, so a
|
|
310
|
+
// non-zero texCoord is ignored and the map samples the one UV set.
|
|
311
|
+
let textureSlot = (ref: any): number | null => {
|
|
312
|
+
if (ref === undefined) return null
|
|
313
|
+
let texture = gltf.textures?.[ref.index]
|
|
314
|
+
return texture?.source !== undefined ? imageSlot(texture.source) : null
|
|
315
|
+
}
|
|
176
316
|
let materials: ModelMaterial[] = (gltf.materials ?? []).map((m: any, i: number): ModelMaterial => {
|
|
177
317
|
let pbr = m.pbrMetallicRoughness ?? {}
|
|
178
318
|
let factor = pbr.baseColorFactor ?? [1, 1, 1, 1]
|
|
179
|
-
let
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (texture?.source !== undefined) map = imageSlot(texture.source)
|
|
183
|
-
}
|
|
319
|
+
let emissiveFactor: number[] = m.emissiveFactor ?? [0, 0, 0]
|
|
320
|
+
let strength = m.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1
|
|
321
|
+
// glTF factors are linear; the material options are sRGB.
|
|
184
322
|
return {
|
|
185
323
|
name: m.name ?? "material" + i,
|
|
186
|
-
color: [factor[0], factor[1], factor[2], factor[3] ?? 1],
|
|
187
|
-
map,
|
|
324
|
+
color: [linearToSrgb(factor[0]), linearToSrgb(factor[1]), linearToSrgb(factor[2]), factor[3] ?? 1],
|
|
325
|
+
map: textureSlot(pbr.baseColorTexture),
|
|
188
326
|
doubleSided: m.doubleSided === true,
|
|
189
327
|
transparent: m.alphaMode === "BLEND",
|
|
190
328
|
alphaMode: m.alphaMode === "MASK" || m.alphaMode === "BLEND" ? m.alphaMode : "OPAQUE",
|
|
191
329
|
alphaCutoff: typeof m.alphaCutoff === "number" ? m.alphaCutoff : GLTF_ALPHA_CUTOFF,
|
|
330
|
+
normalMap: textureSlot(m.normalTexture),
|
|
331
|
+
normalScale: typeof m.normalTexture?.scale === "number" ? m.normalTexture.scale : 1,
|
|
332
|
+
emissive: [linearToSrgb(emissiveFactor[0] ?? 0), linearToSrgb(emissiveFactor[1] ?? 0), linearToSrgb(emissiveFactor[2] ?? 0)],
|
|
333
|
+
emissiveIntensity: strength,
|
|
334
|
+
emissiveMap: textureSlot(m.emissiveTexture),
|
|
335
|
+
metalness: typeof pbr.metallicFactor === "number" ? pbr.metallicFactor : 1,
|
|
336
|
+
roughness: typeof pbr.roughnessFactor === "number" ? pbr.roughnessFactor : 1,
|
|
337
|
+
metalnessRoughnessMap: textureSlot(pbr.metallicRoughnessTexture),
|
|
192
338
|
}
|
|
193
339
|
})
|
|
194
340
|
// Primitives without a material draw the spec's default; it is appended
|
|
@@ -218,11 +364,25 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
218
364
|
return { data: out, elements, count: acc.count }
|
|
219
365
|
}
|
|
220
366
|
|
|
367
|
+
// The node table is built lazily: a glTF node gets a slot only when a
|
|
368
|
+
// part somewhere below it needs the chain (materialize walks ancestors
|
|
369
|
+
// first, so the table stays in pre-order), which is what prunes cameras,
|
|
370
|
+
// lights and unused empties.
|
|
371
|
+
type PendingNode = { name: string; parent: PendingNode | null; position: Vec3; rotation: Quat; scale: Vec3; index: number | null }
|
|
372
|
+
let nodes: ModelNode[] = []
|
|
373
|
+
let materialize = (p: PendingNode): number => {
|
|
374
|
+
if (p.index !== null) return p.index
|
|
375
|
+
let parent = p.parent === null ? null : materialize(p.parent)
|
|
376
|
+
p.index = nodes.length
|
|
377
|
+
nodes.push({ name: p.name, parent, position: p.position, rotation: p.rotation, scale: p.scale })
|
|
378
|
+
return p.index
|
|
379
|
+
}
|
|
380
|
+
|
|
221
381
|
let parts: ModelPart[] = []
|
|
382
|
+
let pendingJointBounds: { skin: number; positions: Float32Array; count: number; joints: Float32Array; weights: Float32Array }[] = []
|
|
222
383
|
let bounds = new Float32Array([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity])
|
|
223
|
-
let normal = mat4()
|
|
224
384
|
|
|
225
|
-
let emit = (prim: any, name: string, world: Mat4): void => {
|
|
385
|
+
let emit = (prim: any, name: string, node: number, world: Mat4, skin: number | null): void => {
|
|
226
386
|
if ((prim.mode ?? MODE_TRIANGLES) !== MODE_TRIANGLES) return
|
|
227
387
|
if (prim.attributes?.POSITION === undefined) return
|
|
228
388
|
let pos = accessorFloats(prim.attributes.POSITION, name + " POSITION")
|
|
@@ -230,18 +390,45 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
230
390
|
let count = pos.count
|
|
231
391
|
let nrm = prim.attributes.NORMAL !== undefined ? accessorFloats(prim.attributes.NORMAL, name + " NORMAL").data : null
|
|
232
392
|
let uv = prim.attributes.TEXCOORD_0 !== undefined ? accessorFloats(prim.attributes.TEXCOORD_0, name + " TEXCOORD_0").data : null
|
|
393
|
+
// A skinned primitive: joints and weights become the "skinned"
|
|
394
|
+
// layout's extra channels; its vertices are ALREADY model-space bind
|
|
395
|
+
// pose (the node transform is ignored, per spec), so no flip and
|
|
396
|
+
// identity bounds.
|
|
397
|
+
let skinned = skin !== null
|
|
398
|
+
let joints: Float32Array | null = null
|
|
399
|
+
let weights: Float32Array | null = null
|
|
400
|
+
if (skinned) {
|
|
401
|
+
if (prim.attributes.JOINTS_0 === undefined || prim.attributes.WEIGHTS_0 === undefined) {
|
|
402
|
+
throw new Error("parseGltf: " + name + " is skinned but lacks JOINTS_0/WEIGHTS_0")
|
|
403
|
+
}
|
|
404
|
+
let j = accessorFloats(prim.attributes.JOINTS_0, name + " JOINTS_0")
|
|
405
|
+
let w = accessorFloats(prim.attributes.WEIGHTS_0, name + " WEIGHTS_0")
|
|
406
|
+
if (j.elements !== 4 || w.elements !== 4) throw new Error("parseGltf: " + name + " JOINTS_0/WEIGHTS_0 are not VEC4")
|
|
407
|
+
joints = j.data
|
|
408
|
+
weights = w.data
|
|
409
|
+
}
|
|
410
|
+
let stride = skinned ? SKINNED_FLOATS : STANDARD_FLOATS
|
|
233
411
|
let indices: ArrayLike<number> =
|
|
234
412
|
prim.indices !== undefined ? accessorFloats(prim.indices, name + " indices").data : Array.from({ length: count }, (_, i) => i)
|
|
235
413
|
if (indices.length % 3 !== 0) throw new Error("parseGltf: " + name + " index count is not a multiple of 3")
|
|
236
414
|
|
|
237
|
-
|
|
238
|
-
|
|
415
|
+
// A mirroring chain (negative world determinant at the rest pose)
|
|
416
|
+
// flips the displayed winding, so the index order is flipped here to
|
|
417
|
+
// compensate - cull: "back" keeps the outside. Stored normals stay the
|
|
418
|
+
// authored ones: the runtime's inverse-transpose maps them outward for
|
|
419
|
+
// the flipped winding. Baked from the REST pose; a scale animated
|
|
420
|
+
// across zero would unbake it, which is pathological.
|
|
421
|
+
let flip = !skinned && det3(world) < 0
|
|
422
|
+
// Joint boxes need the skin's inverse binds, and skins are built after
|
|
423
|
+
// the walk (their joints are ordinary nodes the walk registers), so
|
|
424
|
+
// the arrays are parked under the FILE's skin index and grown then.
|
|
425
|
+
if (skinned) pendingJointBounds.push({ skin: skin!, positions: pos.data, count: pos.count, joints: joints!, weights: weights! })
|
|
239
426
|
|
|
240
427
|
let vertices: Float32Array
|
|
241
428
|
let packedIndices: number[]
|
|
242
429
|
if (nrm !== null) {
|
|
243
|
-
vertices = new Float32Array(count *
|
|
244
|
-
for (let i = 0; i < count; i++) writeVertex(vertices, i, pos.data, nrm, uv, i,
|
|
430
|
+
vertices = new Float32Array(count * stride)
|
|
431
|
+
for (let i = 0; i < count; i++) writeVertex(vertices, i, pos.data, nrm, uv, i, stride, joints, weights)
|
|
245
432
|
packedIndices = new Array(indices.length)
|
|
246
433
|
for (let i = 0; i < indices.length; i += 3) {
|
|
247
434
|
packedIndices[i] = indices[i]!
|
|
@@ -251,22 +438,28 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
251
438
|
} else {
|
|
252
439
|
// No normals: the spec asks for flat shading, which needs one vertex
|
|
253
440
|
// per triangle corner, so the primitive is un-indexed here and each
|
|
254
|
-
// corner takes its face normal
|
|
255
|
-
//
|
|
441
|
+
// corner takes its face normal - of the AUTHORED winding (negated
|
|
442
|
+
// after a flip), the direction authored normals would have, so the
|
|
443
|
+
// runtime maps both the same way.
|
|
256
444
|
let triangles = indices.length / 3
|
|
257
|
-
vertices = new Float32Array(triangles * 3 *
|
|
445
|
+
vertices = new Float32Array(triangles * 3 * stride)
|
|
258
446
|
packedIndices = new Array(triangles * 3)
|
|
259
447
|
let face: Vec3 = [0, 0, 0]
|
|
260
448
|
for (let t = 0; t < triangles; t++) {
|
|
261
449
|
let a = indices[t * 3]!, b = indices[t * 3 + 1]!, c = indices[t * 3 + 2]!
|
|
262
450
|
if (flip) [b, c] = [c, b]
|
|
263
451
|
let out = t * 3
|
|
264
|
-
writeVertex(vertices, out, pos.data, null, uv, a,
|
|
265
|
-
writeVertex(vertices, out + 1, pos.data, null, uv, b,
|
|
266
|
-
writeVertex(vertices, out + 2, pos.data, null, uv, c,
|
|
267
|
-
faceNormal(face, vertices, out)
|
|
452
|
+
writeVertex(vertices, out, pos.data, null, uv, a, stride, joints, weights)
|
|
453
|
+
writeVertex(vertices, out + 1, pos.data, null, uv, b, stride, joints, weights)
|
|
454
|
+
writeVertex(vertices, out + 2, pos.data, null, uv, c, stride, joints, weights)
|
|
455
|
+
faceNormal(face, vertices, out, stride)
|
|
456
|
+
if (flip) {
|
|
457
|
+
face[0] = -face[0]
|
|
458
|
+
face[1] = -face[1]
|
|
459
|
+
face[2] = -face[2]
|
|
460
|
+
}
|
|
268
461
|
for (let k = 0; k < 3; k++) {
|
|
269
|
-
let at = (out + k) *
|
|
462
|
+
let at = (out + k) * stride + 3
|
|
270
463
|
vertices[at] = face[0]
|
|
271
464
|
vertices[at + 1] = face[1]
|
|
272
465
|
vertices[at + 2] = face[2]
|
|
@@ -276,15 +469,32 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
276
469
|
count = triangles * 3
|
|
277
470
|
}
|
|
278
471
|
|
|
472
|
+
// Model bounds: the part's local box through the node's rest-pose
|
|
473
|
+
// world transform (8 corners - conservative under rotation, exact
|
|
474
|
+
// under translation and axis-aligned scale). Skinned vertices are
|
|
475
|
+
// already model-space, so their box goes through identity.
|
|
476
|
+
let bw = skinned ? IDENTITY : world
|
|
477
|
+
let lo: Vec3 = [Infinity, Infinity, Infinity]
|
|
478
|
+
let hi: Vec3 = [-Infinity, -Infinity, -Infinity]
|
|
279
479
|
for (let i = 0; i < count; i++) {
|
|
280
|
-
let at = i *
|
|
281
|
-
let
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
480
|
+
let at = i * stride
|
|
481
|
+
for (let k = 0; k < 3; k++) {
|
|
482
|
+
let v = vertices[at + k]!
|
|
483
|
+
if (v < lo[k]!) lo[k] = v
|
|
484
|
+
if (v > hi[k]!) hi[k] = v
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
488
|
+
let x = corner & 1 ? hi[0] : lo[0], y = corner & 2 ? hi[1] : lo[1], z = corner & 4 ? hi[2] : lo[2]
|
|
489
|
+
let wx = bw[0] * x + bw[4] * y + bw[8] * z + bw[12]
|
|
490
|
+
let wy = bw[1] * x + bw[5] * y + bw[9] * z + bw[13]
|
|
491
|
+
let wz = bw[2] * x + bw[6] * y + bw[10] * z + bw[14]
|
|
492
|
+
if (wx < bounds[0]!) bounds[0] = wx
|
|
493
|
+
if (wy < bounds[1]!) bounds[1] = wy
|
|
494
|
+
if (wz < bounds[2]!) bounds[2] = wz
|
|
495
|
+
if (wx > bounds[3]!) bounds[3] = wx
|
|
496
|
+
if (wy > bounds[4]!) bounds[4] = wy
|
|
497
|
+
if (wz > bounds[5]!) bounds[5] = wz
|
|
288
498
|
}
|
|
289
499
|
|
|
290
500
|
let material = prim.material
|
|
@@ -295,32 +505,160 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
295
505
|
}
|
|
296
506
|
material = defaultMaterial
|
|
297
507
|
}
|
|
298
|
-
|
|
508
|
+
let geometry: Geometry = skinned
|
|
509
|
+
? { vertices, indices: packIndices(packedIndices, count), layout: "skinned", label: name }
|
|
510
|
+
: packGeometry(vertices, packedIndices, { label: name })
|
|
511
|
+
parts.push({ name, node, skin, geometry, material })
|
|
299
512
|
}
|
|
300
513
|
|
|
301
514
|
let local = mat4()
|
|
302
|
-
let
|
|
515
|
+
let pendingByIndex = new Map<number, PendingNode>()
|
|
516
|
+
let walk = (index: number, parent: PendingNode | null, parentWorld: Mat4): void => {
|
|
303
517
|
let node = gltf.nodes[index]
|
|
304
518
|
if (node === undefined) throw new Error("parseGltf: scene names a missing node " + index)
|
|
519
|
+
let position: Vec3 = [0, 0, 0]
|
|
520
|
+
let rotation: Quat = [0, 0, 0, 1]
|
|
521
|
+
let scale: Vec3 = [1, 1, 1]
|
|
522
|
+
if (node.matrix !== undefined) {
|
|
523
|
+
for (let i = 0; i < 16; i++) local[i] = node.matrix[i]
|
|
524
|
+
decompose(local, position, rotation, scale)
|
|
525
|
+
} else {
|
|
526
|
+
if (node.translation !== undefined) position = [node.translation[0], node.translation[1], node.translation[2]]
|
|
527
|
+
if (node.rotation !== undefined) rotation = [node.rotation[0], node.rotation[1], node.rotation[2], node.rotation[3]]
|
|
528
|
+
if (node.scale !== undefined) scale = [node.scale[0], node.scale[1], node.scale[2]]
|
|
529
|
+
}
|
|
530
|
+
// The world composes from the TRS (not a matrix node's raw matrix), so
|
|
531
|
+
// winding and bounds match what the runtime will render.
|
|
305
532
|
let world = mat4()
|
|
306
|
-
multiply(world,
|
|
533
|
+
multiply(world, parentWorld, compose(local, position, rotation, scale))
|
|
534
|
+
let pending: PendingNode = { name: node.name ?? "node" + index, parent, position, rotation, scale, index: null }
|
|
535
|
+
pendingByIndex.set(index, pending)
|
|
307
536
|
if (node.mesh !== undefined) {
|
|
308
537
|
let mesh = gltf.meshes[node.mesh]
|
|
309
538
|
if (mesh === undefined) throw new Error("parseGltf: node " + index + " names a missing mesh " + node.mesh)
|
|
310
|
-
|
|
539
|
+
if (node.name === undefined && mesh.name !== undefined) pending.name = mesh.name
|
|
540
|
+
let slot = materialize(pending)
|
|
541
|
+
let skin = typeof node.skin === "number" ? node.skin : null
|
|
311
542
|
let prims: any[] = mesh.primitives ?? []
|
|
312
|
-
for (let k = 0; k < prims.length; k++) emit(prims[k], prims.length > 1 ? name + "#" + k : name, world)
|
|
543
|
+
for (let k = 0; k < prims.length; k++) emit(prims[k], prims.length > 1 ? pending.name + "#" + k : pending.name, slot, world, skin)
|
|
313
544
|
}
|
|
314
|
-
for (let child of node.children ?? []) walk(child, world)
|
|
545
|
+
for (let child of node.children ?? []) walk(child, pending, world)
|
|
315
546
|
}
|
|
316
547
|
|
|
317
548
|
let scene = gltf.scenes?.[gltf.scene ?? 0]
|
|
318
549
|
let roots: number[] = scene?.nodes ?? (gltf.nodes ?? []).map((_: unknown, i: number) => i)
|
|
319
550
|
let root = mat4()
|
|
320
|
-
for (let index of roots) walk(index, root)
|
|
551
|
+
for (let index of roots) walk(index, null, root)
|
|
552
|
+
|
|
553
|
+
// Skins, after the walk: joints are ordinary nodes (usually meshless),
|
|
554
|
+
// materialized here so the retained table carries them; part.skin
|
|
555
|
+
// remaps from the file's skin index to the compact list.
|
|
556
|
+
let skins: ModelSkin[] = []
|
|
557
|
+
// Per-joint bounds, grown from the parked skinned primitives once the
|
|
558
|
+
// compact skins exist: each influenced vertex (weight above zero) goes
|
|
559
|
+
// through the joint's inverse bind into joint space and grows that
|
|
560
|
+
// joint's box.
|
|
561
|
+
let growJointBounds = (skin: ModelSkin, positions: Float32Array, count: number, joints: Float32Array, weights: Float32Array): void => {
|
|
562
|
+
let jb = skin.jointBounds
|
|
563
|
+
let ib = skin.inverseBind
|
|
564
|
+
for (let i = 0; i < count; i++) {
|
|
565
|
+
let x = positions[i * 3]!, y = positions[i * 3 + 1]!, z = positions[i * 3 + 2]!
|
|
566
|
+
for (let k = 0; k < 4; k++) {
|
|
567
|
+
if (weights[i * 4 + k]! <= 0) continue
|
|
568
|
+
let j = joints[i * 4 + k]!
|
|
569
|
+
if (j >= skin.joints.length) continue
|
|
570
|
+
let m = j * 16
|
|
571
|
+
let px = ib[m]! * x + ib[m + 4]! * y + ib[m + 8]! * z + ib[m + 12]!
|
|
572
|
+
let py = ib[m + 1]! * x + ib[m + 5]! * y + ib[m + 9]! * z + ib[m + 13]!
|
|
573
|
+
let pz = ib[m + 2]! * x + ib[m + 6]! * y + ib[m + 10]! * z + ib[m + 14]!
|
|
574
|
+
let b = j * 6
|
|
575
|
+
if (px < jb[b]!) jb[b] = px
|
|
576
|
+
if (py < jb[b + 1]!) jb[b + 1] = py
|
|
577
|
+
if (pz < jb[b + 2]!) jb[b + 2] = pz
|
|
578
|
+
if (px > jb[b + 3]!) jb[b + 3] = px
|
|
579
|
+
if (py > jb[b + 4]!) jb[b + 4] = py
|
|
580
|
+
if (pz > jb[b + 5]!) jb[b + 5] = pz
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
let skinSlots = new Map<number, number>()
|
|
585
|
+
for (let part of parts) {
|
|
586
|
+
if (part.skin === null) continue
|
|
587
|
+
let slot = skinSlots.get(part.skin)
|
|
588
|
+
if (slot === undefined) {
|
|
589
|
+
let sk = gltf.skins?.[part.skin]
|
|
590
|
+
if (sk === undefined) throw new Error("parseGltf: part '" + part.name + "' names a missing skin " + part.skin)
|
|
591
|
+
let jointIndices: number[] = (sk.joints ?? []).map((j: number): number => {
|
|
592
|
+
let pending = pendingByIndex.get(j)
|
|
593
|
+
if (pending === undefined) throw new Error("parseGltf: skin joint node " + j + " is not in the scene")
|
|
594
|
+
return materialize(pending)
|
|
595
|
+
})
|
|
596
|
+
if (jointIndices.length === 0) throw new Error("parseGltf: skin " + part.skin + " has no joints")
|
|
597
|
+
let inverseBind: Float32Array
|
|
598
|
+
if (sk.inverseBindMatrices !== undefined) {
|
|
599
|
+
let acc = accessorFloats(sk.inverseBindMatrices, "skin inverseBindMatrices")
|
|
600
|
+
if (acc.elements !== 16 || acc.count !== jointIndices.length) {
|
|
601
|
+
throw new Error("parseGltf: skin inverseBindMatrices does not match the joint count")
|
|
602
|
+
}
|
|
603
|
+
inverseBind = acc.data
|
|
604
|
+
} else {
|
|
605
|
+
// The spec default: identity binds.
|
|
606
|
+
inverseBind = new Float32Array(jointIndices.length * 16)
|
|
607
|
+
for (let j = 0; j < jointIndices.length; j++) {
|
|
608
|
+
inverseBind[j * 16] = 1
|
|
609
|
+
inverseBind[j * 16 + 5] = 1
|
|
610
|
+
inverseBind[j * 16 + 10] = 1
|
|
611
|
+
inverseBind[j * 16 + 15] = 1
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
slot = skins.length
|
|
615
|
+
let jointBounds = new Float32Array(jointIndices.length * 6)
|
|
616
|
+
for (let j = 0; j < jointIndices.length; j++) jointBounds.set([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], j * 6)
|
|
617
|
+
skins.push({ joints: jointIndices, inverseBind, jointBounds })
|
|
618
|
+
skinSlots.set(part.skin, slot)
|
|
619
|
+
}
|
|
620
|
+
part.skin = slot
|
|
621
|
+
}
|
|
622
|
+
for (let parked of pendingJointBounds) {
|
|
623
|
+
let slot = skinSlots.get(parked.skin)
|
|
624
|
+
if (slot !== undefined) growJointBounds(skins[slot]!, parked.positions, parked.count, parked.joints, parked.weights)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Animations, after the walk so channels can materialize their target
|
|
628
|
+
// nodes (a channel may target a meshless node - a joint, a rig pivot).
|
|
629
|
+
const CHANNEL_PATHS: Record<string, ModelChannel["path"]> = { translation: "position", rotation: "rotation", scale: "scale" }
|
|
630
|
+
let clips: ModelClip[] = (gltf.animations ?? []).map((anim: any, ai: number): ModelClip => {
|
|
631
|
+
let clipName = anim.name ?? "clip" + ai
|
|
632
|
+
let channels: ModelChannel[] = []
|
|
633
|
+
let duration = 0
|
|
634
|
+
for (let ch of anim.channels ?? []) {
|
|
635
|
+
// Outside the subset: "weights" (morph targets) and extension paths.
|
|
636
|
+
let path = CHANNEL_PATHS[ch.target?.path]
|
|
637
|
+
if (path === undefined) continue
|
|
638
|
+
// A target outside the walked scene has nothing to move.
|
|
639
|
+
let pending = pendingByIndex.get(ch.target.node)
|
|
640
|
+
if (pending === undefined) continue
|
|
641
|
+
let sampler = anim.samplers?.[ch.sampler]
|
|
642
|
+
if (sampler === undefined) throw new Error("parseGltf: animation " + clipName + " channel names a missing sampler " + ch.sampler)
|
|
643
|
+
let what = "animation " + clipName
|
|
644
|
+
let input = accessorFloats(sampler.input, what + " input")
|
|
645
|
+
let output = accessorFloats(sampler.output, what + " output")
|
|
646
|
+
let interpolation: ModelChannel["interpolation"] =
|
|
647
|
+
sampler.interpolation === "STEP" ? "step" : sampler.interpolation === "CUBICSPLINE" ? "cubic" : "linear"
|
|
648
|
+
let elements = path === "rotation" ? 4 : 3
|
|
649
|
+
if (output.elements !== elements) throw new Error("parseGltf: " + what + " " + path + " output is not " + (elements === 4 ? "VEC4" : "VEC3"))
|
|
650
|
+
let perKey = interpolation === "cubic" ? 3 : 1
|
|
651
|
+
if (output.count !== input.count * perKey) {
|
|
652
|
+
throw new Error("parseGltf: " + what + " " + path + " has " + output.count + " values for " + input.count + " keys")
|
|
653
|
+
}
|
|
654
|
+
if (input.count > 0) duration = Math.max(duration, input.data[input.count - 1]!)
|
|
655
|
+
channels.push({ node: materialize(pending), path, interpolation, times: input.data, values: output.data })
|
|
656
|
+
}
|
|
657
|
+
return { name: clipName, duration, channels }
|
|
658
|
+
})
|
|
321
659
|
|
|
322
660
|
if (parts.length === 0) bounds.fill(0)
|
|
323
|
-
return { parts, materials, images, bounds }
|
|
661
|
+
return { nodes, parts, skins, clips, materials, images, bounds }
|
|
324
662
|
}
|
|
325
663
|
|
|
326
664
|
function readGlb(bytes: Uint8Array): { json: any; bin: Uint8Array | null } {
|
|
@@ -372,28 +710,11 @@ function readerFor(componentType: number, normalized: boolean): (dv: DataView, a
|
|
|
372
710
|
}
|
|
373
711
|
}
|
|
374
712
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (node.matrix !== undefined) {
|
|
381
|
-
for (let i = 0; i < 16; i++) out[i] = node.matrix[i]
|
|
382
|
-
return out
|
|
383
|
-
}
|
|
384
|
-
return compose(out, node.translation ?? NO_TRANSLATION, node.rotation ?? NO_ROTATION, node.scale ?? NO_SCALE)
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function det3(m: Mat4): number {
|
|
388
|
-
return (
|
|
389
|
-
m[0] * (m[5] * m[10] - m[6] * m[9]) -
|
|
390
|
-
m[4] * (m[1] * m[10] - m[2] * m[9]) +
|
|
391
|
-
m[8] * (m[1] * m[6] - m[2] * m[5])
|
|
392
|
-
)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// One interleaved vertex: position through the world matrix, normal (when
|
|
396
|
-
// given) through the normal matrix and re-normalized, uv copied or zero.
|
|
713
|
+
// One interleaved vertex, node-local: position and normal copied as
|
|
714
|
+
// authored (the runtime's uModel/uNormal do the placing), uv copied or
|
|
715
|
+
// zero, and for the skinned layout the joint indices plus their weights
|
|
716
|
+
// RENORMALIZED to sum 1 (quantized exports drift a little; the spec asks
|
|
717
|
+
// for normalized weights).
|
|
397
718
|
function writeVertex(
|
|
398
719
|
out: Float32Array,
|
|
399
720
|
slot: number,
|
|
@@ -401,42 +722,41 @@ function writeVertex(
|
|
|
401
722
|
nrm: Float32Array | null,
|
|
402
723
|
uv: Float32Array | null,
|
|
403
724
|
src: number,
|
|
404
|
-
|
|
405
|
-
|
|
725
|
+
stride: number,
|
|
726
|
+
joints: Float32Array | null = null,
|
|
727
|
+
weights: Float32Array | null = null,
|
|
406
728
|
): void {
|
|
407
|
-
let at = slot *
|
|
408
|
-
|
|
409
|
-
out[at
|
|
410
|
-
out[at +
|
|
411
|
-
out[at + 2] = world[2] * x + world[6] * y + world[10] * z + world[14]
|
|
729
|
+
let at = slot * stride
|
|
730
|
+
out[at] = pos[src * 3]!
|
|
731
|
+
out[at + 1] = pos[src * 3 + 1]!
|
|
732
|
+
out[at + 2] = pos[src * 3 + 2]!
|
|
412
733
|
if (nrm !== null) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
let wz = normal[2] * nx + normal[6] * ny + normal[10] * nz
|
|
417
|
-
let len = Math.hypot(wx, wy, wz)
|
|
418
|
-
if (len > 1e-12) {
|
|
419
|
-
wx /= len
|
|
420
|
-
wy /= len
|
|
421
|
-
wz /= len
|
|
422
|
-
} else {
|
|
423
|
-
wx = 0
|
|
424
|
-
wy = 1
|
|
425
|
-
wz = 0
|
|
426
|
-
}
|
|
427
|
-
out[at + 3] = wx
|
|
428
|
-
out[at + 4] = wy
|
|
429
|
-
out[at + 5] = wz
|
|
734
|
+
out[at + 3] = nrm[src * 3]!
|
|
735
|
+
out[at + 4] = nrm[src * 3 + 1]!
|
|
736
|
+
out[at + 5] = nrm[src * 3 + 2]!
|
|
430
737
|
}
|
|
431
738
|
if (uv !== null) {
|
|
432
739
|
out[at + 6] = uv[src * 2]!
|
|
433
740
|
out[at + 7] = uv[src * 2 + 1]!
|
|
434
741
|
}
|
|
742
|
+
if (joints !== null && weights !== null) {
|
|
743
|
+
let j = src * 4
|
|
744
|
+
out[at + 8] = joints[j]!
|
|
745
|
+
out[at + 9] = joints[j + 1]!
|
|
746
|
+
out[at + 10] = joints[j + 2]!
|
|
747
|
+
out[at + 11] = joints[j + 3]!
|
|
748
|
+
let sum = weights[j]! + weights[j + 1]! + weights[j + 2]! + weights[j + 3]!
|
|
749
|
+
let inv = sum > 1e-8 ? 1 / sum : 0
|
|
750
|
+
out[at + 12] = weights[j]! * inv
|
|
751
|
+
out[at + 13] = weights[j + 1]! * inv
|
|
752
|
+
out[at + 14] = weights[j + 2]! * inv
|
|
753
|
+
out[at + 15] = weights[j + 3]! * inv
|
|
754
|
+
}
|
|
435
755
|
}
|
|
436
756
|
|
|
437
757
|
// The unit normal of the triangle at three consecutive vertex slots.
|
|
438
|
-
function faceNormal(out: Vec3, v: Float32Array, first: number): void {
|
|
439
|
-
let a = first *
|
|
758
|
+
function faceNormal(out: Vec3, v: Float32Array, first: number, stride: number): void {
|
|
759
|
+
let a = first * stride, b = (first + 1) * stride, c = (first + 2) * stride
|
|
440
760
|
let abx = v[b]! - v[a]!, aby = v[b + 1]! - v[a + 1]!, abz = v[b + 2]! - v[a + 2]!
|
|
441
761
|
let acx = v[c]! - v[a]!, acy = v[c + 1]! - v[a + 1]!, acz = v[c + 2]! - v[a + 2]!
|
|
442
762
|
let nx = aby * acz - abz * acy
|