@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/fbxRig.js
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
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
|
+
/** One second, in the integer unit `KeyTime` counts. FBX's own constant. */
|
|
27
|
+
const KTIME_PER_SECOND = 46186158000;
|
|
28
|
+
/** The most influences a vertex may carry, which is what the palette shader reads. */
|
|
29
|
+
const MAX_INFLUENCES = 4;
|
|
30
|
+
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
31
|
+
function findChild(node, name) {
|
|
32
|
+
return node.children.find((child) => child.name === name);
|
|
33
|
+
}
|
|
34
|
+
function numbers(node) {
|
|
35
|
+
const value = node?.properties[0];
|
|
36
|
+
return value !== undefined && typeof value === 'object' && value !== null && 'length' in value
|
|
37
|
+
? value
|
|
38
|
+
: null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* An object's own name, with the class the container tags it with taken off.
|
|
42
|
+
*
|
|
43
|
+
* **The two forms are not the same string, and only one of them looks like a separator.** ASCII FBX
|
|
44
|
+
* writes `Model::spine_01`, which is what a reader tends to be written against because it is what
|
|
45
|
+
* the specification's examples show. Binary FBX — which is the only form this reader accepts —
|
|
46
|
+
* writes `spine_01\0\x01Model`: the name first, then a null and a `\x01`, then the class. Knowing
|
|
47
|
+
* only the ASCII form left every joint of a bought character named
|
|
48
|
+
* `rp_nathan_animated_003_walking_spine_01\0\x01Model` and its take named `Take 001\0\x01AnimStack`.
|
|
49
|
+
*
|
|
50
|
+
* Nothing fails on a name, which is why it survived: `Joint.name` is what **retargeting matches
|
|
51
|
+
* on**, so a rig whose every joint carries a class suffix matches nothing, one subsystem away from
|
|
52
|
+
* the reader that wrote it.
|
|
53
|
+
*/
|
|
54
|
+
export function bareName(raw, fallback) {
|
|
55
|
+
if (typeof raw !== 'string')
|
|
56
|
+
return fallback;
|
|
57
|
+
const binary = raw.indexOf('\u0000\u0001');
|
|
58
|
+
const name = binary >= 0
|
|
59
|
+
? raw.slice(0, binary)
|
|
60
|
+
: raw.includes('::')
|
|
61
|
+
? raw.slice(raw.indexOf('::') + 2)
|
|
62
|
+
: raw;
|
|
63
|
+
return name.length > 0 ? name : fallback;
|
|
64
|
+
}
|
|
65
|
+
function multiply(a, b) {
|
|
66
|
+
const out = new Array(16);
|
|
67
|
+
for (let row = 0; row < 4; row++) {
|
|
68
|
+
for (let col = 0; col < 4; col++) {
|
|
69
|
+
let sum = 0;
|
|
70
|
+
for (let k = 0; k < 4; k++)
|
|
71
|
+
sum += a[k * 4 + row] * b[col * 4 + k];
|
|
72
|
+
out[col * 4 + row] = sum;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A full 4x4 inverse, because a bind matrix is not guaranteed to be a rigid motion.
|
|
79
|
+
*
|
|
80
|
+
* The cheap inverse — transpose the basis, negate the translation through it — is correct only
|
|
81
|
+
* without scale, and character rigs carry scale far more often than a car does: a mirrored limb is
|
|
82
|
+
* a negative scale on one axis, and inverting that as if it were a rotation turns the limb inside
|
|
83
|
+
* out. Singular matrices come back as identity rather than as `NaN`, because a `NaN` in a bind
|
|
84
|
+
* matrix takes every vertex the joint touches with it.
|
|
85
|
+
*/
|
|
86
|
+
function invert(m) {
|
|
87
|
+
const a = (i) => m[i];
|
|
88
|
+
const s0 = a(0) * a(5) - a(4) * a(1);
|
|
89
|
+
const s1 = a(0) * a(6) - a(4) * a(2);
|
|
90
|
+
const s2 = a(0) * a(7) - a(4) * a(3);
|
|
91
|
+
const s3 = a(1) * a(6) - a(5) * a(2);
|
|
92
|
+
const s4 = a(1) * a(7) - a(5) * a(3);
|
|
93
|
+
const s5 = a(2) * a(7) - a(6) * a(3);
|
|
94
|
+
const c5 = a(10) * a(15) - a(14) * a(11);
|
|
95
|
+
const c4 = a(9) * a(15) - a(13) * a(11);
|
|
96
|
+
const c3 = a(9) * a(14) - a(13) * a(10);
|
|
97
|
+
const c2 = a(8) * a(15) - a(12) * a(11);
|
|
98
|
+
const c1 = a(8) * a(14) - a(12) * a(10);
|
|
99
|
+
const c0 = a(8) * a(13) - a(12) * a(9);
|
|
100
|
+
const determinant = s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0;
|
|
101
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-20)
|
|
102
|
+
return [...IDENTITY];
|
|
103
|
+
const d = 1 / determinant;
|
|
104
|
+
return [
|
|
105
|
+
(a(5) * c5 - a(6) * c4 + a(7) * c3) * d,
|
|
106
|
+
(-a(1) * c5 + a(2) * c4 - a(3) * c3) * d,
|
|
107
|
+
(a(13) * s5 - a(14) * s4 + a(15) * s3) * d,
|
|
108
|
+
(-a(9) * s5 + a(10) * s4 - a(11) * s3) * d,
|
|
109
|
+
(-a(4) * c5 + a(6) * c2 - a(7) * c1) * d,
|
|
110
|
+
(a(0) * c5 - a(2) * c2 + a(3) * c1) * d,
|
|
111
|
+
(-a(12) * s5 + a(14) * s2 - a(15) * s1) * d,
|
|
112
|
+
(a(8) * s5 - a(10) * s2 + a(11) * s1) * d,
|
|
113
|
+
(a(4) * c4 - a(5) * c2 + a(7) * c0) * d,
|
|
114
|
+
(-a(0) * c4 + a(1) * c2 - a(3) * c0) * d,
|
|
115
|
+
(a(12) * s4 - a(13) * s2 + a(15) * s0) * d,
|
|
116
|
+
(-a(8) * s4 + a(9) * s2 - a(11) * s0) * d,
|
|
117
|
+
(-a(4) * c3 + a(5) * c1 - a(6) * c0) * d,
|
|
118
|
+
(a(0) * c3 - a(1) * c1 + a(2) * c0) * d,
|
|
119
|
+
(-a(12) * s3 + a(13) * s1 - a(14) * s0) * d,
|
|
120
|
+
(a(8) * s3 - a(9) * s1 + a(10) * s0) * d,
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
export function indexObjects(root) {
|
|
124
|
+
const byId = new Map();
|
|
125
|
+
for (const objects of root.children.filter((child) => child.name === 'Objects')) {
|
|
126
|
+
for (const child of objects.children) {
|
|
127
|
+
const id = child.properties[0];
|
|
128
|
+
if (typeof id === 'number')
|
|
129
|
+
byId.set(id, child);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const parentsOf = new Map();
|
|
133
|
+
const drives = new Map();
|
|
134
|
+
for (const connections of root.children.filter((child) => child.name === 'Connections')) {
|
|
135
|
+
for (const c of connections.children) {
|
|
136
|
+
const [kind, child, parent, property] = c.properties;
|
|
137
|
+
if (typeof child !== 'number' || typeof parent !== 'number')
|
|
138
|
+
continue;
|
|
139
|
+
if (kind === 'OO') {
|
|
140
|
+
const list = parentsOf.get(child);
|
|
141
|
+
if (list === undefined)
|
|
142
|
+
parentsOf.set(child, [parent]);
|
|
143
|
+
else
|
|
144
|
+
list.push(parent);
|
|
145
|
+
}
|
|
146
|
+
else if (kind === 'OP' && typeof property === 'string') {
|
|
147
|
+
const list = drives.get(child);
|
|
148
|
+
const entry = { parent, property };
|
|
149
|
+
if (list === undefined)
|
|
150
|
+
drives.set(child, [entry]);
|
|
151
|
+
else
|
|
152
|
+
list.push(entry);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { byId, parentsOf, drives };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The skin bound to one geometry, or null where nothing is.
|
|
160
|
+
*
|
|
161
|
+
* **The joints are sorted parents-first here**, because this is also the layer that can remap
|
|
162
|
+
* every index naming one — the mesh's own joint attribute is written from `influences` afterwards.
|
|
163
|
+
* `Skeleton` refuses an unsorted rig rather than sorting it, for exactly that reason.
|
|
164
|
+
*/
|
|
165
|
+
export function readSkin(objects, geometryId,
|
|
166
|
+
/**
|
|
167
|
+
* Where the mesh this skin deforms sits, from the node graph.
|
|
168
|
+
*
|
|
169
|
+
* **This is the half a cluster's own `Transform` claims to be and cannot be trusted for.** The
|
|
170
|
+
* SDK documents `Transform` as the mesh's global transform at bind time, which makes it one
|
|
171
|
+
* matrix for the whole skin — and a bought character measured here has **sixty-eight of its
|
|
172
|
+
* sixty-nine weight-bearing clusters disagreeing about it**, each holding something close to the
|
|
173
|
+
* inverse of its own `TransformLink` instead. Composed as documented, the bone's frame goes in
|
|
174
|
+
* twice: the inverse bind for that character's hip came back as a pure translation of
|
|
175
|
+
* `(0, -94.4, +94.4)` where the joint is at `(0, 94.4, 0)`, so inverting it put every joint on a
|
|
176
|
+
* mirror of the skeleton that no geometry occupies.
|
|
177
|
+
*
|
|
178
|
+
* So the mesh's bind transform is taken from the one authority this reader already has for where
|
|
179
|
+
* anything is, which is the same walk every static mesh goes through. It is also what the other
|
|
180
|
+
* readers of this format settle on, and for the same reason.
|
|
181
|
+
*/
|
|
182
|
+
meshWorld, warnings) {
|
|
183
|
+
const skinIds = [];
|
|
184
|
+
for (const [id, node] of objects.byId) {
|
|
185
|
+
if (node.name !== 'Deformer' || node.properties[2] !== 'Skin')
|
|
186
|
+
continue;
|
|
187
|
+
if ((objects.parentsOf.get(id) ?? []).includes(geometryId))
|
|
188
|
+
skinIds.push(id);
|
|
189
|
+
}
|
|
190
|
+
if (skinIds.length === 0)
|
|
191
|
+
return null;
|
|
192
|
+
if (skinIds.length > 1) {
|
|
193
|
+
warnings.push(`fbx: a geometry carries ${skinIds.length} skins and only the first is read; the rest are ` +
|
|
194
|
+
`blend shapes or a second bind, neither of which this reader knows.`);
|
|
195
|
+
}
|
|
196
|
+
const skinId = skinIds[0];
|
|
197
|
+
const clusters = [];
|
|
198
|
+
for (const [id, node] of objects.byId) {
|
|
199
|
+
if (node.name !== 'Deformer' || node.properties[2] !== 'Cluster')
|
|
200
|
+
continue;
|
|
201
|
+
if (!(objects.parentsOf.get(id) ?? []).includes(skinId))
|
|
202
|
+
continue;
|
|
203
|
+
/* The bone is the cluster's *child* in FBX's direction: it names the cluster as a parent. */
|
|
204
|
+
let bone = -1;
|
|
205
|
+
for (const [candidate, parents] of objects.parentsOf) {
|
|
206
|
+
if (!parents.includes(id))
|
|
207
|
+
continue;
|
|
208
|
+
const model = objects.byId.get(candidate);
|
|
209
|
+
if (model?.name === 'Model') {
|
|
210
|
+
bone = candidate;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (bone < 0)
|
|
215
|
+
continue;
|
|
216
|
+
const indexes = numbers(findChild(node, 'Indexes'));
|
|
217
|
+
const weights = numbers(findChild(node, 'Weights'));
|
|
218
|
+
const transformLink = numbers(findChild(node, 'TransformLink'));
|
|
219
|
+
/*
|
|
220
|
+
* A cluster with no `Indexes` is legal and common: it is a bone in the skeleton that holds no
|
|
221
|
+
* vertices. It still belongs to the skeleton, because a clip drives it and its children hang
|
|
222
|
+
* off it, so it takes a joint with no influences rather than being dropped.
|
|
223
|
+
*/
|
|
224
|
+
const link = transformLink === null ? IDENTITY : Array.from(transformLink).slice(0, 16);
|
|
225
|
+
clusters.push({
|
|
226
|
+
bone,
|
|
227
|
+
indexes: indexes ?? [],
|
|
228
|
+
weights: weights ?? [],
|
|
229
|
+
inverseBind: multiply(invert(link), meshWorld),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (clusters.length === 0)
|
|
233
|
+
return null;
|
|
234
|
+
/* Bone id to its parent bone, taking only parents that are themselves models in this skeleton. */
|
|
235
|
+
const bones = new Set(clusters.map((cluster) => cluster.bone));
|
|
236
|
+
const parentBone = new Map();
|
|
237
|
+
for (const bone of bones) {
|
|
238
|
+
for (const parent of objects.parentsOf.get(bone) ?? []) {
|
|
239
|
+
if (bones.has(parent)) {
|
|
240
|
+
parentBone.set(bone, parent);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/*
|
|
246
|
+
* Parents before children, by a walk from each root rather than by a comparator.
|
|
247
|
+
*
|
|
248
|
+
* "Before its parent" is not a total order, so a sort would have to invent one; visiting a bone
|
|
249
|
+
* only after its parent is what a depth-first walk gives for free. `gltfSkin.ts` reaches the
|
|
250
|
+
* same conclusion in the same words, and the two orders have to agree because both feed the same
|
|
251
|
+
* `Skeleton`.
|
|
252
|
+
*/
|
|
253
|
+
const order = [];
|
|
254
|
+
const placed = new Set();
|
|
255
|
+
const place = (bone, depth) => {
|
|
256
|
+
if (placed.has(bone) || depth > 512)
|
|
257
|
+
return;
|
|
258
|
+
const parent = parentBone.get(bone);
|
|
259
|
+
if (parent !== undefined && !placed.has(parent))
|
|
260
|
+
place(parent, depth + 1);
|
|
261
|
+
if (placed.has(bone))
|
|
262
|
+
return;
|
|
263
|
+
placed.add(bone);
|
|
264
|
+
order.push(bone);
|
|
265
|
+
};
|
|
266
|
+
for (const bone of bones)
|
|
267
|
+
place(bone, 0);
|
|
268
|
+
const jointOfBone = new Map();
|
|
269
|
+
order.forEach((bone, at) => jointOfBone.set(bone, at));
|
|
270
|
+
const joints = order.map((bone) => {
|
|
271
|
+
const parent = parentBone.get(bone);
|
|
272
|
+
return {
|
|
273
|
+
parent: parent === undefined ? -1 : (jointOfBone.get(parent) ?? -1),
|
|
274
|
+
name: bareName(objects.byId.get(bone)?.properties[1], `joint ${bone}`),
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
const inverseBind = new Float32Array(order.length * 16);
|
|
278
|
+
const influences = new Map();
|
|
279
|
+
for (const cluster of clusters) {
|
|
280
|
+
const joint = jointOfBone.get(cluster.bone);
|
|
281
|
+
if (joint === undefined)
|
|
282
|
+
continue;
|
|
283
|
+
inverseBind.set(cluster.inverseBind, joint * 16);
|
|
284
|
+
const count = Math.min(cluster.indexes.length, cluster.weights.length);
|
|
285
|
+
for (let i = 0; i < count; i++) {
|
|
286
|
+
const point = Math.trunc(cluster.indexes[i]);
|
|
287
|
+
const weight = cluster.weights[i];
|
|
288
|
+
if (!(weight > 0) || point < 0)
|
|
289
|
+
continue;
|
|
290
|
+
const list = influences.get(point);
|
|
291
|
+
if (list === undefined)
|
|
292
|
+
influences.set(point, [{ joint, weight }]);
|
|
293
|
+
else
|
|
294
|
+
list.push({ joint, weight });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { joints, inverseBind, influences, modelOfJoint: order };
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* One control point's four influences, largest first, normalised, written into a caller's arrays.
|
|
301
|
+
*
|
|
302
|
+
* **Four is the shader's number and the file's is unbounded.** A rig authored with six influences
|
|
303
|
+
* on a shoulder is ordinary, so the four largest are kept and renormalised rather than the first
|
|
304
|
+
* four taken: dropping by *order* would discard whichever the exporter happened to write last,
|
|
305
|
+
* which on a shoulder is as likely to be the dominant one as not.
|
|
306
|
+
*
|
|
307
|
+
* Returns true where something was dropped or rescaled, so the caller can say so once for the mesh
|
|
308
|
+
* rather than once per vertex.
|
|
309
|
+
*/
|
|
310
|
+
export function writeInfluences(influences, joints, weights, at) {
|
|
311
|
+
if (influences === undefined || influences.length === 0)
|
|
312
|
+
return { dropped: false, rescaled: false };
|
|
313
|
+
const sorted = [...influences].sort((a, b) => b.weight - a.weight);
|
|
314
|
+
const kept = sorted.slice(0, MAX_INFLUENCES);
|
|
315
|
+
let sum = 0;
|
|
316
|
+
for (const influence of kept)
|
|
317
|
+
sum += influence.weight;
|
|
318
|
+
if (sum <= 0)
|
|
319
|
+
return { dropped: false, rescaled: false };
|
|
320
|
+
for (let i = 0; i < kept.length; i++) {
|
|
321
|
+
const influence = kept[i];
|
|
322
|
+
joints[at * MAX_INFLUENCES + i] = influence.joint;
|
|
323
|
+
weights[at * MAX_INFLUENCES + i] = influence.weight / sum;
|
|
324
|
+
}
|
|
325
|
+
let total = 0;
|
|
326
|
+
for (const influence of sorted)
|
|
327
|
+
total += influence.weight;
|
|
328
|
+
return {
|
|
329
|
+
dropped: sorted.length > MAX_INFLUENCES,
|
|
330
|
+
/* A tenth of a per cent, which is wider than float noise and narrower than an authoring slip. */
|
|
331
|
+
rescaled: Math.abs(total - 1) > 1e-3,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/** Euler degrees in the file's default XYZ order, as a quaternion. */
|
|
335
|
+
function eulerToQuaternion(x, y, z) {
|
|
336
|
+
const half = Math.PI / 360;
|
|
337
|
+
const cx = Math.cos(x * half);
|
|
338
|
+
const sx = Math.sin(x * half);
|
|
339
|
+
const cy = Math.cos(y * half);
|
|
340
|
+
const sy = Math.sin(y * half);
|
|
341
|
+
const cz = Math.cos(z * half);
|
|
342
|
+
const sz = Math.sin(z * half);
|
|
343
|
+
/* Rz * Ry * Rx, which is what `eEulerXYZ` composes to — the same product `compose` builds in
|
|
344
|
+
`fbx.ts`, and the same trap: written the other way round it turns a limb inside out. */
|
|
345
|
+
return [
|
|
346
|
+
sx * cy * cz + cx * sy * sz,
|
|
347
|
+
cx * sy * cz - sx * cy * sz,
|
|
348
|
+
cx * cy * sz + sx * sy * cz,
|
|
349
|
+
cx * cy * cz - sx * sy * sz,
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
/** Hamilton product, which is how two rotations compose. */
|
|
353
|
+
function multiplyQuaternion(a, b) {
|
|
354
|
+
return [
|
|
355
|
+
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
|
356
|
+
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
|
357
|
+
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
|
358
|
+
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
const NO_ROTATION = {
|
|
362
|
+
pre: [0, 0, 0, 1],
|
|
363
|
+
postInverse: [0, 0, 0, 1],
|
|
364
|
+
composed: false,
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* A joint's `PreRotation` and `PostRotation`, as the two quaternions the chain needs.
|
|
368
|
+
*
|
|
369
|
+
* **FBX's local rotation is `PreRotation * R * inverse(PostRotation)`**, not `R`. Both are fixed
|
|
370
|
+
* per joint and written on the model rather than in the curve, so a reader that takes the curve
|
|
371
|
+
* alone plays a clip in which every joint carrying one is turned by a constant amount — twenty-nine
|
|
372
|
+
* of eighty-eight on the bought character this was measured against. Every key interpolates, the
|
|
373
|
+
* take runs, and the figure is wrong in a way that reads as a bad export.
|
|
374
|
+
*/
|
|
375
|
+
function rotationFrame(model) {
|
|
376
|
+
if (model === undefined)
|
|
377
|
+
return NO_ROTATION;
|
|
378
|
+
const properties = findChild(model, 'Properties70');
|
|
379
|
+
let pre = null;
|
|
380
|
+
let post = null;
|
|
381
|
+
for (const property of properties?.children ?? []) {
|
|
382
|
+
const name = property.properties[0];
|
|
383
|
+
if (name !== 'PreRotation' && name !== 'PostRotation')
|
|
384
|
+
continue;
|
|
385
|
+
const x = property.properties[4];
|
|
386
|
+
const y = property.properties[5];
|
|
387
|
+
const z = property.properties[6];
|
|
388
|
+
if (typeof x !== 'number' || typeof y !== 'number' || typeof z !== 'number')
|
|
389
|
+
continue;
|
|
390
|
+
if (x === 0 && y === 0 && z === 0)
|
|
391
|
+
continue;
|
|
392
|
+
if (name === 'PreRotation')
|
|
393
|
+
pre = eulerToQuaternion(x, y, z);
|
|
394
|
+
else
|
|
395
|
+
post = eulerToQuaternion(x, y, z);
|
|
396
|
+
}
|
|
397
|
+
if (pre === null && post === null)
|
|
398
|
+
return NO_ROTATION;
|
|
399
|
+
return {
|
|
400
|
+
pre: pre ?? [0, 0, 0, 1],
|
|
401
|
+
postInverse: post === null ? [0, 0, 0, 1] : [-post[0], -post[1], -post[2], post[3]],
|
|
402
|
+
composed: true,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/** Which of the three transforms an `OP` property name drives, or null for one we do not animate. */
|
|
406
|
+
function pathOf(property) {
|
|
407
|
+
if (property === 'Lcl Translation')
|
|
408
|
+
return 'translation';
|
|
409
|
+
if (property === 'Lcl Rotation')
|
|
410
|
+
return 'rotation';
|
|
411
|
+
if (property === 'Lcl Scaling')
|
|
412
|
+
return 'scale';
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
/** A curve node's per-channel default, which is the value of a channel nothing curves. */
|
|
416
|
+
function channelDefaults(node) {
|
|
417
|
+
const out = [0, 0, 0];
|
|
418
|
+
const properties = findChild(node, 'Properties70');
|
|
419
|
+
for (const property of properties?.children ?? []) {
|
|
420
|
+
const name = property.properties[0];
|
|
421
|
+
const value = property.properties[4];
|
|
422
|
+
if (typeof value !== 'number')
|
|
423
|
+
continue;
|
|
424
|
+
if (name === 'd|X')
|
|
425
|
+
out[0] = value;
|
|
426
|
+
else if (name === 'd|Y')
|
|
427
|
+
out[1] = value;
|
|
428
|
+
else if (name === 'd|Z')
|
|
429
|
+
out[2] = value;
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Every take in the file, as clips over the joints a skin named.
|
|
435
|
+
*
|
|
436
|
+
* **A curve is per channel and a track is per joint**, so the three curves under one curve node are
|
|
437
|
+
* merged onto one timeline: the union of their key times, each channel read at that time. A key a
|
|
438
|
+
* channel does not have is held at its neighbour rather than interpolated, which is what a stepped
|
|
439
|
+
* read of a curve gives and is exact wherever the exporter wrote all three channels together —
|
|
440
|
+
* which is what every exporter does for a joint it is baking.
|
|
441
|
+
*
|
|
442
|
+
* **Rotation arrives as Euler degrees and leaves as a quaternion**, in the file's default XYZ
|
|
443
|
+
* order. A joint carrying a `PreRotation` or a non-default `RotationOrder` is named in a warning
|
|
444
|
+
* rather than approximated: both are silent when read wrongly, and a limb that is ninety degrees
|
|
445
|
+
* out reads as a broken clip rather than as an unread field.
|
|
446
|
+
*/
|
|
447
|
+
export function readClips(objects, jointOfModel, warnings) {
|
|
448
|
+
const stacks = [...objects.byId].filter(([, node]) => node.name === 'AnimationStack');
|
|
449
|
+
if (stacks.length === 0)
|
|
450
|
+
return [];
|
|
451
|
+
/*
|
|
452
|
+
* The one part of the rotation chain that is still not read.
|
|
453
|
+
*
|
|
454
|
+
* `PreRotation` and `PostRotation` are composed below. A `RotationOrder` other than the default
|
|
455
|
+
* `eEulerXYZ` is not: it would change which product the three Euler angles build, and getting it
|
|
456
|
+
* wrong is silent in exactly the way the pre-rotation was. Named rather than approximated.
|
|
457
|
+
*/
|
|
458
|
+
const orders = new Set();
|
|
459
|
+
for (const [model] of jointOfModel) {
|
|
460
|
+
const properties = findChild(objects.byId.get(model) ?? { name: '', properties: [], children: [] }, 'Properties70');
|
|
461
|
+
for (const property of properties?.children ?? []) {
|
|
462
|
+
if (property.properties[0] !== 'RotationOrder')
|
|
463
|
+
continue;
|
|
464
|
+
const order = property.properties[4];
|
|
465
|
+
if (typeof order === 'number' && order !== 0)
|
|
466
|
+
orders.add(order);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (orders.size > 0) {
|
|
470
|
+
warnings.push(`fbx: joints declare rotation order ${[...orders].sort().join(', ')} and this reader composes ` +
|
|
471
|
+
`the default XYZ only, so their rotations may be wrong. Export as glTF if the clip matters.`);
|
|
472
|
+
}
|
|
473
|
+
const clips = [];
|
|
474
|
+
for (const [stackId, stack] of stacks) {
|
|
475
|
+
/* Layers under the stack, and the curve nodes under those. Only the first layer is read: the
|
|
476
|
+
rest are additive or override layers, which is a blend this reader does not perform. */
|
|
477
|
+
const layers = [...objects.parentsOf]
|
|
478
|
+
.filter(([, parents]) => parents.includes(stackId))
|
|
479
|
+
.map(([id]) => id)
|
|
480
|
+
.filter((id) => objects.byId.get(id)?.name === 'AnimationLayer');
|
|
481
|
+
if (layers.length > 1) {
|
|
482
|
+
warnings.push(`fbx: "${bareName(stack.properties[1], 'take')}" has ${layers.length} animation layers ` +
|
|
483
|
+
`and only the first is read; the others would have to be blended.`);
|
|
484
|
+
}
|
|
485
|
+
const layer = layers[0];
|
|
486
|
+
if (layer === undefined)
|
|
487
|
+
continue;
|
|
488
|
+
const tracks = [];
|
|
489
|
+
let duration = 0;
|
|
490
|
+
for (const [curveNodeId, parents] of objects.parentsOf) {
|
|
491
|
+
if (!parents.includes(layer))
|
|
492
|
+
continue;
|
|
493
|
+
const curveNode = objects.byId.get(curveNodeId);
|
|
494
|
+
if (curveNode?.name !== 'AnimationCurveNode')
|
|
495
|
+
continue;
|
|
496
|
+
/* What this node drives, which is an `OP` connection naming the property by its own name. */
|
|
497
|
+
let joint;
|
|
498
|
+
let path = null;
|
|
499
|
+
let modelOfTrack = -1;
|
|
500
|
+
for (const target of objects.drives.get(curveNodeId) ?? []) {
|
|
501
|
+
const candidate = jointOfModel.get(target.parent);
|
|
502
|
+
const kind = pathOf(target.property);
|
|
503
|
+
if (candidate !== undefined && kind !== null) {
|
|
504
|
+
joint = candidate;
|
|
505
|
+
path = kind;
|
|
506
|
+
modelOfTrack = target.parent;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (joint === undefined || path === null)
|
|
511
|
+
continue;
|
|
512
|
+
/* The three channels, each an `AnimationCurve` naming `d|X`, `d|Y` or `d|Z`. */
|
|
513
|
+
const channels = [null, null, null];
|
|
514
|
+
for (const [curveId, targets] of objects.drives) {
|
|
515
|
+
if (objects.byId.get(curveId)?.name !== 'AnimationCurve')
|
|
516
|
+
continue;
|
|
517
|
+
for (const target of targets) {
|
|
518
|
+
if (target.parent !== curveNodeId)
|
|
519
|
+
continue;
|
|
520
|
+
const axis = target.property === 'd|X'
|
|
521
|
+
? 0
|
|
522
|
+
: target.property === 'd|Y'
|
|
523
|
+
? 1
|
|
524
|
+
: target.property === 'd|Z'
|
|
525
|
+
? 2
|
|
526
|
+
: -1;
|
|
527
|
+
if (axis < 0)
|
|
528
|
+
continue;
|
|
529
|
+
const curve = objects.byId.get(curveId);
|
|
530
|
+
const times = numbers(findChild(curve, 'KeyTime'));
|
|
531
|
+
const values = numbers(findChild(curve, 'KeyValueFloat'));
|
|
532
|
+
if (times === null || values === null)
|
|
533
|
+
continue;
|
|
534
|
+
const count = Math.min(times.length, values.length);
|
|
535
|
+
const t = [];
|
|
536
|
+
const v = [];
|
|
537
|
+
for (let i = 0; i < count; i++) {
|
|
538
|
+
t.push(times[i] / KTIME_PER_SECOND);
|
|
539
|
+
v.push(values[i]);
|
|
540
|
+
}
|
|
541
|
+
channels[axis] = { times: t, values: v };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (channels.every((channel) => channel === null))
|
|
545
|
+
continue;
|
|
546
|
+
const merged = new Set();
|
|
547
|
+
for (const channel of channels)
|
|
548
|
+
for (const time of channel?.times ?? [])
|
|
549
|
+
merged.add(time);
|
|
550
|
+
const times = [...merged].sort((a, b) => a - b);
|
|
551
|
+
if (times.length === 0)
|
|
552
|
+
continue;
|
|
553
|
+
duration = Math.max(duration, times[times.length - 1]);
|
|
554
|
+
const defaults = channelDefaults(curveNode);
|
|
555
|
+
/** A channel's value at a time, held at the last key at or before it. */
|
|
556
|
+
const at = (axis, time) => {
|
|
557
|
+
const channel = channels[axis];
|
|
558
|
+
if (channel === null || channel === undefined || channel.times.length === 0) {
|
|
559
|
+
return defaults[axis];
|
|
560
|
+
}
|
|
561
|
+
let index = 0;
|
|
562
|
+
while (index + 1 < channel.times.length && channel.times[index + 1] <= time)
|
|
563
|
+
index++;
|
|
564
|
+
return channel.values[index];
|
|
565
|
+
};
|
|
566
|
+
const stride = path === 'rotation' ? 4 : 3;
|
|
567
|
+
const frame = path === 'rotation' ? rotationFrame(objects.byId.get(modelOfTrack)) : NO_ROTATION;
|
|
568
|
+
const values = new Float32Array(times.length * stride);
|
|
569
|
+
times.forEach((time, key) => {
|
|
570
|
+
const x = at(0, time);
|
|
571
|
+
const y = at(1, time);
|
|
572
|
+
const z = at(2, time);
|
|
573
|
+
if (path !== 'rotation') {
|
|
574
|
+
values.set([x, y, z], key * 3);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const curved = eulerToQuaternion(x, y, z);
|
|
578
|
+
values.set(frame.composed
|
|
579
|
+
? multiplyQuaternion(multiplyQuaternion(frame.pre, curved), frame.postInverse)
|
|
580
|
+
: curved, key * 4);
|
|
581
|
+
});
|
|
582
|
+
tracks.push({ joint, path, times: new Float32Array(times), values });
|
|
583
|
+
}
|
|
584
|
+
if (tracks.length === 0)
|
|
585
|
+
continue;
|
|
586
|
+
/* The stack's own stop time where it states one, since a clip may end after its last key. */
|
|
587
|
+
const properties = findChild(stack, 'Properties70');
|
|
588
|
+
for (const property of properties?.children ?? []) {
|
|
589
|
+
if (property.properties[0] !== 'LocalStop')
|
|
590
|
+
continue;
|
|
591
|
+
const raw = property.properties[4];
|
|
592
|
+
if (typeof raw === 'number')
|
|
593
|
+
duration = Math.max(duration, raw / KTIME_PER_SECOND);
|
|
594
|
+
}
|
|
595
|
+
/* Sorted, so a clip's tracks are in a stable order whatever order the connection table was in
|
|
596
|
+
— two bakes of one file that differ only in track order are two different `.drft` files. */
|
|
597
|
+
tracks.sort((a, b) => a.joint - b.joint || a.path.localeCompare(b.path));
|
|
598
|
+
clips.push({ name: bareName(stack.properties[1], 'take'), durationSec: duration, tracks });
|
|
599
|
+
}
|
|
600
|
+
return clips;
|
|
601
|
+
}
|