@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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +9 -0
- package/dist/assetPath.d.ts +51 -0
- package/dist/assetPath.js +59 -0
- package/dist/coarseLevel.d.ts +94 -0
- package/dist/coarseLevel.js +606 -0
- package/dist/dds.d.ts +41 -0
- package/dist/dds.js +425 -0
- package/dist/drawKey.d.ts +64 -0
- package/dist/drawKey.js +42 -0
- package/dist/drftLoader.d.ts +420 -0
- package/dist/drftLoader.js +996 -0
- package/dist/drftTextures.d.ts +87 -0
- package/dist/drftTextures.js +138 -0
- package/dist/fbx.d.ts +86 -0
- package/dist/fbx.js +792 -0
- package/dist/fbxHarness.d.ts +68 -0
- package/dist/fbxHarness.js +185 -0
- package/dist/fbxInflate.d.ts +60 -0
- package/dist/fbxInflate.js +204 -0
- package/dist/fbxMaterial.d.ts +87 -0
- package/dist/fbxMaterial.js +345 -0
- package/dist/fbxRig.d.ts +130 -0
- package/dist/fbxRig.js +601 -0
- package/dist/gltf.d.ts +242 -0
- package/dist/gltf.js +904 -0
- package/dist/gltfAccessor.d.ts +2 -0
- package/dist/gltfAccessor.js +106 -0
- package/dist/gltfSkin.d.ts +32 -0
- package/dist/gltfSkin.js +144 -0
- package/dist/iesPhotometry.d.ts +44 -0
- package/dist/iesPhotometry.js +94 -0
- package/dist/imageInfo.d.ts +31 -0
- package/dist/imageInfo.js +119 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +132 -0
- package/dist/kn5.d.ts +158 -0
- package/dist/kn5.js +557 -0
- package/dist/loadProgress.d.ts +118 -0
- package/dist/loadProgress.js +1 -0
- package/dist/localise.d.ts +35 -0
- package/dist/localise.js +253 -0
- package/dist/obj.d.ts +54 -0
- package/dist/obj.js +335 -0
- package/dist/orient.d.ts +104 -0
- package/dist/orient.js +241 -0
- package/dist/radianceHdr.d.ts +36 -0
- package/dist/radianceHdr.js +172 -0
- package/dist/readModel.d.ts +153 -0
- package/dist/readModel.js +386 -0
- package/dist/recognise.d.ts +38 -0
- package/dist/recognise.js +97 -0
- package/dist/stl.d.ts +22 -0
- package/dist/stl.js +131 -0
- package/dist/tangentOrder.d.ts +24 -0
- package/dist/tangentOrder.js +31 -0
- package/dist/threemf.d.ts +46 -0
- package/dist/threemf.js +254 -0
- package/dist/uploadBudget.d.ts +82 -0
- package/dist/uploadBudget.js +86 -0
- package/dist/usd.d.ts +49 -0
- package/dist/usd.js +542 -0
- package/dist/weld.d.ts +44 -0
- package/dist/weld.js +237 -0
- package/dist/zip.d.ts +35 -0
- package/dist/zip.js +110 -0
- package/package.json +61 -0
- package/src/assetPath.ts +79 -0
- package/src/coarseLevel.ts +720 -0
- package/src/dds.ts +507 -0
- package/src/drawKey.ts +91 -0
- package/src/drftLoader.ts +1197 -0
- package/src/drftTextures.ts +153 -0
- package/src/fbx.ts +934 -0
- package/src/fbxHarness.ts +215 -0
- package/src/fbxInflate.ts +258 -0
- package/src/fbxMaterial.ts +410 -0
- package/src/fbxRig.ts +652 -0
- package/src/gltf.ts +1161 -0
- package/src/gltfAccessor.ts +127 -0
- package/src/gltfSkin.ts +210 -0
- package/src/iesPhotometry.ts +136 -0
- package/src/imageInfo.ts +130 -0
- package/src/index.ts +162 -0
- package/src/kn5.ts +705 -0
- package/src/loadProgress.ts +131 -0
- package/src/localise.ts +275 -0
- package/src/obj.ts +386 -0
- package/src/orient.ts +262 -0
- package/src/radianceHdr.ts +195 -0
- package/src/readModel.ts +515 -0
- package/src/recognise.ts +124 -0
- package/src/stl.ts +147 -0
- package/src/tangentOrder.ts +32 -0
- package/src/threemf.ts +303 -0
- package/src/uploadBudget.ts +90 -0
- package/src/usd.ts +642 -0
- package/src/weld.ts +264 -0
- 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
|
+
}
|