@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
|
@@ -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
|
+
}
|