@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/src/usd.ts ADDED
@@ -0,0 +1,642 @@
1
+ /**
2
+ * Pixar's USD, in its ASCII form `.usda` and its zip packaging `.usdz`.
3
+ *
4
+ * Tier 1. USD is Apache-2.0 with a published specification, and `.usda` is plain text — so
5
+ * a complete reader is ordinary work rather than reverse engineering, and it does not rot
6
+ * the way a vendor's binary layout does. An earlier draft of `docs/FORMAT.md` filed it as
7
+ * proprietary and was wrong to.
8
+ *
9
+ * **`.usdz` needs no decompressor.** The format *requires* its entries stored uncompressed
10
+ * so a runtime can memory-map them in place, which is why `zip.ts` takes its inflate as an
11
+ * injected parameter and this module passes none.
12
+ *
13
+ * One `MeshData` per `Mesh` prim, matching the glTF and OBJ paths: the unit that carries a
14
+ * single material is the unit `MeshData` represents.
15
+ *
16
+ * **Only what the file states is read.** USD is a composition system with layers, references,
17
+ * variants and inherited schemas, and none of that is resolved here. A file that leans on it
18
+ * is refused by name rather than half-imported, which is the rule tier 1 is held to.
19
+ */
20
+
21
+ import type { MeshData } from '@driftengine/drft';
22
+ import { DrftError } from '@driftengine/drft';
23
+ import type { DrftMaterial } from '@driftengine/drft';
24
+ import type { AssetReference } from './assetPath.ts';
25
+ import { readZip } from './zip.ts';
26
+ import type { UpAxis } from './orient.ts';
27
+
28
+ /** What a `.usda` yields, in the shape every other reader in this directory returns. */
29
+ export interface UsdResult {
30
+ readonly meshes: MeshData[];
31
+ /** One per mesh, by ordinal. */
32
+ readonly materials: DrftMaterial[];
33
+ readonly textures: AssetReference[];
34
+ readonly warnings: string[];
35
+ /** `upAxis` from the stage metadata. USD writes "Y" or "Z" and defaults to Y. */
36
+ readonly declaredUp?: UpAxis;
37
+ /** `metersPerUnit` from the stage metadata, 1 when it says nothing. */
38
+ readonly unitScale: number;
39
+ }
40
+
41
+ /** A prim as the scanner finds it: its type, its name, its body and its children. */
42
+ interface Prim {
43
+ readonly type: string;
44
+ readonly name: string;
45
+ readonly path: string;
46
+ /** Attribute lines belonging to this prim, excluding anything inside a child. */
47
+ readonly body: string;
48
+ readonly children: Prim[];
49
+ }
50
+
51
+ const DEFAULT_MATERIAL: DrftMaterial = {
52
+ name: '',
53
+ color: [0.8, 0.8, 0.8],
54
+ specular: 0,
55
+ roughness: 0.5,
56
+ emissive: 0,
57
+ emissiveColor: [-1, -1, -1],
58
+ opacity: 1,
59
+ albedo: -1,
60
+ reflectivity: 0,
61
+ normalMap: -1,
62
+ ormMap: -1,
63
+ emissiveMap: -1,
64
+ roughnessScale: 1,
65
+ metallicScale: 1,
66
+ occlusionStrength: 0,
67
+ cutout: 0,
68
+ };
69
+
70
+ const clamp01 = (value: number): number => (value < 0 ? 0 : value > 1 ? 1 : value);
71
+
72
+ /**
73
+ * Split a `.usda` body into prims, keeping the nesting.
74
+ *
75
+ * Brace matching rather than a line scan, because a prim's body contains arrays that span
76
+ * lines and strings that contain braces. Quoted spans and `@asset@` paths are stepped over
77
+ * so a brace inside one cannot end a prim early.
78
+ */
79
+ function scanPrims(text: string, from: number, to: number, parentPath: string): Prim[] {
80
+ const prims: Prim[] = [];
81
+ const header = /\b(?:def|over|class)\s+(\w+)?\s*"([^"]+)"/g;
82
+ header.lastIndex = from;
83
+
84
+ let match = header.exec(text);
85
+ while (match !== null && match.index < to) {
86
+ const open = text.indexOf('{', header.lastIndex);
87
+ if (open === -1 || open >= to) break;
88
+ const close = matchBrace(text, open);
89
+ if (close === -1 || close > to) {
90
+ throw new DrftError(`usda: prim "${match[2] as string}" is never closed`);
91
+ }
92
+ const name = match[2] as string;
93
+ const path = `${parentPath}/${name}`;
94
+ const children = scanPrims(text, open + 1, close, path);
95
+ /* The body with every child cut out, so an attribute lookup cannot reach into one. */
96
+ let body = '';
97
+ let cursor = open + 1;
98
+ for (const child of children) {
99
+ const at = text.indexOf(`"${child.name}"`, cursor);
100
+ const childOpen = at === -1 ? -1 : text.indexOf('{', at);
101
+ if (childOpen === -1 || childOpen >= close) continue;
102
+ body += text.slice(cursor, at);
103
+ cursor = matchBrace(text, childOpen) + 1;
104
+ }
105
+ body += text.slice(cursor, close);
106
+ prims.push({ type: match[1] ?? '', name, path, body, children });
107
+ header.lastIndex = close + 1;
108
+ match = header.exec(text);
109
+ }
110
+ return prims;
111
+ }
112
+
113
+ /** The index of the `}` closing the `{` at `open`, skipping quoted and `@asset@` spans. */
114
+ function matchBrace(text: string, open: number): number {
115
+ let depth = 0;
116
+ for (let at = open; at < text.length; at++) {
117
+ const char = text[at];
118
+ if (char === '"' || char === "'") {
119
+ const end = text.indexOf(char, at + 1);
120
+ at = end === -1 ? text.length : end;
121
+ continue;
122
+ }
123
+ if (char === '@') {
124
+ const end = text.indexOf('@', at + 1);
125
+ at = end === -1 ? text.length : end;
126
+ continue;
127
+ }
128
+ if (char === '#') {
129
+ const end = text.indexOf('\n', at);
130
+ at = end === -1 ? text.length : end;
131
+ continue;
132
+ }
133
+ if (char === '{') depth++;
134
+ else if (char === '}' && --depth === 0) return at;
135
+ }
136
+ return -1;
137
+ }
138
+
139
+ /** Every number in a span, in order. USD writes tuples and arrays as parenthesised lists. */
140
+ function numbers(span: string): number[] {
141
+ const found = span.match(/-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?/g);
142
+ return found === null ? [] : found.map(Number);
143
+ }
144
+
145
+ /** The value span of `name` within a prim body, or null. Handles `name = value` and `name.connect`. */
146
+ function attribute(body: string, name: string): string | null {
147
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
148
+ /* Up to the end of the line, unless the value opens a bracket, in which case to its close. */
149
+ const pattern = new RegExp(`\\b${escaped}\\s*=\\s*`, 'g');
150
+ const match = pattern.exec(body);
151
+ if (match === null) return null;
152
+ const start = match.index + match[0].length;
153
+ if (body[start] === '[' || body[start] === '(') {
154
+ const opener = body[start] as string;
155
+ const closer = opener === '[' ? ']' : ')';
156
+ let depth = 0;
157
+ for (let at = start; at < body.length; at++) {
158
+ if (body[at] === opener) depth++;
159
+ else if (body[at] === closer && --depth === 0) return body.slice(start, at + 1);
160
+ }
161
+ return body.slice(start);
162
+ }
163
+ const end = body.indexOf('\n', start);
164
+ return body.slice(start, end === -1 ? body.length : end);
165
+ }
166
+
167
+ /**
168
+ * Compose a prim's local transform from its xform ops.
169
+ *
170
+ * `xformOpOrder` is authoritative when present, because USD applies the ops in the order it
171
+ * names and a reader that assumes translate-rotate-scale gets a file with a different order
172
+ * subtly wrong — the same class of mistake that put a car upside down and reported it as
173
+ * verified. Returns a column-major 4x4, matching `gltf.ts`.
174
+ */
175
+ function localTransform(body: string): Float32Array {
176
+ const out = identity();
177
+ const declared =
178
+ attribute(body, 'uniform token\\[\\] xformOpOrder') ?? attribute(body, 'xformOpOrder');
179
+ const ops =
180
+ declared === null
181
+ ? [
182
+ 'xformOp:transform',
183
+ 'xformOp:translate',
184
+ 'xformOp:orient',
185
+ 'xformOp:rotateXYZ',
186
+ 'xformOp:scale',
187
+ ]
188
+ : (declared.match(/"([^"]+)"/g) ?? []).map((quoted) => quoted.slice(1, -1));
189
+
190
+ for (const op of ops) {
191
+ const value = attribute(body, op);
192
+ if (value === null) continue;
193
+ const n = numbers(value);
194
+ if (op.startsWith('xformOp:transform')) {
195
+ if (n.length >= 16) multiply(out, Float32Array.from(n.slice(0, 16)));
196
+ } else if (op.startsWith('xformOp:translate')) {
197
+ const m = identity();
198
+ m[12] = n[0] ?? 0;
199
+ m[13] = n[1] ?? 0;
200
+ m[14] = n[2] ?? 0;
201
+ multiply(out, m);
202
+ } else if (op.startsWith('xformOp:scale')) {
203
+ const m = identity();
204
+ m[0] = n[0] ?? 1;
205
+ m[5] = n[1] ?? 1;
206
+ m[10] = n[2] ?? 1;
207
+ multiply(out, m);
208
+ } else if (op.startsWith('xformOp:rotate')) {
209
+ /* Degrees, and the axis order is in the op's own name: rotateXYZ, rotateZYX and so on. */
210
+ const order = op.slice(op.indexOf('rotate') + 6).replace(/:.*$/, '');
211
+ const angles = [n[0] ?? 0, n[1] ?? 0, n[2] ?? 0];
212
+ for (let i = 0; i < order.length && i < 3; i++) {
213
+ multiply(out, axisRotation(order[i] as string, ((angles[i] as number) * Math.PI) / 180));
214
+ }
215
+ } else if (op.startsWith('xformOp:orient')) {
216
+ /* USD writes a quaternion as (w, x, y, z). */
217
+ multiply(out, quaternion(n[1] ?? 0, n[2] ?? 0, n[3] ?? 0, n[0] ?? 1));
218
+ }
219
+ }
220
+ return out;
221
+ }
222
+
223
+ function identity(): Float32Array {
224
+ const m = new Float32Array(16);
225
+ m[0] = 1;
226
+ m[5] = 1;
227
+ m[10] = 1;
228
+ m[15] = 1;
229
+ return m;
230
+ }
231
+
232
+ function axisRotation(axis: string, radians: number): Float32Array {
233
+ const m = identity();
234
+ const c = Math.cos(radians);
235
+ const s = Math.sin(radians);
236
+ if (axis === 'X') {
237
+ m[5] = c;
238
+ m[6] = s;
239
+ m[9] = -s;
240
+ m[10] = c;
241
+ } else if (axis === 'Y') {
242
+ m[0] = c;
243
+ m[2] = -s;
244
+ m[8] = s;
245
+ m[10] = c;
246
+ } else {
247
+ m[0] = c;
248
+ m[1] = s;
249
+ m[4] = -s;
250
+ m[5] = c;
251
+ }
252
+ return m;
253
+ }
254
+
255
+ function quaternion(x: number, y: number, z: number, w: number): Float32Array {
256
+ const m = identity();
257
+ m[0] = 1 - 2 * (y * y + z * z);
258
+ m[1] = 2 * (x * y + z * w);
259
+ m[2] = 2 * (x * z - y * w);
260
+ m[4] = 2 * (x * y - z * w);
261
+ m[5] = 1 - 2 * (x * x + z * z);
262
+ m[6] = 2 * (y * z + x * w);
263
+ m[8] = 2 * (x * z + y * w);
264
+ m[9] = 2 * (y * z - x * w);
265
+ m[10] = 1 - 2 * (x * x + y * y);
266
+ return m;
267
+ }
268
+
269
+ /** `into = into * by`, both column-major. */
270
+ function multiply(into: Float32Array, by: Float32Array): void {
271
+ const a = Float32Array.from(into);
272
+ for (let col = 0; col < 4; col++) {
273
+ for (let row = 0; row < 4; row++) {
274
+ let sum = 0;
275
+ for (let k = 0; k < 4; k++) {
276
+ sum += (a[k * 4 + row] as number) * (by[col * 4 + k] as number);
277
+ }
278
+ into[col * 4 + row] = sum;
279
+ }
280
+ }
281
+ }
282
+
283
+ function transformPoint(
284
+ m: Float32Array,
285
+ x: number,
286
+ y: number,
287
+ z: number,
288
+ ): [number, number, number] {
289
+ return [
290
+ (m[0] as number) * x + (m[4] as number) * y + (m[8] as number) * z + (m[12] as number),
291
+ (m[1] as number) * x + (m[5] as number) * y + (m[9] as number) * z + (m[13] as number),
292
+ (m[2] as number) * x + (m[6] as number) * y + (m[10] as number) * z + (m[14] as number),
293
+ ];
294
+ }
295
+
296
+ /** Rotation only, for normals. Uniform scale is assumed, as `MeshBuilder.addMesh` assumes it. */
297
+ function transformDirection(
298
+ m: Float32Array,
299
+ x: number,
300
+ y: number,
301
+ z: number,
302
+ ): [number, number, number] {
303
+ const out: [number, number, number] = [
304
+ (m[0] as number) * x + (m[4] as number) * y + (m[8] as number) * z,
305
+ (m[1] as number) * x + (m[5] as number) * y + (m[9] as number) * z,
306
+ (m[2] as number) * x + (m[6] as number) * y + (m[10] as number) * z,
307
+ ];
308
+ const length = Math.hypot(out[0], out[1], out[2]) || 1;
309
+ out[0] /= length;
310
+ out[1] /= length;
311
+ out[2] /= length;
312
+ return out;
313
+ }
314
+
315
+ /** Every prim in the tree, flattened, each with the world transform it inherits. */
316
+ function flatten(
317
+ prims: readonly Prim[],
318
+ parent: Float32Array,
319
+ out: { prim: Prim; world: Float32Array }[],
320
+ ): void {
321
+ for (const prim of prims) {
322
+ const world = Float32Array.from(parent);
323
+ multiply(world, localTransform(prim.body));
324
+ out.push({ prim, world });
325
+ flatten(prim.children, world, out);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * A `UsdPreviewSurface` and whatever textures it names.
331
+ *
332
+ * The shader network is followed only one hop: an input either states a value or connects to
333
+ * a `UsdUVTexture` whose `inputs:file` is the image. That covers what an exporter writes and
334
+ * refuses to guess at anything deeper, which is the tier 1 rule.
335
+ */
336
+ function readMaterial(
337
+ material: Prim,
338
+ textures: AssetReference[],
339
+ warnings: string[],
340
+ ): DrftMaterial {
341
+ const shaders = material.children.filter((child) => child.type === 'Shader');
342
+ const surface = shaders.find((s) =>
343
+ (attribute(s.body, 'uniform token info:id') ?? '').includes('UsdPreviewSurface'),
344
+ );
345
+ if (surface === undefined) {
346
+ warnings.push(`material "${material.name}" names no UsdPreviewSurface; using defaults`);
347
+ return { ...DEFAULT_MATERIAL, name: material.name };
348
+ }
349
+
350
+ const value = (input: string): number[] | null => {
351
+ const span = attribute(surface.body, `inputs:${input}`);
352
+ return span === null ? null : numbers(span);
353
+ };
354
+ /* An input that connects rather than states: follow it to the texture it names. */
355
+ const connected = (input: string): string | null => {
356
+ const span = attribute(surface.body, `inputs:${input}.connect`);
357
+ if (span === null) return null;
358
+ const target = /<([^>]+)>/.exec(span)?.[1];
359
+ if (target === undefined) return null;
360
+ const shaderPath = target.slice(0, target.lastIndexOf('.'));
361
+ const shader = shaders.find((s) => s.path === shaderPath || shaderPath.endsWith(`/${s.name}`));
362
+ if (shader === undefined) return null;
363
+ return /@([^@]+)@/.exec(attribute(shader.body, 'inputs:file') ?? '')?.[1] ?? null;
364
+ };
365
+
366
+ const diffuse = value('diffuseColor');
367
+ const emissive = value('emissiveColor');
368
+ const roughness = value('roughness')?.[0];
369
+ const metallic = value('metallic')?.[0];
370
+ const opacity = value('opacity')?.[0];
371
+
372
+ let albedo = -1;
373
+ const map = connected('diffuseColor');
374
+ if (map !== null) {
375
+ const existing = textures.findIndex((texture) => texture.name === map);
376
+ albedo = existing === -1 ? textures.push({ name: map }) - 1 : existing;
377
+ }
378
+
379
+ /*
380
+ * Metalness onto specular and reflectivity, stated as the approximation it is and matching
381
+ * what `gltf.ts` does with the same input, so a `.glb` and a `.usdz` of one asset bake to
382
+ * the same `.drft` rather than to two defensible readings.
383
+ */
384
+ const metal = metallic === undefined ? 0 : clamp01(metallic);
385
+ const emissiveStrength =
386
+ emissive === null || emissive.length < 3
387
+ ? 0
388
+ : clamp01(Math.max(emissive[0] as number, emissive[1] as number, emissive[2] as number));
389
+
390
+ return {
391
+ name: material.name,
392
+ color:
393
+ diffuse !== null && diffuse.length >= 3
394
+ ? [
395
+ clamp01(diffuse[0] as number),
396
+ clamp01(diffuse[1] as number),
397
+ clamp01(diffuse[2] as number),
398
+ ]
399
+ : DEFAULT_MATERIAL.color,
400
+ specular: metal,
401
+ roughness: roughness === undefined ? DEFAULT_MATERIAL.roughness : clamp01(roughness),
402
+ emissive: emissiveStrength,
403
+ emissiveColor:
404
+ emissiveStrength > 0 && emissive !== null
405
+ ? [emissive[0] as number, emissive[1] as number, emissive[2] as number]
406
+ : DEFAULT_MATERIAL.emissiveColor,
407
+ opacity: opacity === undefined ? 1 : clamp01(opacity),
408
+ albedo,
409
+ reflectivity: metal,
410
+ normalMap: -1,
411
+ ormMap: -1,
412
+ emissiveMap: -1,
413
+ roughnessScale: 1,
414
+ metallicScale: 1,
415
+ occlusionStrength: 0,
416
+ cutout: 0,
417
+ };
418
+ }
419
+
420
+ /**
421
+ * One `Mesh` prim, triangulated and flattened into world space.
422
+ *
423
+ * USD indexes its face vertices with `faceVertexCounts` and `faceVertexIndices`, and its
424
+ * normals and texture coordinates may be either per point or per *face vertex*. Face-varying
425
+ * data is the common case out of every exporter, and it is why a corner becomes its own
426
+ * vertex here exactly as it does in the OBJ and FBX paths: welding is the baker's job and it
427
+ * already has a module for it.
428
+ */
429
+ function readMesh(prim: Prim, world: Float32Array, warnings: string[]): MeshData | null {
430
+ const points = numbers(
431
+ attribute(prim.body, 'point3f[] points') ?? attribute(prim.body, 'points') ?? '',
432
+ );
433
+ const counts = numbers(
434
+ attribute(prim.body, 'int[] faceVertexCounts') ??
435
+ attribute(prim.body, 'faceVertexCounts') ??
436
+ '',
437
+ );
438
+ const indices = numbers(
439
+ attribute(prim.body, 'int[] faceVertexIndices') ??
440
+ attribute(prim.body, 'faceVertexIndices') ??
441
+ '',
442
+ );
443
+ if (points.length < 9 || counts.length === 0 || indices.length === 0) {
444
+ warnings.push(`mesh "${prim.name}" carries no usable geometry and was skipped`);
445
+ return null;
446
+ }
447
+
448
+ const normalSpan = attribute(prim.body, 'normal3f[] normals') ?? attribute(prim.body, 'normals');
449
+ const uvSpan =
450
+ attribute(prim.body, 'texCoord2f[] primvars:st') ??
451
+ attribute(prim.body, 'float2[] primvars:st') ??
452
+ attribute(prim.body, 'primvars:st');
453
+ const normals = normalSpan === null ? [] : numbers(normalSpan);
454
+ const uvs = uvSpan === null ? [] : numbers(uvSpan);
455
+ const uvIndices = numbers(attribute(prim.body, 'primvars:st:indices') ?? '');
456
+
457
+ const pointCount = Math.floor(points.length / 3);
458
+ const cornerCount = indices.length;
459
+ const normalsPerPoint = normals.length / 3 === pointCount && normals.length / 3 !== cornerCount;
460
+ const uvsPerPoint =
461
+ uvIndices.length === 0 && uvs.length / 2 === pointCount && uvs.length / 2 !== cornerCount;
462
+
463
+ const outPositions: number[] = [];
464
+ const outNormals: number[] = [];
465
+ const outUvs: number[] = [];
466
+ const outIndices: number[] = [];
467
+
468
+ let corner = 0;
469
+ for (const count of counts) {
470
+ if (count < 3) {
471
+ corner += count;
472
+ continue;
473
+ }
474
+ const base = outPositions.length / 3;
475
+ for (let i = 0; i < count; i++) {
476
+ const at = corner + i;
477
+ const point = indices[at] as number;
478
+ if (point < 0 || point >= pointCount) {
479
+ throw new DrftError(`usda: mesh "${prim.name}" indexes point ${point} of ${pointCount}`);
480
+ }
481
+ const [x, y, z] = transformPoint(
482
+ world,
483
+ points[point * 3] as number,
484
+ points[point * 3 + 1] as number,
485
+ points[point * 3 + 2] as number,
486
+ );
487
+ outPositions.push(x, y, z);
488
+
489
+ const normalAt = normalsPerPoint ? point : at;
490
+ if (normals.length >= (normalAt + 1) * 3) {
491
+ const [nx, ny, nz] = transformDirection(
492
+ world,
493
+ normals[normalAt * 3] as number,
494
+ normals[normalAt * 3 + 1] as number,
495
+ normals[normalAt * 3 + 2] as number,
496
+ );
497
+ outNormals.push(nx, ny, nz);
498
+ } else {
499
+ outNormals.push(0, 1, 0);
500
+ }
501
+
502
+ if (uvs.length > 0) {
503
+ const uvAt = uvIndices.length > 0 ? (uvIndices[at] ?? 0) : uvsPerPoint ? point : at;
504
+ /*
505
+ * V is flipped, once, here. USD puts the origin at the bottom left and this engine
506
+ * uploads with it at the top left, which is the same single flip `fbx.ts` documents.
507
+ */
508
+ outUvs.push(uvs[uvAt * 2] ?? 0, 1 - (uvs[uvAt * 2 + 1] ?? 0));
509
+ }
510
+ }
511
+ /* Fan triangulation, which is what a convex polygon from an exporter wants. */
512
+ for (let i = 1; i + 1 < count; i++) outIndices.push(base, base + i, base + i + 1);
513
+ corner += count;
514
+ }
515
+
516
+ if (outIndices.length === 0) {
517
+ warnings.push(`mesh "${prim.name}" had no faces with three or more corners`);
518
+ return null;
519
+ }
520
+
521
+ const vertices = outPositions.length / 3;
522
+ return {
523
+ positions: Float32Array.from(outPositions),
524
+ normals: Float32Array.from(outNormals),
525
+ colors: new Float32Array(vertices * 3).fill(1),
526
+ emissive: new Float32Array(vertices),
527
+ ...(outUvs.length === vertices * 2 ? { uvs: Float32Array.from(outUvs) } : {}),
528
+ indices: Uint32Array.from(outIndices),
529
+ };
530
+ }
531
+
532
+ /** Read a `.usda` document. */
533
+ export function parseUsda(text: string): UsdResult {
534
+ if (!text.trimStart().startsWith('#usda')) {
535
+ throw new DrftError('usda: the file does not begin with #usda, so it is not ASCII USD');
536
+ }
537
+
538
+ const warnings: string[] = [];
539
+ const stageEnd = text.indexOf('{') === -1 ? text.length : text.indexOf('{');
540
+ const stage = text.slice(0, stageEnd);
541
+ const up = /upAxis\s*=\s*"([YZ])"/.exec(stage)?.[1];
542
+ const metersPerUnit = Number(/metersPerUnit\s*=\s*([\d.eE+-]+)/.exec(stage)?.[1] ?? '1');
543
+
544
+ const roots = scanPrims(text, 0, text.length, '');
545
+ const flat: { prim: Prim; world: Float32Array }[] = [];
546
+ flatten(roots, identity(), flat);
547
+
548
+ /* Materials first, so a mesh's binding resolves to one that already exists. */
549
+ const textures: AssetReference[] = [];
550
+ const materialsByPath = new Map<string, DrftMaterial>();
551
+ for (const { prim } of flat) {
552
+ if (prim.type === 'Material')
553
+ materialsByPath.set(prim.path, readMaterial(prim, textures, warnings));
554
+ }
555
+
556
+ const meshes: MeshData[] = [];
557
+ const materials: DrftMaterial[] = [];
558
+ for (const { prim, world } of flat) {
559
+ if (prim.type !== 'Mesh') continue;
560
+ const mesh = readMesh(prim, world, warnings);
561
+ if (mesh === null) continue;
562
+ meshes.push(mesh);
563
+
564
+ const binding = /<([^>]+)>/.exec(
565
+ attribute(prim.body, 'rel material:binding') ??
566
+ attribute(prim.body, 'material:binding') ??
567
+ '',
568
+ )?.[1];
569
+ const bound =
570
+ binding === undefined
571
+ ? undefined
572
+ : (materialsByPath.get(binding) ??
573
+ [...materialsByPath.entries()].find(
574
+ ([path]) => binding.endsWith(path) || path.endsWith(binding),
575
+ )?.[1]);
576
+ materials.push(bound === undefined ? { ...DEFAULT_MATERIAL, name: prim.name } : bound);
577
+ }
578
+
579
+ if (meshes.length === 0) throw new DrftError('usda: the stage carries no readable Mesh prim');
580
+
581
+ return {
582
+ meshes,
583
+ materials,
584
+ textures,
585
+ warnings,
586
+ /* USD writes a bare "Y" or "Z" and means the positive axis; the engine names the sign. */
587
+ ...(up === 'Z'
588
+ ? { declaredUp: '+z' as UpAxis }
589
+ : up === 'Y'
590
+ ? { declaredUp: '+y' as UpAxis }
591
+ : {}),
592
+ unitScale: Number.isFinite(metersPerUnit) && metersPerUnit > 0 ? metersPerUnit : 1,
593
+ };
594
+ }
595
+
596
+ /**
597
+ * Read a `.usdz`: a zip holding a `.usda` or a `.usdc`, plus whatever images it names.
598
+ *
599
+ * No inflate is passed, and that is a property of the format rather than an omission — a
600
+ * `.usdz` must store its entries uncompressed so they can be memory mapped, so an archive
601
+ * that deflates them is not a conforming `.usdz` and `zip.ts` says so by number.
602
+ *
603
+ * A `.usdc` inside is refused rather than attempted. The binary crate format is specified
604
+ * and is a separate reader; half-reading it would be exactly the partial import tier 1
605
+ * forbids.
606
+ */
607
+ export function usdzToMeshes(buffer: ArrayBuffer): UsdResult {
608
+ const entries = readZip(buffer);
609
+ const ascii = entries.find((entry) => entry.name.toLowerCase().endsWith('.usda'));
610
+ if (ascii === undefined) {
611
+ const crate = entries.find((entry) => /\.usdc?$/i.test(entry.name));
612
+ if (crate !== undefined) {
613
+ throw new DrftError(
614
+ `usdz: "${crate.name}" is the binary crate format, which this reader does not read. ` +
615
+ `Export the usdz with an ASCII .usda inside, or convert it with usdcat.`,
616
+ );
617
+ }
618
+ throw new DrftError(
619
+ `usdz: the archive holds no .usda (it holds ${entries.map((entry) => entry.name).join(', ')})`,
620
+ );
621
+ }
622
+
623
+ const result = parseUsda(new TextDecoder().decode(ascii.bytes));
624
+
625
+ /*
626
+ * Images travel inside the archive, so a texture the stage names is resolved here rather
627
+ * than left to the baker's filesystem lookup. Matched on the basename as well as the
628
+ * declared path, because a stage may say `./textures/x.png` while the archive stores
629
+ * `textures/x.png`.
630
+ */
631
+ const textures = result.textures.map((texture) => {
632
+ const wanted = texture.name.replace(/\\/g, '/').replace(/^\.\//, '');
633
+ const base = wanted.slice(wanted.lastIndexOf('/') + 1).toLowerCase();
634
+ const found = entries.find((entry) => {
635
+ const name = entry.name.toLowerCase();
636
+ return name === wanted.toLowerCase() || name.endsWith(`/${base}`) || name === base;
637
+ });
638
+ return found === undefined ? texture : { name: texture.name, bytes: found.bytes };
639
+ });
640
+
641
+ return { ...result, textures };
642
+ }