@driftengine/assets 3.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/assetPath.d.ts +51 -0
  5. package/dist/assetPath.js +59 -0
  6. package/dist/coarseLevel.d.ts +94 -0
  7. package/dist/coarseLevel.js +606 -0
  8. package/dist/dds.d.ts +41 -0
  9. package/dist/dds.js +425 -0
  10. package/dist/drawKey.d.ts +64 -0
  11. package/dist/drawKey.js +42 -0
  12. package/dist/drftLoader.d.ts +420 -0
  13. package/dist/drftLoader.js +996 -0
  14. package/dist/drftTextures.d.ts +87 -0
  15. package/dist/drftTextures.js +138 -0
  16. package/dist/fbx.d.ts +86 -0
  17. package/dist/fbx.js +792 -0
  18. package/dist/fbxHarness.d.ts +68 -0
  19. package/dist/fbxHarness.js +185 -0
  20. package/dist/fbxInflate.d.ts +60 -0
  21. package/dist/fbxInflate.js +204 -0
  22. package/dist/fbxMaterial.d.ts +87 -0
  23. package/dist/fbxMaterial.js +345 -0
  24. package/dist/fbxRig.d.ts +130 -0
  25. package/dist/fbxRig.js +601 -0
  26. package/dist/gltf.d.ts +242 -0
  27. package/dist/gltf.js +904 -0
  28. package/dist/gltfAccessor.d.ts +2 -0
  29. package/dist/gltfAccessor.js +106 -0
  30. package/dist/gltfSkin.d.ts +32 -0
  31. package/dist/gltfSkin.js +144 -0
  32. package/dist/iesPhotometry.d.ts +44 -0
  33. package/dist/iesPhotometry.js +94 -0
  34. package/dist/imageInfo.d.ts +31 -0
  35. package/dist/imageInfo.js +119 -0
  36. package/dist/index.d.ts +58 -0
  37. package/dist/index.js +132 -0
  38. package/dist/kn5.d.ts +158 -0
  39. package/dist/kn5.js +557 -0
  40. package/dist/loadProgress.d.ts +118 -0
  41. package/dist/loadProgress.js +1 -0
  42. package/dist/localise.d.ts +35 -0
  43. package/dist/localise.js +253 -0
  44. package/dist/obj.d.ts +54 -0
  45. package/dist/obj.js +335 -0
  46. package/dist/orient.d.ts +104 -0
  47. package/dist/orient.js +241 -0
  48. package/dist/radianceHdr.d.ts +36 -0
  49. package/dist/radianceHdr.js +172 -0
  50. package/dist/readModel.d.ts +153 -0
  51. package/dist/readModel.js +386 -0
  52. package/dist/recognise.d.ts +38 -0
  53. package/dist/recognise.js +97 -0
  54. package/dist/stl.d.ts +22 -0
  55. package/dist/stl.js +131 -0
  56. package/dist/tangentOrder.d.ts +24 -0
  57. package/dist/tangentOrder.js +31 -0
  58. package/dist/threemf.d.ts +46 -0
  59. package/dist/threemf.js +254 -0
  60. package/dist/uploadBudget.d.ts +82 -0
  61. package/dist/uploadBudget.js +86 -0
  62. package/dist/usd.d.ts +49 -0
  63. package/dist/usd.js +542 -0
  64. package/dist/weld.d.ts +44 -0
  65. package/dist/weld.js +237 -0
  66. package/dist/zip.d.ts +35 -0
  67. package/dist/zip.js +110 -0
  68. package/package.json +61 -0
  69. package/src/assetPath.ts +79 -0
  70. package/src/coarseLevel.ts +720 -0
  71. package/src/dds.ts +507 -0
  72. package/src/drawKey.ts +91 -0
  73. package/src/drftLoader.ts +1197 -0
  74. package/src/drftTextures.ts +153 -0
  75. package/src/fbx.ts +934 -0
  76. package/src/fbxHarness.ts +215 -0
  77. package/src/fbxInflate.ts +258 -0
  78. package/src/fbxMaterial.ts +410 -0
  79. package/src/fbxRig.ts +652 -0
  80. package/src/gltf.ts +1161 -0
  81. package/src/gltfAccessor.ts +127 -0
  82. package/src/gltfSkin.ts +210 -0
  83. package/src/iesPhotometry.ts +136 -0
  84. package/src/imageInfo.ts +130 -0
  85. package/src/index.ts +162 -0
  86. package/src/kn5.ts +705 -0
  87. package/src/loadProgress.ts +131 -0
  88. package/src/localise.ts +275 -0
  89. package/src/obj.ts +386 -0
  90. package/src/orient.ts +262 -0
  91. package/src/radianceHdr.ts +195 -0
  92. package/src/readModel.ts +515 -0
  93. package/src/recognise.ts +124 -0
  94. package/src/stl.ts +147 -0
  95. package/src/tangentOrder.ts +32 -0
  96. package/src/threemf.ts +303 -0
  97. package/src/uploadBudget.ts +90 -0
  98. package/src/usd.ts +642 -0
  99. package/src/weld.ts +264 -0
  100. package/src/zip.ts +130 -0
package/dist/gltf.js ADDED
@@ -0,0 +1,904 @@
1
+ /**
2
+ * glTF 2.0 → `MeshData`, with no dependencies and no filesystem.
3
+ *
4
+ * glTF is the only third-party format this engine reads at tier 1, and it earns that by
5
+ * being an open Khronos standard with a published specification and a conformance suite.
6
+ * Everything the baker guarantees rests here; nothing guaranteed rests on a vendor's
7
+ * internal layout. See docs/FORMAT.md §2.
8
+ *
9
+ * **Buffers arrive as bytes, never as paths.** The parser takes the JSON and the binary
10
+ * blobs it refers to, so it runs identically in the CLI, in a test, and in a browser, and
11
+ * so a test can build a document by hand instead of shipping fixture files for cases that
12
+ * are three lines of JSON.
13
+ *
14
+ * **Transforms are baked.** `MeshData` has no node and no matrix — a mesh is geometry in
15
+ * world space, which is what makes drawing it one call with no per-object state. So the
16
+ * scene graph is flattened here: each primitive's vertices are transformed by its node's
17
+ * world matrix on the way through, and the graph does not survive into the engine.
18
+ */
19
+ import { DrftError } from '@driftengine/drft';
20
+ import { generateTangents } from '@driftengine/core';
21
+ import { MAX_MORPH_TARGETS } from '@driftengine/core';
22
+ import { accessorFloats } from './gltfAccessor.js';
23
+ import { readGltfSkins } from './gltfSkin.js';
24
+ /** The engine's own roughness, for a material whose stated one cannot be believed. */
25
+ const DEFAULT_ROUGHNESS = 0.4277;
26
+ /** `TRIANGLES`. The only primitive mode a `MeshData` can express. */
27
+ const MODE_TRIANGLES = 4;
28
+ /**
29
+ * Split a `.glb` into its JSON and its binary chunk.
30
+ *
31
+ * A `.glb` is the same document as a `.gltf` with the JSON and the buffer packed into one
32
+ * file, which is why an asset store hands you both — and why preferring it costs nothing.
33
+ */
34
+ export function readGlb(buffer) {
35
+ const view = new DataView(buffer);
36
+ if (buffer.byteLength < 12)
37
+ throw new DrftError('glb: shorter than its own header');
38
+ if (view.getUint32(0, true) !== 0x46546c67)
39
+ throw new DrftError('glb: bad magic, not a glb file');
40
+ const version = view.getUint32(4, true);
41
+ if (version !== 2)
42
+ throw new DrftError(`glb: version ${version}, and only 2 is defined`);
43
+ let json = null;
44
+ let binary = null;
45
+ let at = 12;
46
+ while (at + 8 <= buffer.byteLength) {
47
+ const length = view.getUint32(at, true);
48
+ const kind = view.getUint32(at + 4, true);
49
+ const start = at + 8;
50
+ if (start + length > buffer.byteLength) {
51
+ throw new DrftError(`glb: a chunk at ${at} claims ${length} bytes and runs past the file`);
52
+ }
53
+ if (kind === 0x4e4f534a) {
54
+ json = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, start, length)));
55
+ }
56
+ else if (kind === 0x004e4942) {
57
+ binary = new Uint8Array(buffer, start, length);
58
+ }
59
+ // Any other chunk kind is ignored, which the specification requires.
60
+ at = start + length + ((4 - (length % 4)) % 4);
61
+ }
62
+ if (json === null)
63
+ throw new DrftError('glb: no JSON chunk');
64
+ return { json, binary };
65
+ }
66
+ /**
67
+ * The matrix a skinned primitive is built with: none.
68
+ *
69
+ * A palette entry already carries its joint's inverse bind, so it takes a vertex from model space
70
+ * to where the joint moved it — and `uModel` then places the character. Baking the node's world
71
+ * matrix in as well applies that placement twice and throws the rig across the scene.
72
+ */
73
+ const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
74
+ /* --- Node transforms, composed to world space --- */
75
+ function identity() {
76
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
77
+ }
78
+ function multiply(a, b) {
79
+ const out = new Array(16);
80
+ for (let row = 0; row < 4; row++) {
81
+ for (let col = 0; col < 4; col++) {
82
+ let sum = 0;
83
+ for (let k = 0; k < 4; k++)
84
+ sum += a[k * 4 + row] * b[col * 4 + k];
85
+ out[col * 4 + row] = sum;
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ /** A node's local matrix, from either the explicit matrix or its TRS. */
91
+ function localMatrix(node) {
92
+ if (node.matrix !== undefined)
93
+ return node.matrix.slice();
94
+ const [tx, ty, tz] = node.translation ?? [0, 0, 0];
95
+ const [qx, qy, qz, qw] = node.rotation ?? [0, 0, 0, 1];
96
+ const [sx, sy, sz] = node.scale ?? [1, 1, 1];
97
+ const x2 = qx + qx;
98
+ const y2 = qy + qy;
99
+ const z2 = qz + qz;
100
+ const xx = qx * x2;
101
+ const xy = qx * y2;
102
+ const xz = qx * z2;
103
+ const yy = qy * y2;
104
+ const yz = qy * z2;
105
+ const zz = qz * z2;
106
+ const wx = qw * x2;
107
+ const wy = qw * y2;
108
+ const wz = qw * z2;
109
+ return [
110
+ (1 - (yy + zz)) * sx,
111
+ (xy + wz) * sx,
112
+ (xz - wy) * sx,
113
+ 0,
114
+ (xy - wz) * sy,
115
+ (1 - (xx + zz)) * sy,
116
+ (yz + wx) * sy,
117
+ 0,
118
+ (xz + wy) * sz,
119
+ (yz - wx) * sz,
120
+ (1 - (xx + yy)) * sz,
121
+ 0,
122
+ tx,
123
+ ty,
124
+ tz,
125
+ 1,
126
+ ];
127
+ }
128
+ /**
129
+ * The matrix a normal must be transformed by: the inverse transpose of the upper 3×3.
130
+ *
131
+ * Not the same matrix the positions use, and the difference is visible the moment an
132
+ * asset is scaled unevenly — a surface stretched on one axis has normals that lean the
133
+ * *other* way, and using the position matrix tilts every one of them wrongly. That reads
134
+ * as lighting sliding across a face, which is exactly the kind of fault that gets blamed
135
+ * on a renderer.
136
+ */
137
+ function normalMatrix(m) {
138
+ const a = m[0];
139
+ const b = m[1];
140
+ const c = m[2];
141
+ const d = m[4];
142
+ const e = m[5];
143
+ const f = m[6];
144
+ const g = m[8];
145
+ const h = m[9];
146
+ const i = m[10];
147
+ const det = a * (e * i - f * h) - d * (b * i - c * h) + g * (b * f - c * e);
148
+ if (Math.abs(det) < 1e-12)
149
+ return [1, 0, 0, 0, 1, 0, 0, 0, 1];
150
+ const inv = 1 / det;
151
+ /* Inverse, then transposed — written out, because the two steps cancel into this. */
152
+ return [
153
+ (e * i - f * h) * inv,
154
+ (g * f - d * i) * inv,
155
+ (d * h - g * e) * inv,
156
+ (h * c - b * i) * inv,
157
+ (a * i - g * c) * inv,
158
+ (g * b - a * h) * inv,
159
+ (b * f - e * c) * inv,
160
+ (d * c - a * f) * inv,
161
+ (a * e - d * b) * inv,
162
+ ];
163
+ }
164
+ /**
165
+ * The images a document carries, in `textures` order, as references the baker can act on.
166
+ *
167
+ * glTF states an image three ways and all three appear in the wild: inside the binary chunk
168
+ * as a buffer view, inline as a `data:` URI, and as a file beside the document. The first two
169
+ * arrive with their bytes and need nothing resolved; only the third is a lookup, and it is
170
+ * the same lookup FBX's relative paths go through.
171
+ *
172
+ * Indexed by *texture* rather than by image, because a material names a texture and two
173
+ * textures may share one image with different samplers. Following the indirection here keeps
174
+ * the material's index meaning what the file said it meant.
175
+ */
176
+ function readImages(doc, buffers, warnings) {
177
+ const out = [];
178
+ for (let i = 0; i < (doc.textures ?? []).length; i++) {
179
+ const source = doc.textures?.[i]?.source;
180
+ const image = source === undefined ? undefined : doc.images?.[source];
181
+ if (image === undefined) {
182
+ warnings.push(`texture ${i} names no image this reader can find; it will draw untextured`);
183
+ out.push({ name: `texture-${i}` });
184
+ continue;
185
+ }
186
+ const name = image.name ?? image.uri ?? `image-${source}`;
187
+ if (image.bufferView !== undefined) {
188
+ const view = doc.bufferViews?.[image.bufferView];
189
+ const buffer = view === undefined ? undefined : buffers[view.buffer];
190
+ if (view === undefined || buffer === undefined) {
191
+ warnings.push(`image ${source} names a bufferView that is absent`);
192
+ out.push({ name });
193
+ continue;
194
+ }
195
+ const at = view.byteOffset ?? 0;
196
+ /* Copied rather than viewed: the caller embeds this and the source buffer is the
197
+ whole binary chunk, which would otherwise be kept alive by one texture. */
198
+ out.push({ name, bytes: buffer.slice(at, at + view.byteLength) });
199
+ continue;
200
+ }
201
+ if (image.uri !== undefined && image.uri.startsWith('data:')) {
202
+ const comma = image.uri.indexOf(',');
203
+ const base64 = image.uri.slice(comma + 1);
204
+ /* `atob` rather than a Node Buffer: this module runs in a browser too. */
205
+ const binary = atob(base64);
206
+ const bytes = new Uint8Array(binary.length);
207
+ for (let b = 0; b < binary.length; b++)
208
+ bytes[b] = binary.charCodeAt(b);
209
+ out.push({ name: image.name ?? `image-${source}`, bytes });
210
+ continue;
211
+ }
212
+ /* A plain URI: a path relative to the document, which the caller resolves. */
213
+ out.push({ name: image.uri === undefined ? name : decodeURIComponent(image.uri) });
214
+ }
215
+ return out;
216
+ }
217
+ /**
218
+ * The reflectance of a dielectric at normal incidence: 4%. Both material models are built on it.
219
+ */
220
+ const DIELECTRIC_SPECULAR = 0.04;
221
+ /** Guards the two divisions in the base-colour derivation; Khronos' own conversion uses this value. */
222
+ const CONVERSION_EPSILON = 1e-6;
223
+ /**
224
+ * Rec. 601 perceived brightness, which is the measure Khronos' conversion is written in.
225
+ *
226
+ * Not Rec. 709 luminance, which is what the renderer measures brightness with elsewhere. The
227
+ * difference is small and the reason to keep this one is that the quadratic below was derived
228
+ * against it: substituting a different weighting changes which surfaces solve to metal.
229
+ */
230
+ function perceivedBrightness(c) {
231
+ const r = c[0] ?? 0;
232
+ const g = c[1] ?? 0;
233
+ const b = c[2] ?? 0;
234
+ return Math.sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b);
235
+ }
236
+ /**
237
+ * The metalness a diffuse and a specular brightness imply, as the roots of Khronos' quadratic.
238
+ *
239
+ * Specular-glossiness and metallic-roughness describe the same surface with different unknowns, so
240
+ * one is not a rename of the other: the pair has to be **solved for jointly**. A dielectric's
241
+ * reflectance is fixed at 4% and its colour lives in the diffuse; a metal has no diffuse at all and
242
+ * its colour lives in the specular. The quadratic is the statement that a given diffuse and
243
+ * specular can be produced by exactly one metalness between those two ends.
244
+ *
245
+ * This is Appendix B of the extension, unchanged. It is stated in *brightnesses* rather than in
246
+ * colours because there is one metalness for the surface and three channels arguing about it.
247
+ */
248
+ function solveMetallic(diffuse, specular, oneMinusSpecularStrength) {
249
+ if (specular < DIELECTRIC_SPECULAR)
250
+ return 0;
251
+ const a = DIELECTRIC_SPECULAR;
252
+ const b = (diffuse * oneMinusSpecularStrength) / (1 - DIELECTRIC_SPECULAR) +
253
+ specular -
254
+ 2 * DIELECTRIC_SPECULAR;
255
+ const c = DIELECTRIC_SPECULAR - specular;
256
+ return Math.min(1, Math.max(0, (-b + Math.sqrt(Math.max(b * b - 4 * a * c, 0))) / (2 * a)));
257
+ }
258
+ /**
259
+ * A specular-glossiness material, restated as the metallic-roughness one the rest of this file reads.
260
+ *
261
+ * **What this gives up.** The specular colour and the glossiness are a pair, and where they live in
262
+ * a `specularGlossinessTexture` — RGB the colour, A the gloss — no rearrangement of numbers can
263
+ * reach them: glTF's ORM packing wants roughness in G and metallic in B, so producing one would
264
+ * mean decoding every image and re-encoding it with its channels moved. That is a baker's job and
265
+ * not a reader's, and binding the map unconverted would be worse than binding nothing, because the
266
+ * renderer would read a specular colour's green as a roughness. So the pair is left **absent** in
267
+ * that case rather than guessed: metallic 0 and the engine's own `DEFAULT_ROUGHNESS`, which is the
268
+ * same rule `materialOf` already applies where a map supplies values a scalar cannot express. The
269
+ * diffuse map still binds, which is the whole of what was missing before.
270
+ *
271
+ * **What would make this wrong**: a reader that decoded images. It would have the texels and could
272
+ * repack them, and then this function should produce an `ormMap` instead of declining to.
273
+ *
274
+ * With no `specularGlossinessTexture` the conversion is exact and is Appendix B of the extension,
275
+ * including its base-colour derivation — which is what makes a chrome surface, whose diffuse is
276
+ * black and whose colour is entirely in its specular, arrive as chrome rather than as a black
277
+ * mirror.
278
+ */
279
+ function specularGlossinessAsPbr(sg) {
280
+ const diffuse = sg.diffuseFactor ?? [1, 1, 1, 1];
281
+ const alpha = diffuse[3] ?? 1;
282
+ const map = sg.diffuseTexture === undefined ? {} : { baseColorTexture: sg.diffuseTexture };
283
+ if (sg.specularGlossinessTexture !== undefined) {
284
+ return {
285
+ ...map,
286
+ baseColorFactor: [diffuse[0] ?? 1, diffuse[1] ?? 1, diffuse[2] ?? 1, alpha],
287
+ metallicFactor: 0,
288
+ roughnessFactor: DEFAULT_ROUGHNESS,
289
+ };
290
+ }
291
+ const specular = sg.specularFactor ?? [1, 1, 1];
292
+ const oneMinusSpecularStrength = 1 - Math.max(specular[0] ?? 1, specular[1] ?? 1, specular[2] ?? 1);
293
+ const metallic = solveMetallic(perceivedBrightness(diffuse), perceivedBrightness(specular), oneMinusSpecularStrength);
294
+ /* Two readings of the same colour — the one a dielectric implies and the one a metal implies —
295
+ blended by `metallic²`, which is Appendix B's own weighting. The epsilons stop a division by
296
+ zero at either end; at both ends the term they guard is multiplied out by the blend. */
297
+ const fromDielectric = oneMinusSpecularStrength /
298
+ (1 - DIELECTRIC_SPECULAR) /
299
+ Math.max(1 - metallic, CONVERSION_EPSILON);
300
+ const fromMetal = 1 / Math.max(metallic, CONVERSION_EPSILON);
301
+ const blend = metallic * metallic;
302
+ const colour = [];
303
+ for (let i = 0; i < 3; i++) {
304
+ const dielectric = (diffuse[i] ?? 1) * fromDielectric;
305
+ const metal = ((specular[i] ?? 1) - DIELECTRIC_SPECULAR * (1 - metallic)) * fromMetal;
306
+ colour.push(Math.min(1, Math.max(0, dielectric + (metal - dielectric) * blend)));
307
+ }
308
+ return {
309
+ ...map,
310
+ baseColorFactor: [colour[0], colour[1], colour[2], alpha],
311
+ metallicFactor: metallic,
312
+ /* Glossiness is roughness counted from the other end, and that half of the pair is exact. */
313
+ roughnessFactor: Math.min(1, Math.max(0, 1 - (sg.glossinessFactor ?? 1))),
314
+ };
315
+ }
316
+ /**
317
+ * The metallic-roughness block this reader reads, whichever of the two models the file states.
318
+ *
319
+ * **The core object wins where a file carries both.** The extension's own text says the opposite —
320
+ * a renderer that supports it should prefer it — and that rule is written for a renderer that
321
+ * supports it *completely*. This one cannot repack a `specularGlossinessTexture`, while an exporter
322
+ * that wrote both blocks did the conversion with the texels in hand and put a real
323
+ * `metallicRoughnessTexture` in the core one. Preferring the extension there would throw away a
324
+ * better answer than this function can compute.
325
+ *
326
+ * **What would make this wrong**: an exporter that writes a token `pbrMetallicRoughness` — factors
327
+ * and no maps — purely so a non-supporting reader gets something. Such a file would take the token
328
+ * block and lose its diffuse map, and the fix is to prefer whichever block names more textures.
329
+ * Nothing in the corpus this was built against writes one, so the simpler rule stands until one
330
+ * appears.
331
+ */
332
+ function pbrOf(material) {
333
+ if (material === undefined)
334
+ return undefined;
335
+ if (material.pbrMetallicRoughness !== undefined)
336
+ return material.pbrMetallicRoughness;
337
+ const sg = material.extensions?.KHR_materials_pbrSpecularGlossiness;
338
+ return sg === undefined ? undefined : specularGlossinessAsPbr(sg);
339
+ }
340
+ /**
341
+ * A primitive's material, in the terms `.drft` stores.
342
+ *
343
+ * `alphaMode` decides whether the alpha channel means anything at all, and it has **three**
344
+ * settings rather than two. `OPAQUE`, which is the default the specification states, means a base
345
+ * colour's fourth component is to be *ignored* rather than honoured — so reading it
346
+ * unconditionally would make every material that happens to carry a non-one alpha in an opaque
347
+ * file turn see-through. `BLEND` is the one that becomes an opacity.
348
+ *
349
+ * **`MASK` is neither, and was dropped for as long as this reader has existed.** It means the
350
+ * alpha is a *test*: a fragment below `alphaCutoff` is discarded and everything above it is fully
351
+ * opaque. Read as an opacity it would be wrong in both directions, and ignored — which is what
352
+ * happened — a masked surface arrives as the solid rectangle its geometry is, with the shape that
353
+ * was supposed to be cut out of it painted over. `cutout` is where it goes, and the 0.5 default is
354
+ * the specification's own.
355
+ */
356
+ function materialOf(material) {
357
+ const pbr = pbrOf(material);
358
+ const base = pbr?.baseColorFactor ?? [1, 1, 1, 1];
359
+ const emissiveFactor = material?.emissiveFactor ?? [0, 0, 0];
360
+ const emissive = Math.min(1, Math.max(emissiveFactor[0] ?? 0, emissiveFactor[1] ?? 0, emissiveFactor[2] ?? 0));
361
+ const blends = material?.alphaMode === 'BLEND';
362
+ const masked = material?.alphaMode === 'MASK';
363
+ /*
364
+ * **A factor that multiplies a texture is not a value, and now it does not have to be thrown
365
+ * away.**
366
+ *
367
+ * glTF's `metallicFactor` and `roughnessFactor` default to 1, and where a
368
+ * `metallicRoughnessTexture` is present they *scale* it per texel rather than standing in for
369
+ * it. This reader had no metallic term and did not sample that map, so taking the scalars at
370
+ * face value asserted "maximally metallic, maximally rough" over every surface that meant
371
+ * "whatever the map says".
372
+ *
373
+ * It was not a subtle error. A test character whose base colour averages RGB 26, 27, 27 —
374
+ * nearly black — arrived blown to white, because the albedo contributed almost nothing and a
375
+ * specular of 1 contributed everything. So the map's scalars were treated as unknown and the
376
+ * engine's defaults used instead, which was the only safe reading available.
377
+ *
378
+ * There is somewhere to put them now. The map carries the per-texel values and the factors
379
+ * become `roughnessScale` and `metallicScale`, which is what glTF says they are. The scalar
380
+ * `specular`, `roughness` and `reflectivity` readings stay for the untextured case and keep the
381
+ * engine's defaults where a map supplies them, because a vertex attribute cannot express a
382
+ * value that varies per texel and a wrong constant is worse than a neutral one.
383
+ */
384
+ const ormMap = pbr?.metallicRoughnessTexture?.index ?? -1;
385
+ const modulated = ormMap >= 0;
386
+ const metallic = modulated ? 0 : (pbr?.metallicFactor ?? 1);
387
+ const roughness = modulated ? DEFAULT_ROUGHNESS : (pbr?.roughnessFactor ?? 1);
388
+ /*
389
+ * **glTF leaves `metallicRoughnessTexture`'s R channel undefined.** The specification assigns G
390
+ * to roughness and B to metallic and says nothing at all about R, so occlusion lives there only
391
+ * when `occlusionTexture` names the *same image* — which is exactly what the ORM convention is.
392
+ * Anywhere else, reading R is reading whatever the exporter happened to leave, and the symptom
393
+ * is a model arriving blotchy with nothing in the file to point at.
394
+ *
395
+ * A separate occlusion image is dropped rather than loaded into a second unit. One map, one
396
+ * unit, and `textureUnitBudget.test.ts` says the next unit belongs to emissive.
397
+ */
398
+ const occlusion = material?.occlusionTexture;
399
+ const occlusionStrength = modulated && occlusion !== undefined && occlusion.index === ormMap
400
+ ? (occlusion.strength ?? 1)
401
+ : 0;
402
+ return {
403
+ name: material?.name ?? '',
404
+ color: [base[0] ?? 1, base[1] ?? 1, base[2] ?? 1],
405
+ specular: metallic,
406
+ roughness,
407
+ emissive,
408
+ emissiveColor: emissive > 0
409
+ ? [emissiveFactor[0] ?? 0, emissiveFactor[1] ?? 0, emissiveFactor[2] ?? 0]
410
+ : [-1, -1, -1],
411
+ opacity: blends ? Math.min(1, Math.max(0, base[3] ?? 1)) : 1,
412
+ /* Only in MASK mode: the specification says `alphaCutoff` has no meaning in the other two. */
413
+ cutout: masked ? Math.min(1, Math.max(0, material?.alphaCutoff ?? 0.5)) : 0,
414
+ /* Metalness is glTF's statement about reflection: a metal mirrors its surroundings and
415
+ a dielectric mostly does not. It also feeds `specular`, which is not double counting,
416
+ because the two describe different halves of the same fact. */
417
+ reflectivity: Math.min(1, Math.max(0, modulated ? 0 : (pbr?.metallicFactor ?? 0))),
418
+ normalMap: material?.normalTexture?.index ?? -1,
419
+ ormMap,
420
+ emissiveMap: material?.emissiveTexture?.index ?? -1,
421
+ roughnessScale: modulated ? (pbr?.roughnessFactor ?? 1) : 1,
422
+ metallicScale: modulated ? (pbr?.metallicFactor ?? 1) : 1,
423
+ occlusionStrength,
424
+ albedo: pbr?.baseColorTexture?.index ?? -1,
425
+ };
426
+ }
427
+ /**
428
+ * Turn a document into meshes in world space, one per primitive.
429
+ *
430
+ * One `MeshData` per primitive rather than per glTF mesh, because a primitive is the unit
431
+ * that carries a single material — and a `MeshData` has one material's worth of vertex
432
+ * attributes. Merging them would mean losing the distinction the file was drawing.
433
+ */
434
+ export function gltfToMeshes(doc, buffers, options) {
435
+ const version = doc.asset?.version ?? '';
436
+ if (!version.startsWith('2')) {
437
+ throw new DrftError(`gltf: asset version "${version}", and this reader implements 2.0`);
438
+ }
439
+ const meshes = [];
440
+ const materials = [];
441
+ /* One per labelled material, by the ordinal `materials` is about to give it. */
442
+ const substances = [];
443
+ const warnings = [];
444
+ const textures = readImages(doc, buffers, warnings);
445
+ /*
446
+ * Said once per material rather than once per primitive that uses it.
447
+ *
448
+ * A specular-glossiness file imports now, and half of what it states still cannot be carried:
449
+ * `specularGlossinessAsPbr` says why, and this is where a person doing a bake finds out. On a car
450
+ * whose 144 primitives share four materials the per-primitive spelling would be 144 lines of the
451
+ * same sentence, which is how a real warning stops being read.
452
+ */
453
+ for (let i = 0; i < (doc.materials ?? []).length; i++) {
454
+ const material = doc.materials?.[i];
455
+ if (material === undefined || material.pbrMetallicRoughness !== undefined)
456
+ continue;
457
+ const sg = material.extensions?.KHR_materials_pbrSpecularGlossiness;
458
+ if (sg?.specularGlossinessTexture === undefined)
459
+ continue;
460
+ warnings.push(`material ${i}${material.name === undefined ? '' : ` "${material.name}"`}: its specular and ` +
461
+ 'glossiness are in a map whose channels no rearrangement fits into an ORM one, so it draws ' +
462
+ 'as a dielectric at the default roughness. Its diffuse map is bound.');
463
+ }
464
+ /*
465
+ * Read once for the whole document, because a skin's sort is what every joint index in it has to
466
+ * agree with — a primitive resolving its own would be a second sort and the two would drift.
467
+ */
468
+ const { skins } = readGltfSkins(doc, buffers);
469
+ const sceneIndex = doc.scene ?? 0;
470
+ const roots = doc.scenes?.[sceneIndex]?.nodes ?? doc.nodes?.map((_, at) => at) ?? [];
471
+ const seen = new Set();
472
+ /*
473
+ * The graph, reported beside the flattened geometry and never instead of it.
474
+ *
475
+ * **Flattening is unchanged and that is the point.** `MESH` stays world-space under §4.4 rule 4,
476
+ * so every file this reader has ever produced still means exactly what it meant; this is an
477
+ * additional, optional statement about how the parts relate. A consumer that wants parts it can
478
+ * move calls `localiseNodes`, and one that does not is untouched.
479
+ *
480
+ * The walk already computes a world matrix per node, so this is bookkeeping and not a second
481
+ * traversal.
482
+ */
483
+ const nodes = [];
484
+ const walk = (nodeIndex, parent, parentNode) => {
485
+ if (seen.has(nodeIndex)) {
486
+ // A cycle would otherwise recurse until the stack gave out, with no useful message.
487
+ throw new DrftError(`gltf: node ${nodeIndex} is reachable twice — the graph has a cycle`);
488
+ }
489
+ seen.add(nodeIndex);
490
+ const node = doc.nodes?.[nodeIndex];
491
+ if (node === undefined)
492
+ throw new DrftError(`gltf: a scene names node ${nodeIndex}, which is absent`);
493
+ const world = multiply(parent, localMatrix(node));
494
+ const entry = nodes.length;
495
+ nodes.push({
496
+ parent: parentNode,
497
+ translation: [
498
+ node.translation?.[0] ?? 0,
499
+ node.translation?.[1] ?? 0,
500
+ node.translation?.[2] ?? 0,
501
+ ],
502
+ rotation: [
503
+ node.rotation?.[0] ?? 0,
504
+ node.rotation?.[1] ?? 0,
505
+ node.rotation?.[2] ?? 0,
506
+ node.rotation?.[3] ?? 1,
507
+ ],
508
+ scale: [node.scale?.[0] ?? 1, node.scale?.[1] ?? 1, node.scale?.[2] ?? 1],
509
+ mesh: -1,
510
+ name: node.name ?? `node ${nodeIndex}`,
511
+ });
512
+ if (node.mesh !== undefined) {
513
+ const mesh = doc.meshes?.[node.mesh];
514
+ if (mesh === undefined)
515
+ throw new DrftError(`gltf: node ${nodeIndex} names an absent mesh`);
516
+ /*
517
+ * **Flattening stays the default and a skin is the one exception.** `gltf.ts`'s header says
518
+ * the graph does not survive into the engine, which is right for static geometry and
519
+ * destroys exactly what a skin points at. A document with no `skins` takes the identical
520
+ * path it took before this existed, so no existing import changes by a byte.
521
+ */
522
+ const skin = node.skin !== undefined ? skins[node.skin] : undefined;
523
+ const placement = skin === undefined ? world : IDENTITY;
524
+ for (const primitive of mesh.primitives) {
525
+ const built = buildPrimitive(doc, buffers, primitive, placement, mesh.name ?? `mesh ${node.mesh}`, warnings, skin, options?.deriveTangents ?? true);
526
+ if (built !== null) {
527
+ if (built.substance !== undefined) {
528
+ substances.push({ material: materials.length, substance: built.substance });
529
+ }
530
+ const at = meshes.length;
531
+ meshes.push(built.mesh);
532
+ materials.push(built.material);
533
+ /*
534
+ * Name the mesh on the graph, but **only where the geometry is actually this node's**.
535
+ * A skinned primitive was placed at `IDENTITY` above and its vertices live in the skin's
536
+ * space, so claiming it here would invite `localiseNodes` to transform it a second time.
537
+ *
538
+ * A glTF node may hold several primitives while `DrftNode.mesh` names one, so extras
539
+ * become identity children: inventing a node is better than leaving a mesh that nothing
540
+ * in the graph points at, which is a part a game cannot find by name.
541
+ */
542
+ if (skin === undefined) {
543
+ if (nodes[entry].mesh < 0) {
544
+ nodes[entry] = { ...nodes[entry], mesh: at };
545
+ }
546
+ else {
547
+ nodes.push({
548
+ parent: entry,
549
+ translation: [0, 0, 0],
550
+ rotation: [0, 0, 0, 1],
551
+ scale: [1, 1, 1],
552
+ mesh: at,
553
+ name: `${nodes[entry].name}#${at}`,
554
+ });
555
+ }
556
+ }
557
+ }
558
+ }
559
+ }
560
+ for (const child of node.children ?? [])
561
+ walk(child, world, entry);
562
+ seen.delete(nodeIndex);
563
+ };
564
+ for (const root of roots)
565
+ walk(root, identity(), -1);
566
+ if (meshes.length === 0)
567
+ throw new DrftError('gltf: the scene contains no triangle geometry');
568
+ return {
569
+ meshes,
570
+ warnings,
571
+ materials,
572
+ textures,
573
+ nodes,
574
+ ...(substances.length === 0 ? {} : { substances }),
575
+ };
576
+ }
577
+ function buildPrimitive(doc, buffers, primitive, world, label, warnings,
578
+ /** The skin this primitive is deformed by, or undefined for static geometry. */
579
+ skin,
580
+ /** See `GltfReadOptions.deriveTangents`. A caller that welds afterwards passes false. */
581
+ deriveTangents = true) {
582
+ const mode = primitive.mode ?? MODE_TRIANGLES;
583
+ if (mode !== MODE_TRIANGLES) {
584
+ /*
585
+ * Skipped with a warning rather than refused. Points and lines are legitimate glTF
586
+ * and legitimately not geometry this engine draws, so a model carrying a debug line
587
+ * set should still import — but silently dropping part of a file is how somebody ends
588
+ * up wondering where their model went.
589
+ */
590
+ warnings.push(`${label}: skipped a primitive with mode ${mode}; only triangles are drawn`);
591
+ return null;
592
+ }
593
+ const positionIndex = primitive.attributes['POSITION'];
594
+ if (positionIndex === undefined)
595
+ throw new DrftError(`gltf: ${label} has a primitive with no POSITION`);
596
+ const positions = accessorFloats(doc, buffers, positionIndex, `${label} POSITION`);
597
+ const vertices = positions.length / 3;
598
+ const normalIndex = primitive.attributes['NORMAL'];
599
+ const normals = normalIndex === undefined
600
+ ? new Float32Array(vertices * 3)
601
+ : accessorFloats(doc, buffers, normalIndex, `${label} NORMAL`);
602
+ if (normalIndex === undefined)
603
+ warnings.push(`${label}: no NORMAL, so it will shade flat`);
604
+ /* Transform into world space, positions by the matrix and normals by its inverse transpose. */
605
+ const nm = normalMatrix(world);
606
+ for (let i = 0; i < vertices; i++) {
607
+ const x = positions[i * 3];
608
+ const y = positions[i * 3 + 1];
609
+ const z = positions[i * 3 + 2];
610
+ positions[i * 3] =
611
+ world[0] * x +
612
+ world[4] * y +
613
+ world[8] * z +
614
+ world[12];
615
+ positions[i * 3 + 1] =
616
+ world[1] * x +
617
+ world[5] * y +
618
+ world[9] * z +
619
+ world[13];
620
+ positions[i * 3 + 2] =
621
+ world[2] * x +
622
+ world[6] * y +
623
+ world[10] * z +
624
+ world[14];
625
+ const nx = normals[i * 3];
626
+ const ny = normals[i * 3 + 1];
627
+ const nz = normals[i * 3 + 2];
628
+ const tx = nm[0] * nx + nm[3] * ny + nm[6] * nz;
629
+ const ty = nm[1] * nx + nm[4] * ny + nm[7] * nz;
630
+ const tz = nm[2] * nx + nm[5] * ny + nm[8] * nz;
631
+ const length = Math.hypot(tx, ty, tz);
632
+ if (length > 1e-8) {
633
+ normals[i * 3] = tx / length;
634
+ normals[i * 3 + 1] = ty / length;
635
+ normals[i * 3 + 2] = tz / length;
636
+ }
637
+ }
638
+ /* Material, folded into vertex attributes, because that is where this engine keeps it. */
639
+ const material = primitive.material === undefined ? undefined : doc.materials?.[primitive.material];
640
+ const emissiveFactor = material?.emissiveFactor ?? [0, 0, 0];
641
+ const emissiveStrength = Math.max(emissiveFactor[0] ?? 0, emissiveFactor[1] ?? 0, emissiveFactor[2] ?? 0);
642
+ /*
643
+ * Derived once, from the same place the MATL record comes from.
644
+ *
645
+ * These were computed here a second time and drifted the moment the material rules got
646
+ * more careful: MATL said specular 0 while every vertex said 1, and the shader reads the
647
+ * vertices, so the fix looked applied and changed nothing on screen.
648
+ *
649
+ * **The base colour is read off `surface` for the same reason**, rather than off the document a
650
+ * second time. Which of the two material models a file states is now a decision `pbrOf` makes,
651
+ * and a specular-glossiness document reading its own `pbrMetallicRoughness.baseColorFactor` here
652
+ * would find nothing and paint every vertex white while MATL said otherwise.
653
+ */
654
+ const surface = materialOf(material);
655
+ const base = surface.color;
656
+ /*
657
+ * Metalness becomes highlight strength. They are not the same quantity — this renderer
658
+ * has no metallic term — but a metal is the thing in a glTF scene that takes a sharp
659
+ * highlight, and mapping it to `specular` is what makes an imported gold prop read as
660
+ * gold rather than as beige plastic. It is an approximation, and stated as one.
661
+ */
662
+ const colorIndex = primitive.attributes['COLOR_0'];
663
+ const colors = new Float32Array(vertices * 3);
664
+ if (colorIndex === undefined) {
665
+ for (let i = 0; i < vertices; i++) {
666
+ colors[i * 3] = base[0] ?? 1;
667
+ colors[i * 3 + 1] = base[1] ?? 1;
668
+ colors[i * 3 + 2] = base[2] ?? 1;
669
+ }
670
+ }
671
+ else {
672
+ const vertexColors = accessorFloats(doc, buffers, colorIndex, `${label} COLOR_0`);
673
+ const stride = vertexColors.length / vertices >= 4 ? 4 : 3;
674
+ for (let i = 0; i < vertices; i++) {
675
+ colors[i * 3] = (vertexColors[i * stride] ?? 1) * (base[0] ?? 1);
676
+ colors[i * 3 + 1] = (vertexColors[i * stride + 1] ?? 1) * (base[1] ?? 1);
677
+ colors[i * 3 + 2] = (vertexColors[i * stride + 2] ?? 1) * (base[2] ?? 1);
678
+ }
679
+ }
680
+ const emissive = new Float32Array(vertices).fill(emissiveStrength);
681
+ const roughness = new Float32Array(vertices).fill(surface.roughness);
682
+ const specular = new Float32Array(vertices).fill(surface.specular);
683
+ const emissiveColor = new Float32Array(vertices * 3);
684
+ for (let i = 0; i < vertices; i++) {
685
+ /* A negative component means "inherit the albedo", which is the engine's own default. */
686
+ const named = emissiveStrength > 0;
687
+ emissiveColor[i * 3] = named ? (emissiveFactor[0] ?? 0) : -1;
688
+ emissiveColor[i * 3 + 1] = named ? (emissiveFactor[1] ?? 0) : -1;
689
+ emissiveColor[i * 3 + 2] = named ? (emissiveFactor[2] ?? 0) : -1;
690
+ }
691
+ const uvIndex = primitive.attributes['TEXCOORD_0'];
692
+ const uvs = uvIndex === undefined
693
+ ? undefined
694
+ : accessorFloats(doc, buffers, uvIndex, `${label} TEXCOORD_0`);
695
+ /*
696
+ * A tangent frame, read where the source has one and derived where it does not.
697
+ *
698
+ * **Read rather than always derived, and the difference is not academic.** A tangent baked by
699
+ * the tool that authored a normal map agrees with that map exactly; one derived here agrees
700
+ * only where the derivation matches the tool's, and the two differ at UV seams and mirrored
701
+ * shells. glTF says to use the supplied frame where it exists, and this does.
702
+ *
703
+ * Only where there are texture coordinates at all: with no UVs there is no direction along a
704
+ * texture to point at, and a derived frame would be an arbitrary one dressed as a measurement.
705
+ * And only where a material declares a normal map, which is the other half of the same
706
+ * sentence and is argued where the derivation happens, below.
707
+ */
708
+ const tangentIndex = primitive.attributes['TANGENT'];
709
+ let tangents;
710
+ if (tangentIndex !== undefined) {
711
+ tangents = accessorFloats(doc, buffers, tangentIndex, `${label} TANGENT`);
712
+ /*
713
+ * Rotated into world space by the same normal matrix the normals took, and `w` left alone:
714
+ * it is a handedness rather than a direction, and a matrix that mirrors flips the frame
715
+ * itself, which the sign already describes.
716
+ */
717
+ for (let i = 0; i < vertices; i++) {
718
+ const x = tangents[i * 4];
719
+ const y = tangents[i * 4 + 1];
720
+ const z = tangents[i * 4 + 2];
721
+ const tx = nm[0] * x + nm[3] * y + nm[6] * z;
722
+ const ty = nm[1] * x + nm[4] * y + nm[7] * z;
723
+ const tz = nm[2] * x + nm[5] * y + nm[8] * z;
724
+ const length = Math.hypot(tx, ty, tz);
725
+ if (length > 1e-8) {
726
+ tangents[i * 4] = tx / length;
727
+ tangents[i * 4 + 1] = ty / length;
728
+ tangents[i * 4 + 2] = tz / length;
729
+ }
730
+ }
731
+ }
732
+ /* Indices, widened to u32, or generated for a non-indexed primitive. */
733
+ let indices;
734
+ if (primitive.indices === undefined) {
735
+ indices = new Uint32Array(vertices);
736
+ for (let i = 0; i < vertices; i++)
737
+ indices[i] = i;
738
+ }
739
+ else {
740
+ const raw = accessorFloats(doc, buffers, primitive.indices, `${label} indices`);
741
+ indices = new Uint32Array(raw.length);
742
+ for (let i = 0; i < raw.length; i++)
743
+ indices[i] = raw[i];
744
+ }
745
+ /*
746
+ * Derived only now, because it needs the indices and they are widened above — and only for a
747
+ * material that declares a normal map.
748
+ *
749
+ * **A tangent frame exists to sample a normal map, and there is exactly one place in this
750
+ * engine that reads one**: `tangentFrame` in the flat shader's `main`, inside
751
+ * `if (uNormalStrength > 0.0)`. So a frame derived for a material with no map is four floats a
752
+ * vertex that no draw can ever fetch. Measured on a 253-mesh CAD export whose eight materials
753
+ * declare no `normalTexture` at all: **8.3 MB of a 51.5 MB model**, and a shard on the wire
754
+ * with it. It is not a frame cost — the buffer uploads once and the vertex stride is the same
755
+ * either way — which is why it survived this long.
756
+ *
757
+ * **A supplied `TANGENT` is still read**, above, whatever the material says. That is authored
758
+ * data and the file is entitled to be believed; this is only about not *inventing* one.
759
+ *
760
+ * The other half is that deriving before a weld is the wrong order: a corner soup gets one
761
+ * triangle's frame per corner, no two corners agree, and `weldMesh` — which now keys on the
762
+ * frame, because a mirrored shell differs in nothing else — cannot merge them. **That half is
763
+ * closed as of 3.30.0 by `deriveTangents`**, which the baker sets false so it can derive on the
764
+ * welded mesh instead. It stays on by default here, because a consumer reading a model straight
765
+ * into a renderer never welds and would otherwise lose its normal mapping silently.
766
+ *
767
+ * **What would make this wrong** is a second consumer of the frame that does not need a map:
768
+ * anisotropic specular, hair or cloth. There is none today; the day one lands, it wants
769
+ * `deriveTangentsFor` after the weld and not a wider gate here.
770
+ */
771
+ if (deriveTangents && tangents === undefined && uvs !== undefined && surface.normalMap >= 0) {
772
+ tangents = generateTangents(positions, normals, uvs, indices);
773
+ }
774
+ /*
775
+ * The rig's two attributes, read only when a skin says there is one.
776
+ *
777
+ * **The indices are remapped onto the sorted joint order.** `gltfSkin.ts` sorts parents-first
778
+ * because a palette is resolved in index order, so a mesh left naming the document's own indices
779
+ * weights its vertices to the wrong joints — which reads as a bad rig rather than a bad
780
+ * importer, and moves the wrong limb.
781
+ */
782
+ /*
783
+ * Morph targets, interleaved by vertex so every target of one vertex is adjacent — which is the
784
+ * layout `MeshData.morphTargets` specifies and the shader reads, because it fetches all of a
785
+ * vertex's targets together. glTF stores them the other way round, one accessor per target, so
786
+ * this transposes on the way in rather than making the shader stride across the whole mesh.
787
+ *
788
+ * **Positions only, and a `NORMAL` target is dropped rather than refused.** The engine morphs
789
+ * positions and not normals, so a file carrying both is a file this reader can serve most of.
790
+ */
791
+ let morphTargets;
792
+ let morphTargetCount;
793
+ const targets = primitive.targets ?? [];
794
+ if (targets.length > 0) {
795
+ const used = Math.min(targets.length, MAX_MORPH_TARGETS);
796
+ if (targets.length > used) {
797
+ /*
798
+ * Taken rather than refused, and said out loud. A facial rig routinely carries dozens of
799
+ * targets and refusing the file outright would serve nobody; taking the first few silently
800
+ * would ship a face that cannot make most of its expressions with nothing to point at.
801
+ */
802
+ warnings.push(`${label}: ${targets.length} morph targets, and this engine carries ${MAX_MORPH_TARGETS}. ` +
803
+ `The first ${used} are kept and the rest are dropped.`);
804
+ }
805
+ morphTargets = new Float32Array(vertices * used * 3);
806
+ morphTargetCount = used;
807
+ for (let t = 0; t < used; t++) {
808
+ const positionAt = targets[t]?.['POSITION'];
809
+ if (positionAt === undefined)
810
+ continue;
811
+ const deltas = accessorFloats(doc, buffers, positionAt, `${label} morph target ${t} POSITION`);
812
+ for (let v = 0; v < vertices; v++) {
813
+ const to = (v * used + t) * 3;
814
+ morphTargets[to] = deltas[v * 3] ?? 0;
815
+ morphTargets[to + 1] = deltas[v * 3 + 1] ?? 0;
816
+ morphTargets[to + 2] = deltas[v * 3 + 2] ?? 0;
817
+ }
818
+ }
819
+ }
820
+ let joints;
821
+ let weights;
822
+ const jointIndex = primitive.attributes['JOINTS_0'];
823
+ const weightIndex = primitive.attributes['WEIGHTS_0'];
824
+ if (skin !== undefined && jointIndex !== undefined && weightIndex !== undefined) {
825
+ joints = accessorFloats(doc, buffers, jointIndex, `${label} JOINTS_0`);
826
+ weights = accessorFloats(doc, buffers, weightIndex, `${label} WEIGHTS_0`);
827
+ for (let i = 0; i < joints.length; i++) {
828
+ const old = joints[i];
829
+ const moved = skin.remap[old];
830
+ if (moved === undefined) {
831
+ throw new DrftError(`gltf: ${label} weights a vertex to joint ${old}, and its skin has ${skin.remap.length}`);
832
+ }
833
+ joints[i] = moved;
834
+ }
835
+ normaliseWeights(weights, label, warnings);
836
+ }
837
+ return {
838
+ mesh: {
839
+ positions,
840
+ normals,
841
+ colors,
842
+ emissive,
843
+ specular,
844
+ roughness,
845
+ emissiveColor,
846
+ indices,
847
+ ...(uvs === undefined ? {} : { uvs }),
848
+ ...(tangents === undefined ? {} : { tangents }),
849
+ ...(joints === undefined || weights === undefined ? {} : { joints, weights }),
850
+ ...(morphTargets === undefined || morphTargetCount === undefined
851
+ ? {}
852
+ : { morphTargets, morphTargetCount }),
853
+ },
854
+ /*
855
+ * The colour map's *texture* index, not its image index. A material names a texture and
856
+ * two textures may share one image, so following the indirection anywhere but here would
857
+ * make the material point at whichever of them was read first.
858
+ */
859
+ material: surface,
860
+ /* The label an artist put on it, where they put one. A non-string is not a label. */
861
+ ...(typeof material?.extras?.substance === 'string' && material.extras.substance.length > 0
862
+ ? { substance: material.extras.substance }
863
+ : {}),
864
+ };
865
+ }
866
+ /**
867
+ * Make every weight set sum to one, warning once per mesh when any did not.
868
+ *
869
+ * **Here rather than in the shader**, because normalising per vertex per frame costs a divide on
870
+ * every vertex to correct data that should have been fixed once. A set summing to less shrinks the
871
+ * surface toward the origin and a set summing to more inflates it; both read as a deforming mesh
872
+ * rather than as bad data, which is why this warns rather than passing it on silently.
873
+ *
874
+ * A set summing to zero is left alone and reported: it is a vertex nobody weighted, and inventing
875
+ * an influence for it would move geometry the author never rigged. The absent-attribute default is
876
+ * what such a mesh gets if it has no attribute at all; a present-but-zero set is a different fact.
877
+ */
878
+ function normaliseWeights(weights, label, warnings) {
879
+ let corrected = 0;
880
+ let unweighted = 0;
881
+ for (let i = 0; i < weights.length; i += 4) {
882
+ const sum = weights[i] +
883
+ weights[i + 1] +
884
+ weights[i + 2] +
885
+ weights[i + 3];
886
+ if (sum <= 0) {
887
+ unweighted += 1;
888
+ continue;
889
+ }
890
+ if (Math.abs(sum - 1) < 1e-3)
891
+ continue;
892
+ corrected += 1;
893
+ weights[i] = weights[i] / sum;
894
+ weights[i + 1] = weights[i + 1] / sum;
895
+ weights[i + 2] = weights[i + 2] / sum;
896
+ weights[i + 3] = weights[i + 3] / sum;
897
+ }
898
+ if (corrected > 0) {
899
+ warnings.push(`${label}: ${corrected} vertex weight sets did not sum to one and were rescaled`);
900
+ }
901
+ if (unweighted > 0) {
902
+ warnings.push(`${label}: ${unweighted} vertices carry no weight at all and will not deform`);
903
+ }
904
+ }