@babylonjs/loaders 9.25.0 → 9.26.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/fbxConstraintBehavior.d.ts +188 -0
- package/FBX/fbxConstraintBehavior.js +532 -0
- package/FBX/fbxConstraintBehavior.js.map +1 -0
- package/FBX/fbxFileLoader.pure.d.ts +175 -6
- package/FBX/fbxFileLoader.pure.js +1521 -286
- package/FBX/fbxFileLoader.pure.js.map +1 -1
- package/FBX/index.d.ts +4 -1
- package/FBX/index.js +1 -0
- package/FBX/index.js.map +1 -1
- package/FBX/interpreter/animation.d.ts +73 -33
- package/FBX/interpreter/animation.js +620 -223
- package/FBX/interpreter/animation.js.map +1 -1
- package/FBX/interpreter/animationCurve.d.ts +91 -0
- package/FBX/interpreter/animationCurve.js +623 -0
- package/FBX/interpreter/animationCurve.js.map +1 -0
- package/FBX/interpreter/blendShapes.js +9 -18
- package/FBX/interpreter/blendShapes.js.map +1 -1
- package/FBX/interpreter/connections.d.ts +1 -1
- package/FBX/interpreter/connections.js +173 -34
- package/FBX/interpreter/connections.js.map +1 -1
- package/FBX/interpreter/constraints.d.ts +68 -0
- package/FBX/interpreter/constraints.js +143 -0
- package/FBX/interpreter/constraints.js.map +1 -0
- package/FBX/interpreter/fbxInterpreter.d.ts +89 -4
- package/FBX/interpreter/fbxInterpreter.js +474 -122
- package/FBX/interpreter/fbxInterpreter.js.map +1 -1
- package/FBX/interpreter/geometry.d.ts +1 -1
- package/FBX/interpreter/geometry.js +150 -92
- package/FBX/interpreter/geometry.js.map +1 -1
- package/FBX/interpreter/legacyDocument.d.ts +11 -0
- package/FBX/interpreter/legacyDocument.js +425 -0
- package/FBX/interpreter/legacyDocument.js.map +1 -0
- package/FBX/interpreter/materialModel.d.ts +71 -0
- package/FBX/interpreter/materialModel.js +622 -0
- package/FBX/interpreter/materialModel.js.map +1 -0
- package/FBX/interpreter/materials.d.ts +13 -1
- package/FBX/interpreter/materials.js +88 -5
- package/FBX/interpreter/materials.js.map +1 -1
- package/FBX/interpreter/nodeTransform.d.ts +32 -0
- package/FBX/interpreter/nodeTransform.js +54 -0
- package/FBX/interpreter/nodeTransform.js.map +1 -0
- package/FBX/interpreter/nurbs.d.ts +99 -0
- package/FBX/interpreter/nurbs.js +627 -0
- package/FBX/interpreter/nurbs.js.map +1 -0
- package/FBX/interpreter/propertyTemplates.d.ts +23 -0
- package/FBX/interpreter/propertyTemplates.js +109 -2
- package/FBX/interpreter/propertyTemplates.js.map +1 -1
- package/FBX/interpreter/rig.js +16 -0
- package/FBX/interpreter/rig.js.map +1 -1
- package/FBX/interpreter/sceneDiagnostics.js +28 -3
- package/FBX/interpreter/sceneDiagnostics.js.map +1 -1
- package/FBX/interpreter/skeleton.d.ts +4 -18
- package/FBX/interpreter/skeleton.js +16 -94
- package/FBX/interpreter/skeleton.js.map +1 -1
- package/FBX/parsers/fbxAsciiParser.js +119 -9
- package/FBX/parsers/fbxAsciiParser.js.map +1 -1
- package/FBX/parsers/fbxBinaryParser.js +74 -22
- package/FBX/parsers/fbxBinaryParser.js.map +1 -1
- package/FBX/pure.d.ts +1 -0
- package/FBX/pure.js +1 -0
- package/FBX/pure.js.map +1 -1
- package/FBX/types/fbxTypes.d.ts +18 -2
- package/FBX/types/fbxTypes.js +40 -12
- package/FBX/types/fbxTypes.js.map +1 -1
- package/SPLAT/gaussianSplattingStream.d.ts +31 -0
- package/SPLAT/gaussianSplattingStream.js +49 -0
- package/SPLAT/gaussianSplattingStream.js.map +1 -1
- package/USD/index.d.ts +2 -0
- package/USD/index.js +4 -0
- package/USD/index.js.map +1 -0
- package/USD/pure.d.ts +3 -0
- package/USD/pure.js +5 -0
- package/USD/pure.js.map +1 -0
- package/USD/usdCommandProtocol.d.ts +83 -0
- package/USD/usdCommandProtocol.js +173 -0
- package/USD/usdCommandProtocol.js.map +1 -0
- package/USD/usdFileLoader.d.ts +6 -0
- package/USD/usdFileLoader.js +9 -0
- package/USD/usdFileLoader.js.map +1 -0
- package/USD/usdFileLoader.metadata.d.ts +20 -0
- package/USD/usdFileLoader.metadata.js +18 -0
- package/USD/usdFileLoader.metadata.js.map +1 -0
- package/USD/usdFileLoader.pure.d.ts +102 -0
- package/USD/usdFileLoader.pure.js +341 -0
- package/USD/usdFileLoader.pure.js.map +1 -0
- package/USD/usdFileLoader.types.d.ts +11 -0
- package/USD/usdFileLoader.types.js +2 -0
- package/USD/usdFileLoader.types.js.map +1 -0
- package/USD/usdLoadingOptions.d.ts +123 -0
- package/USD/usdLoadingOptions.js +3 -0
- package/USD/usdLoadingOptions.js.map +1 -0
- package/USD/usdSceneMaterializer.d.ts +9 -0
- package/USD/usdSceneMaterializer.js +982 -0
- package/USD/usdSceneMaterializer.js.map +1 -0
- package/USD/usdWorkerMessages.d.ts +57 -0
- package/USD/usdWorkerMessages.js +3 -0
- package/USD/usdWorkerMessages.js.map +1 -0
- package/dynamic.js +10 -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 +4 -3
- package/pure.d.ts +1 -0
- package/pure.js +1 -0
- package/pure.js.map +1 -1
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention, jsdoc/require-param, jsdoc/require-returns */
|
|
2
|
-
import { findChildByName, getPropertyValue, cleanFBXName } from "../types/fbxTypes.js";
|
|
2
|
+
import { findChildByName, findDocumentNode, getNodeArray, getPropertyValue, cleanFBXName } from "../types/fbxTypes.js";
|
|
3
3
|
import { getChildren } from "./connections.js";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { getPropertyEntries } from "./propertyTemplates.js";
|
|
5
|
+
import { FBX_TIME_UNIT, buildKeyframes, evaluateCurve, keyframesFromSlopes, parseExtrapolation, combineLayerValue, } from "./animationCurve.js";
|
|
6
|
+
export { FBX_TIME_UNIT } from "./animationCurve.js";
|
|
7
|
+
export { combineLayerValue, evaluateCurve, eulerToQuat, quatToEuler } from "./animationCurve.js";
|
|
6
8
|
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
9
|
/**
|
|
15
10
|
* Extract all animation stacks from the FBX scene.
|
|
16
11
|
*/
|
|
17
|
-
export function extractAnimations(objectMap) {
|
|
12
|
+
export function extractAnimations(objectMap, doc, options = {}) {
|
|
18
13
|
const stacks = [];
|
|
14
|
+
const rebaseKeyframes = options.rebaseKeyframes ?? true;
|
|
19
15
|
for (const [id, node] of Array.from(objectMap.objects)) {
|
|
20
16
|
if (node.name === "AnimationStack") {
|
|
21
|
-
const stack = extractAnimStack(id, node, objectMap);
|
|
17
|
+
const stack = extractAnimStack(id, node, objectMap, rebaseKeyframes);
|
|
22
18
|
if (stack) {
|
|
23
19
|
stacks.push(stack);
|
|
24
20
|
}
|
|
25
21
|
}
|
|
26
22
|
}
|
|
23
|
+
// Pre-7000 files have no AnimationStack objects; their animation lives in the top-level Takes block.
|
|
24
|
+
if (stacks.length === 0 && doc) {
|
|
25
|
+
stacks.push(...extractLegacyTakes(doc, objectMap, rebaseKeyframes));
|
|
26
|
+
}
|
|
27
27
|
return stacks;
|
|
28
28
|
}
|
|
29
|
-
function extractAnimStack(stackId, stackNode, objectMap) {
|
|
29
|
+
function extractAnimStack(stackId, stackNode, objectMap, rebaseKeyframes) {
|
|
30
30
|
const name = cleanFBXName(getPropertyValue(stackNode, 1) ?? "Animation");
|
|
31
31
|
const declaredTimeSpan = extractAnimationStackTimeSpan(stackNode);
|
|
32
32
|
// Find AnimationLayer children of this stack
|
|
@@ -40,43 +40,49 @@ function extractAnimStack(stackId, stackNode, objectMap) {
|
|
|
40
40
|
const layers = [];
|
|
41
41
|
const diagnostics = [];
|
|
42
42
|
let minTime = Infinity;
|
|
43
|
-
let maxTime =
|
|
43
|
+
let maxTime = -Infinity;
|
|
44
44
|
for (const { id: layerId, node: layerNode } of layerEntries) {
|
|
45
45
|
// Extract layer properties
|
|
46
46
|
const layerName = cleanFBXName(getPropertyValue(layerNode, 1) ?? "Layer");
|
|
47
47
|
let weight = 100;
|
|
48
48
|
let blendMode = 0;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
49
|
+
let rotationAccumulationMode = 0;
|
|
50
|
+
let scaleAccumulationMode = 0;
|
|
51
|
+
for (const entry of getPropertyEntries(layerNode)) {
|
|
52
|
+
const v = entry.values[0];
|
|
53
|
+
if (typeof v !== "number") {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (entry.name === "Weight") {
|
|
57
|
+
weight = v;
|
|
58
|
+
}
|
|
59
|
+
else if (entry.name === "BlendMode") {
|
|
60
|
+
blendMode = v;
|
|
61
|
+
}
|
|
62
|
+
else if (entry.name === "RotationAccumulationMode") {
|
|
63
|
+
rotationAccumulationMode = v;
|
|
64
|
+
}
|
|
65
|
+
else if (entry.name === "ScaleAccumulationMode") {
|
|
66
|
+
scaleAccumulationMode = v;
|
|
68
67
|
}
|
|
69
68
|
}
|
|
69
|
+
const layerIndex = layers.length;
|
|
70
70
|
// AnimationCurveNodes are children of the layer
|
|
71
71
|
const curveNodeEntries = getChildren(objectMap, layerId, "AnimationCurveNode");
|
|
72
72
|
const layerCurveNodes = [];
|
|
73
73
|
const layerUnsupportedCurveNodes = [];
|
|
74
74
|
const layerDiagnostics = [];
|
|
75
|
+
let layerWeightCurve;
|
|
75
76
|
for (const { id: curveNodeId, node: curveNodeNode } of curveNodeEntries) {
|
|
76
|
-
const curveNodeData = extractCurveNode(curveNodeId, curveNodeNode, objectMap);
|
|
77
|
+
const curveNodeData = extractCurveNode(curveNodeId, curveNodeNode, objectMap, layerIndex);
|
|
77
78
|
if (!curveNodeData) {
|
|
78
|
-
const unsupported = extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap);
|
|
79
|
+
const unsupported = extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap, layerIndex);
|
|
79
80
|
if (unsupported) {
|
|
81
|
+
// The layer's own Weight property can be animated; it drives blending rather than a target.
|
|
82
|
+
if (unsupported.targetId === layerId && unsupported.propertyName === "Weight" && unsupported.curves.length > 0) {
|
|
83
|
+
layerWeightCurve = unsupported.curves[0];
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
80
86
|
scanCurveTimes(unsupported.curves, (time) => {
|
|
81
87
|
if (time < minTime) {
|
|
82
88
|
minTime = time;
|
|
@@ -119,6 +125,8 @@ function extractAnimStack(stackId, stackNode, objectMap) {
|
|
|
119
125
|
weight,
|
|
120
126
|
normalizedWeight: weight / 100,
|
|
121
127
|
blendMode,
|
|
128
|
+
blend: makeLayerBlend(blendMode, weight, rotationAccumulationMode, scaleAccumulationMode),
|
|
129
|
+
weightCurve: layerWeightCurve,
|
|
122
130
|
curveNodes: layerCurveNodes,
|
|
123
131
|
unsupportedCurveNodes: layerUnsupportedCurveNodes,
|
|
124
132
|
diagnostics: layerDiagnostics,
|
|
@@ -127,33 +135,15 @@ function extractAnimStack(stackId, stackNode, objectMap) {
|
|
|
127
135
|
if (allCurveNodes.length === 0 && allUnsupportedCurveNodes.length === 0) {
|
|
128
136
|
return null;
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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;
|
|
138
|
+
return finalizeAnimStack(name, layers, allCurveNodes, allUnsupportedCurveNodes, diagnostics, minTime, maxTime, declaredTimeSpan, rebaseKeyframes);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolves the clip span from the declared time span or the key range and, when requested, rebases key times so
|
|
142
|
+
* the first key sits at zero.
|
|
143
|
+
*/
|
|
144
|
+
function finalizeAnimStack(name, layers, allCurveNodes, allUnsupportedCurveNodes, diagnostics, minTime, maxTime, declaredTimeSpan, rebaseKeyframes) {
|
|
145
|
+
const hasKeys = isFinite(minTime) && isFinite(maxTime);
|
|
146
|
+
const timeOffset = rebaseKeyframes && hasKeys && minTime > 0 ? minTime : 0;
|
|
157
147
|
// Rebase all keyframe times so the animation starts at 0
|
|
158
148
|
if (timeOffset > 0) {
|
|
159
149
|
for (const cn of allCurveNodes) {
|
|
@@ -172,11 +162,14 @@ function extractAnimStack(stackId, stackNode, objectMap) {
|
|
|
172
162
|
}
|
|
173
163
|
maxTime -= timeOffset;
|
|
174
164
|
}
|
|
175
|
-
const declaredStart = declaredTimeSpan ? Math.max(declaredTimeSpan.start - timeOffset, 0) : 0;
|
|
165
|
+
const declaredStart = declaredTimeSpan ? (rebaseKeyframes ? Math.max(declaredTimeSpan.start - timeOffset, 0) : declaredTimeSpan.start) : 0;
|
|
176
166
|
const declaredStop = declaredTimeSpan ? Math.max(declaredTimeSpan.stop - timeOffset, declaredStart) : 0;
|
|
177
167
|
const hasDeclaredDuration = declaredStop > declaredStart;
|
|
178
|
-
|
|
179
|
-
const
|
|
168
|
+
// Without a declared span the clip covers its keys (a clip whose keys are all negative ends before zero).
|
|
169
|
+
const keyStart = hasKeys ? minTime - timeOffset : 0;
|
|
170
|
+
const keyStop = hasKeys ? maxTime : 0;
|
|
171
|
+
const startTime = hasDeclaredDuration ? declaredStart : rebaseKeyframes ? Math.min(0, keyStart) : keyStart;
|
|
172
|
+
const stopTime = hasDeclaredDuration ? declaredStop : Math.max(keyStop, startTime);
|
|
180
173
|
return {
|
|
181
174
|
name,
|
|
182
175
|
startTime,
|
|
@@ -209,24 +202,39 @@ function extractAnimationStackTimeSpan(stackNode) {
|
|
|
209
202
|
}
|
|
210
203
|
return stop !== null ? { start, stop } : null;
|
|
211
204
|
}
|
|
212
|
-
function extractCurveNode(curveNodeId, curveNodeNode, objectMap) {
|
|
205
|
+
function extractCurveNode(curveNodeId, curveNodeNode, objectMap, layerIndex) {
|
|
213
206
|
const typeName = cleanFBXName(getPropertyValue(curveNodeNode, 1) ?? "");
|
|
207
|
+
const rawDefaults = extractCurveNodeDefaultValues(curveNodeNode);
|
|
208
|
+
const defaultValues = "d|X" in rawDefaults || "d|Y" in rawDefaults || "d|Z" in rawDefaults ? [rawDefaults["d|X"] ?? 0, rawDefaults["d|Y"] ?? 0, rawDefaults["d|Z"] ?? 0] : undefined;
|
|
214
209
|
// Handle T (translation), R (rotation), S (scale) targeting Models
|
|
215
210
|
if (typeName === "T" || typeName === "R" || typeName === "S") {
|
|
216
211
|
const targetModelId = findCurveNodeTarget(curveNodeId, objectMap);
|
|
217
212
|
if (targetModelId === null) {
|
|
218
213
|
return null;
|
|
219
214
|
}
|
|
215
|
+
// A curve node without curves still contributes its default values to layer blending (Maya writes such
|
|
216
|
+
// nodes on the base layer when only additive layers animate a property).
|
|
220
217
|
const curves = extractCurves(curveNodeId, objectMap);
|
|
221
|
-
if (curves.length === 0) {
|
|
218
|
+
if (curves.length === 0 && !defaultValues) {
|
|
222
219
|
return null;
|
|
223
220
|
}
|
|
224
221
|
return {
|
|
225
222
|
type: typeName,
|
|
226
223
|
targetModelId,
|
|
227
224
|
curves,
|
|
225
|
+
layerIndex,
|
|
226
|
+
defaultValues,
|
|
228
227
|
};
|
|
229
228
|
}
|
|
229
|
+
// FBX 7.1 writes blend shape weight curves against the geometry, named after the shape channel
|
|
230
|
+
const geometryChannelId = findCurveNodeGeometryShapeChannel(curveNodeId, objectMap);
|
|
231
|
+
if (geometryChannelId !== null) {
|
|
232
|
+
const curves = extractCurves(curveNodeId, objectMap);
|
|
233
|
+
if (curves.length === 0) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
return { type: "DeformPercent", targetModelId: geometryChannelId, curves, layerIndex, defaultValues };
|
|
237
|
+
}
|
|
230
238
|
// Handle DeformPercent targeting BlendShapeChannels
|
|
231
239
|
if (typeName === "DeformPercent") {
|
|
232
240
|
const targetId = findCurveNodeBlendShapeTarget(curveNodeId, objectMap);
|
|
@@ -241,11 +249,13 @@ function extractCurveNode(curveNodeId, curveNodeNode, objectMap) {
|
|
|
241
249
|
type: "DeformPercent",
|
|
242
250
|
targetModelId: targetId,
|
|
243
251
|
curves,
|
|
252
|
+
layerIndex,
|
|
253
|
+
defaultValues,
|
|
244
254
|
};
|
|
245
255
|
}
|
|
246
256
|
return null;
|
|
247
257
|
}
|
|
248
|
-
function extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap) {
|
|
258
|
+
function extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap, layerIndex) {
|
|
249
259
|
const typeName = cleanFBXName(getPropertyValue(curveNodeNode, 1) ?? "");
|
|
250
260
|
const curves = extractCurves(curveNodeId, objectMap);
|
|
251
261
|
const defaultValues = extractCurveNodeDefaultValues(curveNodeNode);
|
|
@@ -264,6 +274,7 @@ function extractUnsupportedCurveNode(curveNodeId, curveNodeNode, objectMap) {
|
|
|
264
274
|
return {
|
|
265
275
|
type: typeName,
|
|
266
276
|
id: curveNodeId,
|
|
277
|
+
layerIndex,
|
|
267
278
|
targetId,
|
|
268
279
|
propertyName,
|
|
269
280
|
curveCount: curves.length,
|
|
@@ -295,6 +306,29 @@ function findCurveNodeTarget(curveNodeId, objectMap) {
|
|
|
295
306
|
}
|
|
296
307
|
return null;
|
|
297
308
|
}
|
|
309
|
+
/** When a curve node is connected to a Geometry through a property named like one of its blend shape channels. */
|
|
310
|
+
function findCurveNodeGeometryShapeChannel(curveNodeId, objectMap) {
|
|
311
|
+
for (const conn of objectMap.connections) {
|
|
312
|
+
if (conn.childId !== curveNodeId || conn.type !== "OP" || !conn.propertyName) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const parentNode = objectMap.objects.get(conn.parentId);
|
|
316
|
+
if (!parentNode || parentNode.name !== "Geometry") {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
for (const deformer of getChildren(objectMap, conn.parentId, "Deformer")) {
|
|
320
|
+
if (getPropertyValue(deformer.node, 2) !== "BlendShape") {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
for (const channel of getChildren(objectMap, deformer.id, "Deformer")) {
|
|
324
|
+
if (getPropertyValue(channel.node, 2) === "BlendShapeChannel" && cleanFBXName(getPropertyValue(channel.node, 1) ?? "") === conn.propertyName) {
|
|
325
|
+
return channel.id;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
298
332
|
/**
|
|
299
333
|
* Find the BlendShapeChannel that a DeformPercent AnimationCurveNode targets.
|
|
300
334
|
*/
|
|
@@ -340,8 +374,7 @@ function extractCurves(curveNodeId, objectMap) {
|
|
|
340
374
|
const channel = conn.propertyName ?? "d|X";
|
|
341
375
|
const keys = extractKeyframes(curveNode);
|
|
342
376
|
if (keys.length > 0) {
|
|
343
|
-
|
|
344
|
-
curves.push({ channel, keys: isSampled ? makeLinearSampleKeys(keys) : keys, isSampled });
|
|
377
|
+
curves.push({ channel, keys, ...extractCurveExtrapolation(curveNode) });
|
|
345
378
|
}
|
|
346
379
|
}
|
|
347
380
|
}
|
|
@@ -353,8 +386,7 @@ function extractCurves(curveNodeId, objectMap) {
|
|
|
353
386
|
for (let i = 0; i < ooChildren.length && i < 3; i++) {
|
|
354
387
|
const keys = extractKeyframes(ooChildren[i].node);
|
|
355
388
|
if (keys.length > 0) {
|
|
356
|
-
|
|
357
|
-
curves.push({ channel: channelNames[i], keys: isSampled ? makeLinearSampleKeys(keys) : keys, isSampled });
|
|
389
|
+
curves.push({ channel: channelNames[i], keys, ...extractCurveExtrapolation(ooChildren[i].node) });
|
|
358
390
|
}
|
|
359
391
|
}
|
|
360
392
|
}
|
|
@@ -387,11 +419,11 @@ function extractKeyframes(curveNode) {
|
|
|
387
419
|
if (!keyTimeNode || !keyValueNode) {
|
|
388
420
|
return [];
|
|
389
421
|
}
|
|
390
|
-
const keyTimes = toInt64Array(keyTimeNode
|
|
391
|
-
const keyValues = toFloat32Array(keyValueNode
|
|
392
|
-
const keyAttrFlags = toInt32Array(findChildByName(curveNode, "KeyAttrFlags")
|
|
393
|
-
const keyAttrData = toFloat32Array(findChildByName(curveNode, "KeyAttrDataFloat")
|
|
394
|
-
const keyAttrRefCount = toInt32Array(findChildByName(curveNode, "KeyAttrRefCount")
|
|
422
|
+
const keyTimes = toInt64Array(getNodeArray(keyTimeNode));
|
|
423
|
+
const keyValues = toFloat32Array(getNodeArray(keyValueNode));
|
|
424
|
+
const keyAttrFlags = toInt32Array(getNodeArray(findChildByName(curveNode, "KeyAttrFlags")));
|
|
425
|
+
const keyAttrData = toFloat32Array(getNodeArray(findChildByName(curveNode, "KeyAttrDataFloat")));
|
|
426
|
+
const keyAttrRefCount = toInt32Array(getNodeArray(findChildByName(curveNode, "KeyAttrRefCount")));
|
|
395
427
|
if (!keyTimes || !keyValues) {
|
|
396
428
|
return [];
|
|
397
429
|
}
|
|
@@ -399,109 +431,50 @@ function extractKeyframes(curveNode) {
|
|
|
399
431
|
return [];
|
|
400
432
|
}
|
|
401
433
|
const keyAttributeIndices = buildKeyAttributeIndices(keyTimes.length, keyAttrFlags, keyAttrRefCount);
|
|
402
|
-
|
|
403
|
-
for (let i = 0; i < keyTimes.length; i++) {
|
|
434
|
+
return buildKeyframes(keyTimes, keyValues, (i) => {
|
|
404
435
|
const attrIndex = keyAttributeIndices[i];
|
|
405
436
|
const flag = attrIndex >= 0 ? (keyAttrFlags?.[attrIndex] ?? 0) : 0;
|
|
406
437
|
const dataOffset = attrIndex * KEY_ATTR_DATA_STRIDE;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
438
|
+
return {
|
|
439
|
+
flags: flag,
|
|
440
|
+
data: [
|
|
441
|
+
getFiniteKeyAttrData(keyAttrData, dataOffset) ?? 0,
|
|
442
|
+
getFiniteKeyAttrData(keyAttrData, dataOffset + 1) ?? 0,
|
|
443
|
+
getFiniteKeyAttrData(keyAttrData, dataOffset + 2) ?? 0,
|
|
444
|
+
getFiniteKeyAttrData(keyAttrData, dataOffset + 3) ?? 0,
|
|
445
|
+
],
|
|
446
|
+
};
|
|
447
|
+
});
|
|
417
448
|
}
|
|
418
|
-
function
|
|
419
|
-
const
|
|
420
|
-
|
|
449
|
+
function extractCurveExtrapolation(curveNode) {
|
|
450
|
+
const read = (name) => {
|
|
451
|
+
const node = findChildByName(curveNode, name);
|
|
452
|
+
return parseExtrapolation(findChildByName(node ?? curveNode, "Type")?.properties[0]?.value, findChildByName(node ?? curveNode, "Repetition")?.properties[0]?.value);
|
|
453
|
+
};
|
|
454
|
+
return { preExtrapolation: read("Pre-Extrapolation"), postExtrapolation: read("Post-Extrapolation") };
|
|
421
455
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
}
|
|
456
|
+
function makeLayerBlend(blendMode, weight, rotationAccumulationMode, scaleAccumulationMode) {
|
|
457
|
+
let blended = false;
|
|
458
|
+
let additive = false;
|
|
459
|
+
switch (blendMode) {
|
|
460
|
+
case 0:
|
|
461
|
+
blended = true;
|
|
462
|
+
additive = true;
|
|
463
|
+
break;
|
|
464
|
+
case 2:
|
|
465
|
+
blended = true;
|
|
466
|
+
break;
|
|
467
|
+
default:
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
let w = weight / 100;
|
|
471
|
+
if (w < 0) {
|
|
472
|
+
w = 0;
|
|
498
473
|
}
|
|
499
|
-
if (
|
|
500
|
-
|
|
474
|
+
if (w > 0.99999) {
|
|
475
|
+
w = 1;
|
|
501
476
|
}
|
|
502
|
-
|
|
503
|
-
const deviationTolerance = Math.max(SAMPLED_CURVE_LINEAR_DEVIATION_ABSOLUTE, range * SAMPLED_CURVE_LINEAR_DEVIATION_RATIO);
|
|
504
|
-
return maxLinearDeviation > deviationTolerance;
|
|
477
|
+
return { blended, additive, composeRotation: rotationAccumulationMode === 0, composeScale: scaleAccumulationMode === 0, weight: w };
|
|
505
478
|
}
|
|
506
479
|
/**
|
|
507
480
|
* Samples an FBX animation curve at a specific time.
|
|
@@ -513,36 +486,7 @@ export function sampleFBXCurveAtTime(curveData, time) {
|
|
|
513
486
|
if (!curveData || curveData.keys.length === 0) {
|
|
514
487
|
return null;
|
|
515
488
|
}
|
|
516
|
-
|
|
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;
|
|
489
|
+
return evaluateCurve(curveData, time, curveData.keys[0].value);
|
|
546
490
|
}
|
|
547
491
|
// ── Utilities ──────────────────────────────────────────────────────────────────
|
|
548
492
|
function toInt64Array(value) {
|
|
@@ -617,18 +561,6 @@ function buildKeyAttributeIndices(keyCount, keyAttrFlags, keyAttrRefCount) {
|
|
|
617
561
|
}
|
|
618
562
|
return Array.from({ length: keyCount }, (_, i) => Math.min(i, keyAttrFlags.length - 1));
|
|
619
563
|
}
|
|
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
564
|
function getFiniteKeyAttrData(keyAttrData, index) {
|
|
633
565
|
if (!keyAttrData || index < 0 || index >= keyAttrData.length) {
|
|
634
566
|
return undefined;
|
|
@@ -636,13 +568,478 @@ function getFiniteKeyAttrData(keyAttrData, index) {
|
|
|
636
568
|
const value = keyAttrData[index];
|
|
637
569
|
return Number.isFinite(value) ? value : undefined;
|
|
638
570
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
571
|
+
// ── Legacy Takes (FBX 5.x / 6.x) ───────────────────────────────────────────────
|
|
572
|
+
/**
|
|
573
|
+
* Pre-7000 files store animation in a top-level `Takes` block instead of AnimationStack/Layer/CurveNode objects:
|
|
574
|
+
*
|
|
575
|
+
* Takes: { Take: "name" { LocalTime: start, stop
|
|
576
|
+
* Model: "Model::joint1" { Channel: "Transform" { Channel: "T" { Channel: "X" { Default, KeyVer, KeyCount, Key } } } } } }
|
|
577
|
+
*
|
|
578
|
+
* Each take becomes one animation stack with a single layer. Models are matched through the same legacy string ids
|
|
579
|
+
* that the connection resolver synthesizes for 6.x objects.
|
|
580
|
+
*/
|
|
581
|
+
export function extractLegacyTakes(doc, objectMap, rebaseKeyframes = true) {
|
|
582
|
+
const takesNode = findDocumentNode(doc, "Takes");
|
|
583
|
+
if (!takesNode) {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
// Take entries name models by their full "Class::Name" string; older writers may use the bare name.
|
|
587
|
+
const legacyIds = new Map();
|
|
588
|
+
const legacyIdsByCleanName = new Map();
|
|
589
|
+
for (const entry of objectMap.objectEntries) {
|
|
590
|
+
if (entry.legacyName !== undefined && !entry.synthetic && !legacyIds.has(entry.legacyName)) {
|
|
591
|
+
legacyIds.set(entry.legacyName, entry.id);
|
|
592
|
+
const clean = cleanFBXName(entry.legacyName);
|
|
593
|
+
if (!legacyIdsByCleanName.has(clean)) {
|
|
594
|
+
legacyIdsByCleanName.set(clean, entry.id);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const stacks = [];
|
|
599
|
+
let syntheticId = -1000000;
|
|
600
|
+
for (const take of takesNode.children) {
|
|
601
|
+
if (take.name !== "Take") {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const name = cleanFBXName(getPropertyValue(take, 0) ?? "Take");
|
|
605
|
+
const timeSpan = readLegacyTimeSpan(findChildByName(take, "LocalTime")) ?? readLegacyTimeSpan(findChildByName(take, "ReferenceTime"));
|
|
606
|
+
const curveNodes = [];
|
|
607
|
+
const unsupportedCurveNodes = [];
|
|
608
|
+
let minTime = Infinity;
|
|
609
|
+
let maxTime = -Infinity;
|
|
610
|
+
const track = (curves) => {
|
|
611
|
+
for (const curve of curves) {
|
|
612
|
+
for (const key of curve.keys) {
|
|
613
|
+
if (key.time < minTime) {
|
|
614
|
+
minTime = key.time;
|
|
615
|
+
}
|
|
616
|
+
if (key.time > maxTime) {
|
|
617
|
+
maxTime = key.time;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
for (const model of take.children) {
|
|
623
|
+
if (model.name !== "Model") {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const rawModelName = getPropertyValue(model, 0) ?? "";
|
|
627
|
+
const targetModelId = legacyIds.get(rawModelName) ?? legacyIdsByCleanName.get(cleanFBXName(rawModelName));
|
|
628
|
+
if (targetModelId === undefined) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
for (const channel of model.children) {
|
|
632
|
+
if (channel.name !== "Channel") {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const channelName = getPropertyValue(channel, 0) ?? "";
|
|
636
|
+
if (channelName === "Transform") {
|
|
637
|
+
for (const sub of channel.children) {
|
|
638
|
+
if (sub.name !== "Channel") {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const subName = getPropertyValue(sub, 0);
|
|
642
|
+
if (subName !== "T" && subName !== "R" && subName !== "S") {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const curves = readLegacyCompoundChannel(sub, doc.version);
|
|
646
|
+
if (curves.length > 0) {
|
|
647
|
+
track(curves);
|
|
648
|
+
curveNodes.push({ type: subName, targetModelId, curves, layerIndex: 0 });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
const curves = readLegacyCompoundChannel(channel, doc.version);
|
|
654
|
+
if (curves.length > 0) {
|
|
655
|
+
track(curves);
|
|
656
|
+
// Blend shape weights are channels named after the shape ("name (Shape)" before 6.0).
|
|
657
|
+
const shapeChannelId = findLegacyShapeChannel(objectMap, targetModelId, channelName);
|
|
658
|
+
if (shapeChannelId !== undefined) {
|
|
659
|
+
curveNodes.push({ type: "DeformPercent", targetModelId: shapeChannelId, curves, layerIndex: 0 });
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
// Light and camera properties live on the synthesized node attribute.
|
|
663
|
+
const attributeId = LEGACY_ATTRIBUTE_CHANNELS.has(channelName) ? getChildren(objectMap, targetModelId, "NodeAttribute")[0]?.id : undefined;
|
|
664
|
+
unsupportedCurveNodes.push({
|
|
665
|
+
type: channelName,
|
|
666
|
+
id: syntheticId--,
|
|
667
|
+
layerIndex: 0,
|
|
668
|
+
targetId: attributeId ?? targetModelId,
|
|
669
|
+
propertyName: channelName,
|
|
670
|
+
curveCount: curves.length,
|
|
671
|
+
curves,
|
|
672
|
+
defaultValues: {},
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (curveNodes.length === 0 && unsupportedCurveNodes.length === 0) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const layer = {
|
|
682
|
+
name: "BaseLayer",
|
|
683
|
+
weight: 100,
|
|
684
|
+
normalizedWeight: 1,
|
|
685
|
+
blendMode: 0,
|
|
686
|
+
blend: makeLayerBlend(0, 100, 0, 0),
|
|
687
|
+
curveNodes,
|
|
688
|
+
unsupportedCurveNodes,
|
|
689
|
+
diagnostics: [],
|
|
690
|
+
};
|
|
691
|
+
stacks.push(finalizeAnimStack(name, [layer], curveNodes, unsupportedCurveNodes, [], minTime, maxTime, timeSpan, rebaseKeyframes));
|
|
692
|
+
}
|
|
693
|
+
return stacks;
|
|
694
|
+
}
|
|
695
|
+
/** Take channel names that animate light or camera attribute properties rather than the model itself. */
|
|
696
|
+
const LEGACY_ATTRIBUTE_CHANNELS = new Set([
|
|
697
|
+
"Color",
|
|
698
|
+
"Intensity",
|
|
699
|
+
"OuterAngle",
|
|
700
|
+
"InnerAngle",
|
|
701
|
+
"ConeAngle",
|
|
702
|
+
"HotSpot",
|
|
703
|
+
"FieldOfView",
|
|
704
|
+
"FieldOfViewX",
|
|
705
|
+
"FieldOfViewY",
|
|
706
|
+
"FocalLength",
|
|
707
|
+
"NearPlane",
|
|
708
|
+
"FarPlane",
|
|
709
|
+
"OrthoZoom",
|
|
710
|
+
"Roll",
|
|
711
|
+
"FilmWidth",
|
|
712
|
+
"FilmHeight",
|
|
713
|
+
]);
|
|
714
|
+
/** Finds the synthesized BlendShapeChannel of a legacy model whose shape is named like the take channel. */
|
|
715
|
+
function findLegacyShapeChannel(objectMap, modelId, channelName) {
|
|
716
|
+
const suffix = " (Shape)";
|
|
717
|
+
const name = channelName.endsWith(suffix) ? channelName.slice(0, -suffix.length) : channelName;
|
|
718
|
+
for (const geometry of getChildren(objectMap, modelId, "Geometry")) {
|
|
719
|
+
for (const deformer of getChildren(objectMap, geometry.id, "Deformer")) {
|
|
720
|
+
if (getPropertyValue(deformer.node, 2) !== "BlendShape") {
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
for (const channel of getChildren(objectMap, deformer.id, "Deformer")) {
|
|
724
|
+
if (getPropertyValue(channel.node, 2) === "BlendShapeChannel" && cleanFBXName(getPropertyValue(channel.node, 1) ?? "") === name) {
|
|
725
|
+
return channel.id;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return undefined;
|
|
731
|
+
}
|
|
732
|
+
function readLegacyTimeSpan(node) {
|
|
733
|
+
if (!node) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
const start = fbxTimeToSeconds(node.properties[0]?.value);
|
|
737
|
+
const stop = fbxTimeToSeconds(node.properties[1]?.value);
|
|
738
|
+
return start !== null && stop !== null ? { start, stop } : null;
|
|
739
|
+
}
|
|
740
|
+
/** A channel either holds keys directly (scalar property) or has X/Y/Z child channels (vector property). */
|
|
741
|
+
function readLegacyCompoundChannel(node, version) {
|
|
742
|
+
const hasKeys = (n) => n.children.some((c) => c.name === "Key" || c.name === "Default");
|
|
743
|
+
if (hasKeys(node)) {
|
|
744
|
+
const keys = decodeLegacyKeys(node, version);
|
|
745
|
+
return keys.length > 0 ? [{ channel: "d|X", keys, ...extractCurveExtrapolation(node) }] : [];
|
|
746
|
+
}
|
|
747
|
+
const curves = [];
|
|
748
|
+
for (const child of node.children) {
|
|
749
|
+
if (child.name !== "Channel" || !hasKeys(child)) {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const axis = getPropertyValue(child, 0) ?? "X";
|
|
753
|
+
const keys = decodeLegacyKeys(child, version);
|
|
754
|
+
if (keys.length > 0) {
|
|
755
|
+
curves.push({ channel: `d|${axis}`, keys, ...extractCurveExtrapolation(child) });
|
|
756
|
+
}
|
|
757
|
+
if (curves.length === 3) {
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return curves;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Decodes the heterogeneous legacy `Key:` list (int64 times, double values and bare mode characters).
|
|
765
|
+
* The layout per key is `time, value, mode[, params...]`, mirroring the FBX SDK's pre-7000 key encoding.
|
|
766
|
+
*/
|
|
767
|
+
function decodeLegacyKeys(channel, version) {
|
|
768
|
+
const keyNode = findChildByName(channel, "Key");
|
|
769
|
+
if (!keyNode) {
|
|
770
|
+
return [];
|
|
771
|
+
}
|
|
772
|
+
const vals = keyNode.properties.map((p) => p.value);
|
|
773
|
+
const num = (v) => (typeof v === "number" ? v : typeof v === "boolean" ? (v ? 1 : 0) : NaN);
|
|
774
|
+
const chr = (v) => (typeof v === "string" ? v : typeof v === "number" ? String.fromCharCode(v) : typeof v === "boolean" ? (v ? "\x01" : "\0") : "?");
|
|
775
|
+
let keyVer = toNumber(findChildByName(channel, "KeyVer")?.properties[0]?.value) ?? 0;
|
|
776
|
+
if (keyVer <= 0) {
|
|
777
|
+
keyVer = version < 5000 ? 4003 : version < 6000 ? 4004 : 4005;
|
|
778
|
+
}
|
|
779
|
+
const keyCount = toNumber(findChildByName(channel, "KeyCount")?.properties[0]?.value) ?? 0;
|
|
780
|
+
const times = [];
|
|
781
|
+
const values = [];
|
|
782
|
+
const interp = [];
|
|
783
|
+
const constNext = [];
|
|
784
|
+
const leftSlope = [];
|
|
785
|
+
const rightSlope = [];
|
|
786
|
+
const leftWeight = [];
|
|
787
|
+
const rightWeight = [];
|
|
788
|
+
const autoFlag = [];
|
|
789
|
+
let pendingLeft = 0;
|
|
790
|
+
let pendingLeftWeight = 1 / 3;
|
|
791
|
+
let p = 0;
|
|
792
|
+
for (let i = 0; i < keyCount; i++) {
|
|
793
|
+
if (p + 3 > vals.length) {
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
const time = num(vals[p]) / FBX_TIME_UNIT;
|
|
797
|
+
const value = num(vals[p + 1]);
|
|
798
|
+
const mode = chr(vals[p + 2]);
|
|
799
|
+
p += 3;
|
|
800
|
+
let right = 0;
|
|
801
|
+
let nextLeft = 0;
|
|
802
|
+
let weightRight = 1 / 3;
|
|
803
|
+
let nextWeightLeft = 1 / 3;
|
|
804
|
+
let auto = false;
|
|
805
|
+
let kind = "cubic";
|
|
806
|
+
let isNext = false;
|
|
807
|
+
if (mode === "U") {
|
|
808
|
+
const slopeMode = chr(vals[p]);
|
|
809
|
+
p += 1;
|
|
810
|
+
let numWeights = 1;
|
|
811
|
+
if (slopeMode === "s" || slopeMode === "b") {
|
|
812
|
+
right = num(vals[p]);
|
|
813
|
+
nextLeft = num(vals[p + 1]);
|
|
814
|
+
p += 2;
|
|
815
|
+
if (keyVer === 4003) {
|
|
816
|
+
numWeights = 0;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
else if (slopeMode === "a") {
|
|
820
|
+
auto = true;
|
|
821
|
+
if (keyVer <= 4004) {
|
|
822
|
+
numWeights = 0;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else if (slopeMode === "p" || slopeMode === "q") {
|
|
826
|
+
auto = true;
|
|
827
|
+
p += 2;
|
|
828
|
+
numWeights = keyVer <= 4004 ? 1 : 2;
|
|
829
|
+
}
|
|
830
|
+
else if (slopeMode === "t") {
|
|
831
|
+
auto = true;
|
|
832
|
+
p += 3;
|
|
833
|
+
numWeights = 0;
|
|
834
|
+
}
|
|
835
|
+
else if (slopeMode === "d") {
|
|
836
|
+
auto = true;
|
|
837
|
+
p += 1;
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
for (; numWeights > 0; numWeights--) {
|
|
843
|
+
const weightMode = chr(vals[p]);
|
|
844
|
+
p += 1;
|
|
845
|
+
if (weightMode === "n" || weightMode === "c") {
|
|
846
|
+
// automatic weights
|
|
847
|
+
}
|
|
848
|
+
else if (weightMode === "a") {
|
|
849
|
+
weightRight = num(vals[p]);
|
|
850
|
+
nextWeightLeft = num(vals[p + 1]);
|
|
851
|
+
p += 2;
|
|
852
|
+
}
|
|
853
|
+
else if (weightMode === "l") {
|
|
854
|
+
nextWeightLeft = num(vals[p]);
|
|
855
|
+
p += 1;
|
|
856
|
+
}
|
|
857
|
+
else if (weightMode === "r") {
|
|
858
|
+
weightRight = num(vals[p]);
|
|
859
|
+
p += 1;
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
p = vals.length;
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else if (mode === "L") {
|
|
868
|
+
kind = "linear";
|
|
869
|
+
}
|
|
870
|
+
else if (mode === "C") {
|
|
871
|
+
kind = "constant";
|
|
872
|
+
if (keyVer >= 4004) {
|
|
873
|
+
isNext = chr(vals[p]) === "n";
|
|
874
|
+
p += 1;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
times.push(time);
|
|
881
|
+
values.push(value);
|
|
882
|
+
interp.push(kind);
|
|
883
|
+
constNext.push(isNext);
|
|
884
|
+
leftSlope.push(pendingLeft);
|
|
885
|
+
rightSlope.push(right);
|
|
886
|
+
leftWeight.push(pendingLeftWeight);
|
|
887
|
+
rightWeight.push(weightRight);
|
|
888
|
+
autoFlag.push(auto);
|
|
889
|
+
pendingLeft = nextLeft;
|
|
890
|
+
pendingLeftWeight = nextWeightLeft;
|
|
891
|
+
}
|
|
892
|
+
// Resolve automatic and linear tangents now that neighbouring keys are known.
|
|
893
|
+
for (let i = 0; i < times.length; i++) {
|
|
894
|
+
if (autoFlag[i]) {
|
|
895
|
+
const s = i > 0 && i + 1 < times.length
|
|
896
|
+
? solveLegacyAutoTangent(times[i - 1], times[i], times[i + 1], values[i - 1], values[i], values[i + 1], leftWeight[i], rightWeight[i])
|
|
897
|
+
: 0;
|
|
898
|
+
leftSlope[i] = s;
|
|
899
|
+
rightSlope[i] = s;
|
|
900
|
+
}
|
|
901
|
+
if (interp[i] === "linear" && i + 1 < times.length) {
|
|
902
|
+
const dt = times[i + 1] - times[i];
|
|
903
|
+
const s = dt > 0 ? (values[i + 1] - values[i]) / dt : 0;
|
|
904
|
+
rightSlope[i] = s;
|
|
905
|
+
if (!autoFlag[i + 1]) {
|
|
906
|
+
leftSlope[i + 1] = s;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return keyframesFromSlopes(times, values, interp, constNext, leftSlope, rightSlope, leftWeight, rightWeight);
|
|
911
|
+
}
|
|
912
|
+
/** Time-independent auto tangent with progressive clamping and 1/3 weights, as the SDK computes legacy 'a' keys. */
|
|
913
|
+
function solveLegacyAutoTangent(t0, t1, t2, v0, v1, v2, weightLeft, weightRight) {
|
|
914
|
+
if (!(t2 > t0)) {
|
|
915
|
+
return 0;
|
|
916
|
+
}
|
|
917
|
+
const slope = (v2 - v0) / (t2 - t0);
|
|
918
|
+
const sign = slope >= 0 ? 1 : -1;
|
|
919
|
+
let abs = sign * slope;
|
|
920
|
+
const rangeLeft = weightLeft * (t1 - t0);
|
|
921
|
+
const rangeRight = weightRight * (t2 - t1);
|
|
922
|
+
let maxLeft = rangeLeft > 0 ? (sign * (v1 - v0)) / rangeLeft : 0;
|
|
923
|
+
let maxRight = rangeRight > 0 ? (sign * (v2 - v1)) / rangeRight : 0;
|
|
924
|
+
if (!(maxLeft > 0)) {
|
|
925
|
+
maxLeft = 0;
|
|
926
|
+
}
|
|
927
|
+
if (!(maxRight > 0)) {
|
|
928
|
+
maxRight = 0;
|
|
929
|
+
}
|
|
930
|
+
abs = Math.min(abs, maxLeft, maxRight);
|
|
931
|
+
return sign * abs;
|
|
932
|
+
}
|
|
933
|
+
// ── Layered evaluation ─────────────────────────────────────────────────────────
|
|
934
|
+
/**
|
|
935
|
+
* Evaluates one transform channel (T, R or S) of a target at `time`, blending every animation layer of the stack the
|
|
936
|
+
* way the FBX SDK does: the first layer animating the channel replaces the static value, later layers are combined
|
|
937
|
+
* according to their blend mode, weight and accumulation modes.
|
|
938
|
+
* @param curveNodes - Curve nodes targeting this model (any layers, any types)
|
|
939
|
+
* @param layers - Stack layers, in order
|
|
940
|
+
* @param type - Channel to evaluate
|
|
941
|
+
* @param staticValue - Value when nothing animates the channel
|
|
942
|
+
* @param rotationOrder - Rotation order of the target (for rotation composition)
|
|
943
|
+
* @param time - Time in seconds
|
|
944
|
+
*/
|
|
945
|
+
export function evaluateLayeredChannel(curveNodes, layers, type, staticValue, rotationOrder, time) {
|
|
946
|
+
let result = [staticValue[0], staticValue[1], staticValue[2]];
|
|
947
|
+
for (let layerIndex = 0; layerIndex < Math.max(layers.length, 1); layerIndex++) {
|
|
948
|
+
const node = curveNodes.find((cn) => cn.type === type && cn.layerIndex === layerIndex);
|
|
949
|
+
if (!node) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const defaults = node.defaultValues ?? result;
|
|
953
|
+
const value = [
|
|
954
|
+
evaluateCurve(node.curves.find((c) => c.channel === "d|X"), time, defaults[0]),
|
|
955
|
+
evaluateCurve(node.curves.find((c) => c.channel === "d|Y"), time, defaults[1]),
|
|
956
|
+
evaluateCurve(node.curves.find((c) => c.channel === "d|Z"), time, defaults[2]),
|
|
957
|
+
];
|
|
958
|
+
const layer = layers[layerIndex];
|
|
959
|
+
// The base layer replaces the static value; every other layer blends onto the running result, even when the
|
|
960
|
+
// base layer does not animate this channel (then it blends with the static value), as in the SDK.
|
|
961
|
+
if (layerIndex === 0 || !layer) {
|
|
962
|
+
result = value;
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
let blend = layer.blend;
|
|
966
|
+
if (layer.weightCurve && blend.blended) {
|
|
967
|
+
let w = evaluateCurve(layer.weightCurve, time, layer.weight) / 100;
|
|
968
|
+
w = w < 0 ? 0 : w > 0.99999 ? 1 : w;
|
|
969
|
+
blend = { ...blend, weight: w };
|
|
970
|
+
}
|
|
971
|
+
result = combineLayerValue(result, value, blend, type, rotationOrder);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return result;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Evaluates an animated property through the animation layers: the base layer replaces the static value, every
|
|
978
|
+
* further layer blends onto the running result according to its blend mode and (possibly animated) weight, like
|
|
979
|
+
* `evaluateLayeredChannel` does for transforms.
|
|
980
|
+
* @param sources - Per-layer curves of the property
|
|
981
|
+
* @param layers - Stack layers, in order
|
|
982
|
+
* @param channels - Channel names to evaluate, in output order
|
|
983
|
+
* @param staticValue - Value per channel when nothing animates it
|
|
984
|
+
* @param time - Time in seconds
|
|
985
|
+
* @returns One value per channel
|
|
986
|
+
*/
|
|
987
|
+
export function evaluateLayeredProperty(sources, layers, channels, staticValue, time) {
|
|
988
|
+
const result = channels.map((_, i) => staticValue[i] ?? 0);
|
|
989
|
+
for (let layerIndex = 0; layerIndex < Math.max(layers.length, 1); layerIndex++) {
|
|
990
|
+
const source = sources.find((s) => s.layerIndex === layerIndex);
|
|
991
|
+
if (!source) {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
const value = channels.map((channel, i) => evaluateCurve(source.curves.find((c) => c.channel === channel), time, source.defaultValues?.[channel] ?? result[i]));
|
|
995
|
+
const layer = layers[layerIndex];
|
|
996
|
+
if (layerIndex === 0 || !layer) {
|
|
997
|
+
for (let i = 0; i < result.length; i++) {
|
|
998
|
+
result[i] = value[i];
|
|
999
|
+
}
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
let blend = layer.blend;
|
|
1003
|
+
if (layer.weightCurve && blend.blended) {
|
|
1004
|
+
let w = evaluateCurve(layer.weightCurve, time, layer.weight) / 100;
|
|
1005
|
+
w = w < 0 ? 0 : w > 0.99999 ? 1 : w;
|
|
1006
|
+
blend = { ...blend, weight: w };
|
|
1007
|
+
}
|
|
1008
|
+
for (let i = 0; i < result.length; i++) {
|
|
1009
|
+
result[i] = combineLayerValue([result[i], 0, 0], [value[i], 0, 0], blend, "other", 0)[0];
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* True when every curve of the given channel is inside a constant (stepped) segment at `time`, so a baked key at
|
|
1016
|
+
* that time should hold its value instead of interpolating towards the next sample.
|
|
1017
|
+
*/
|
|
1018
|
+
export function isChannelSteppedAt(curveNodes, type, time) {
|
|
1019
|
+
let sawCurve = false;
|
|
1020
|
+
for (const node of curveNodes) {
|
|
1021
|
+
if (node.type !== type) {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
for (const curve of node.curves) {
|
|
1025
|
+
const keys = curve.keys;
|
|
1026
|
+
if (keys.length < 2) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
sawCurve = true;
|
|
1030
|
+
// Outside the key range the curve extrapolates continuously; only interior constant segments step.
|
|
1031
|
+
if (time < keys[0].time || time >= keys[keys.length - 1].time) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
let i = 0;
|
|
1035
|
+
while (i + 1 < keys.length && keys[i + 1].time <= time) {
|
|
1036
|
+
i++;
|
|
1037
|
+
}
|
|
1038
|
+
if (keys[i].interpolation !== "constant") {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return sawCurve;
|
|
647
1044
|
}
|
|
648
1045
|
//# sourceMappingURL=animation.js.map
|