@babylonjs/loaders 9.11.0 → 9.12.1
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/FBX/fbxFileLoader.d.ts +194 -0
- package/FBX/fbxFileLoader.js +2440 -0
- package/FBX/fbxFileLoader.js.map +1 -0
- package/FBX/fbxFileLoader.metadata.d.ts +11 -0
- package/FBX/fbxFileLoader.metadata.js +11 -0
- package/FBX/fbxFileLoader.metadata.js.map +1 -0
- package/FBX/index.d.ts +3 -0
- package/FBX/index.js +3 -0
- package/FBX/index.js.map +1 -0
- package/FBX/interpreter/animation.d.ts +122 -0
- package/FBX/interpreter/animation.js +648 -0
- package/FBX/interpreter/animation.js.map +1 -0
- package/FBX/interpreter/blendShapes.d.ts +44 -0
- package/FBX/interpreter/blendShapes.js +192 -0
- package/FBX/interpreter/blendShapes.js.map +1 -0
- package/FBX/interpreter/connections.d.ts +95 -0
- package/FBX/interpreter/connections.js +233 -0
- package/FBX/interpreter/connections.js.map +1 -0
- package/FBX/interpreter/fbxInterpreter.d.ts +149 -0
- package/FBX/interpreter/fbxInterpreter.js +496 -0
- package/FBX/interpreter/fbxInterpreter.js.map +1 -0
- package/FBX/interpreter/geometry.d.ts +55 -0
- package/FBX/interpreter/geometry.js +573 -0
- package/FBX/interpreter/geometry.js.map +1 -0
- package/FBX/interpreter/materials.d.ts +50 -0
- package/FBX/interpreter/materials.js +144 -0
- package/FBX/interpreter/materials.js.map +1 -0
- package/FBX/interpreter/propertyTemplates.d.ts +22 -0
- package/FBX/interpreter/propertyTemplates.js +125 -0
- package/FBX/interpreter/propertyTemplates.js.map +1 -0
- package/FBX/interpreter/rig.d.ts +20 -0
- package/FBX/interpreter/rig.js +259 -0
- package/FBX/interpreter/rig.js.map +1 -0
- package/FBX/interpreter/sceneDiagnostics.d.ts +14 -0
- package/FBX/interpreter/sceneDiagnostics.js +55 -0
- package/FBX/interpreter/sceneDiagnostics.js.map +1 -0
- package/FBX/interpreter/skeleton.d.ts +93 -0
- package/FBX/interpreter/skeleton.js +515 -0
- package/FBX/interpreter/skeleton.js.map +1 -0
- package/FBX/interpreter/transform.d.ts +21 -0
- package/FBX/interpreter/transform.js +92 -0
- package/FBX/interpreter/transform.js.map +1 -0
- package/FBX/parsers/fbxAsciiParser.d.ts +5 -0
- package/FBX/parsers/fbxAsciiParser.js +330 -0
- package/FBX/parsers/fbxAsciiParser.js.map +1 -0
- package/FBX/parsers/fbxBinaryParser.d.ts +6 -0
- package/FBX/parsers/fbxBinaryParser.js +255 -0
- package/FBX/parsers/fbxBinaryParser.js.map +1 -0
- package/FBX/parsers/zlibInflate.d.ts +7 -0
- package/FBX/parsers/zlibInflate.js +350 -0
- package/FBX/parsers/zlibInflate.js.map +1 -0
- package/FBX/types/fbxTypes.d.ts +54 -0
- package/FBX/types/fbxTypes.js +66 -0
- package/FBX/types/fbxTypes.js.map +1 -0
- package/SPLAT/gaussianSplattingStream.d.ts +341 -0
- package/SPLAT/gaussianSplattingStream.js +976 -0
- package/SPLAT/gaussianSplattingStream.js.map +1 -0
- package/SPLAT/gaussianSplattingWorkBuffer.d.ts +51 -0
- package/SPLAT/gaussianSplattingWorkBuffer.js +159 -0
- package/SPLAT/gaussianSplattingWorkBuffer.js.map +1 -0
- package/SPLAT/gaussianSplattingWorkBufferShaders.d.ts +25 -0
- package/SPLAT/gaussianSplattingWorkBufferShaders.js +255 -0
- package/SPLAT/gaussianSplattingWorkBufferShaders.js.map +1 -0
- package/SPLAT/index.d.ts +1 -0
- package/SPLAT/index.js +1 -0
- package/SPLAT/index.js.map +1 -1
- package/SPLAT/sog.js +18 -16
- package/SPLAT/sog.js.map +1 -1
- package/SPLAT/splatFileLoader.d.ts +8 -0
- package/SPLAT/splatFileLoader.js +49 -0
- package/SPLAT/splatFileLoader.js.map +1 -1
- package/dynamic.js +9 -0
- package/dynamic.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/naming-convention, jsdoc/require-param, jsdoc/require-returns */
|
|
2
|
+
import { findChildByName, getPropertyValue, cleanFBXName } from "../types/fbxTypes.js";
|
|
3
|
+
import { getChildren } from "./connections.js";
|
|
4
|
+
/** FBX time units: 46186158000 ticks per second */
|
|
5
|
+
const FBX_TIME_UNIT = 46186158000;
|
|
6
|
+
const KEY_ATTR_DATA_STRIDE = 4;
|
|
7
|
+
const SAMPLED_CURVE_MIN_KEY_COUNT = 8;
|
|
8
|
+
const SAMPLED_CURVE_MAX_INTERVAL_SECONDS = 1 / 23;
|
|
9
|
+
const SAMPLED_CURVE_UNIFORM_TOLERANCE_RATIO = 0.05;
|
|
10
|
+
const SAMPLED_CURVE_LINEAR_DEVIATION_RATIO = 0.01;
|
|
11
|
+
const SAMPLED_CURVE_LINEAR_DEVIATION_ABSOLUTE = 1e-4;
|
|
12
|
+
const SAMPLED_CURVE_DEGENERATE_SLOPE_ABSOLUTE = 1e-5;
|
|
13
|
+
const SAMPLED_CURVE_COMMON_FPS = [24, 25, 30, 48, 50, 60, 100, 120];
|
|
14
|
+
/**
|
|
15
|
+
* Extract all animation stacks from the FBX scene.
|
|
16
|
+
*/
|
|
17
|
+
export function extractAnimations(objectMap) {
|
|
18
|
+
const stacks = [];
|
|
19
|
+
for (const [id, node] of Array.from(objectMap.objects)) {
|
|
20
|
+
if (node.name === "AnimationStack") {
|
|
21
|
+
const stack = extractAnimStack(id, node, objectMap);
|
|
22
|
+
if (stack) {
|
|
23
|
+
stacks.push(stack);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return stacks;
|
|
28
|
+
}
|
|
29
|
+
function extractAnimStack(stackId, stackNode, objectMap) {
|
|
30
|
+
const name = cleanFBXName(getPropertyValue(stackNode, 1) ?? "Animation");
|
|
31
|
+
const declaredTimeSpan = extractAnimationStackTimeSpan(stackNode);
|
|
32
|
+
// Find AnimationLayer children of this stack
|
|
33
|
+
const layerEntries = getChildren(objectMap, stackId, "AnimationLayer");
|
|
34
|
+
if (layerEntries.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
// Collect all CurveNodes from all layers
|
|
38
|
+
const allCurveNodes = [];
|
|
39
|
+
const allUnsupportedCurveNodes = [];
|
|
40
|
+
const layers = [];
|
|
41
|
+
const diagnostics = [];
|
|
42
|
+
let minTime = Infinity;
|
|
43
|
+
let maxTime = 0;
|
|
44
|
+
for (const { id: layerId, node: layerNode } of layerEntries) {
|
|
45
|
+
// Extract layer properties
|
|
46
|
+
const layerName = cleanFBXName(getPropertyValue(layerNode, 1) ?? "Layer");
|
|
47
|
+
let weight = 100;
|
|
48
|
+
let blendMode = 0;
|
|
49
|
+
const props70 = findChildByName(layerNode, "Properties70");
|
|
50
|
+
if (props70) {
|
|
51
|
+
for (const p of props70.children) {
|
|
52
|
+
if (p.name !== "P") {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const pName = getPropertyValue(p, 0);
|
|
56
|
+
if (pName === "Weight") {
|
|
57
|
+
const v = p.properties[4]?.value;
|
|
58
|
+
if (typeof v === "number") {
|
|
59
|
+
weight = v;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (pName === "BlendMode") {
|
|
63
|
+
const v = p.properties[4]?.value;
|
|
64
|
+
if (typeof v === "number") {
|
|
65
|
+
blendMode = v;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// AnimationCurveNodes are children of the layer
|
|
71
|
+
const curveNodeEntries = getChildren(objectMap, layerId, "AnimationCurveNode");
|
|
72
|
+
const layerCurveNodes = [];
|
|
73
|
+
const layerUnsupportedCurveNodes = [];
|
|
74
|
+
const layerDiagnostics = [];
|
|
75
|
+
for (const { id: curveNodeId, node: curveNodeNode } of curveNodeEntries) {
|
|
76
|
+
const curveNodeData = extractCurveNode(curveNodeId, curveNodeNode, objectMap);
|
|
77
|
+
if (!curveNodeData) {
|
|
78
|
+
const unsupported = extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap);
|
|
79
|
+
if (unsupported) {
|
|
80
|
+
scanCurveTimes(unsupported.curves, (time) => {
|
|
81
|
+
if (time < minTime) {
|
|
82
|
+
minTime = time;
|
|
83
|
+
}
|
|
84
|
+
if (time > maxTime) {
|
|
85
|
+
maxTime = time;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
layerUnsupportedCurveNodes.push(unsupported);
|
|
89
|
+
allUnsupportedCurveNodes.push(unsupported);
|
|
90
|
+
const diagnostic = {
|
|
91
|
+
type: "unsupported-curve-node",
|
|
92
|
+
message: `AnimationCurveNode '${unsupported.type}' is preserved as diagnostic data but not evaluated at runtime.`,
|
|
93
|
+
layerName,
|
|
94
|
+
curveNodeId,
|
|
95
|
+
curveNodeType: unsupported.type,
|
|
96
|
+
targetId: unsupported.targetId,
|
|
97
|
+
propertyName: unsupported.propertyName,
|
|
98
|
+
};
|
|
99
|
+
layerDiagnostics.push(diagnostic);
|
|
100
|
+
diagnostics.push(diagnostic);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const curve of curveNodeData.curves) {
|
|
105
|
+
for (const key of curve.keys) {
|
|
106
|
+
if (key.time < minTime) {
|
|
107
|
+
minTime = key.time;
|
|
108
|
+
}
|
|
109
|
+
if (key.time > maxTime) {
|
|
110
|
+
maxTime = key.time;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
layerCurveNodes.push(curveNodeData);
|
|
115
|
+
allCurveNodes.push(curveNodeData);
|
|
116
|
+
}
|
|
117
|
+
layers.push({
|
|
118
|
+
name: layerName,
|
|
119
|
+
weight,
|
|
120
|
+
normalizedWeight: weight / 100,
|
|
121
|
+
blendMode,
|
|
122
|
+
curveNodes: layerCurveNodes,
|
|
123
|
+
unsupportedCurveNodes: layerUnsupportedCurveNodes,
|
|
124
|
+
diagnostics: layerDiagnostics,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (allCurveNodes.length === 0 && allUnsupportedCurveNodes.length === 0) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (layers.length > 1) {
|
|
131
|
+
diagnostics.push({
|
|
132
|
+
type: "multiple-animation-layers",
|
|
133
|
+
message: "Multiple animation layers are preserved, but runtime blending is not yet evaluated.",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
for (const layer of layers) {
|
|
137
|
+
if (layer.blendMode !== 0) {
|
|
138
|
+
const diagnostic = {
|
|
139
|
+
type: "unsupported-layer-blend-mode",
|
|
140
|
+
message: `Animation layer blend mode ${layer.blendMode} is preserved but not yet blended at runtime.`,
|
|
141
|
+
layerName: layer.name,
|
|
142
|
+
};
|
|
143
|
+
layer.diagnostics.push(diagnostic);
|
|
144
|
+
diagnostics.push(diagnostic);
|
|
145
|
+
}
|
|
146
|
+
if (layer.weight !== 100) {
|
|
147
|
+
const diagnostic = {
|
|
148
|
+
type: "partial-layer-weight",
|
|
149
|
+
message: `Animation layer weight ${layer.weight} is preserved but not yet applied at runtime.`,
|
|
150
|
+
layerName: layer.name,
|
|
151
|
+
};
|
|
152
|
+
layer.diagnostics.push(diagnostic);
|
|
153
|
+
diagnostics.push(diagnostic);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const timeOffset = minTime > 0 && isFinite(minTime) ? minTime : 0;
|
|
157
|
+
// Rebase all keyframe times so the animation starts at 0
|
|
158
|
+
if (timeOffset > 0) {
|
|
159
|
+
for (const cn of allCurveNodes) {
|
|
160
|
+
for (const curve of cn.curves) {
|
|
161
|
+
for (const key of curve.keys) {
|
|
162
|
+
key.time -= timeOffset;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const cn of allUnsupportedCurveNodes) {
|
|
167
|
+
for (const curve of cn.curves) {
|
|
168
|
+
for (const key of curve.keys) {
|
|
169
|
+
key.time -= timeOffset;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
maxTime -= timeOffset;
|
|
174
|
+
}
|
|
175
|
+
const declaredStart = declaredTimeSpan ? Math.max(declaredTimeSpan.start - timeOffset, 0) : 0;
|
|
176
|
+
const declaredStop = declaredTimeSpan ? Math.max(declaredTimeSpan.stop - timeOffset, declaredStart) : 0;
|
|
177
|
+
const hasDeclaredDuration = declaredStop > declaredStart;
|
|
178
|
+
const startTime = hasDeclaredDuration ? declaredStart : 0;
|
|
179
|
+
const stopTime = hasDeclaredDuration ? declaredStop : maxTime;
|
|
180
|
+
return {
|
|
181
|
+
name,
|
|
182
|
+
startTime,
|
|
183
|
+
stopTime,
|
|
184
|
+
duration: Math.max(stopTime - startTime, 0),
|
|
185
|
+
curveNodes: allCurveNodes,
|
|
186
|
+
layers,
|
|
187
|
+
unsupportedCurveNodes: allUnsupportedCurveNodes,
|
|
188
|
+
diagnostics,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function extractAnimationStackTimeSpan(stackNode) {
|
|
192
|
+
const props70 = findChildByName(stackNode, "Properties70");
|
|
193
|
+
if (!props70) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
let start = 0;
|
|
197
|
+
let stop = null;
|
|
198
|
+
for (const p of props70.children) {
|
|
199
|
+
if (p.name !== "P") {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const pName = getPropertyValue(p, 0);
|
|
203
|
+
if (pName === "LocalStart" || pName === "ReferenceStart") {
|
|
204
|
+
start = fbxTimeToSeconds(p.properties[4]?.value) ?? start;
|
|
205
|
+
}
|
|
206
|
+
else if (pName === "LocalStop" || pName === "ReferenceStop") {
|
|
207
|
+
stop = fbxTimeToSeconds(p.properties[4]?.value) ?? stop;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return stop !== null ? { start, stop } : null;
|
|
211
|
+
}
|
|
212
|
+
function extractCurveNode(curveNodeId, curveNodeNode, objectMap) {
|
|
213
|
+
const typeName = cleanFBXName(getPropertyValue(curveNodeNode, 1) ?? "");
|
|
214
|
+
// Handle T (translation), R (rotation), S (scale) targeting Models
|
|
215
|
+
if (typeName === "T" || typeName === "R" || typeName === "S") {
|
|
216
|
+
const targetModelId = findCurveNodeTarget(curveNodeId, objectMap);
|
|
217
|
+
if (targetModelId === null) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const curves = extractCurves(curveNodeId, objectMap);
|
|
221
|
+
if (curves.length === 0) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
type: typeName,
|
|
226
|
+
targetModelId,
|
|
227
|
+
curves,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Handle DeformPercent targeting BlendShapeChannels
|
|
231
|
+
if (typeName === "DeformPercent") {
|
|
232
|
+
const targetId = findCurveNodeBlendShapeTarget(curveNodeId, objectMap);
|
|
233
|
+
if (targetId === null) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const curves = extractCurves(curveNodeId, objectMap);
|
|
237
|
+
if (curves.length === 0) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
type: "DeformPercent",
|
|
242
|
+
targetModelId: targetId,
|
|
243
|
+
curves,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
function extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap) {
|
|
249
|
+
const typeName = cleanFBXName(getPropertyValue(curveNodeNode, 1) ?? "");
|
|
250
|
+
const curves = extractCurves(curveNodeId, objectMap);
|
|
251
|
+
const defaultValues = extractCurveNodeDefaultValues(curveNodeNode);
|
|
252
|
+
if (curves.length === 0 && Object.keys(defaultValues).length === 0) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
let targetId = null;
|
|
256
|
+
let propertyName;
|
|
257
|
+
for (const conn of objectMap.connections) {
|
|
258
|
+
if (conn.childId === curveNodeId && conn.type === "OP") {
|
|
259
|
+
targetId = conn.parentId;
|
|
260
|
+
propertyName = conn.propertyName;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
type: typeName,
|
|
266
|
+
id: curveNodeId,
|
|
267
|
+
targetId,
|
|
268
|
+
propertyName,
|
|
269
|
+
curveCount: curves.length,
|
|
270
|
+
curves,
|
|
271
|
+
defaultValues,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function scanCurveTimes(curves, visit) {
|
|
275
|
+
for (const curve of curves) {
|
|
276
|
+
for (const key of curve.keys) {
|
|
277
|
+
visit(key.time);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Find the Model that an AnimationCurveNode targets.
|
|
283
|
+
* The CurveNode connects to the Model via OP connection with a property name.
|
|
284
|
+
*/
|
|
285
|
+
function findCurveNodeTarget(curveNodeId, objectMap) {
|
|
286
|
+
// Look for connections where this curveNode is a child (going up to parent)
|
|
287
|
+
// The OP connection from curveNode → Model has the property name (e.g. "Lcl Translation")
|
|
288
|
+
for (const conn of objectMap.connections) {
|
|
289
|
+
if (conn.childId === curveNodeId && conn.type === "OP") {
|
|
290
|
+
const parentNode = objectMap.objects.get(conn.parentId);
|
|
291
|
+
if (parentNode && parentNode.name === "Model") {
|
|
292
|
+
return conn.parentId;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Find the BlendShapeChannel that a DeformPercent AnimationCurveNode targets.
|
|
300
|
+
*/
|
|
301
|
+
function findCurveNodeBlendShapeTarget(curveNodeId, objectMap) {
|
|
302
|
+
for (const conn of objectMap.connections) {
|
|
303
|
+
if (conn.childId === curveNodeId && conn.type === "OP") {
|
|
304
|
+
const parentNode = objectMap.objects.get(conn.parentId);
|
|
305
|
+
if (parentNode && parentNode.name === "Deformer") {
|
|
306
|
+
const subType = getPropertyValue(parentNode, 2);
|
|
307
|
+
if (subType === "BlendShapeChannel") {
|
|
308
|
+
return conn.parentId;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Also check OO connections
|
|
314
|
+
for (const conn of objectMap.connections) {
|
|
315
|
+
if (conn.childId === curveNodeId && conn.type === "OO") {
|
|
316
|
+
const parentNode = objectMap.objects.get(conn.parentId);
|
|
317
|
+
if (parentNode && parentNode.name === "Deformer") {
|
|
318
|
+
const subType = getPropertyValue(parentNode, 2);
|
|
319
|
+
if (subType === "BlendShapeChannel") {
|
|
320
|
+
return conn.parentId;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Extract AnimationCurves connected to a CurveNode.
|
|
329
|
+
* Each curve connects via OP with channel "d|X", "d|Y", or "d|Z".
|
|
330
|
+
*/
|
|
331
|
+
function extractCurves(curveNodeId, objectMap) {
|
|
332
|
+
const curves = [];
|
|
333
|
+
// Find AnimationCurve children of this CurveNode
|
|
334
|
+
for (const conn of objectMap.connections) {
|
|
335
|
+
if (conn.parentId === curveNodeId && conn.type === "OP") {
|
|
336
|
+
const curveNode = objectMap.objects.get(conn.childId);
|
|
337
|
+
if (!curveNode || curveNode.name !== "AnimationCurve") {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const channel = conn.propertyName ?? "d|X";
|
|
341
|
+
const keys = extractKeyframes(curveNode);
|
|
342
|
+
if (keys.length > 0) {
|
|
343
|
+
const isSampled = isSampledAnimationCurve(curveNode, keys);
|
|
344
|
+
curves.push({ channel, keys: isSampled ? makeLinearSampleKeys(keys) : keys, isSampled });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Also check OO connections (some exporters use OO for curve→curveNode)
|
|
349
|
+
if (curves.length === 0) {
|
|
350
|
+
const ooChildren = getChildren(objectMap, curveNodeId, "AnimationCurve");
|
|
351
|
+
// For OO connections, infer channel from order (X, Y, Z)
|
|
352
|
+
const channelNames = ["d|X", "d|Y", "d|Z"];
|
|
353
|
+
for (let i = 0; i < ooChildren.length && i < 3; i++) {
|
|
354
|
+
const keys = extractKeyframes(ooChildren[i].node);
|
|
355
|
+
if (keys.length > 0) {
|
|
356
|
+
const isSampled = isSampledAnimationCurve(ooChildren[i].node, keys);
|
|
357
|
+
curves.push({ channel: channelNames[i], keys: isSampled ? makeLinearSampleKeys(keys) : keys, isSampled });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return curves;
|
|
362
|
+
}
|
|
363
|
+
function extractCurveNodeDefaultValues(curveNodeNode) {
|
|
364
|
+
const defaults = {};
|
|
365
|
+
const props70 = findChildByName(curveNodeNode, "Properties70");
|
|
366
|
+
for (const p of props70?.children ?? []) {
|
|
367
|
+
if (p.name !== "P") {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const propName = getPropertyValue(p, 0);
|
|
371
|
+
if (!propName?.startsWith("d|")) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const value = toNumber(p.properties[4]?.value);
|
|
375
|
+
if (value !== null) {
|
|
376
|
+
defaults[propName] = value;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return defaults;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Extract keyframes from an AnimationCurve node.
|
|
383
|
+
*/
|
|
384
|
+
function extractKeyframes(curveNode) {
|
|
385
|
+
const keyTimeNode = findChildByName(curveNode, "KeyTime");
|
|
386
|
+
const keyValueNode = findChildByName(curveNode, "KeyValueFloat");
|
|
387
|
+
if (!keyTimeNode || !keyValueNode) {
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
const keyTimes = toInt64Array(keyTimeNode.properties[0]?.value);
|
|
391
|
+
const keyValues = toFloat32Array(keyValueNode.properties[0]?.value);
|
|
392
|
+
const keyAttrFlags = toInt32Array(findChildByName(curveNode, "KeyAttrFlags")?.properties[0]?.value);
|
|
393
|
+
const keyAttrData = toFloat32Array(findChildByName(curveNode, "KeyAttrDataFloat")?.properties[0]?.value);
|
|
394
|
+
const keyAttrRefCount = toInt32Array(findChildByName(curveNode, "KeyAttrRefCount")?.properties[0]?.value);
|
|
395
|
+
if (!keyTimes || !keyValues) {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
if (keyTimes.length !== keyValues.length) {
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
const keyAttributeIndices = buildKeyAttributeIndices(keyTimes.length, keyAttrFlags, keyAttrRefCount);
|
|
402
|
+
const keys = [];
|
|
403
|
+
for (let i = 0; i < keyTimes.length; i++) {
|
|
404
|
+
const attrIndex = keyAttributeIndices[i];
|
|
405
|
+
const flag = attrIndex >= 0 ? (keyAttrFlags?.[attrIndex] ?? 0) : 0;
|
|
406
|
+
const dataOffset = attrIndex * KEY_ATTR_DATA_STRIDE;
|
|
407
|
+
keys.push({
|
|
408
|
+
time: Number(keyTimes[i]) / FBX_TIME_UNIT,
|
|
409
|
+
value: keyValues[i],
|
|
410
|
+
interpolation: getInterpolationType(flag),
|
|
411
|
+
constantMode: (flag & 0x00000100) !== 0 ? "next" : "standard",
|
|
412
|
+
rightSlope: getFiniteKeyAttrData(keyAttrData, dataOffset),
|
|
413
|
+
nextLeftSlope: getFiniteKeyAttrData(keyAttrData, dataOffset + 1),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return keys;
|
|
417
|
+
}
|
|
418
|
+
function isSampledAnimationCurve(curveNode, keys) {
|
|
419
|
+
const rawName = getPropertyValue(curveNode, 1) ?? "";
|
|
420
|
+
return cleanFBXName(rawName) === "FbxMayaSample Curve" || isFrameBakedSampledCurve(keys);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Determines whether a key sequence appears to be a uniformly frame-baked sampled curve.
|
|
424
|
+
* @param keys - Keyframes to inspect
|
|
425
|
+
* @returns true if the keys look like sampled frame data rather than authored interpolation
|
|
426
|
+
*/
|
|
427
|
+
export function isFrameBakedSampledCurve(keys) {
|
|
428
|
+
if (keys.length < SAMPLED_CURVE_MIN_KEY_COUNT) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
const deltas = [];
|
|
432
|
+
for (let i = 1; i < keys.length; i++) {
|
|
433
|
+
const delta = keys[i].time - keys[i - 1].time;
|
|
434
|
+
if (!(delta > 0)) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
deltas.push(delta);
|
|
438
|
+
}
|
|
439
|
+
const averageDelta = deltas.reduce((sum, delta) => sum + delta, 0) / deltas.length;
|
|
440
|
+
if (averageDelta > SAMPLED_CURVE_MAX_INTERVAL_SECONDS) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
const uniformTolerance = Math.max(1e-6, averageDelta * SAMPLED_CURVE_UNIFORM_TOLERANCE_RATIO);
|
|
444
|
+
if (deltas.some((delta) => Math.abs(delta - averageDelta) > uniformTolerance)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const sampledFps = 1 / averageDelta;
|
|
448
|
+
const matchesCommonFps = SAMPLED_CURVE_COMMON_FPS.some((fps) => Math.abs(sampledFps - fps) <= Math.max(0.25, fps * 0.02));
|
|
449
|
+
if (!matchesCommonFps) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
return !hasMeaningfulCubicTangents(keys);
|
|
453
|
+
}
|
|
454
|
+
function makeLinearSampleKeys(keys) {
|
|
455
|
+
return keys.map((key) => ({
|
|
456
|
+
time: key.time,
|
|
457
|
+
value: key.value,
|
|
458
|
+
interpolation: "linear",
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
function hasMeaningfulCubicTangents(keys) {
|
|
462
|
+
let hasCubicSegment = false;
|
|
463
|
+
let hasCompleteTangents = true;
|
|
464
|
+
let allSlopesDegenerate = true;
|
|
465
|
+
let minValue = Number.POSITIVE_INFINITY;
|
|
466
|
+
let maxValue = Number.NEGATIVE_INFINITY;
|
|
467
|
+
let maxLinearDeviation = 0;
|
|
468
|
+
for (const key of keys) {
|
|
469
|
+
minValue = Math.min(minValue, key.value);
|
|
470
|
+
maxValue = Math.max(maxValue, key.value);
|
|
471
|
+
}
|
|
472
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
473
|
+
const key = keys[i];
|
|
474
|
+
const nextKey = keys[i + 1];
|
|
475
|
+
if (key.interpolation !== "cubic") {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
hasCubicSegment = true;
|
|
479
|
+
const segmentDuration = nextKey.time - key.time;
|
|
480
|
+
if (!(segmentDuration > 0)) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const linearSlope = (nextKey.value - key.value) / segmentDuration;
|
|
484
|
+
const rightSlope = key.rightSlope;
|
|
485
|
+
const nextLeftSlope = key.nextLeftSlope;
|
|
486
|
+
if (rightSlope === undefined || nextLeftSlope === undefined) {
|
|
487
|
+
hasCompleteTangents = false;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (Math.abs(rightSlope) > SAMPLED_CURVE_DEGENERATE_SLOPE_ABSOLUTE || Math.abs(nextLeftSlope) > SAMPLED_CURVE_DEGENERATE_SLOPE_ABSOLUTE) {
|
|
491
|
+
allSlopesDegenerate = false;
|
|
492
|
+
}
|
|
493
|
+
for (const t of [0.25, 0.5, 0.75]) {
|
|
494
|
+
const cubic = cubicHermite(key.value, nextKey.value, rightSlope, nextLeftSlope, segmentDuration, t);
|
|
495
|
+
const linear = key.value + t * segmentDuration * linearSlope;
|
|
496
|
+
maxLinearDeviation = Math.max(maxLinearDeviation, Math.abs(cubic - linear));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (!hasCubicSegment || !hasCompleteTangents || allSlopesDegenerate) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
const range = maxValue - minValue;
|
|
503
|
+
const deviationTolerance = Math.max(SAMPLED_CURVE_LINEAR_DEVIATION_ABSOLUTE, range * SAMPLED_CURVE_LINEAR_DEVIATION_RATIO);
|
|
504
|
+
return maxLinearDeviation > deviationTolerance;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Samples an FBX animation curve at a specific time.
|
|
508
|
+
* @param curveData - Curve data to sample
|
|
509
|
+
* @param time - Time in seconds
|
|
510
|
+
* @returns The sampled value, or null when the curve has no keys
|
|
511
|
+
*/
|
|
512
|
+
export function sampleFBXCurveAtTime(curveData, time) {
|
|
513
|
+
if (!curveData || curveData.keys.length === 0) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
const keys = curveData.keys;
|
|
517
|
+
if (time <= keys[0].time) {
|
|
518
|
+
return keys[0].value;
|
|
519
|
+
}
|
|
520
|
+
if (time >= keys[keys.length - 1].time) {
|
|
521
|
+
return keys[keys.length - 1].value;
|
|
522
|
+
}
|
|
523
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
524
|
+
const key = keys[i];
|
|
525
|
+
const nextKey = keys[i + 1];
|
|
526
|
+
if (time < key.time || time > nextKey.time) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (nextKey.time === key.time) {
|
|
530
|
+
return key.value;
|
|
531
|
+
}
|
|
532
|
+
if (key.interpolation === "constant") {
|
|
533
|
+
return key.constantMode === "next" ? nextKey.value : key.value;
|
|
534
|
+
}
|
|
535
|
+
const segmentDuration = nextKey.time - key.time;
|
|
536
|
+
const t = (time - key.time) / segmentDuration;
|
|
537
|
+
if (key.interpolation === "cubic" && !curveData.isSampled) {
|
|
538
|
+
const linearSlope = (nextKey.value - key.value) / segmentDuration;
|
|
539
|
+
const rightSlope = key.rightSlope ?? linearSlope;
|
|
540
|
+
const nextLeftSlope = key.nextLeftSlope ?? linearSlope;
|
|
541
|
+
return cubicHermite(key.value, nextKey.value, rightSlope, nextLeftSlope, segmentDuration, t);
|
|
542
|
+
}
|
|
543
|
+
return key.value + t * (nextKey.value - key.value);
|
|
544
|
+
}
|
|
545
|
+
return keys[keys.length - 1].value;
|
|
546
|
+
}
|
|
547
|
+
// ── Utilities ──────────────────────────────────────────────────────────────────
|
|
548
|
+
function toInt64Array(value) {
|
|
549
|
+
if (value instanceof Float64Array) {
|
|
550
|
+
return value;
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
function toInt32Array(value) {
|
|
555
|
+
if (value instanceof Int32Array) {
|
|
556
|
+
return value;
|
|
557
|
+
}
|
|
558
|
+
if (value instanceof Float32Array || value instanceof Float64Array) {
|
|
559
|
+
const result = new Int32Array(value.length);
|
|
560
|
+
for (let i = 0; i < value.length; i++) {
|
|
561
|
+
result[i] = value[i];
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
function fbxTimeToSeconds(value) {
|
|
568
|
+
if (typeof value === "number") {
|
|
569
|
+
return value / FBX_TIME_UNIT;
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
function toNumber(value) {
|
|
574
|
+
if (typeof value === "number") {
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
function toFloat32Array(value) {
|
|
580
|
+
if (value instanceof Float32Array) {
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
if (value instanceof Float64Array) {
|
|
584
|
+
const result = new Float32Array(value.length);
|
|
585
|
+
for (let i = 0; i < value.length; i++) {
|
|
586
|
+
result[i] = value[i];
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
function buildKeyAttributeIndices(keyCount, keyAttrFlags, keyAttrRefCount) {
|
|
593
|
+
if (!keyAttrFlags || keyAttrFlags.length === 0) {
|
|
594
|
+
return new Array(keyCount).fill(-1);
|
|
595
|
+
}
|
|
596
|
+
if (keyAttrRefCount && keyAttrRefCount.length > 0) {
|
|
597
|
+
let total = 0;
|
|
598
|
+
for (const count of keyAttrRefCount) {
|
|
599
|
+
total += count;
|
|
600
|
+
}
|
|
601
|
+
if (total === keyCount) {
|
|
602
|
+
const indices = [];
|
|
603
|
+
for (let attrIndex = 0; attrIndex < keyAttrRefCount.length; attrIndex++) {
|
|
604
|
+
const count = keyAttrRefCount[attrIndex];
|
|
605
|
+
for (let i = 0; i < count; i++) {
|
|
606
|
+
indices.push(attrIndex);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return indices;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (keyAttrFlags.length === keyCount) {
|
|
613
|
+
return Array.from({ length: keyCount }, (_, i) => i);
|
|
614
|
+
}
|
|
615
|
+
if (keyAttrFlags.length === 1) {
|
|
616
|
+
return new Array(keyCount).fill(0);
|
|
617
|
+
}
|
|
618
|
+
return Array.from({ length: keyCount }, (_, i) => Math.min(i, keyAttrFlags.length - 1));
|
|
619
|
+
}
|
|
620
|
+
function getInterpolationType(flag) {
|
|
621
|
+
if ((flag & 0x00000008) !== 0) {
|
|
622
|
+
return "cubic";
|
|
623
|
+
}
|
|
624
|
+
if ((flag & 0x00000004) !== 0) {
|
|
625
|
+
return "linear";
|
|
626
|
+
}
|
|
627
|
+
if ((flag & 0x00000002) !== 0) {
|
|
628
|
+
return "constant";
|
|
629
|
+
}
|
|
630
|
+
return "linear";
|
|
631
|
+
}
|
|
632
|
+
function getFiniteKeyAttrData(keyAttrData, index) {
|
|
633
|
+
if (!keyAttrData || index < 0 || index >= keyAttrData.length) {
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
const value = keyAttrData[index];
|
|
637
|
+
return Number.isFinite(value) ? value : undefined;
|
|
638
|
+
}
|
|
639
|
+
function cubicHermite(value0, value1, slope0, slope1, segmentDuration, t) {
|
|
640
|
+
const t2 = t * t;
|
|
641
|
+
const t3 = t2 * t;
|
|
642
|
+
const h00 = 2 * t3 - 3 * t2 + 1;
|
|
643
|
+
const h10 = t3 - 2 * t2 + t;
|
|
644
|
+
const h01 = -2 * t3 + 3 * t2;
|
|
645
|
+
const h11 = t3 - t2;
|
|
646
|
+
return h00 * value0 + h10 * segmentDuration * slope0 + h01 * value1 + h11 * segmentDuration * slope1;
|
|
647
|
+
}
|
|
648
|
+
//# sourceMappingURL=animation.js.map
|