@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,345 @@
1
+ /**
2
+ * FBX materials and the textures they name. One responsibility: what a surface looks like.
3
+ *
4
+ * Split from `fbx.ts` because reading a record tree and interpreting a shading model are
5
+ * different jobs with different ways of being wrong. The container either parses or it does
6
+ * not; a material silently comes out the wrong colour.
7
+ *
8
+ * **This resolves nothing from disk.** A texture is reported as the string the file wrote,
9
+ * and turning that into bytes belongs to the caller, which is the only party that knows
10
+ * whether it is reading a folder, a zip or a URL. The same reasoning as the injected
11
+ * `Inflate` in `fbx.ts`, for the same reason: this module is part of an engine that runs in
12
+ * a browser and must not reach for a filesystem.
13
+ *
14
+ * **Why the declared path is the only one worth keeping.** These records carry an absolute
15
+ * `FileName` as well, and it is the author's own machine: the asset this was built against
16
+ * says `G:\SketchfabRipper_v1.18.0-b2\tools\blender-292\_sfTemp\textures\carbon.jpg`. That
17
+ * path is dead everywhere except one computer that no longer has the file either. The
18
+ * `RelativeFilename` beside it says `textures\carbon.jpg`, which is true relative to the
19
+ * model and is what every other reader uses.
20
+ */
21
+ /** The engine's own defaults, for a property a material does not state. */
22
+ const DEFAULT_COLOR = [0.8, 0.8, 0.8];
23
+ const DEFAULT_ROUGHNESS = 0.4277;
24
+ /**
25
+ * What a surface with no material at all is shaded as.
26
+ *
27
+ * Exported so the geometry reader has no null to branch on. A geometry can genuinely reach
28
+ * the reader wearing nothing, and a fallback stated once here is better than the same three
29
+ * defaults spelled out at each use, which is how two of them end up disagreeing.
30
+ */
31
+ export const FALLBACK_MATERIAL = {
32
+ name: '',
33
+ color: DEFAULT_COLOR,
34
+ specular: 0,
35
+ roughness: DEFAULT_ROUGHNESS,
36
+ emissive: 0,
37
+ opacity: 1,
38
+ reflectivity: 0,
39
+ normalMap: -1,
40
+ ormMap: -1,
41
+ emissiveMap: -1,
42
+ roughnessScale: 1,
43
+ metallicScale: 1,
44
+ occlusionStrength: 0,
45
+ cutout: 0,
46
+ emissiveColor: [-1, -1, -1],
47
+ albedo: -1,
48
+ };
49
+ /** A record's first property, when it is a string. */
50
+ function stringChild(node, name) {
51
+ const value = node.children.find((child) => child.name === name)?.properties[0];
52
+ return typeof value === 'string' && value.trim() !== '' ? value : undefined;
53
+ }
54
+ /**
55
+ * The bytes of an embedded image, if this video carries one.
56
+ *
57
+ * A `Content` record with nothing in it is common and means "this image lives outside the
58
+ * file" rather than "this image is empty", so a zero length payload has to read as absent.
59
+ * Treating it as present embeds a zero byte texture, which decodes to nothing and takes the
60
+ * place of the path that would have worked.
61
+ */
62
+ function contentOf(video) {
63
+ const value = video.children.find((child) => child.name === 'Content')?.properties[0];
64
+ return value instanceof Uint8Array && value.length > 0 ? value : undefined;
65
+ }
66
+ function propertiesOf(node) {
67
+ const out = new Map();
68
+ for (const child of node.children) {
69
+ if (child.name !== 'Properties70')
70
+ continue;
71
+ for (const property of child.children) {
72
+ const name = property.properties[0];
73
+ if (typeof name === 'string')
74
+ out.set(name, property.properties.slice(4));
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+ function numberAt(values, at, fallback) {
80
+ const value = values?.[at];
81
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
82
+ }
83
+ function triple(values, fallback, scale = 1) {
84
+ if (values === undefined)
85
+ return [fallback[0], fallback[1], fallback[2]];
86
+ return normaliseColor([
87
+ numberAt(values, 0, fallback[0]) * scale,
88
+ numberAt(values, 1, fallback[1]) * scale,
89
+ numberAt(values, 2, fallback[2]) * scale,
90
+ ]);
91
+ }
92
+ /**
93
+ * A colour whose components exceed one is a fraction that lost its decimal point.
94
+ *
95
+ * Measured in the asset this was built against, which stores `silver` as
96
+ * `815686, 815686, 815686` and `copper` as `235294, 184314, 137255`. Those are not colours
97
+ * and they are not a scale factor either: they are `0.815686` and `0.235294, 0.184314,
98
+ * 0.137255` written as their fractional digits alone. Every one is six digits, which is what
99
+ * makes the repair well defined rather than a guess, and `calipers` proves the reading —
100
+ * it stores `8, 564706, 160784`, where the `8` is `000008` with its leading zeros lost the
101
+ * moment it was read as a number.
102
+ *
103
+ * **Each channel is divided by its own digit count**, because each is its own fraction. That
104
+ * is settled by the asset rather than argued: read this way the calipers come out
105
+ * `0.8, 0.564706, 0.160784`, a gold, and the vendor's own reference render shows gold brake
106
+ * calipers. A shared divisor taken from the largest channel was tried first, on the theory
107
+ * that the `8` was `000008` with its leading zeros lost, and it produces a green caliper.
108
+ * Looking at the reference is what decided it, which is the whole lesson of this pipeline.
109
+ *
110
+ * A component above one is never legitimate here: FBX's `DiffuseColor` is defined over nought
111
+ * to one, and this engine has no high dynamic range material input for it to mean instead. So
112
+ * the test cannot fire on a colour somebody meant.
113
+ */
114
+ function normaliseColor(color) {
115
+ const digits = (value) => {
116
+ if (!(value > 1))
117
+ return value;
118
+ let divisor = 1;
119
+ while (value / divisor > 1)
120
+ divisor *= 10;
121
+ return value / divisor;
122
+ };
123
+ return [digits(color[0]), digits(color[1]), digits(color[2])];
124
+ }
125
+ /**
126
+ * A Phong exponent as a roughness.
127
+ *
128
+ * The two describe the same thing from opposite ends: the exponent says how *tightly* a
129
+ * highlight is gathered and rises without bound, roughness says how widely it is spread over
130
+ * nought to one. `sqrt(2 / (n + 2))` is the usual correspondence and it lands where intuition
131
+ * expects at both ends, with a mirror-like 1000 arriving at 0.04 and a flat 2 at 0.7.
132
+ *
133
+ * It is an approximation between two different shading models and is stated as one. What it
134
+ * must not do is default to zero: an unstated exponent read as a mirror makes every surface
135
+ * in an imported scene a black mirror, which reads as a lighting fault rather than a material
136
+ * one.
137
+ */
138
+ /**
139
+ * How opaque a surface is, from whichever of the two spellings a writer used.
140
+ *
141
+ * They mean opposite things and that is the trap: `TransparencyFactor` is how *transparent*
142
+ * a surface is, so opacity is one minus it, while `Opacity` is already what it says. Reading
143
+ * one as the other turns every solid surface in an export invisible, and an invisible model
144
+ * reads as a failed load rather than as a material fault.
145
+ *
146
+ * Silence means opaque. Defaulting the other way would make every material in a file that
147
+ * omits both vanish, which is most files.
148
+ */
149
+ function opacityOf(p) {
150
+ const clamp = (value) => Math.min(1, Math.max(0, value));
151
+ if (p.has('TransparencyFactor'))
152
+ return clamp(1 - numberAt(p.get('TransparencyFactor'), 0, 0));
153
+ if (p.has('Opacity'))
154
+ return clamp(numberAt(p.get('Opacity'), 0, 1));
155
+ return 1;
156
+ }
157
+ function roughnessFromShininess(exponent) {
158
+ if (!(exponent > 0))
159
+ return DEFAULT_ROUGHNESS;
160
+ return Math.min(1, Math.max(0.02, Math.sqrt(2 / (exponent + 2))));
161
+ }
162
+ /**
163
+ * Materials, their textures, and which model wears which.
164
+ *
165
+ * Textures reach a material through an `OP` connection, which is FBX for "this object feeds
166
+ * that object's *named property*" — the property being `DiffuseColor`, `NormalMap`,
167
+ * `SpecularColor` and so on. Only the albedo is used, because it is the only one this
168
+ * renderer has a slot for; the rest are counted and reported rather than dropped in silence,
169
+ * so a model that looks flatter than it should says why.
170
+ */
171
+ export function readMaterials(root, warnings) {
172
+ const materialNodes = new Map();
173
+ const textureNodes = new Map();
174
+ const videoNodes = new Map();
175
+ for (const objects of root.children.filter((child) => child.name === 'Objects')) {
176
+ for (const node of objects.children) {
177
+ const id = node.properties[0];
178
+ if (typeof id !== 'number')
179
+ continue;
180
+ if (node.name === 'Material')
181
+ materialNodes.set(id, node);
182
+ else if (node.name === 'Texture')
183
+ textureNodes.set(id, node);
184
+ else if (node.name === 'Video')
185
+ videoNodes.set(id, node);
186
+ }
187
+ }
188
+ /*
189
+ * Which video backs which texture, from the connection table.
190
+ *
191
+ * A `Media` property naming the video by string exists too and is what older writers use,
192
+ * so it is the fallback. The connection is preferred because a name is a name and an id is
193
+ * a fact: two videos may be called the same thing, and only one of them is connected.
194
+ */
195
+ const videoOfTexture = new Map();
196
+ for (const connections of root.children.filter((child) => child.name === 'Connections')) {
197
+ for (const c of connections.children) {
198
+ const [kind, from, to] = c.properties;
199
+ if (kind !== 'OO' || typeof from !== 'number' || typeof to !== 'number')
200
+ continue;
201
+ const video = videoNodes.get(from);
202
+ if (video !== undefined && textureNodes.has(to))
203
+ videoOfTexture.set(to, video);
204
+ }
205
+ }
206
+ for (const [id, node] of textureNodes) {
207
+ if (videoOfTexture.has(id))
208
+ continue;
209
+ const media = stringChild(node, 'Media');
210
+ if (media === undefined)
211
+ continue;
212
+ for (const [, video] of videoNodes) {
213
+ if (String(video.properties[1] ?? '') === media) {
214
+ videoOfTexture.set(id, video);
215
+ break;
216
+ }
217
+ }
218
+ }
219
+ /*
220
+ * Texture id to an index in the reference list, deduplicated by what it resolves to.
221
+ *
222
+ * **FBX embeds its images as often as it points at them**, and the two look nothing alike.
223
+ * An embedded texture's `RelativeFilename` is `*0`, a reference into the file's own video
224
+ * list rather than a path, and the bytes live in the matching `Video` record's `Content`.
225
+ * Treating that as a filename produces a hunt for a file called `*0`, which fails, and a
226
+ * model that draws untextured while the image it needs sits inside the file being read.
227
+ */
228
+ const textures = [];
229
+ const candidates = new Map();
230
+ for (const [id, node] of textureNodes) {
231
+ const declared = stringChild(node, 'RelativeFilename') ?? stringChild(node, 'FileName') ?? '';
232
+ const video = videoOfTexture.get(id);
233
+ const content = video === undefined ? undefined : contentOf(video);
234
+ /* An embedded image names itself by its video's filename, because `*0` names nothing. */
235
+ const name = content !== undefined
236
+ ? (stringChild(video, 'RelativeFilename') ??
237
+ stringChild(video, 'Filename') ??
238
+ declared)
239
+ : declared;
240
+ if (content === undefined && (name === '' || name.startsWith('*'))) {
241
+ warnings.push(name.startsWith('*')
242
+ ? `texture "${name}" is embedded but its image is missing from the file; it will draw untextured`
243
+ : 'a texture record states no filename and was skipped');
244
+ continue;
245
+ }
246
+ /*
247
+ * Held as a candidate, not embedded yet.
248
+ *
249
+ * **A file names far more textures than this renderer can use.** One test character
250
+ * carries normal, roughness and metallic maps beside every colour map, and embedding all
251
+ * of them produced a 401 MB asset against the 94 MB the same model makes through OBJ —
252
+ * three quarters of it maps that were reported as ignored in the same run. Nothing was
253
+ * wrong with the ignoring; the list was simply built from every `Texture` record rather
254
+ * than from the ones a material actually reaches. So an entry earns its place below, when
255
+ * something binds it.
256
+ */
257
+ candidates.set(id, {
258
+ reference: content === undefined ? { name } : { name, bytes: content },
259
+ key: content === undefined ? `path:${name}` : `embedded:${id}`,
260
+ });
261
+ }
262
+ /* Which texture feeds which material, and through which of the material's properties. */
263
+ const albedoOfMaterial = new Map();
264
+ const ignoredSlots = new Map();
265
+ /** Allocate a slot for a texture the first time something actually binds it. */
266
+ const claim = new Map();
267
+ for (const connections of root.children.filter((child) => child.name === 'Connections')) {
268
+ for (const c of connections.children) {
269
+ const [kind, from, to, slot] = c.properties;
270
+ if (kind !== 'OP' || typeof from !== 'number' || typeof to !== 'number')
271
+ continue;
272
+ const candidate = candidates.get(from);
273
+ if (candidate === undefined || !materialNodes.has(to))
274
+ continue;
275
+ if (slot !== 'DiffuseColor') {
276
+ if (typeof slot === 'string')
277
+ ignoredSlots.set(slot, (ignoredSlots.get(slot) ?? 0) + 1);
278
+ continue;
279
+ }
280
+ let at = claim.get(candidate.key);
281
+ if (at === undefined) {
282
+ at = textures.length;
283
+ claim.set(candidate.key, at);
284
+ textures.push(candidate.reference);
285
+ }
286
+ albedoOfMaterial.set(to, at);
287
+ }
288
+ }
289
+ for (const [slot, count] of ignoredSlots) {
290
+ warnings.push(`${count} ${slot} texture${count === 1 ? '' : 's'} ignored; this renderer has one texture slot, for colour`);
291
+ }
292
+ const byId = new Map();
293
+ for (const [id, node] of materialNodes) {
294
+ const p = propertiesOf(node);
295
+ const emissiveColor = triple(p.get('EmissiveColor'), [0, 0, 0], numberAt(p.get('EmissiveFactor'), 0, 1));
296
+ const emissive = Math.min(1, Math.max(emissiveColor[0], emissiveColor[1], emissiveColor[2]));
297
+ byId.set(id, {
298
+ name: typeof node.properties[1] === 'string'
299
+ ? (node.properties[1].split('\u0000')[0] ?? '')
300
+ : '',
301
+ color: triple(p.get('DiffuseColor'), DEFAULT_COLOR, numberAt(p.get('DiffuseFactor'), 0, 1)),
302
+ specular: Math.min(1, Math.max(0, numberAt(p.get('SpecularFactor'), 0, 0))),
303
+ roughness: roughnessFromShininess(numberAt(p.get('ShininessExponent'), 0, numberAt(p.get('Shininess'), 0, 0))),
304
+ /*
305
+ * FBX states how transparent a surface is, so opacity is one minus it. Both
306
+ * spellings are read because writers disagree, and a file that says nothing is
307
+ * opaque rather than invisible: defaulting the other way would make every material
308
+ * in an export that omits it disappear.
309
+ */
310
+ opacity: opacityOf(p),
311
+ /* FBX states this directly, and it is the property that decides whether paint reads as
312
+ paint. Read past until now, which is why an imported car looked flat. */
313
+ reflectivity: Math.min(1, Math.max(0, numberAt(p.get('ReflectionFactor'), 0, 0))),
314
+ normalMap: -1,
315
+ ormMap: -1,
316
+ emissiveMap: -1,
317
+ roughnessScale: 1,
318
+ metallicScale: 1,
319
+ occlusionStrength: 0,
320
+ cutout: 0,
321
+ emissive,
322
+ /* A negative component means "inherit the albedo", which is the engine's own default
323
+ and the right answer for a surface that names no emissive colour of its own. */
324
+ emissiveColor: emissive > 0 ? emissiveColor : [-1, -1, -1],
325
+ albedo: albedoOfMaterial.get(id) ?? -1,
326
+ });
327
+ }
328
+ /* Model to its materials, in file order, because that order is what an index means. */
329
+ const ofModel = new Map();
330
+ for (const connections of root.children.filter((child) => child.name === 'Connections')) {
331
+ for (const c of connections.children) {
332
+ const [kind, from, to] = c.properties;
333
+ if (kind !== 'OO' || typeof from !== 'number' || typeof to !== 'number')
334
+ continue;
335
+ if (!byId.has(from))
336
+ continue;
337
+ const list = ofModel.get(to);
338
+ if (list === undefined)
339
+ ofModel.set(to, [from]);
340
+ else
341
+ list.push(from);
342
+ }
343
+ }
344
+ return { byId, ofModel, textures };
345
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The rig inside an FBX: a skeleton, what it is bound to, and the takes that move it.
3
+ *
4
+ * **Split from `fbx.ts` on the terms `fbxMaterial.ts` already set.** That file reads geometry and
5
+ * knows nothing about deformers; this one reads deformers and knows nothing about polygons. They
6
+ * meet over two small things — a control point's influences, and which model each joint is — and
7
+ * keeping them apart is what stops the mesh loop growing a second subject.
8
+ *
9
+ * **What FBX calls a skin is three record kinds and four connections.** A `Deformer` of subtype
10
+ * `Skin` hangs off a `Geometry`; a `Deformer` of subtype `Cluster` hangs off that skin, one per
11
+ * bone; and a `Model` of subtype `LimbNode` hangs off each cluster. The weights live on the
12
+ * cluster, keyed by control point, which is why they are read here and applied where the corners
13
+ * are walked.
14
+ *
15
+ * **The bind pose is two matrices and not one.** A cluster carries `TransformLink`, where the bone
16
+ * was when the mesh was bound, and `Transform`, where the *mesh* was. The engine's `inverseBind`
17
+ * takes a vertex from model space into the joint's bind space, so it is the inverse of the first
18
+ * times the second. Taking `TransformLink` alone is right exactly while the mesh sits at the
19
+ * origin with no rotation, which is what a hand-made test file does and what a bought character
20
+ * does not — the failure is a figure that is correctly posed and standing in the wrong place.
21
+ *
22
+ * **This is tier 2 and says so.** What it does not read: `PostRotation`, rotation orders other than
23
+ * the default, blend-shape channels driven by curves, and the `Take` records of pre-7000 files.
24
+ * Each is named where it is refused rather than silently approximated.
25
+ */
26
+ import type { AnimationClip, Joint } from '@driftengine/drft';
27
+ import type { FbxNode } from './fbx.ts';
28
+ /**
29
+ * An object's own name, with the class the container tags it with taken off.
30
+ *
31
+ * **The two forms are not the same string, and only one of them looks like a separator.** ASCII FBX
32
+ * writes `Model::spine_01`, which is what a reader tends to be written against because it is what
33
+ * the specification's examples show. Binary FBX — which is the only form this reader accepts —
34
+ * writes `spine_01\0\x01Model`: the name first, then a null and a `\x01`, then the class. Knowing
35
+ * only the ASCII form left every joint of a bought character named
36
+ * `rp_nathan_animated_003_walking_spine_01\0\x01Model` and its take named `Take 001\0\x01AnimStack`.
37
+ *
38
+ * Nothing fails on a name, which is why it survived: `Joint.name` is what **retargeting matches
39
+ * on**, so a rig whose every joint carries a class suffix matches nothing, one subsystem away from
40
+ * the reader that wrote it.
41
+ */
42
+ export declare function bareName(raw: unknown, fallback: string): string;
43
+ /** Every `Objects` child, by id, with the class the record's third property names. */
44
+ export interface FbxObjects {
45
+ readonly byId: Map<number, FbxNode>;
46
+ /** child id to every parent it names, in file order. A bone names its cluster *and* its parent. */
47
+ readonly parentsOf: Map<number, number[]>;
48
+ /** `OP` connections: child id to the [parent, property] pairs it drives. */
49
+ readonly drives: Map<number, {
50
+ readonly parent: number;
51
+ readonly property: string;
52
+ }[]>;
53
+ }
54
+ export declare function indexObjects(root: FbxNode): FbxObjects;
55
+ /** A skin, as this reader hands it on: the skeleton, and the influences keyed by control point. */
56
+ export interface FbxSkin {
57
+ readonly joints: readonly Joint[];
58
+ readonly inverseBind: Float32Array;
59
+ /**
60
+ * Control point index to its influences, as [joint, weight] pairs.
61
+ *
62
+ * A map rather than a dense array because a cluster covers the control points it covers: a
63
+ * character's rig leaves nothing unweighted, and a prop bound to one bone leaves almost
64
+ * everything out of every cluster but one.
65
+ */
66
+ readonly influences: Map<number, {
67
+ joint: number;
68
+ weight: number;
69
+ }[]>;
70
+ /** Model id of each joint, in the sorted order, so a curve can find the track it belongs to. */
71
+ readonly modelOfJoint: readonly number[];
72
+ }
73
+ /**
74
+ * The skin bound to one geometry, or null where nothing is.
75
+ *
76
+ * **The joints are sorted parents-first here**, because this is also the layer that can remap
77
+ * every index naming one — the mesh's own joint attribute is written from `influences` afterwards.
78
+ * `Skeleton` refuses an unsorted rig rather than sorting it, for exactly that reason.
79
+ */
80
+ export declare function readSkin(objects: FbxObjects, geometryId: number,
81
+ /**
82
+ * Where the mesh this skin deforms sits, from the node graph.
83
+ *
84
+ * **This is the half a cluster's own `Transform` claims to be and cannot be trusted for.** The
85
+ * SDK documents `Transform` as the mesh's global transform at bind time, which makes it one
86
+ * matrix for the whole skin — and a bought character measured here has **sixty-eight of its
87
+ * sixty-nine weight-bearing clusters disagreeing about it**, each holding something close to the
88
+ * inverse of its own `TransformLink` instead. Composed as documented, the bone's frame goes in
89
+ * twice: the inverse bind for that character's hip came back as a pure translation of
90
+ * `(0, -94.4, +94.4)` where the joint is at `(0, 94.4, 0)`, so inverting it put every joint on a
91
+ * mirror of the skeleton that no geometry occupies.
92
+ *
93
+ * So the mesh's bind transform is taken from the one authority this reader already has for where
94
+ * anything is, which is the same walk every static mesh goes through. It is also what the other
95
+ * readers of this format settle on, and for the same reason.
96
+ */
97
+ meshWorld: readonly number[], warnings: string[]): FbxSkin | null;
98
+ /**
99
+ * One control point's four influences, largest first, normalised, written into a caller's arrays.
100
+ *
101
+ * **Four is the shader's number and the file's is unbounded.** A rig authored with six influences
102
+ * on a shoulder is ordinary, so the four largest are kept and renormalised rather than the first
103
+ * four taken: dropping by *order* would discard whichever the exporter happened to write last,
104
+ * which on a shoulder is as likely to be the dominant one as not.
105
+ *
106
+ * Returns true where something was dropped or rescaled, so the caller can say so once for the mesh
107
+ * rather than once per vertex.
108
+ */
109
+ export declare function writeInfluences(influences: readonly {
110
+ joint: number;
111
+ weight: number;
112
+ }[] | undefined, joints: Float32Array, weights: Float32Array, at: number): {
113
+ dropped: boolean;
114
+ rescaled: boolean;
115
+ };
116
+ /**
117
+ * Every take in the file, as clips over the joints a skin named.
118
+ *
119
+ * **A curve is per channel and a track is per joint**, so the three curves under one curve node are
120
+ * merged onto one timeline: the union of their key times, each channel read at that time. A key a
121
+ * channel does not have is held at its neighbour rather than interpolated, which is what a stepped
122
+ * read of a curve gives and is exact wherever the exporter wrote all three channels together —
123
+ * which is what every exporter does for a joint it is baking.
124
+ *
125
+ * **Rotation arrives as Euler degrees and leaves as a quaternion**, in the file's default XYZ
126
+ * order. A joint carrying a `PreRotation` or a non-default `RotationOrder` is named in a warning
127
+ * rather than approximated: both are silent when read wrongly, and a limb that is ninety degrees
128
+ * out reads as a broken clip rather than as an unread field.
129
+ */
130
+ export declare function readClips(objects: FbxObjects, jointOfModel: ReadonlyMap<number, number>, warnings: string[]): AnimationClip[];