@driftengine/assets 3.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +9 -0
- package/dist/assetPath.d.ts +51 -0
- package/dist/assetPath.js +59 -0
- package/dist/coarseLevel.d.ts +94 -0
- package/dist/coarseLevel.js +606 -0
- package/dist/dds.d.ts +41 -0
- package/dist/dds.js +425 -0
- package/dist/drawKey.d.ts +64 -0
- package/dist/drawKey.js +42 -0
- package/dist/drftLoader.d.ts +420 -0
- package/dist/drftLoader.js +996 -0
- package/dist/drftTextures.d.ts +87 -0
- package/dist/drftTextures.js +138 -0
- package/dist/fbx.d.ts +86 -0
- package/dist/fbx.js +792 -0
- package/dist/fbxHarness.d.ts +68 -0
- package/dist/fbxHarness.js +185 -0
- package/dist/fbxInflate.d.ts +60 -0
- package/dist/fbxInflate.js +204 -0
- package/dist/fbxMaterial.d.ts +87 -0
- package/dist/fbxMaterial.js +345 -0
- package/dist/fbxRig.d.ts +130 -0
- package/dist/fbxRig.js +601 -0
- package/dist/gltf.d.ts +242 -0
- package/dist/gltf.js +904 -0
- package/dist/gltfAccessor.d.ts +2 -0
- package/dist/gltfAccessor.js +106 -0
- package/dist/gltfSkin.d.ts +32 -0
- package/dist/gltfSkin.js +144 -0
- package/dist/iesPhotometry.d.ts +44 -0
- package/dist/iesPhotometry.js +94 -0
- package/dist/imageInfo.d.ts +31 -0
- package/dist/imageInfo.js +119 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +132 -0
- package/dist/kn5.d.ts +158 -0
- package/dist/kn5.js +557 -0
- package/dist/loadProgress.d.ts +118 -0
- package/dist/loadProgress.js +1 -0
- package/dist/localise.d.ts +35 -0
- package/dist/localise.js +253 -0
- package/dist/obj.d.ts +54 -0
- package/dist/obj.js +335 -0
- package/dist/orient.d.ts +104 -0
- package/dist/orient.js +241 -0
- package/dist/radianceHdr.d.ts +36 -0
- package/dist/radianceHdr.js +172 -0
- package/dist/readModel.d.ts +153 -0
- package/dist/readModel.js +386 -0
- package/dist/recognise.d.ts +38 -0
- package/dist/recognise.js +97 -0
- package/dist/stl.d.ts +22 -0
- package/dist/stl.js +131 -0
- package/dist/tangentOrder.d.ts +24 -0
- package/dist/tangentOrder.js +31 -0
- package/dist/threemf.d.ts +46 -0
- package/dist/threemf.js +254 -0
- package/dist/uploadBudget.d.ts +82 -0
- package/dist/uploadBudget.js +86 -0
- package/dist/usd.d.ts +49 -0
- package/dist/usd.js +542 -0
- package/dist/weld.d.ts +44 -0
- package/dist/weld.js +237 -0
- package/dist/zip.d.ts +35 -0
- package/dist/zip.js +110 -0
- package/package.json +61 -0
- package/src/assetPath.ts +79 -0
- package/src/coarseLevel.ts +720 -0
- package/src/dds.ts +507 -0
- package/src/drawKey.ts +91 -0
- package/src/drftLoader.ts +1197 -0
- package/src/drftTextures.ts +153 -0
- package/src/fbx.ts +934 -0
- package/src/fbxHarness.ts +215 -0
- package/src/fbxInflate.ts +258 -0
- package/src/fbxMaterial.ts +410 -0
- package/src/fbxRig.ts +652 -0
- package/src/gltf.ts +1161 -0
- package/src/gltfAccessor.ts +127 -0
- package/src/gltfSkin.ts +210 -0
- package/src/iesPhotometry.ts +136 -0
- package/src/imageInfo.ts +130 -0
- package/src/index.ts +162 -0
- package/src/kn5.ts +705 -0
- package/src/loadProgress.ts +131 -0
- package/src/localise.ts +275 -0
- package/src/obj.ts +386 -0
- package/src/orient.ts +262 -0
- package/src/radianceHdr.ts +195 -0
- package/src/readModel.ts +515 -0
- package/src/recognise.ts +124 -0
- package/src/stl.ts +147 -0
- package/src/tangentOrder.ts +32 -0
- package/src/threemf.ts +303 -0
- package/src/uploadBudget.ts +90 -0
- package/src/usd.ts +642 -0
- package/src/weld.ts +264 -0
- package/src/zip.ts +130 -0
package/dist/fbx.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary FBX → `MeshData`. Tier 2: a real attempt, and explicitly not a guarantee.
|
|
3
|
+
*
|
|
4
|
+
* FBX is what an asset store hands you as the source, so reading it removes a round trip
|
|
5
|
+
* through an export dialog for the common case. The obstacle was never the format — it is
|
|
6
|
+
* Autodesk's *SDK licence*, and nothing here uses or ships any of it. The binary container
|
|
7
|
+
* is a self-describing tree of typed records, which is why a reader can be written against
|
|
8
|
+
* it at all.
|
|
9
|
+
*
|
|
10
|
+
* **Inflate is injected.** Array properties are usually deflate-compressed, and the only
|
|
11
|
+
* decompressor available without a dependency is Node's `zlib` — which must not be
|
|
12
|
+
* imported here, because this file is part of an engine that runs in a browser. So the
|
|
13
|
+
* caller supplies one. The baker passes `zlib.inflateSync`; a browser could pass
|
|
14
|
+
* `DecompressionStream`. Neither is this module's business.
|
|
15
|
+
*
|
|
16
|
+
* What it reads: geometry, its normals and UVs, and per-model transforms. What it refuses:
|
|
17
|
+
* anything it cannot understand, by name. It never returns the part of a scene it managed
|
|
18
|
+
* — a model with holes in it is a worse outcome than an error, because the error says what
|
|
19
|
+
* to do next.
|
|
20
|
+
*
|
|
21
|
+
* **It reports orientation and does not apply it.** The file's declared up axis comes back
|
|
22
|
+
* as `declaredUp`, and standing the asset up is `orient.ts`, one implementation shared by
|
|
23
|
+
* every format. That split is deliberate: a reader that quietly corrected its own files
|
|
24
|
+
* would make this format's habits into the engine's defaults, and the next reader would be
|
|
25
|
+
* written to match those rather than its own specification.
|
|
26
|
+
*/
|
|
27
|
+
import { DrftError } from '@driftengine/drft';
|
|
28
|
+
import { parseUpAxis } from './orient.js';
|
|
29
|
+
import { FALLBACK_MATERIAL, readMaterials } from './fbxMaterial.js';
|
|
30
|
+
import { bareName, indexObjects, readClips, readSkin, writeInfluences } from './fbxRig.js';
|
|
31
|
+
/** One value repeated per vertex, for a material constant this format has nowhere else to put. */
|
|
32
|
+
function repeatTriple(count, value) {
|
|
33
|
+
const out = new Float32Array(count * 3);
|
|
34
|
+
for (let i = 0; i < count; i++) {
|
|
35
|
+
out[i * 3] = value[0];
|
|
36
|
+
out[i * 3 + 1] = value[1];
|
|
37
|
+
out[i * 3 + 2] = value[2];
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
const MAGIC = 'Kaydara FBX Binary ';
|
|
42
|
+
/** Parse the record tree. Structure only — nothing here knows what a mesh is. */
|
|
43
|
+
export function parseFbxTree(buffer, inflate) {
|
|
44
|
+
const bytes = new Uint8Array(buffer);
|
|
45
|
+
const view = new DataView(buffer);
|
|
46
|
+
const header = new TextDecoder('latin1').decode(bytes.subarray(0, MAGIC.length));
|
|
47
|
+
if (header !== MAGIC) {
|
|
48
|
+
throw new DrftError('fbx: not a binary FBX. ASCII FBX is not read — re-export as binary, or as glTF.');
|
|
49
|
+
}
|
|
50
|
+
const version = view.getUint32(23, true);
|
|
51
|
+
/*
|
|
52
|
+
* At 7500 the record offsets widened from 32 to 64 bits. It is the only structural
|
|
53
|
+
* break in the container's history, and getting it wrong reads garbage rather than
|
|
54
|
+
* failing, so it is decided once here from the file's own version.
|
|
55
|
+
*/
|
|
56
|
+
const wide = version >= 7500;
|
|
57
|
+
const root = { name: '', properties: [], children: [] };
|
|
58
|
+
let at = 27;
|
|
59
|
+
const readNode = () => {
|
|
60
|
+
const endOffset = wide ? Number(view.getBigUint64(at, true)) : view.getUint32(at, true);
|
|
61
|
+
const propertyCount = wide
|
|
62
|
+
? Number(view.getBigUint64(at + 8, true))
|
|
63
|
+
: view.getUint32(at + 4, true);
|
|
64
|
+
const headerBytes = wide ? 25 : 13;
|
|
65
|
+
const nameLength = view.getUint8(at + (wide ? 24 : 12));
|
|
66
|
+
/* A record of all zeroes is the terminator for a list of siblings. */
|
|
67
|
+
if (endOffset === 0) {
|
|
68
|
+
at += headerBytes;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
let cursor = at + headerBytes;
|
|
72
|
+
const name = new TextDecoder('latin1').decode(bytes.subarray(cursor, cursor + nameLength));
|
|
73
|
+
cursor += nameLength;
|
|
74
|
+
const properties = [];
|
|
75
|
+
for (let i = 0; i < propertyCount; i++) {
|
|
76
|
+
const [value, next] = readProperty(view, bytes, cursor, inflate);
|
|
77
|
+
properties.push(value);
|
|
78
|
+
cursor = next;
|
|
79
|
+
}
|
|
80
|
+
const node = { name, properties, children: [] };
|
|
81
|
+
/* Anything left before the recorded end of this record is a nested list. */
|
|
82
|
+
while (cursor < endOffset) {
|
|
83
|
+
const saved = at;
|
|
84
|
+
at = cursor;
|
|
85
|
+
const child = readNode();
|
|
86
|
+
cursor = at;
|
|
87
|
+
at = saved;
|
|
88
|
+
if (child === null)
|
|
89
|
+
break;
|
|
90
|
+
node.children.push(child);
|
|
91
|
+
}
|
|
92
|
+
at = endOffset;
|
|
93
|
+
return node;
|
|
94
|
+
};
|
|
95
|
+
while (at < bytes.length - 16) {
|
|
96
|
+
const node = readNode();
|
|
97
|
+
if (node === null)
|
|
98
|
+
break;
|
|
99
|
+
root.children.push(node);
|
|
100
|
+
}
|
|
101
|
+
return { root, version };
|
|
102
|
+
}
|
|
103
|
+
function readProperty(view, bytes, at, inflate) {
|
|
104
|
+
const type = String.fromCharCode(view.getUint8(at));
|
|
105
|
+
const start = at + 1;
|
|
106
|
+
switch (type) {
|
|
107
|
+
case 'Y':
|
|
108
|
+
return [view.getInt16(start, true), start + 2];
|
|
109
|
+
case 'C':
|
|
110
|
+
return [view.getUint8(start) !== 0, start + 1];
|
|
111
|
+
case 'I':
|
|
112
|
+
return [view.getInt32(start, true), start + 4];
|
|
113
|
+
case 'F':
|
|
114
|
+
return [view.getFloat32(start, true), start + 4];
|
|
115
|
+
case 'D':
|
|
116
|
+
return [view.getFloat64(start, true), start + 8];
|
|
117
|
+
case 'L':
|
|
118
|
+
return [Number(view.getBigInt64(start, true)), start + 8];
|
|
119
|
+
case 'S':
|
|
120
|
+
case 'R': {
|
|
121
|
+
const length = view.getUint32(start, true);
|
|
122
|
+
const raw = bytes.subarray(start + 4, start + 4 + length);
|
|
123
|
+
return [type === 'S' ? new TextDecoder('latin1').decode(raw) : raw, start + 4 + length];
|
|
124
|
+
}
|
|
125
|
+
case 'f':
|
|
126
|
+
case 'd':
|
|
127
|
+
case 'l':
|
|
128
|
+
case 'i':
|
|
129
|
+
case 'b': {
|
|
130
|
+
const count = view.getUint32(start, true);
|
|
131
|
+
const encoding = view.getUint32(start + 4, true);
|
|
132
|
+
const compressedLength = view.getUint32(start + 8, true);
|
|
133
|
+
const elementBytes = type === 'd' || type === 'l' ? 8 : type === 'b' ? 1 : 4;
|
|
134
|
+
const payload = bytes.subarray(start + 12, start + 12 + compressedLength);
|
|
135
|
+
const raw = encoding === 1 ? inflate(payload, count * elementBytes) : payload;
|
|
136
|
+
return [decodeArray(type, raw, count), start + 12 + compressedLength];
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
throw new DrftError(`fbx: property type "${type}" at ${at} is not one this reader knows`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function decodeArray(type, raw, count) {
|
|
143
|
+
/* `slice` rather than a view: the payload may be unaligned within the file. */
|
|
144
|
+
const aligned = raw.byteOffset % 8 === 0 ? raw : raw.slice();
|
|
145
|
+
const buffer = aligned.buffer;
|
|
146
|
+
const offset = aligned.byteOffset;
|
|
147
|
+
switch (type) {
|
|
148
|
+
case 'f':
|
|
149
|
+
return new Float32Array(buffer, offset, count);
|
|
150
|
+
case 'd':
|
|
151
|
+
return new Float64Array(buffer, offset, count);
|
|
152
|
+
case 'i':
|
|
153
|
+
return new Int32Array(buffer, offset, count);
|
|
154
|
+
case 'l': {
|
|
155
|
+
const big = new BigInt64Array(buffer, offset, count);
|
|
156
|
+
const out = new Float64Array(count);
|
|
157
|
+
for (let i = 0; i < count; i++)
|
|
158
|
+
out[i] = Number(big[i]);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
default:
|
|
162
|
+
return new Uint8Array(buffer, offset, count);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function findAll(node, name, out = []) {
|
|
166
|
+
for (const child of node.children) {
|
|
167
|
+
if (child.name === name)
|
|
168
|
+
out.push(child);
|
|
169
|
+
findAll(child, name, out);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
function findChild(node, name) {
|
|
174
|
+
return node.children.find((child) => child.name === name);
|
|
175
|
+
}
|
|
176
|
+
function numbers(node) {
|
|
177
|
+
const value = node?.properties[0];
|
|
178
|
+
return value !== undefined && typeof value === 'object' && value !== null && 'length' in value
|
|
179
|
+
? value
|
|
180
|
+
: null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Which way this file *says* is up, from its own `GlobalSettings`.
|
|
184
|
+
*
|
|
185
|
+
* FBX records the convention rather than fixing one, and the tools disagree: 3ds Max and
|
|
186
|
+
* most game exports are Z-up, Maya is Y-up. Ignoring it does not produce a small error —
|
|
187
|
+
* a Z-up car imported as Y-up lies on its side, half sunk through the floor, with its
|
|
188
|
+
* longest span on the wrong axis. It was reported in exactly those words.
|
|
189
|
+
*
|
|
190
|
+
* **The sign is half of the answer and was previously dropped.** `UpAxis` names an axis and
|
|
191
|
+
* `UpAxisSign` says which end of it, so `1, -1` means up is *minus* Y — a file that reads
|
|
192
|
+
* as innocent while importing every model in it upside down, which is the same symptom as
|
|
193
|
+
* a rotation bug and none of the same cause. Reading the axis and discarding the sign is
|
|
194
|
+
* how a reader produces that silently.
|
|
195
|
+
*
|
|
196
|
+
* This only *reports*. Turning the asset is `orient.ts`, once, for every format, so no one
|
|
197
|
+
* format's habits become the engine's defaults.
|
|
198
|
+
*/
|
|
199
|
+
/**
|
|
200
|
+
* Metres per file unit, from `GlobalSettings.UnitScaleFactor`.
|
|
201
|
+
*
|
|
202
|
+
* **The container's own unit is the centimetre**, and `UnitScaleFactor` counts how many of them one
|
|
203
|
+
* file unit is: one for a file authored in centimetres, a hundred for one authored in metres. So
|
|
204
|
+
* metres per unit is that figure over a hundred, and a file stating nothing is centimetres because
|
|
205
|
+
* that is what the container defines rather than what this reader would prefer.
|
|
206
|
+
*
|
|
207
|
+
* **It is read rather than applied**, on exactly the terms `declaredUp` beside it is: a reader that
|
|
208
|
+
* quietly rescaled its own files would make this format's habits into the engine's defaults. What
|
|
209
|
+
* it replaces is worse than a rescale — a consumer with no figure to scale by was dividing by a
|
|
210
|
+
* measured height, which is a guess dressed as arithmetic and is wrong for anything that is not a
|
|
211
|
+
* standing human.
|
|
212
|
+
*/
|
|
213
|
+
function unitScaleOf(root) {
|
|
214
|
+
for (const settings of root.children.filter((child) => child.name === 'GlobalSettings')) {
|
|
215
|
+
const properties = findChild(settings, 'Properties70');
|
|
216
|
+
for (const property of properties?.children ?? []) {
|
|
217
|
+
if (property.properties[0] !== 'UnitScaleFactor')
|
|
218
|
+
continue;
|
|
219
|
+
const factor = property.properties[4];
|
|
220
|
+
/* A zero or a negative would scale a model to nothing or inside out, so it is refused in
|
|
221
|
+
favour of the container's default rather than trusted for being present. */
|
|
222
|
+
if (typeof factor === 'number' && Number.isFinite(factor) && factor > 0)
|
|
223
|
+
return factor / 100;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return 0.01;
|
|
227
|
+
}
|
|
228
|
+
function declaredUpOf(root) {
|
|
229
|
+
/* 0 is X, 1 is Y, 2 is Z. Anything else is treated as Y, which is the engine's own. */
|
|
230
|
+
let axis = 1;
|
|
231
|
+
let sign = 1;
|
|
232
|
+
for (const settings of root.children.filter((child) => child.name === 'GlobalSettings')) {
|
|
233
|
+
const properties = settings.children.find((child) => child.name === 'Properties70');
|
|
234
|
+
for (const property of properties?.children ?? []) {
|
|
235
|
+
const value = property.properties[4];
|
|
236
|
+
if (typeof value !== 'number')
|
|
237
|
+
continue;
|
|
238
|
+
if (property.properties[0] === 'UpAxis')
|
|
239
|
+
axis = value;
|
|
240
|
+
else if (property.properties[0] === 'UpAxisSign')
|
|
241
|
+
sign = value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return parseUpAxis(`${sign < 0 ? '-' : '+'}${axis === 0 ? 'x' : axis === 2 ? 'z' : 'y'}`);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Where each geometry belongs, composed from the model hierarchy.
|
|
248
|
+
*
|
|
249
|
+
* **This is what a car looks like without it.** A `Geometry` record holds vertices in its
|
|
250
|
+
* *own* local space, and the transform that puts it on the car lives on a separate `Model`
|
|
251
|
+
* record joined to it through the file's `Connections` table. Skip that and all 187 parts
|
|
252
|
+
* arrive stacked at the origin, overlapping in a heap — which reads as random lines rather
|
|
253
|
+
* than as a missing transform, and is why it looked like a parsing fault.
|
|
254
|
+
*
|
|
255
|
+
* Connections are `C` records of `["OO", childId, parentId]`, so the graph is walked from
|
|
256
|
+
* a geometry up through its model's ancestors, and the transforms composed on the way.
|
|
257
|
+
*/
|
|
258
|
+
function readTransforms(root) {
|
|
259
|
+
const models = new Map();
|
|
260
|
+
const geometryIds = new Map();
|
|
261
|
+
for (const objects of root.children.filter((child) => child.name === 'Objects')) {
|
|
262
|
+
for (const node of objects.children) {
|
|
263
|
+
const id = node.properties[0];
|
|
264
|
+
if (typeof id !== 'number')
|
|
265
|
+
continue;
|
|
266
|
+
if (node.name === 'Model')
|
|
267
|
+
models.set(id, node);
|
|
268
|
+
else if (node.name === 'Geometry')
|
|
269
|
+
geometryIds.set(node, id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/*
|
|
273
|
+
* child -> parent, from the file's own connection table, **taking only parents that are models**.
|
|
274
|
+
*
|
|
275
|
+
* An object names every parent it has and the table is in whatever order the exporter wrote it,
|
|
276
|
+
* so "the first `OO` parent" is not the hierarchy. A bone names its skin cluster as well as its
|
|
277
|
+
* parent bone, and a cluster is not a model: taking it made the walk to the world stop there and
|
|
278
|
+
* silently return identity, which drops every ancestor above that bone. A prop parented to a
|
|
279
|
+
* hand then drew as though the hand were a root — right relative to the hand, in the wrong place
|
|
280
|
+
* in the scene, which reads as a rigging fault rather than as a connection read in the wrong
|
|
281
|
+
* order. Geometry is exempt because a geometry's parent is the model wearing it, which is the
|
|
282
|
+
* one edge this map is asked for that does not run between two models.
|
|
283
|
+
*/
|
|
284
|
+
const parentOf = new Map();
|
|
285
|
+
for (const connections of root.children.filter((child) => child.name === 'Connections')) {
|
|
286
|
+
for (const c of connections.children) {
|
|
287
|
+
const [kind, child, parent] = c.properties;
|
|
288
|
+
if (kind !== 'OO' || typeof child !== 'number' || typeof parent !== 'number')
|
|
289
|
+
continue;
|
|
290
|
+
if (parentOf.has(child) || !models.has(parent))
|
|
291
|
+
continue;
|
|
292
|
+
parentOf.set(child, parent);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** A model's own transform, from its `Properties70` entries. */
|
|
296
|
+
const localOf = (model) => {
|
|
297
|
+
let tx = 0;
|
|
298
|
+
let ty = 0;
|
|
299
|
+
let tz = 0;
|
|
300
|
+
let rx = 0;
|
|
301
|
+
let ry = 0;
|
|
302
|
+
let rz = 0;
|
|
303
|
+
let sx = 1;
|
|
304
|
+
let sy = 1;
|
|
305
|
+
let sz = 1;
|
|
306
|
+
const properties = model.children.find((child) => child.name === 'Properties70');
|
|
307
|
+
for (const property of properties?.children ?? []) {
|
|
308
|
+
const name = property.properties[0];
|
|
309
|
+
const a = property.properties[4];
|
|
310
|
+
const b = property.properties[5];
|
|
311
|
+
const c = property.properties[6];
|
|
312
|
+
if (typeof a !== 'number' || typeof b !== 'number' || typeof c !== 'number')
|
|
313
|
+
continue;
|
|
314
|
+
if (name === 'Lcl Translation')
|
|
315
|
+
[tx, ty, tz] = [a, b, c];
|
|
316
|
+
else if (name === 'Lcl Rotation')
|
|
317
|
+
[rx, ry, rz] = [a, b, c];
|
|
318
|
+
else if (name === 'Lcl Scaling')
|
|
319
|
+
[sx, sy, sz] = [a, b, c];
|
|
320
|
+
}
|
|
321
|
+
return compose(tx, ty, tz, rx, ry, rz, sx, sy, sz);
|
|
322
|
+
};
|
|
323
|
+
const worldCache = new Map();
|
|
324
|
+
const worldOf = (id, depth = 0) => {
|
|
325
|
+
const cached = worldCache.get(id);
|
|
326
|
+
if (cached !== undefined)
|
|
327
|
+
return cached;
|
|
328
|
+
const model = models.get(id);
|
|
329
|
+
if (model === undefined || depth > 32)
|
|
330
|
+
return IDENTITY_4;
|
|
331
|
+
const parent = parentOf.get(id);
|
|
332
|
+
const world = parent === undefined ? localOf(model) : multiply4(worldOf(parent, depth + 1), localOf(model));
|
|
333
|
+
worldCache.set(id, world);
|
|
334
|
+
return world;
|
|
335
|
+
};
|
|
336
|
+
const world = new Map();
|
|
337
|
+
const modelOf = new Map();
|
|
338
|
+
for (const [, id] of geometryIds) {
|
|
339
|
+
const model = parentOf.get(id);
|
|
340
|
+
world.set(id, model === undefined ? IDENTITY_4 : worldOf(model));
|
|
341
|
+
if (model !== undefined)
|
|
342
|
+
modelOf.set(id, model);
|
|
343
|
+
}
|
|
344
|
+
return { world, modelOf };
|
|
345
|
+
}
|
|
346
|
+
function readLayer(geometry, element, valueName, indexName, components) {
|
|
347
|
+
const node = findChild(geometry, element);
|
|
348
|
+
if (node === undefined)
|
|
349
|
+
return null;
|
|
350
|
+
const values = numbers(findChild(node, valueName));
|
|
351
|
+
if (values === null)
|
|
352
|
+
return null;
|
|
353
|
+
const reference = findChild(node, 'ReferenceInformationType')?.properties[0] ?? 'Direct';
|
|
354
|
+
const mapping = findChild(node, 'MappingInformationType')?.properties[0] ?? 'ByPolygonVertex';
|
|
355
|
+
const indices = reference.startsWith('Index') ? numbers(findChild(node, indexName)) : null;
|
|
356
|
+
return { values, indices, mapping, components };
|
|
357
|
+
}
|
|
358
|
+
/** Where this corner's datum starts in a layer's values, or -1 if the layer does not cover it. */
|
|
359
|
+
function layerAt(layer, polygonVertex, vertexIndex, polygon) {
|
|
360
|
+
const key = layer.mapping === 'ByVertice' || layer.mapping === 'ByVertex'
|
|
361
|
+
? vertexIndex
|
|
362
|
+
: layer.mapping === 'ByPolygon'
|
|
363
|
+
? polygon
|
|
364
|
+
: layer.mapping === 'AllSame'
|
|
365
|
+
? 0
|
|
366
|
+
: polygonVertex;
|
|
367
|
+
const at = layer.indices === null ? key : Math.trunc(layer.indices[key]);
|
|
368
|
+
if (!(at >= 0))
|
|
369
|
+
return -1;
|
|
370
|
+
const offset = at * layer.components;
|
|
371
|
+
return offset + layer.components <= layer.values.length ? offset : -1;
|
|
372
|
+
}
|
|
373
|
+
const IDENTITY_4 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
374
|
+
function multiply4(a, b) {
|
|
375
|
+
const out = new Array(16);
|
|
376
|
+
for (let row = 0; row < 4; row++) {
|
|
377
|
+
for (let col = 0; col < 4; col++) {
|
|
378
|
+
let sum = 0;
|
|
379
|
+
for (let k = 0; k < 4; k++)
|
|
380
|
+
sum += a[k * 4 + row] * b[col * 4 + k];
|
|
381
|
+
out[col * 4 + row] = sum;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
/** A model's matrix from its translation, rotation and scale. Degrees, column-major. */
|
|
387
|
+
function compose(tx, ty, tz, rx, ry, rz, sx, sy, sz) {
|
|
388
|
+
const toRad = Math.PI / 180;
|
|
389
|
+
const cx = Math.cos(rx * toRad);
|
|
390
|
+
const sxr = Math.sin(rx * toRad);
|
|
391
|
+
const cy = Math.cos(ry * toRad);
|
|
392
|
+
const syr = Math.sin(ry * toRad);
|
|
393
|
+
const cz = Math.cos(rz * toRad);
|
|
394
|
+
const szr = Math.sin(rz * toRad);
|
|
395
|
+
/*
|
|
396
|
+
* Rz * Ry * Rx, and the order is the bug this replaces.
|
|
397
|
+
*
|
|
398
|
+
* FBX's default is `eEulerXYZ`, which names the order the axes are rotated *in* — X,
|
|
399
|
+
* then Y, then Z — and therefore this product, not Rx * Ry * Rz. Backwards it does not
|
|
400
|
+
* tilt a model slightly: on a hierarchy whose parts carry 90 and 180 degree rotations it
|
|
401
|
+
* turns the assembly inside out, and the symptom is a car arriving upside down with its
|
|
402
|
+
* body hanging below the origin. Found only after the file's own `UpAxis` had been
|
|
403
|
+
* checked and was innocent.
|
|
404
|
+
*/
|
|
405
|
+
const m00 = cz * cy;
|
|
406
|
+
const m01 = cz * syr * sxr - szr * cx;
|
|
407
|
+
const m02 = cz * syr * cx + szr * sxr;
|
|
408
|
+
const m10 = szr * cy;
|
|
409
|
+
const m11 = szr * syr * sxr + cz * cx;
|
|
410
|
+
const m12 = szr * syr * cx - cz * sxr;
|
|
411
|
+
const m20 = -syr;
|
|
412
|
+
const m21 = cy * sxr;
|
|
413
|
+
const m22 = cy * cx;
|
|
414
|
+
return [
|
|
415
|
+
m00 * sx,
|
|
416
|
+
m10 * sx,
|
|
417
|
+
m20 * sx,
|
|
418
|
+
0,
|
|
419
|
+
m01 * sy,
|
|
420
|
+
m11 * sy,
|
|
421
|
+
m21 * sy,
|
|
422
|
+
0,
|
|
423
|
+
m02 * sz,
|
|
424
|
+
m12 * sz,
|
|
425
|
+
m22 * sz,
|
|
426
|
+
0,
|
|
427
|
+
tx,
|
|
428
|
+
ty,
|
|
429
|
+
tz,
|
|
430
|
+
1,
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Geometry out of a parsed tree, one `MeshData` per `Geometry` record.
|
|
435
|
+
*
|
|
436
|
+
* FBX stores polygons of any size as a flat index list where a **negative index marks the
|
|
437
|
+
* last corner of a polygon**, encoded as its bitwise complement. Triangulating as a fan is
|
|
438
|
+
* correct for the convex polygons these files contain and is what every other reader does.
|
|
439
|
+
*
|
|
440
|
+
* Normals and UVs arrive as layer elements with their own mapping and reference modes, and
|
|
441
|
+
* the combination decides how to look a value up. Getting that wrong does not fail — it
|
|
442
|
+
* shades the model wrongly — so each supported combination is handled explicitly and an
|
|
443
|
+
* unsupported one is named rather than approximated.
|
|
444
|
+
*/
|
|
445
|
+
export function fbxToMeshes(buffer, inflate) {
|
|
446
|
+
const { root, version } = parseFbxTree(buffer, inflate);
|
|
447
|
+
const warnings = [];
|
|
448
|
+
const meshes = [];
|
|
449
|
+
const perMesh = [];
|
|
450
|
+
const transforms = readTransforms(root);
|
|
451
|
+
const materials = readMaterials(root, warnings);
|
|
452
|
+
const declaredUp = declaredUpOf(root);
|
|
453
|
+
const unitScale = unitScaleOf(root);
|
|
454
|
+
const objects = indexObjects(root);
|
|
455
|
+
let seenGeometries = 0;
|
|
456
|
+
let emptyGeometries = 0;
|
|
457
|
+
const skins = [];
|
|
458
|
+
/** Model id to joint index, across every skin, so a clip's curves can find their tracks. */
|
|
459
|
+
const jointOfModel = new Map();
|
|
460
|
+
for (const geometry of findAll(root, 'Geometry')) {
|
|
461
|
+
const geometryId = geometry.properties[0];
|
|
462
|
+
/*
|
|
463
|
+
* The skin first, because it decides which space this geometry's vertices stay in. See the
|
|
464
|
+
* note on `skins` in the return type: a bound mesh is placed by its palette, so taking the
|
|
465
|
+
* model transform as well places it twice.
|
|
466
|
+
*/
|
|
467
|
+
const meshWorld = transforms.world.get(geometryId) ?? IDENTITY_4;
|
|
468
|
+
const skin = readSkin(objects, geometryId, meshWorld, warnings);
|
|
469
|
+
if (skin !== null) {
|
|
470
|
+
skins.push({ joints: skin.joints, inverseBind: skin.inverseBind });
|
|
471
|
+
skin.modelOfJoint.forEach((model, joint) => jointOfModel.set(model, joint));
|
|
472
|
+
}
|
|
473
|
+
const world = skin !== null ? IDENTITY_4 : meshWorld;
|
|
474
|
+
const vertices = numbers(findChild(geometry, 'Vertices'));
|
|
475
|
+
const polygonIndex = numbers(findChild(geometry, 'PolygonVertexIndex'));
|
|
476
|
+
seenGeometries++;
|
|
477
|
+
if (vertices === null || polygonIndex === null) {
|
|
478
|
+
/* Counted rather than skipped in silence, so the refusal below can say which of the two
|
|
479
|
+
shapes of nothing this file is: no geometry records at all, or records carrying something
|
|
480
|
+
that is not a polygon mesh — a blend-shape target, a NURBS patch, a subdivision cage. */
|
|
481
|
+
emptyGeometries++;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
/* Through the same helper the rig's names go through: a binary file tags a name with its class
|
|
485
|
+
after a `\0\x01`, and splitting on a space was a guess that happened to survive because the
|
|
486
|
+
result is only ever quoted in a warning. See `bareName`. */
|
|
487
|
+
const name = bareName(geometry.properties[1], 'geometry');
|
|
488
|
+
/* Corners, with the polygon boundaries resolved and fans emitted. The polygon a corner
|
|
489
|
+
came from travels with it, because that is what a per-polygon layer is keyed by. */
|
|
490
|
+
const corners = [];
|
|
491
|
+
const polygonOfCorner = [];
|
|
492
|
+
let polygonStart = 0;
|
|
493
|
+
let polygon = 0;
|
|
494
|
+
for (let i = 0; i < polygonIndex.length; i++) {
|
|
495
|
+
if (polygonIndex[i] >= 0)
|
|
496
|
+
continue;
|
|
497
|
+
const end = i;
|
|
498
|
+
for (let k = polygonStart + 1; k < end; k++) {
|
|
499
|
+
corners.push(polygonStart, k, k + 1);
|
|
500
|
+
polygonOfCorner.push(polygon, polygon, polygon);
|
|
501
|
+
}
|
|
502
|
+
polygonStart = end + 1;
|
|
503
|
+
polygon++;
|
|
504
|
+
}
|
|
505
|
+
if (corners.length === 0) {
|
|
506
|
+
warnings.push(`${name}: no polygons`);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const indexOf = (polygonVertex) => {
|
|
510
|
+
const raw = polygonIndex[polygonVertex];
|
|
511
|
+
return raw < 0 ? ~raw : raw;
|
|
512
|
+
};
|
|
513
|
+
const normalLayer = readLayer(geometry, 'LayerElementNormal', 'Normals', 'NormalsIndex', 3);
|
|
514
|
+
const uvLayer = readLayer(geometry, 'LayerElementUV', 'UV', 'UVIndex', 2);
|
|
515
|
+
const colorLayer = readLayer(geometry, 'LayerElementColor', 'Colors', 'ColorIndex', 4);
|
|
516
|
+
/*
|
|
517
|
+
* Which material each polygon wears.
|
|
518
|
+
*
|
|
519
|
+
* `LayerElementMaterial` is the one layer whose values *are* its indices, so it does not
|
|
520
|
+
* go through `readLayer`. `AllSame` means the whole geometry and is the common case;
|
|
521
|
+
* `ByPolygon` is what a mesh authored with several materials writes, and it is the reason
|
|
522
|
+
* the corners below are bucketed rather than emitted as a single mesh.
|
|
523
|
+
*/
|
|
524
|
+
const materialElement = findChild(geometry, 'LayerElementMaterial');
|
|
525
|
+
const materialValues = materialElement === undefined ? null : numbers(findChild(materialElement, 'Materials'));
|
|
526
|
+
const perPolygon = materialElement !== undefined &&
|
|
527
|
+
findChild(materialElement, 'MappingInformationType')?.properties[0] ===
|
|
528
|
+
'ByPolygon';
|
|
529
|
+
const slotOfPolygon = (at) => {
|
|
530
|
+
if (materialValues === null || materialValues.length === 0)
|
|
531
|
+
return 0;
|
|
532
|
+
const key = perPolygon ? Math.min(at, materialValues.length - 1) : 0;
|
|
533
|
+
const slot = Math.trunc(materialValues[key]);
|
|
534
|
+
return slot >= 0 ? slot : 0;
|
|
535
|
+
};
|
|
536
|
+
const modelId = transforms.modelOf.get(geometryId);
|
|
537
|
+
const worn = modelId === undefined ? [] : (materials.ofModel.get(modelId) ?? []);
|
|
538
|
+
/* One bucket of corners per material, so that a mesh carries exactly one surface. */
|
|
539
|
+
const buckets = new Map();
|
|
540
|
+
for (let c = 0; c < corners.length; c++) {
|
|
541
|
+
const slot = slotOfPolygon(polygonOfCorner[c]);
|
|
542
|
+
const bucket = buckets.get(slot);
|
|
543
|
+
if (bucket === undefined)
|
|
544
|
+
buckets.set(slot, [c]);
|
|
545
|
+
else
|
|
546
|
+
bucket.push(c);
|
|
547
|
+
}
|
|
548
|
+
for (const [slot, bucket] of buckets) {
|
|
549
|
+
const material = materials.byId.get(worn[slot] ?? -1) ?? FALLBACK_MATERIAL;
|
|
550
|
+
const base = material.color;
|
|
551
|
+
/*
|
|
552
|
+
* **Indexed, by the offsets a corner's attributes actually resolved to.**
|
|
553
|
+
*
|
|
554
|
+
* This loop used to write `indices[i] = i` over every corner it had fanned, so a mesh came
|
|
555
|
+
* back with one vertex per corner — a bought character at 64,518 vertices where its own file
|
|
556
|
+
* describes 12,216, five times the geometry for the same triangles. The baker welds, so a
|
|
557
|
+
* baked asset never showed it; anything calling `readModel` directly carried all of it.
|
|
558
|
+
*
|
|
559
|
+
* **Keyed on the values, and bucketed by control point to make that affordable.** Keying on
|
|
560
|
+
* the layer *offsets* instead is the obvious cheap answer and it deduplicates almost nothing:
|
|
561
|
+
* an exporter writing normals `ByPolygonVertex` `Direct` gives every corner its own offset
|
|
562
|
+
* into an array of repeated values, so a car whose corners are 1,958,016 came back as
|
|
563
|
+
* 1,958,002 vertices — fourteen shared out of two million. What a reader is asked for is
|
|
564
|
+
* whether two corners *are* the same vertex, which is a question about values.
|
|
565
|
+
*
|
|
566
|
+
* Comparing values could have been a string key per corner; it is a bucket per control point
|
|
567
|
+
* instead, because the candidates for any corner are the handful of vertices already emitted
|
|
568
|
+
* for its own control point — typically one, and never more than the seams meeting there. A
|
|
569
|
+
* control point split across a UV seam or a hard edge fails the comparison and stays two
|
|
570
|
+
* vertices, which is correct rather than merely conservative.
|
|
571
|
+
*/
|
|
572
|
+
const positions = [];
|
|
573
|
+
const outNormals = [];
|
|
574
|
+
const outColors = [];
|
|
575
|
+
const outUvs = uvLayer === null ? null : [];
|
|
576
|
+
const outJoints = skin === null ? null : [];
|
|
577
|
+
const outWeights = skin === null ? null : [];
|
|
578
|
+
const indices = [];
|
|
579
|
+
/*
|
|
580
|
+
* A mesh with no normals is not deduplicated, and that is the geometry speaking rather than
|
|
581
|
+
* a shortcut: the normals below are computed per *face*, so two triangles meeting at a
|
|
582
|
+
* control point need two vertices to hold their two normals. Sharing one would average
|
|
583
|
+
* nothing and shade both faces with whichever was written last.
|
|
584
|
+
*/
|
|
585
|
+
const shared = normalLayer === null ? null : new Map();
|
|
586
|
+
let droppedInfluences = false;
|
|
587
|
+
let rescaledInfluences = false;
|
|
588
|
+
for (const c of bucket) {
|
|
589
|
+
const polygonVertex = corners[c];
|
|
590
|
+
const at = polygonOfCorner[c];
|
|
591
|
+
const vertexIndex = indexOf(polygonVertex);
|
|
592
|
+
const normalAt = normalLayer === null ? -1 : layerAt(normalLayer, polygonVertex, vertexIndex, at);
|
|
593
|
+
const uvAt = uvLayer === null ? -1 : layerAt(uvLayer, polygonVertex, vertexIndex, at);
|
|
594
|
+
const colorAt = colorLayer === null ? -1 : layerAt(colorLayer, polygonVertex, vertexIndex, at);
|
|
595
|
+
const vx = vertices[vertexIndex * 3];
|
|
596
|
+
const vy = vertices[vertexIndex * 3 + 1];
|
|
597
|
+
const vz = vertices[vertexIndex * 3 + 2];
|
|
598
|
+
const px = world[0] * vx +
|
|
599
|
+
world[4] * vy +
|
|
600
|
+
world[8] * vz +
|
|
601
|
+
world[12];
|
|
602
|
+
const py = world[1] * vx +
|
|
603
|
+
world[5] * vy +
|
|
604
|
+
world[9] * vz +
|
|
605
|
+
world[13];
|
|
606
|
+
const pz = world[2] * vx +
|
|
607
|
+
world[6] * vy +
|
|
608
|
+
world[10] * vz +
|
|
609
|
+
world[14];
|
|
610
|
+
let normalX = 0;
|
|
611
|
+
let normalY = 0;
|
|
612
|
+
let normalZ = 0;
|
|
613
|
+
if (normalLayer !== null && normalAt >= 0) {
|
|
614
|
+
const nx = normalLayer.values[normalAt];
|
|
615
|
+
const ny = normalLayer.values[normalAt + 1];
|
|
616
|
+
const nz = normalLayer.values[normalAt + 2];
|
|
617
|
+
/* Rotated by the same basis. Scale is left out: these files scale uniformly or
|
|
618
|
+
not at all, and normalising after covers the rest. */
|
|
619
|
+
const tnx = world[0] * nx + world[4] * ny + world[8] * nz;
|
|
620
|
+
const tny = world[1] * nx + world[5] * ny + world[9] * nz;
|
|
621
|
+
const tnz = world[2] * nx + world[6] * ny + world[10] * nz;
|
|
622
|
+
const length = Math.hypot(tnx, tny, tnz) || 1;
|
|
623
|
+
normalX = tnx / length;
|
|
624
|
+
normalY = tny / length;
|
|
625
|
+
normalZ = tnz / length;
|
|
626
|
+
}
|
|
627
|
+
/* The material's colour, modulated by a vertex colour wherever the file painted one. */
|
|
628
|
+
let r = base[0];
|
|
629
|
+
let g = base[1];
|
|
630
|
+
let b = base[2];
|
|
631
|
+
if (colorLayer !== null && colorAt >= 0) {
|
|
632
|
+
r *= colorLayer.values[colorAt];
|
|
633
|
+
g *= colorLayer.values[colorAt + 1];
|
|
634
|
+
b *= colorLayer.values[colorAt + 2];
|
|
635
|
+
}
|
|
636
|
+
let u = 0;
|
|
637
|
+
let v = 0;
|
|
638
|
+
if (uvLayer !== null && uvAt >= 0) {
|
|
639
|
+
u = uvLayer.values[uvAt];
|
|
640
|
+
/*
|
|
641
|
+
* V is flipped once, here. FBX puts the origin at the bottom left and this engine
|
|
642
|
+
* uploads every texture with the origin at the top left, so a map applied without
|
|
643
|
+
* this arrives mirrored vertically. On a car that reads as badges upside down and
|
|
644
|
+
* tread running the wrong way, which looks like a bad texture rather than a bad
|
|
645
|
+
* coordinate, and so is looked for in the wrong place.
|
|
646
|
+
*/
|
|
647
|
+
v = 1 - uvLayer.values[uvAt + 1];
|
|
648
|
+
}
|
|
649
|
+
const candidates = shared?.get(vertexIndex);
|
|
650
|
+
let found = -1;
|
|
651
|
+
for (const candidate of candidates ?? []) {
|
|
652
|
+
if (outNormals[candidate * 3] === normalX &&
|
|
653
|
+
outNormals[candidate * 3 + 1] === normalY &&
|
|
654
|
+
outNormals[candidate * 3 + 2] === normalZ &&
|
|
655
|
+
outColors[candidate * 3] === r &&
|
|
656
|
+
outColors[candidate * 3 + 1] === g &&
|
|
657
|
+
outColors[candidate * 3 + 2] === b &&
|
|
658
|
+
(outUvs === null || (outUvs[candidate * 2] === u && outUvs[candidate * 2 + 1] === v))) {
|
|
659
|
+
found = candidate;
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (found >= 0) {
|
|
664
|
+
indices.push(found);
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const emitted = positions.length / 3;
|
|
668
|
+
if (shared !== null) {
|
|
669
|
+
if (candidates === undefined)
|
|
670
|
+
shared.set(vertexIndex, [emitted]);
|
|
671
|
+
else
|
|
672
|
+
candidates.push(emitted);
|
|
673
|
+
}
|
|
674
|
+
indices.push(emitted);
|
|
675
|
+
positions.push(px, py, pz);
|
|
676
|
+
outNormals.push(normalX, normalY, normalZ);
|
|
677
|
+
outColors.push(r, g, b);
|
|
678
|
+
if (outUvs !== null)
|
|
679
|
+
outUvs.push(u, v);
|
|
680
|
+
if (outJoints !== null && outWeights !== null && skin !== null) {
|
|
681
|
+
outJoints.push(0, 0, 0, 0);
|
|
682
|
+
outWeights.push(0, 0, 0, 0);
|
|
683
|
+
const scratchJoints = new Float32Array(4);
|
|
684
|
+
const scratchWeights = new Float32Array(4);
|
|
685
|
+
const report = writeInfluences(skin.influences.get(vertexIndex), scratchJoints, scratchWeights, 0);
|
|
686
|
+
for (let k = 0; k < 4; k++) {
|
|
687
|
+
outJoints[emitted * 4 + k] = scratchJoints[k];
|
|
688
|
+
outWeights[emitted * 4 + k] = scratchWeights[k];
|
|
689
|
+
}
|
|
690
|
+
droppedInfluences ||= report.dropped;
|
|
691
|
+
rescaledInfluences ||= report.rescaled;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const count = positions.length / 3;
|
|
695
|
+
const outPositions = Float32Array.from(positions);
|
|
696
|
+
const normalsOut = Float32Array.from(outNormals);
|
|
697
|
+
if (normalLayer === null) {
|
|
698
|
+
warnings.push(`${name}: no normals, computed from the winding`);
|
|
699
|
+
computeFaceNormals(outPositions, normalsOut);
|
|
700
|
+
}
|
|
701
|
+
if (droppedInfluences) {
|
|
702
|
+
warnings.push(`${name}: a vertex is held by more than four joints; the four largest are kept and ` +
|
|
703
|
+
`renormalised, because four is what the skinning palette reads.`);
|
|
704
|
+
}
|
|
705
|
+
if (rescaledInfluences) {
|
|
706
|
+
warnings.push(`${name}: skin weights did not sum to one and were normalised.`);
|
|
707
|
+
}
|
|
708
|
+
/*
|
|
709
|
+
* A material's constants arrive as full arrays, which `weld` then shrinks where it can.
|
|
710
|
+
* That is the wrong shape for the data and knowingly so: a mesh now carries exactly one
|
|
711
|
+
* material, so each of these is one number repeated a million times. `MATL` is where it
|
|
712
|
+
* gets stored once instead, and where these become constant attributes.
|
|
713
|
+
*/
|
|
714
|
+
meshes.push({
|
|
715
|
+
positions: outPositions,
|
|
716
|
+
normals: normalsOut,
|
|
717
|
+
colors: Float32Array.from(outColors),
|
|
718
|
+
emissive: new Float32Array(count).fill(material.emissive),
|
|
719
|
+
specular: new Float32Array(count).fill(material.specular),
|
|
720
|
+
roughness: new Float32Array(count).fill(material.roughness),
|
|
721
|
+
emissiveColor: repeatTriple(count, material.emissiveColor),
|
|
722
|
+
indices: Uint32Array.from(indices),
|
|
723
|
+
...(outUvs === null ? {} : { uvs: Float32Array.from(outUvs) }),
|
|
724
|
+
...(outJoints === null || outWeights === null
|
|
725
|
+
? {}
|
|
726
|
+
: { joints: Float32Array.from(outJoints), weights: Float32Array.from(outWeights) }),
|
|
727
|
+
});
|
|
728
|
+
perMesh.push(material);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (meshes.length === 0) {
|
|
732
|
+
/*
|
|
733
|
+
* **The refusal says what it found, because "no geometry" is three different faults.**
|
|
734
|
+
*
|
|
735
|
+
* It used to say only that there was none, which is where the trail ended for whoever met it:
|
|
736
|
+
* two rig-specific exports of one character both failed here and the message could not tell
|
|
737
|
+
* whether the records were missing, present but not polygon meshes, or present and empty. Each
|
|
738
|
+
* of those is a different next step, and the counts separate them without the reader having to
|
|
739
|
+
* guess which one it is looking at.
|
|
740
|
+
*/
|
|
741
|
+
const found = seenGeometries === 0
|
|
742
|
+
? `It carries no Geometry records at all, across ${objects.byId.size} objects: this is an ` +
|
|
743
|
+
`animation-only export, which is a skeleton and its curves with no mesh. The two that ` +
|
|
744
|
+
`ship beside a bought character for a particular engine are usually exactly that, and the ` +
|
|
745
|
+
`mesh is in the file next to them.`
|
|
746
|
+
: emptyGeometries === seenGeometries
|
|
747
|
+
? `All ${seenGeometries} of its Geometry records lack Vertices or PolygonVertexIndex, so ` +
|
|
748
|
+
`they are blend-shape targets, patches or cages rather than polygon meshes.`
|
|
749
|
+
: `${seenGeometries} Geometry records were read and every one produced no polygons.`;
|
|
750
|
+
throw new DrftError(`fbx: no geometry found in a version ${version} file. ${found} This reader is experimental ` +
|
|
751
|
+
`(tier 2) — export glTF from your own software and bake that instead.`);
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
meshes,
|
|
755
|
+
warnings,
|
|
756
|
+
version,
|
|
757
|
+
declaredUp,
|
|
758
|
+
materials: perMesh,
|
|
759
|
+
textures: [...materials.textures],
|
|
760
|
+
skins,
|
|
761
|
+
/* After the meshes, because a curve is matched to a joint and the joints are numbered by the
|
|
762
|
+
skins above. A file with takes and no skin has nothing for a track to drive. */
|
|
763
|
+
clips: skins.length === 0 ? [] : readClips(objects, jointOfModel, warnings),
|
|
764
|
+
unitScale,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
/** A zero normal is not neutral: everything multiplies by it, so the mesh arrives black. */
|
|
768
|
+
function computeFaceNormals(positions, normals) {
|
|
769
|
+
for (let i = 0; i + 2 < positions.length / 3; i += 3) {
|
|
770
|
+
const ax = positions[i * 3];
|
|
771
|
+
const ay = positions[i * 3 + 1];
|
|
772
|
+
const az = positions[i * 3 + 2];
|
|
773
|
+
const ux = positions[(i + 1) * 3] - ax;
|
|
774
|
+
const uy = positions[(i + 1) * 3 + 1] - ay;
|
|
775
|
+
const uz = positions[(i + 1) * 3 + 2] - az;
|
|
776
|
+
const vx = positions[(i + 2) * 3] - ax;
|
|
777
|
+
const vy = positions[(i + 2) * 3 + 1] - ay;
|
|
778
|
+
const vz = positions[(i + 2) * 3 + 2] - az;
|
|
779
|
+
let nx = uy * vz - uz * vy;
|
|
780
|
+
let ny = uz * vx - ux * vz;
|
|
781
|
+
let nz = ux * vy - uy * vx;
|
|
782
|
+
const length = Math.hypot(nx, ny, nz) || 1;
|
|
783
|
+
nx /= length;
|
|
784
|
+
ny /= length;
|
|
785
|
+
nz /= length;
|
|
786
|
+
for (let c = 0; c < 3; c++) {
|
|
787
|
+
normals[(i + c) * 3] = nx;
|
|
788
|
+
normals[(i + c) * 3 + 1] = ny;
|
|
789
|
+
normals[(i + c) * 3 + 2] = nz;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|