@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
@@ -0,0 +1,86 @@
1
+ /**
2
+ * How much uploading fits in one frame, and how to tell bytes from an error page.
3
+ *
4
+ * Pure decisions with no loader state behind them: whether another upload fits the frame's
5
+ * budget, and whether a server answered a request for a model with HTML.
6
+ */
7
+ export const DEFAULT_UPLOADS_PER_FRAME = 3;
8
+ export const DEFAULT_REVEAL_SEC = 0.35;
9
+ /**
10
+ * How long one `update` may spend *starting* new stream work.
11
+ *
12
+ * A minority of a 60 Hz frame, and under the whole of a 120 Hz one, because the scene has to
13
+ * draw as well as load. It is a budget for beginning work rather than for finishing it: a piece
14
+ * already started is always finished, so a frame overruns by whatever the last piece cost — see
15
+ * `mayBeginMore` for why that residue is a real limit and not a rounding error.
16
+ */
17
+ export const DEFAULT_UPLOAD_MS_PER_FRAME = 6;
18
+ /**
19
+ * Whether another piece of stream work may begin in this frame.
20
+ *
21
+ * **The clock decides, because a count cannot.** `uploadsPerFrame` bounded the number of parts a
22
+ * frame took, on the reasoning in its own docstring: that a part is a small buffer upload and
23
+ * that the allocations are the cost. Measured through the demo page on the shipped Audi, that is
24
+ * backwards. `createMesh` costs 0.6 to 3.1 ms; the per-part CPU work around it costs 6 to 254 ms,
25
+ * most of it the consumer's `transform` plus the two further walks over the same vertices this
26
+ * class does itself. A count bounded the cheap term and left the expensive one free, so three
27
+ * large parts made one 318 ms frame.
28
+ *
29
+ * The loader cannot know what a part costs before it pays for it, and it does not have to: a
30
+ * clock read after each one is exact, needs no per-asset tuning, and is as right on a phone as on
31
+ * a workstation.
32
+ *
33
+ * **The first piece always begins.** One part of a heavy model can cost more than the entire
34
+ * budget, and a rule that asked only "is there room left" would then start nothing on any frame
35
+ * and the load would never finish. A hitch is worse than smooth; a load stuck at 40% is worse
36
+ * than a hitch.
37
+ *
38
+ * **`StepBudget` in `@driftengine/core` is the general form of this and is the one to reach for
39
+ * in new code.** It adds what this cannot do: an estimate of the worst stop, so it refuses to
40
+ * *begin* a unit there is no room to finish rather than discovering that afterwards, and a name
41
+ * for the unit that blew the budget. This one stays because the loader's update is a priority
42
+ * ladder rather than a queue of units — an outline, then parts, then one image, then a merge,
43
+ * each with its own reason to go before the next — and the numbers below were measured against
44
+ * that order.
45
+ *
46
+ * **What this does not fix, stated because measuring it is what found it.** A single part is
47
+ * indivisible here, and the worst one in that car costs 254 ms on an RX 9070 XT. This stops the
48
+ * others joining it in the same frame — the measured worst frame falls from 318 ms to 254 — and
49
+ * that is the whole of the improvement. Making that one part fit a frame is a different change
50
+ * to a different place: either its per-part work moves off the main thread, or a part large
51
+ * enough is split across frames. Neither is a budget, which is why neither is here.
52
+ *
53
+ * **The second of those now exists** as `createMeshIncremental`, and it is deliberately not
54
+ * wired up here: measured on that same car, `createMesh` is 0.6 to 3.1 ms of a part and the 254
55
+ * belongs to the per-part CPU work around it. Splitting the upload would divide the small half.
56
+ *
57
+ * @param begunThisFrame How many pieces this frame has already started.
58
+ * @param elapsedMs How long this frame has been spending on the stream.
59
+ * @param budgetMs What it is allowed.
60
+ */
61
+ export function mayBeginMore(begunThisFrame, elapsedMs, budgetMs) {
62
+ return begunThisFrame === 0 || elapsedMs < budgetMs;
63
+ }
64
+ /**
65
+ * Whether a response carrying 200 is actually a *document* rather than a container.
66
+ *
67
+ * `response.ok` is not enough to tell a missing model from a broken one, and the gap is not
68
+ * exotic: a dev server and most static hosts answer an unknown path with the site's own page
69
+ * under a 200 rather than with a 404. The bytes then reach the reader, the magic does not
70
+ * match, and a model that was simply never deployed is reported as a corrupt file.
71
+ *
72
+ * That is what this exists to prevent, and it was found on a published demo page: the site
73
+ * serves no model, its dev server answered `/car.drft` with 2,947 bytes of HTML under a 200,
74
+ * and the scene read **"the model failed: not a drft file"** at a reader. The deployed site
75
+ * was correct at the same moment, because it answers a real 404 — so the two disagreed, and
76
+ * the honest state was the one nobody could see locally.
77
+ *
78
+ * A declared content type is the right signal rather than sniffing the first bytes: a host
79
+ * answering with a page says so in the header, and it says so *before* a byte of body is
80
+ * read, which is what lets the caller skip downloading a page it has no use for. A genuinely
81
+ * corrupt container still reaches the reader and still fails loudly, which is the distinction
82
+ * worth keeping.
83
+ */
84
+ export function isDocumentResponse(contentType) {
85
+ return (contentType ?? '').trim().toLowerCase().startsWith('text/html');
86
+ }
package/dist/usd.d.ts ADDED
@@ -0,0 +1,49 @@
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
+ import type { MeshData } from '@driftengine/drft';
21
+ import type { DrftMaterial } from '@driftengine/drft';
22
+ import type { AssetReference } from './assetPath.ts';
23
+ import type { UpAxis } from './orient.ts';
24
+ /** What a `.usda` yields, in the shape every other reader in this directory returns. */
25
+ export interface UsdResult {
26
+ readonly meshes: MeshData[];
27
+ /** One per mesh, by ordinal. */
28
+ readonly materials: DrftMaterial[];
29
+ readonly textures: AssetReference[];
30
+ readonly warnings: string[];
31
+ /** `upAxis` from the stage metadata. USD writes "Y" or "Z" and defaults to Y. */
32
+ readonly declaredUp?: UpAxis;
33
+ /** `metersPerUnit` from the stage metadata, 1 when it says nothing. */
34
+ readonly unitScale: number;
35
+ }
36
+ /** Read a `.usda` document. */
37
+ export declare function parseUsda(text: string): UsdResult;
38
+ /**
39
+ * Read a `.usdz`: a zip holding a `.usda` or a `.usdc`, plus whatever images it names.
40
+ *
41
+ * No inflate is passed, and that is a property of the format rather than an omission — a
42
+ * `.usdz` must store its entries uncompressed so they can be memory mapped, so an archive
43
+ * that deflates them is not a conforming `.usdz` and `zip.ts` says so by number.
44
+ *
45
+ * A `.usdc` inside is refused rather than attempted. The binary crate format is specified
46
+ * and is a separate reader; half-reading it would be exactly the partial import tier 1
47
+ * forbids.
48
+ */
49
+ export declare function usdzToMeshes(buffer: ArrayBuffer): UsdResult;
package/dist/usd.js ADDED
@@ -0,0 +1,542 @@
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
+ import { DrftError } from '@driftengine/drft';
21
+ import { readZip } from './zip.js';
22
+ const DEFAULT_MATERIAL = {
23
+ name: '',
24
+ color: [0.8, 0.8, 0.8],
25
+ specular: 0,
26
+ roughness: 0.5,
27
+ emissive: 0,
28
+ emissiveColor: [-1, -1, -1],
29
+ opacity: 1,
30
+ albedo: -1,
31
+ reflectivity: 0,
32
+ normalMap: -1,
33
+ ormMap: -1,
34
+ emissiveMap: -1,
35
+ roughnessScale: 1,
36
+ metallicScale: 1,
37
+ occlusionStrength: 0,
38
+ cutout: 0,
39
+ };
40
+ const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);
41
+ /**
42
+ * Split a `.usda` body into prims, keeping the nesting.
43
+ *
44
+ * Brace matching rather than a line scan, because a prim's body contains arrays that span
45
+ * lines and strings that contain braces. Quoted spans and `@asset@` paths are stepped over
46
+ * so a brace inside one cannot end a prim early.
47
+ */
48
+ function scanPrims(text, from, to, parentPath) {
49
+ const prims = [];
50
+ const header = /\b(?:def|over|class)\s+(\w+)?\s*"([^"]+)"/g;
51
+ header.lastIndex = from;
52
+ let match = header.exec(text);
53
+ while (match !== null && match.index < to) {
54
+ const open = text.indexOf('{', header.lastIndex);
55
+ if (open === -1 || open >= to)
56
+ break;
57
+ const close = matchBrace(text, open);
58
+ if (close === -1 || close > to) {
59
+ throw new DrftError(`usda: prim "${match[2]}" is never closed`);
60
+ }
61
+ const name = match[2];
62
+ const path = `${parentPath}/${name}`;
63
+ const children = scanPrims(text, open + 1, close, path);
64
+ /* The body with every child cut out, so an attribute lookup cannot reach into one. */
65
+ let body = '';
66
+ let cursor = open + 1;
67
+ for (const child of children) {
68
+ const at = text.indexOf(`"${child.name}"`, cursor);
69
+ const childOpen = at === -1 ? -1 : text.indexOf('{', at);
70
+ if (childOpen === -1 || childOpen >= close)
71
+ continue;
72
+ body += text.slice(cursor, at);
73
+ cursor = matchBrace(text, childOpen) + 1;
74
+ }
75
+ body += text.slice(cursor, close);
76
+ prims.push({ type: match[1] ?? '', name, path, body, children });
77
+ header.lastIndex = close + 1;
78
+ match = header.exec(text);
79
+ }
80
+ return prims;
81
+ }
82
+ /** The index of the `}` closing the `{` at `open`, skipping quoted and `@asset@` spans. */
83
+ function matchBrace(text, open) {
84
+ let depth = 0;
85
+ for (let at = open; at < text.length; at++) {
86
+ const char = text[at];
87
+ if (char === '"' || char === "'") {
88
+ const end = text.indexOf(char, at + 1);
89
+ at = end === -1 ? text.length : end;
90
+ continue;
91
+ }
92
+ if (char === '@') {
93
+ const end = text.indexOf('@', at + 1);
94
+ at = end === -1 ? text.length : end;
95
+ continue;
96
+ }
97
+ if (char === '#') {
98
+ const end = text.indexOf('\n', at);
99
+ at = end === -1 ? text.length : end;
100
+ continue;
101
+ }
102
+ if (char === '{')
103
+ depth++;
104
+ else if (char === '}' && --depth === 0)
105
+ return at;
106
+ }
107
+ return -1;
108
+ }
109
+ /** Every number in a span, in order. USD writes tuples and arrays as parenthesised lists. */
110
+ function numbers(span) {
111
+ const found = span.match(/-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?/g);
112
+ return found === null ? [] : found.map(Number);
113
+ }
114
+ /** The value span of `name` within a prim body, or null. Handles `name = value` and `name.connect`. */
115
+ function attribute(body, name) {
116
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
117
+ /* Up to the end of the line, unless the value opens a bracket, in which case to its close. */
118
+ const pattern = new RegExp(`\\b${escaped}\\s*=\\s*`, 'g');
119
+ const match = pattern.exec(body);
120
+ if (match === null)
121
+ return null;
122
+ const start = match.index + match[0].length;
123
+ if (body[start] === '[' || body[start] === '(') {
124
+ const opener = body[start];
125
+ const closer = opener === '[' ? ']' : ')';
126
+ let depth = 0;
127
+ for (let at = start; at < body.length; at++) {
128
+ if (body[at] === opener)
129
+ depth++;
130
+ else if (body[at] === closer && --depth === 0)
131
+ return body.slice(start, at + 1);
132
+ }
133
+ return body.slice(start);
134
+ }
135
+ const end = body.indexOf('\n', start);
136
+ return body.slice(start, end === -1 ? body.length : end);
137
+ }
138
+ /**
139
+ * Compose a prim's local transform from its xform ops.
140
+ *
141
+ * `xformOpOrder` is authoritative when present, because USD applies the ops in the order it
142
+ * names and a reader that assumes translate-rotate-scale gets a file with a different order
143
+ * subtly wrong — the same class of mistake that put a car upside down and reported it as
144
+ * verified. Returns a column-major 4x4, matching `gltf.ts`.
145
+ */
146
+ function localTransform(body) {
147
+ const out = identity();
148
+ const declared = attribute(body, 'uniform token\\[\\] xformOpOrder') ?? attribute(body, 'xformOpOrder');
149
+ const ops = declared === null
150
+ ? [
151
+ 'xformOp:transform',
152
+ 'xformOp:translate',
153
+ 'xformOp:orient',
154
+ 'xformOp:rotateXYZ',
155
+ 'xformOp:scale',
156
+ ]
157
+ : (declared.match(/"([^"]+)"/g) ?? []).map((quoted) => quoted.slice(1, -1));
158
+ for (const op of ops) {
159
+ const value = attribute(body, op);
160
+ if (value === null)
161
+ continue;
162
+ const n = numbers(value);
163
+ if (op.startsWith('xformOp:transform')) {
164
+ if (n.length >= 16)
165
+ multiply(out, Float32Array.from(n.slice(0, 16)));
166
+ }
167
+ else if (op.startsWith('xformOp:translate')) {
168
+ const m = identity();
169
+ m[12] = n[0] ?? 0;
170
+ m[13] = n[1] ?? 0;
171
+ m[14] = n[2] ?? 0;
172
+ multiply(out, m);
173
+ }
174
+ else if (op.startsWith('xformOp:scale')) {
175
+ const m = identity();
176
+ m[0] = n[0] ?? 1;
177
+ m[5] = n[1] ?? 1;
178
+ m[10] = n[2] ?? 1;
179
+ multiply(out, m);
180
+ }
181
+ else if (op.startsWith('xformOp:rotate')) {
182
+ /* Degrees, and the axis order is in the op's own name: rotateXYZ, rotateZYX and so on. */
183
+ const order = op.slice(op.indexOf('rotate') + 6).replace(/:.*$/, '');
184
+ const angles = [n[0] ?? 0, n[1] ?? 0, n[2] ?? 0];
185
+ for (let i = 0; i < order.length && i < 3; i++) {
186
+ multiply(out, axisRotation(order[i], (angles[i] * Math.PI) / 180));
187
+ }
188
+ }
189
+ else if (op.startsWith('xformOp:orient')) {
190
+ /* USD writes a quaternion as (w, x, y, z). */
191
+ multiply(out, quaternion(n[1] ?? 0, n[2] ?? 0, n[3] ?? 0, n[0] ?? 1));
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+ function identity() {
197
+ const m = new Float32Array(16);
198
+ m[0] = 1;
199
+ m[5] = 1;
200
+ m[10] = 1;
201
+ m[15] = 1;
202
+ return m;
203
+ }
204
+ function axisRotation(axis, radians) {
205
+ const m = identity();
206
+ const c = Math.cos(radians);
207
+ const s = Math.sin(radians);
208
+ if (axis === 'X') {
209
+ m[5] = c;
210
+ m[6] = s;
211
+ m[9] = -s;
212
+ m[10] = c;
213
+ }
214
+ else if (axis === 'Y') {
215
+ m[0] = c;
216
+ m[2] = -s;
217
+ m[8] = s;
218
+ m[10] = c;
219
+ }
220
+ else {
221
+ m[0] = c;
222
+ m[1] = s;
223
+ m[4] = -s;
224
+ m[5] = c;
225
+ }
226
+ return m;
227
+ }
228
+ function quaternion(x, y, z, w) {
229
+ const m = identity();
230
+ m[0] = 1 - 2 * (y * y + z * z);
231
+ m[1] = 2 * (x * y + z * w);
232
+ m[2] = 2 * (x * z - y * w);
233
+ m[4] = 2 * (x * y - z * w);
234
+ m[5] = 1 - 2 * (x * x + z * z);
235
+ m[6] = 2 * (y * z + x * w);
236
+ m[8] = 2 * (x * z + y * w);
237
+ m[9] = 2 * (y * z - x * w);
238
+ m[10] = 1 - 2 * (x * x + y * y);
239
+ return m;
240
+ }
241
+ /** `into = into * by`, both column-major. */
242
+ function multiply(into, by) {
243
+ const a = Float32Array.from(into);
244
+ for (let col = 0; col < 4; col++) {
245
+ for (let row = 0; row < 4; row++) {
246
+ let sum = 0;
247
+ for (let k = 0; k < 4; k++) {
248
+ sum += a[k * 4 + row] * by[col * 4 + k];
249
+ }
250
+ into[col * 4 + row] = sum;
251
+ }
252
+ }
253
+ }
254
+ function transformPoint(m, x, y, z) {
255
+ return [
256
+ m[0] * x + m[4] * y + m[8] * z + m[12],
257
+ m[1] * x + m[5] * y + m[9] * z + m[13],
258
+ m[2] * x + m[6] * y + m[10] * z + m[14],
259
+ ];
260
+ }
261
+ /** Rotation only, for normals. Uniform scale is assumed, as `MeshBuilder.addMesh` assumes it. */
262
+ function transformDirection(m, x, y, z) {
263
+ const out = [
264
+ m[0] * x + m[4] * y + m[8] * z,
265
+ m[1] * x + m[5] * y + m[9] * z,
266
+ m[2] * x + m[6] * y + m[10] * z,
267
+ ];
268
+ const length = Math.hypot(out[0], out[1], out[2]) || 1;
269
+ out[0] /= length;
270
+ out[1] /= length;
271
+ out[2] /= length;
272
+ return out;
273
+ }
274
+ /** Every prim in the tree, flattened, each with the world transform it inherits. */
275
+ function flatten(prims, parent, out) {
276
+ for (const prim of prims) {
277
+ const world = Float32Array.from(parent);
278
+ multiply(world, localTransform(prim.body));
279
+ out.push({ prim, world });
280
+ flatten(prim.children, world, out);
281
+ }
282
+ }
283
+ /**
284
+ * A `UsdPreviewSurface` and whatever textures it names.
285
+ *
286
+ * The shader network is followed only one hop: an input either states a value or connects to
287
+ * a `UsdUVTexture` whose `inputs:file` is the image. That covers what an exporter writes and
288
+ * refuses to guess at anything deeper, which is the tier 1 rule.
289
+ */
290
+ function readMaterial(material, textures, warnings) {
291
+ const shaders = material.children.filter((child) => child.type === 'Shader');
292
+ const surface = shaders.find((s) => (attribute(s.body, 'uniform token info:id') ?? '').includes('UsdPreviewSurface'));
293
+ if (surface === undefined) {
294
+ warnings.push(`material "${material.name}" names no UsdPreviewSurface; using defaults`);
295
+ return { ...DEFAULT_MATERIAL, name: material.name };
296
+ }
297
+ const value = (input) => {
298
+ const span = attribute(surface.body, `inputs:${input}`);
299
+ return span === null ? null : numbers(span);
300
+ };
301
+ /* An input that connects rather than states: follow it to the texture it names. */
302
+ const connected = (input) => {
303
+ const span = attribute(surface.body, `inputs:${input}.connect`);
304
+ if (span === null)
305
+ return null;
306
+ const target = /<([^>]+)>/.exec(span)?.[1];
307
+ if (target === undefined)
308
+ return null;
309
+ const shaderPath = target.slice(0, target.lastIndexOf('.'));
310
+ const shader = shaders.find((s) => s.path === shaderPath || shaderPath.endsWith(`/${s.name}`));
311
+ if (shader === undefined)
312
+ return null;
313
+ return /@([^@]+)@/.exec(attribute(shader.body, 'inputs:file') ?? '')?.[1] ?? null;
314
+ };
315
+ const diffuse = value('diffuseColor');
316
+ const emissive = value('emissiveColor');
317
+ const roughness = value('roughness')?.[0];
318
+ const metallic = value('metallic')?.[0];
319
+ const opacity = value('opacity')?.[0];
320
+ let albedo = -1;
321
+ const map = connected('diffuseColor');
322
+ if (map !== null) {
323
+ const existing = textures.findIndex((texture) => texture.name === map);
324
+ albedo = existing === -1 ? textures.push({ name: map }) - 1 : existing;
325
+ }
326
+ /*
327
+ * Metalness onto specular and reflectivity, stated as the approximation it is and matching
328
+ * what `gltf.ts` does with the same input, so a `.glb` and a `.usdz` of one asset bake to
329
+ * the same `.drft` rather than to two defensible readings.
330
+ */
331
+ const metal = metallic === undefined ? 0 : clamp01(metallic);
332
+ const emissiveStrength = emissive === null || emissive.length < 3
333
+ ? 0
334
+ : clamp01(Math.max(emissive[0], emissive[1], emissive[2]));
335
+ return {
336
+ name: material.name,
337
+ color: diffuse !== null && diffuse.length >= 3
338
+ ? [
339
+ clamp01(diffuse[0]),
340
+ clamp01(diffuse[1]),
341
+ clamp01(diffuse[2]),
342
+ ]
343
+ : DEFAULT_MATERIAL.color,
344
+ specular: metal,
345
+ roughness: roughness === undefined ? DEFAULT_MATERIAL.roughness : clamp01(roughness),
346
+ emissive: emissiveStrength,
347
+ emissiveColor: emissiveStrength > 0 && emissive !== null
348
+ ? [emissive[0], emissive[1], emissive[2]]
349
+ : DEFAULT_MATERIAL.emissiveColor,
350
+ opacity: opacity === undefined ? 1 : clamp01(opacity),
351
+ albedo,
352
+ reflectivity: metal,
353
+ normalMap: -1,
354
+ ormMap: -1,
355
+ emissiveMap: -1,
356
+ roughnessScale: 1,
357
+ metallicScale: 1,
358
+ occlusionStrength: 0,
359
+ cutout: 0,
360
+ };
361
+ }
362
+ /**
363
+ * One `Mesh` prim, triangulated and flattened into world space.
364
+ *
365
+ * USD indexes its face vertices with `faceVertexCounts` and `faceVertexIndices`, and its
366
+ * normals and texture coordinates may be either per point or per *face vertex*. Face-varying
367
+ * data is the common case out of every exporter, and it is why a corner becomes its own
368
+ * vertex here exactly as it does in the OBJ and FBX paths: welding is the baker's job and it
369
+ * already has a module for it.
370
+ */
371
+ function readMesh(prim, world, warnings) {
372
+ const points = numbers(attribute(prim.body, 'point3f[] points') ?? attribute(prim.body, 'points') ?? '');
373
+ const counts = numbers(attribute(prim.body, 'int[] faceVertexCounts') ??
374
+ attribute(prim.body, 'faceVertexCounts') ??
375
+ '');
376
+ const indices = numbers(attribute(prim.body, 'int[] faceVertexIndices') ??
377
+ attribute(prim.body, 'faceVertexIndices') ??
378
+ '');
379
+ if (points.length < 9 || counts.length === 0 || indices.length === 0) {
380
+ warnings.push(`mesh "${prim.name}" carries no usable geometry and was skipped`);
381
+ return null;
382
+ }
383
+ const normalSpan = attribute(prim.body, 'normal3f[] normals') ?? attribute(prim.body, 'normals');
384
+ const uvSpan = attribute(prim.body, 'texCoord2f[] primvars:st') ??
385
+ attribute(prim.body, 'float2[] primvars:st') ??
386
+ attribute(prim.body, 'primvars:st');
387
+ const normals = normalSpan === null ? [] : numbers(normalSpan);
388
+ const uvs = uvSpan === null ? [] : numbers(uvSpan);
389
+ const uvIndices = numbers(attribute(prim.body, 'primvars:st:indices') ?? '');
390
+ const pointCount = Math.floor(points.length / 3);
391
+ const cornerCount = indices.length;
392
+ const normalsPerPoint = normals.length / 3 === pointCount && normals.length / 3 !== cornerCount;
393
+ const uvsPerPoint = uvIndices.length === 0 && uvs.length / 2 === pointCount && uvs.length / 2 !== cornerCount;
394
+ const outPositions = [];
395
+ const outNormals = [];
396
+ const outUvs = [];
397
+ const outIndices = [];
398
+ let corner = 0;
399
+ for (const count of counts) {
400
+ if (count < 3) {
401
+ corner += count;
402
+ continue;
403
+ }
404
+ const base = outPositions.length / 3;
405
+ for (let i = 0; i < count; i++) {
406
+ const at = corner + i;
407
+ const point = indices[at];
408
+ if (point < 0 || point >= pointCount) {
409
+ throw new DrftError(`usda: mesh "${prim.name}" indexes point ${point} of ${pointCount}`);
410
+ }
411
+ const [x, y, z] = transformPoint(world, points[point * 3], points[point * 3 + 1], points[point * 3 + 2]);
412
+ outPositions.push(x, y, z);
413
+ const normalAt = normalsPerPoint ? point : at;
414
+ if (normals.length >= (normalAt + 1) * 3) {
415
+ const [nx, ny, nz] = transformDirection(world, normals[normalAt * 3], normals[normalAt * 3 + 1], normals[normalAt * 3 + 2]);
416
+ outNormals.push(nx, ny, nz);
417
+ }
418
+ else {
419
+ outNormals.push(0, 1, 0);
420
+ }
421
+ if (uvs.length > 0) {
422
+ const uvAt = uvIndices.length > 0 ? (uvIndices[at] ?? 0) : uvsPerPoint ? point : at;
423
+ /*
424
+ * V is flipped, once, here. USD puts the origin at the bottom left and this engine
425
+ * uploads with it at the top left, which is the same single flip `fbx.ts` documents.
426
+ */
427
+ outUvs.push(uvs[uvAt * 2] ?? 0, 1 - (uvs[uvAt * 2 + 1] ?? 0));
428
+ }
429
+ }
430
+ /* Fan triangulation, which is what a convex polygon from an exporter wants. */
431
+ for (let i = 1; i + 1 < count; i++)
432
+ outIndices.push(base, base + i, base + i + 1);
433
+ corner += count;
434
+ }
435
+ if (outIndices.length === 0) {
436
+ warnings.push(`mesh "${prim.name}" had no faces with three or more corners`);
437
+ return null;
438
+ }
439
+ const vertices = outPositions.length / 3;
440
+ return {
441
+ positions: Float32Array.from(outPositions),
442
+ normals: Float32Array.from(outNormals),
443
+ colors: new Float32Array(vertices * 3).fill(1),
444
+ emissive: new Float32Array(vertices),
445
+ ...(outUvs.length === vertices * 2 ? { uvs: Float32Array.from(outUvs) } : {}),
446
+ indices: Uint32Array.from(outIndices),
447
+ };
448
+ }
449
+ /** Read a `.usda` document. */
450
+ export function parseUsda(text) {
451
+ if (!text.trimStart().startsWith('#usda')) {
452
+ throw new DrftError('usda: the file does not begin with #usda, so it is not ASCII USD');
453
+ }
454
+ const warnings = [];
455
+ const stageEnd = text.indexOf('{') === -1 ? text.length : text.indexOf('{');
456
+ const stage = text.slice(0, stageEnd);
457
+ const up = /upAxis\s*=\s*"([YZ])"/.exec(stage)?.[1];
458
+ const metersPerUnit = Number(/metersPerUnit\s*=\s*([\d.eE+-]+)/.exec(stage)?.[1] ?? '1');
459
+ const roots = scanPrims(text, 0, text.length, '');
460
+ const flat = [];
461
+ flatten(roots, identity(), flat);
462
+ /* Materials first, so a mesh's binding resolves to one that already exists. */
463
+ const textures = [];
464
+ const materialsByPath = new Map();
465
+ for (const { prim } of flat) {
466
+ if (prim.type === 'Material')
467
+ materialsByPath.set(prim.path, readMaterial(prim, textures, warnings));
468
+ }
469
+ const meshes = [];
470
+ const materials = [];
471
+ for (const { prim, world } of flat) {
472
+ if (prim.type !== 'Mesh')
473
+ continue;
474
+ const mesh = readMesh(prim, world, warnings);
475
+ if (mesh === null)
476
+ continue;
477
+ meshes.push(mesh);
478
+ const binding = /<([^>]+)>/.exec(attribute(prim.body, 'rel material:binding') ??
479
+ attribute(prim.body, 'material:binding') ??
480
+ '')?.[1];
481
+ const bound = binding === undefined
482
+ ? undefined
483
+ : (materialsByPath.get(binding) ??
484
+ [...materialsByPath.entries()].find(([path]) => binding.endsWith(path) || path.endsWith(binding))?.[1]);
485
+ materials.push(bound === undefined ? { ...DEFAULT_MATERIAL, name: prim.name } : bound);
486
+ }
487
+ if (meshes.length === 0)
488
+ throw new DrftError('usda: the stage carries no readable Mesh prim');
489
+ return {
490
+ meshes,
491
+ materials,
492
+ textures,
493
+ warnings,
494
+ /* USD writes a bare "Y" or "Z" and means the positive axis; the engine names the sign. */
495
+ ...(up === 'Z'
496
+ ? { declaredUp: '+z' }
497
+ : up === 'Y'
498
+ ? { declaredUp: '+y' }
499
+ : {}),
500
+ unitScale: Number.isFinite(metersPerUnit) && metersPerUnit > 0 ? metersPerUnit : 1,
501
+ };
502
+ }
503
+ /**
504
+ * Read a `.usdz`: a zip holding a `.usda` or a `.usdc`, plus whatever images it names.
505
+ *
506
+ * No inflate is passed, and that is a property of the format rather than an omission — a
507
+ * `.usdz` must store its entries uncompressed so they can be memory mapped, so an archive
508
+ * that deflates them is not a conforming `.usdz` and `zip.ts` says so by number.
509
+ *
510
+ * A `.usdc` inside is refused rather than attempted. The binary crate format is specified
511
+ * and is a separate reader; half-reading it would be exactly the partial import tier 1
512
+ * forbids.
513
+ */
514
+ export function usdzToMeshes(buffer) {
515
+ const entries = readZip(buffer);
516
+ const ascii = entries.find((entry) => entry.name.toLowerCase().endsWith('.usda'));
517
+ if (ascii === undefined) {
518
+ const crate = entries.find((entry) => /\.usdc?$/i.test(entry.name));
519
+ if (crate !== undefined) {
520
+ throw new DrftError(`usdz: "${crate.name}" is the binary crate format, which this reader does not read. ` +
521
+ `Export the usdz with an ASCII .usda inside, or convert it with usdcat.`);
522
+ }
523
+ throw new DrftError(`usdz: the archive holds no .usda (it holds ${entries.map((entry) => entry.name).join(', ')})`);
524
+ }
525
+ const result = parseUsda(new TextDecoder().decode(ascii.bytes));
526
+ /*
527
+ * Images travel inside the archive, so a texture the stage names is resolved here rather
528
+ * than left to the baker's filesystem lookup. Matched on the basename as well as the
529
+ * declared path, because a stage may say `./textures/x.png` while the archive stores
530
+ * `textures/x.png`.
531
+ */
532
+ const textures = result.textures.map((texture) => {
533
+ const wanted = texture.name.replace(/\\/g, '/').replace(/^\.\//, '');
534
+ const base = wanted.slice(wanted.lastIndexOf('/') + 1).toLowerCase();
535
+ const found = entries.find((entry) => {
536
+ const name = entry.name.toLowerCase();
537
+ return name === wanted.toLowerCase() || name.endsWith(`/${base}`) || name === base;
538
+ });
539
+ return found === undefined ? texture : { name: texture.name, bytes: found.bytes };
540
+ });
541
+ return { ...result, textures };
542
+ }