@aura3d/engine 1.0.3 → 1.0.5
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/README.md +309 -7
- package/dist/animation/AnimationClipEvents.d.ts +57 -0
- package/dist/animation/AnimationClipEvents.d.ts.map +1 -0
- package/dist/animation/AnimationClipEvents.js +171 -0
- package/dist/animation/AnimationClipEvents.js.map +1 -0
- package/dist/animation/AnimationClipRegistry.d.ts +76 -0
- package/dist/animation/AnimationClipRegistry.d.ts.map +1 -0
- package/dist/animation/AnimationClipRegistry.js +130 -0
- package/dist/animation/AnimationClipRegistry.js.map +1 -0
- package/dist/animation/AnimationController.d.ts +168 -0
- package/dist/animation/AnimationController.d.ts.map +1 -0
- package/dist/animation/AnimationController.js +619 -0
- package/dist/animation/AnimationController.js.map +1 -0
- package/dist/animation/HumanoidRetargeting.d.ts +76 -0
- package/dist/animation/HumanoidRetargeting.d.ts.map +1 -0
- package/dist/animation/HumanoidRetargeting.js +331 -0
- package/dist/animation/HumanoidRetargeting.js.map +1 -0
- package/dist/animation/browser-index.d.ts +18 -0
- package/dist/animation/browser-index.d.ts.map +1 -1
- package/dist/animation/browser-index.js +13 -0
- package/dist/animation/browser-index.js.map +1 -1
- package/dist/animation/index.d.ts +16 -1
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/animation/index.js +11 -1
- package/dist/animation/index.js.map +1 -1
- package/dist/animation/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -1
- package/dist/animation/threejs-compatibility/AnimationDiagnostics.js +3 -5
- package/dist/animation/threejs-compatibility/AnimationDiagnostics.js.map +1 -1
- package/dist/assets/GLTFAnimationRuntime.js +1 -1
- package/dist/assets/GLTFLoader.js +1 -1
- package/dist/aura3d-cli/cli.js +194 -8
- package/dist/aura3d-cli/cli.js.map +1 -1
- package/dist/aura3d-cli/index.d.ts +280 -3
- package/dist/aura3d-cli/index.d.ts.map +1 -1
- package/dist/aura3d-cli/index.js +886 -4
- package/dist/aura3d-cli/index.js.map +1 -1
- package/dist/aura3d-cli/pull-bridge.d.ts +95 -0
- package/dist/aura3d-cli/pull-bridge.d.ts.map +1 -0
- package/dist/aura3d-cli/pull-bridge.js +247 -0
- package/dist/aura3d-cli/pull-bridge.js.map +1 -0
- package/dist/create-aura3d/index.d.ts +1 -1
- package/dist/create-aura3d/index.d.ts.map +1 -1
- package/dist/create-aura3d/index.js +9 -2
- package/dist/create-aura3d/index.js.map +1 -1
- package/dist/editor-runtime/ProjectSerializer.d.ts +74 -1
- package/dist/editor-runtime/ProjectSerializer.d.ts.map +1 -1
- package/dist/editor-runtime/ProjectSerializer.js +123 -6
- package/dist/editor-runtime/ProjectSerializer.js.map +1 -1
- package/dist/editor-runtime/TimelineModel.d.ts +18 -0
- package/dist/editor-runtime/TimelineModel.d.ts.map +1 -1
- package/dist/editor-runtime/TimelineModel.js +67 -3
- package/dist/editor-runtime/TimelineModel.js.map +1 -1
- package/dist/editor-runtime/TimelineRuntimeBridge.d.ts +98 -0
- package/dist/editor-runtime/TimelineRuntimeBridge.d.ts.map +1 -0
- package/dist/editor-runtime/TimelineRuntimeBridge.js +186 -0
- package/dist/editor-runtime/TimelineRuntimeBridge.js.map +1 -0
- package/dist/editor-runtime/index.d.ts +3 -1
- package/dist/editor-runtime/index.d.ts.map +1 -1
- package/dist/editor-runtime/index.js +1 -0
- package/dist/editor-runtime/index.js.map +1 -1
- package/dist/engine/agent-api/AnimationController.d.ts +607 -0
- package/dist/engine/agent-api/AnimationController.d.ts.map +1 -0
- package/dist/engine/agent-api/AnimationController.js +2192 -0
- package/dist/engine/agent-api/AnimationController.js.map +1 -0
- package/dist/engine/agent-api/AssetEvidence.d.ts +88 -0
- package/dist/engine/agent-api/AssetEvidence.d.ts.map +1 -0
- package/dist/engine/agent-api/AssetEvidence.js +157 -0
- package/dist/engine/agent-api/AssetEvidence.js.map +1 -0
- package/dist/engine/agent-api/AuraAppHandle.d.ts +55 -0
- package/dist/engine/agent-api/AuraAppHandle.d.ts.map +1 -0
- package/dist/engine/agent-api/AuraAppHandle.js +15 -0
- package/dist/engine/agent-api/AuraAppHandle.js.map +1 -0
- package/dist/engine/agent-api/AuraVoiceBridge.d.ts +96 -0
- package/dist/engine/agent-api/AuraVoiceBridge.d.ts.map +1 -0
- package/dist/engine/agent-api/AuraVoiceBridge.js +370 -0
- package/dist/engine/agent-api/AuraVoiceBridge.js.map +1 -0
- package/dist/engine/agent-api/CartoonDirector.d.ts +95 -0
- package/dist/engine/agent-api/CartoonDirector.d.ts.map +1 -0
- package/dist/engine/agent-api/CartoonDirector.js +342 -0
- package/dist/engine/agent-api/CartoonDirector.js.map +1 -0
- package/dist/engine/agent-api/CartoonPerformance.d.ts +149 -0
- package/dist/engine/agent-api/CartoonPerformance.d.ts.map +1 -0
- package/dist/engine/agent-api/CartoonPerformance.js +317 -0
- package/dist/engine/agent-api/CartoonPerformance.js.map +1 -0
- package/dist/engine/agent-api/CartoonRenderQueue.d.ts +132 -0
- package/dist/engine/agent-api/CartoonRenderQueue.d.ts.map +1 -0
- package/dist/engine/agent-api/CartoonRenderQueue.js +385 -0
- package/dist/engine/agent-api/CartoonRenderQueue.js.map +1 -0
- package/dist/engine/agent-api/CharacterAssembly.d.ts +126 -0
- package/dist/engine/agent-api/CharacterAssembly.d.ts.map +1 -0
- package/dist/engine/agent-api/CharacterAssembly.js +280 -0
- package/dist/engine/agent-api/CharacterAssembly.js.map +1 -0
- package/dist/engine/agent-api/DialoguePerformance.d.ts +150 -0
- package/dist/engine/agent-api/DialoguePerformance.d.ts.map +1 -0
- package/dist/engine/agent-api/DialoguePerformance.js +335 -0
- package/dist/engine/agent-api/DialoguePerformance.js.map +1 -0
- package/dist/engine/agent-api/FrameLoop.d.ts +70 -0
- package/dist/engine/agent-api/FrameLoop.d.ts.map +1 -0
- package/dist/engine/agent-api/FrameLoop.js +165 -0
- package/dist/engine/agent-api/FrameLoop.js.map +1 -0
- package/dist/engine/agent-api/GameAssetValidation.d.ts +279 -0
- package/dist/engine/agent-api/GameAssetValidation.d.ts.map +1 -0
- package/dist/engine/agent-api/GameAssetValidation.js +719 -0
- package/dist/engine/agent-api/GameAssetValidation.js.map +1 -0
- package/dist/engine/agent-api/GameEvidence.d.ts +148 -0
- package/dist/engine/agent-api/GameEvidence.d.ts.map +1 -0
- package/dist/engine/agent-api/GameEvidence.js +269 -0
- package/dist/engine/agent-api/GameEvidence.js.map +1 -0
- package/dist/engine/agent-api/GameRuntime.d.ts +931 -0
- package/dist/engine/agent-api/GameRuntime.d.ts.map +1 -0
- package/dist/engine/agent-api/GameRuntime.js +2229 -0
- package/dist/engine/agent-api/GameRuntime.js.map +1 -0
- package/dist/engine/agent-api/GameSceneBridge.d.ts +54 -0
- package/dist/engine/agent-api/GameSceneBridge.d.ts.map +1 -0
- package/dist/engine/agent-api/GameSceneBridge.js +110 -0
- package/dist/engine/agent-api/GameSceneBridge.js.map +1 -0
- package/dist/engine/agent-api/PromptAnimationContract.d.ts +278 -0
- package/dist/engine/agent-api/PromptAnimationContract.d.ts.map +1 -0
- package/dist/engine/agent-api/PromptAnimationContract.js +238 -0
- package/dist/engine/agent-api/PromptAnimationContract.js.map +1 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.d.ts +183 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.d.ts.map +1 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.js +454 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.js.map +1 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.d.ts +100 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.d.ts.map +1 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.js +36 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.js.map +1 -0
- package/dist/engine/agent-api/ShotTimeline.d.ts +179 -0
- package/dist/engine/agent-api/ShotTimeline.d.ts.map +1 -0
- package/dist/engine/agent-api/ShotTimeline.js +264 -0
- package/dist/engine/agent-api/ShotTimeline.js.map +1 -0
- package/dist/engine/agent-api/VisemeController.d.ts +89 -0
- package/dist/engine/agent-api/VisemeController.d.ts.map +1 -0
- package/dist/engine/agent-api/VisemeController.js +207 -0
- package/dist/engine/agent-api/VisemeController.js.map +1 -0
- package/dist/engine/agent-api/game-kits/fighting.d.ts +123 -0
- package/dist/engine/agent-api/game-kits/fighting.d.ts.map +1 -0
- package/dist/engine/agent-api/game-kits/fighting.js +483 -0
- package/dist/engine/agent-api/game-kits/fighting.js.map +1 -0
- package/dist/engine/agent-api/game-kits/index.d.ts +15 -0
- package/dist/engine/agent-api/game-kits/index.d.ts.map +1 -0
- package/dist/engine/agent-api/game-kits/index.js +6 -0
- package/dist/engine/agent-api/game-kits/index.js.map +1 -0
- package/dist/engine/agent-api/humanoid-walk-runtime.d.ts +1 -0
- package/dist/engine/agent-api/humanoid-walk-runtime.d.ts.map +1 -1
- package/dist/engine/agent-api/index.d.ts +487 -1
- package/dist/engine/agent-api/index.d.ts.map +1 -1
- package/dist/engine/agent-api/index.js +740 -6
- package/dist/engine/agent-api/index.js.map +1 -1
- package/dist/engine/agent-api/product-viewer-runtime.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/physics/CollisionVolumes.d.ts +57 -0
- package/dist/physics/CollisionVolumes.d.ts.map +1 -0
- package/dist/physics/CollisionVolumes.js +159 -0
- package/dist/physics/CollisionVolumes.js.map +1 -0
- package/dist/physics/HitboxWorld.d.ts +229 -0
- package/dist/physics/HitboxWorld.d.ts.map +1 -0
- package/dist/physics/HitboxWorld.js +640 -0
- package/dist/physics/HitboxWorld.js.map +1 -0
- package/dist/physics/KinematicBody.d.ts +157 -0
- package/dist/physics/KinematicBody.d.ts.map +1 -0
- package/dist/physics/KinematicBody.js +405 -0
- package/dist/physics/KinematicBody.js.map +1 -0
- package/dist/physics/KinematicWorld.d.ts +58 -0
- package/dist/physics/KinematicWorld.d.ts.map +1 -0
- package/dist/physics/KinematicWorld.js +246 -0
- package/dist/physics/KinematicWorld.js.map +1 -0
- package/dist/physics/index.d.ts +4 -0
- package/dist/physics/index.d.ts.map +1 -1
- package/dist/physics/index.js +4 -0
- package/dist/physics/index.js.map +1 -1
- package/dist/rendering/ForwardPass.js +2 -2
- package/dist/rendering/ShaderLibrary.js +2 -2
- package/dist/rendering/SkinnedLitMaterial.js +3 -3
- package/dist/rendering/SkinnedUnlitMaterial.js +3 -3
- package/dist/scene/Renderable.js +2 -2
- package/dist/scripting/VisualGraph.d.ts +2 -1
- package/dist/scripting/VisualGraph.d.ts.map +1 -1
- package/dist/scripting/VisualGraph.js +118 -1
- package/dist/scripting/VisualGraph.js.map +1 -1
- package/dist/scripting/VisualGraphContext.d.ts +123 -0
- package/dist/scripting/VisualGraphContext.d.ts.map +1 -0
- package/dist/scripting/VisualGraphContext.js +2 -0
- package/dist/scripting/VisualGraphContext.js.map +1 -0
- package/dist/scripting/VisualGraphExecutor.d.ts +6 -1
- package/dist/scripting/VisualGraphExecutor.d.ts.map +1 -1
- package/dist/scripting/VisualGraphExecutor.js +364 -7
- package/dist/scripting/VisualGraphExecutor.js.map +1 -1
- package/dist/scripting/VisualNodeCatalog.d.ts +1 -1
- package/dist/scripting/VisualNodeCatalog.d.ts.map +1 -1
- package/dist/scripting/VisualNodeCatalog.js +61 -1
- package/dist/scripting/VisualNodeCatalog.js.map +1 -1
- package/dist/scripting/index.d.ts +1 -0
- package/dist/scripting/index.d.ts.map +1 -1
- package/dist/scripting/index.js.map +1 -1
- package/package.json +192 -118
|
@@ -0,0 +1,2192 @@
|
|
|
1
|
+
import { animationClipEventKey, sampleClipEvents } from "../../animation/index.js";
|
|
2
|
+
const defaultLayerName = "base";
|
|
3
|
+
export const auraAnimationRetargetDocumentedConstraints = [
|
|
4
|
+
{
|
|
5
|
+
code: "explicit-humanoid-bone-map",
|
|
6
|
+
severity: "error",
|
|
7
|
+
message: "Retargeting requires explicit humanoid bone-map metadata; Aura3D does not infer arbitrary external rigs."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
code: "matching-rest-pose",
|
|
11
|
+
severity: "warning",
|
|
12
|
+
message: "Retargeting metadata must document source and target rest-pose assumptions such as T-pose, A-pose, or bind pose."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
code: "uniform-scale",
|
|
16
|
+
severity: "warning",
|
|
17
|
+
message: "Retargeting supports source-level uniform scale metadata only; runtime proportion warping is not implied."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
code: "root-motion-policy",
|
|
21
|
+
severity: "warning",
|
|
22
|
+
message: "Retargeting metadata must state whether root motion is preserved or suppressed by gameplay."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
code: "no-runtime-ik",
|
|
26
|
+
severity: "info",
|
|
27
|
+
message: "Aura3D source retarget metadata does not claim runtime IK solving."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
code: "no-automatic-proportion-warp",
|
|
31
|
+
severity: "info",
|
|
32
|
+
message: "Aura3D source retarget metadata does not claim automatic humanoid proportion correction."
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
export class AnimationController {
|
|
36
|
+
id;
|
|
37
|
+
clips = new Map();
|
|
38
|
+
listeners = new Map();
|
|
39
|
+
states = new Map();
|
|
40
|
+
requiredClips;
|
|
41
|
+
requiredBones;
|
|
42
|
+
layerWeights = new Map();
|
|
43
|
+
layerMetadata = new Map();
|
|
44
|
+
runtimeNodeBindings = new Map();
|
|
45
|
+
defaultLayer;
|
|
46
|
+
embeddedGLB;
|
|
47
|
+
skeleton;
|
|
48
|
+
retarget;
|
|
49
|
+
externalHumanoidLibrary;
|
|
50
|
+
rootMotion;
|
|
51
|
+
poseBakedFallback;
|
|
52
|
+
poseBakedFallbackMetadata;
|
|
53
|
+
suppressRootMotion;
|
|
54
|
+
clockTime = 0;
|
|
55
|
+
nextRegistryIndex = 0;
|
|
56
|
+
nextPlaybackIndex = 0;
|
|
57
|
+
pendingCrossFades = [];
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.id = options.id;
|
|
60
|
+
this.defaultLayer = options.defaultLayer ?? defaultLayerName;
|
|
61
|
+
this.requiredClips = [...(options.requiredClips ?? [])];
|
|
62
|
+
this.requiredBones = [...(options.requiredBones ?? [])];
|
|
63
|
+
this.skeleton = options.skeleton;
|
|
64
|
+
this.retarget = normalizeRetargetBinding(options.retarget, options.externalHumanoidLibrary);
|
|
65
|
+
this.externalHumanoidLibrary = options.externalHumanoidLibrary;
|
|
66
|
+
this.rootMotion = options.rootMotion;
|
|
67
|
+
this.poseBakedFallback = clonePose(options.poseBakedFallback);
|
|
68
|
+
this.poseBakedFallbackMetadata = options.poseBakedFallback
|
|
69
|
+
? {
|
|
70
|
+
enabled: true,
|
|
71
|
+
source: "controller-options",
|
|
72
|
+
fallbackKind: "registry-pose"
|
|
73
|
+
}
|
|
74
|
+
: undefined;
|
|
75
|
+
this.suppressRootMotion = options.suppressRootMotion ?? options.rootMotion?.suppress ?? false;
|
|
76
|
+
this.layerWeights.set(this.defaultLayer, 1);
|
|
77
|
+
this.registerLayerMetadata(inferLayerMetadata(this.defaultLayer));
|
|
78
|
+
for (const layer of options.layers ?? []) {
|
|
79
|
+
this.registerLayerMetadata(layer);
|
|
80
|
+
}
|
|
81
|
+
if (options.clipRegistry) {
|
|
82
|
+
this.registerEmbeddedGLBClips(options.clipRegistry);
|
|
83
|
+
}
|
|
84
|
+
if (options.clips) {
|
|
85
|
+
this.registerClips(options.clips);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
on(type, listener) {
|
|
89
|
+
const listeners = this.listeners.get(type) ?? new Set();
|
|
90
|
+
listeners.add(listener);
|
|
91
|
+
this.listeners.set(type, listeners);
|
|
92
|
+
return () => {
|
|
93
|
+
const current = this.listeners.get(type);
|
|
94
|
+
if (!current)
|
|
95
|
+
return;
|
|
96
|
+
current.delete(listener);
|
|
97
|
+
if (current.size === 0)
|
|
98
|
+
this.listeners.delete(type);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
once(type, listener) {
|
|
102
|
+
const unsubscribe = this.on(type, (payload) => {
|
|
103
|
+
unsubscribe();
|
|
104
|
+
listener(payload);
|
|
105
|
+
});
|
|
106
|
+
return unsubscribe;
|
|
107
|
+
}
|
|
108
|
+
onEvent(filterOrListener, maybeListener) {
|
|
109
|
+
if (typeof filterOrListener === "function") {
|
|
110
|
+
return this.on("event", filterOrListener);
|
|
111
|
+
}
|
|
112
|
+
const filter = filterOrListener;
|
|
113
|
+
const listener = maybeListener;
|
|
114
|
+
if (!listener) {
|
|
115
|
+
throw new Error("onEvent(filter, listener) requires a listener.");
|
|
116
|
+
}
|
|
117
|
+
return this.on("event", (invocation) => {
|
|
118
|
+
const event = invocation.event;
|
|
119
|
+
if (event.name === filter || event.type === filter || event.tags?.includes(filter)) {
|
|
120
|
+
listener(invocation);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
registerClip(definition) {
|
|
125
|
+
const registered = this.createRegisteredClip(definition);
|
|
126
|
+
this.clips.set(registered.id, registered);
|
|
127
|
+
return registered;
|
|
128
|
+
}
|
|
129
|
+
registerClips(definitions) {
|
|
130
|
+
return definitions.map((definition) => this.registerClip(definition));
|
|
131
|
+
}
|
|
132
|
+
registerLayerMetadata(layer) {
|
|
133
|
+
const normalized = cloneLayerMetadata(layer);
|
|
134
|
+
this.layerMetadata.set(normalized.id, normalized);
|
|
135
|
+
if (!this.layerWeights.has(normalized.id)) {
|
|
136
|
+
this.layerWeights.set(normalized.id, 1);
|
|
137
|
+
}
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
getLayerMetadata(layer) {
|
|
141
|
+
return this.layerMetadata.get(layer);
|
|
142
|
+
}
|
|
143
|
+
listLayerMetadata() {
|
|
144
|
+
return [...this.layerMetadata.values()];
|
|
145
|
+
}
|
|
146
|
+
bindRuntimeNode(node, options = {}) {
|
|
147
|
+
const id = options.id ?? `${node.id}:animation`;
|
|
148
|
+
const binding = {
|
|
149
|
+
id,
|
|
150
|
+
node,
|
|
151
|
+
options: {
|
|
152
|
+
applyOnUpdate: true,
|
|
153
|
+
applyPose: true,
|
|
154
|
+
applyMorphTargets: true,
|
|
155
|
+
syncCaptureTime: true,
|
|
156
|
+
syncLoop: true,
|
|
157
|
+
syncSpeed: true,
|
|
158
|
+
...options,
|
|
159
|
+
id
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
this.runtimeNodeBindings.set(id, binding);
|
|
163
|
+
binding.snapshot = this.applyRuntimeNodeBinding(binding);
|
|
164
|
+
return {
|
|
165
|
+
id,
|
|
166
|
+
nodeId: node.id,
|
|
167
|
+
node,
|
|
168
|
+
update: () => this.applyRuntimeNodeBinding(binding),
|
|
169
|
+
snapshot: () => binding.snapshot ?? this.createRuntimeNodeBindingSnapshot(binding),
|
|
170
|
+
dispose: () => {
|
|
171
|
+
this.runtimeNodeBindings.delete(id);
|
|
172
|
+
if (typeof node.setAnimationBinding === "function") {
|
|
173
|
+
node.setAnimationBinding(undefined);
|
|
174
|
+
}
|
|
175
|
+
if (typeof node.setAnimationPose === "function") {
|
|
176
|
+
node.setAnimationPose(undefined);
|
|
177
|
+
}
|
|
178
|
+
if (typeof node.setMorphTargets === "function") {
|
|
179
|
+
node.setMorphTargets({});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
unbindRuntimeNode(idOrNode) {
|
|
185
|
+
const id = typeof idOrNode === "string" ? idOrNode : `${idOrNode.id}:animation`;
|
|
186
|
+
const binding = this.runtimeNodeBindings.get(id);
|
|
187
|
+
if (binding && typeof binding.node.setAnimationBinding === "function") {
|
|
188
|
+
binding.node.setAnimationBinding(undefined);
|
|
189
|
+
}
|
|
190
|
+
if (binding && typeof binding.node.setAnimationPose === "function") {
|
|
191
|
+
binding.node.setAnimationPose(undefined);
|
|
192
|
+
}
|
|
193
|
+
if (binding && typeof binding.node.setMorphTargets === "function") {
|
|
194
|
+
binding.node.setMorphTargets({});
|
|
195
|
+
}
|
|
196
|
+
this.runtimeNodeBindings.delete(id);
|
|
197
|
+
}
|
|
198
|
+
runtimeNodeBindingSnapshots() {
|
|
199
|
+
return [...this.runtimeNodeBindings.values()].map((binding) => binding.snapshot ?? this.createRuntimeNodeBindingSnapshot(binding));
|
|
200
|
+
}
|
|
201
|
+
registerEmbeddedGLBClips(source) {
|
|
202
|
+
const registry = createEmbeddedGLBAnimationClipRegistryMetadata(source);
|
|
203
|
+
this.embeddedGLB = registry;
|
|
204
|
+
this.skeleton = registry.skeleton ?? this.skeleton;
|
|
205
|
+
for (const layer of registry.layers ?? []) {
|
|
206
|
+
this.registerLayerMetadata(layer);
|
|
207
|
+
}
|
|
208
|
+
this.externalHumanoidLibrary = registry.externalHumanoidLibrary ?? this.externalHumanoidLibrary;
|
|
209
|
+
this.retarget = normalizeRetargetBinding(registry.retarget ?? this.retarget, registry.externalHumanoidLibrary ?? this.externalHumanoidLibrary);
|
|
210
|
+
this.rootMotion = registry.rootMotion ?? this.rootMotion;
|
|
211
|
+
this.suppressRootMotion = registry.suppressRootMotion ?? registry.rootMotion?.suppress ?? this.suppressRootMotion;
|
|
212
|
+
this.poseBakedFallback = clonePose(extractPoseBakedFallback(registry.poseBakedFallback)) ?? this.poseBakedFallback ?? createIdentityPose(this.skeleton);
|
|
213
|
+
this.poseBakedFallbackMetadata = extractPoseBakedFallbackRuntimeMetadata(registry.poseBakedFallback, {
|
|
214
|
+
enabled: Boolean(this.poseBakedFallback),
|
|
215
|
+
source: "embedded-glb-registry",
|
|
216
|
+
sourceAssetId: registry.assetId,
|
|
217
|
+
sourceAssetName: registry.assetName,
|
|
218
|
+
fallbackKind: "registry-pose"
|
|
219
|
+
}) ?? this.poseBakedFallbackMetadata;
|
|
220
|
+
const clips = registry.clips ?? registry.animations ?? [];
|
|
221
|
+
return clips.map((clipInput) => {
|
|
222
|
+
const definition = embeddedClipToDefinition(clipInput, registry, this.defaultLayer);
|
|
223
|
+
return this.registerClip(definition);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
play(clipId, options = {}) {
|
|
227
|
+
const resolvedClipId = this.clips.has(clipId) ? clipId : options.fallbackClipId;
|
|
228
|
+
if (!resolvedClipId || !this.clips.has(resolvedClipId)) {
|
|
229
|
+
const diagnostic = createDiagnostic("error", "ANIMATION_CLIP_MISSING", `Animation clip "${clipId}" is not registered.`, clipId);
|
|
230
|
+
this.emit("diagnostic", diagnostic);
|
|
231
|
+
throw new Error(diagnostic.message);
|
|
232
|
+
}
|
|
233
|
+
const clip = this.requireClip(resolvedClipId);
|
|
234
|
+
const restartFromFrameZero = shouldRestartFromFrameZero(clip, options, this.layerMetadata);
|
|
235
|
+
const shouldRestart = options.restart === true || restartFromFrameZero;
|
|
236
|
+
const playOptions = restartFromFrameZero
|
|
237
|
+
? {
|
|
238
|
+
...options,
|
|
239
|
+
restart: true,
|
|
240
|
+
startTime: 0,
|
|
241
|
+
metadata: {
|
|
242
|
+
...options.metadata,
|
|
243
|
+
restartFromFrameZero: true
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
: options;
|
|
247
|
+
const existing = this.findStateByClip(resolvedClipId);
|
|
248
|
+
if (existing && !shouldRestart) {
|
|
249
|
+
existing.status = options.paused ? "paused" : "playing";
|
|
250
|
+
existing.speed = sanitizeSpeed(options.speed ?? existing.speed);
|
|
251
|
+
existing.targetWeight = sanitizeWeight(options.weight ?? existing.targetWeight);
|
|
252
|
+
existing.weight = existing.fade ? existing.weight : existing.targetWeight;
|
|
253
|
+
existing.layer = options.layer ?? existing.layer;
|
|
254
|
+
existing.layerMetadata = this.layerMetadata.get(existing.layer) ?? inferLayerMetadata(existing.layer);
|
|
255
|
+
existing.layerWeight = this.layerWeight(existing.layer);
|
|
256
|
+
existing.inputDirection = options.direction ?? existing.inputDirection;
|
|
257
|
+
existing.rootMotionSuppressed = shouldSuppressRootMotion(options, existing.clip, this.suppressRootMotion);
|
|
258
|
+
existing.eventSource = options.eventSource ?? existing.clip.eventSource;
|
|
259
|
+
existing.metadata = options.metadata ?? existing.metadata;
|
|
260
|
+
this.applyRuntimeNodeBindings();
|
|
261
|
+
this.emitStateChanged();
|
|
262
|
+
return cloneState(existing);
|
|
263
|
+
}
|
|
264
|
+
if (existing && shouldRestart) {
|
|
265
|
+
this.states.delete(existing.id);
|
|
266
|
+
this.endState(existing, "stopped");
|
|
267
|
+
}
|
|
268
|
+
if (options.exclusive ?? true) {
|
|
269
|
+
for (const state of this.getInternalStates()) {
|
|
270
|
+
this.endState(state, "stopped");
|
|
271
|
+
}
|
|
272
|
+
this.states.clear();
|
|
273
|
+
}
|
|
274
|
+
const playback = this.createPlaybackState(clip, playOptions);
|
|
275
|
+
this.states.set(playback.id, playback);
|
|
276
|
+
this.emit("start", cloneState(playback));
|
|
277
|
+
this.applyRuntimeNodeBindings();
|
|
278
|
+
this.emitStateChanged();
|
|
279
|
+
return cloneState(playback);
|
|
280
|
+
}
|
|
281
|
+
pause(clipId) {
|
|
282
|
+
for (const state of this.selectStates(clipId)) {
|
|
283
|
+
if (state.status === "playing")
|
|
284
|
+
state.status = "paused";
|
|
285
|
+
}
|
|
286
|
+
this.applyRuntimeNodeBindings();
|
|
287
|
+
this.emitStateChanged();
|
|
288
|
+
}
|
|
289
|
+
resume(clipId) {
|
|
290
|
+
for (const state of this.selectStates(clipId)) {
|
|
291
|
+
if (state.status === "paused")
|
|
292
|
+
state.status = "playing";
|
|
293
|
+
}
|
|
294
|
+
this.applyRuntimeNodeBindings();
|
|
295
|
+
this.emitStateChanged();
|
|
296
|
+
}
|
|
297
|
+
restart(clipId, options = {}) {
|
|
298
|
+
const resolvedClipId = clipId ?? this.activeClipId();
|
|
299
|
+
if (!resolvedClipId) {
|
|
300
|
+
throw new Error("Cannot restart animation without an active clip or clipId.");
|
|
301
|
+
}
|
|
302
|
+
return this.play(resolvedClipId, {
|
|
303
|
+
...options,
|
|
304
|
+
restart: true
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
stop(clipId, options = {}) {
|
|
308
|
+
const targets = this.selectStates(clipId);
|
|
309
|
+
const fadeOut = sanitizeDuration(options.fadeOut ?? 0);
|
|
310
|
+
for (const state of targets) {
|
|
311
|
+
if (fadeOut > 0) {
|
|
312
|
+
state.fade = {
|
|
313
|
+
kind: "out",
|
|
314
|
+
elapsed: 0,
|
|
315
|
+
duration: fadeOut,
|
|
316
|
+
fromWeight: state.weight,
|
|
317
|
+
toWeight: 0
|
|
318
|
+
};
|
|
319
|
+
state.targetWeight = 0;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
this.states.delete(state.id);
|
|
323
|
+
this.endState(state, "stopped");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
this.applyRuntimeNodeBindings();
|
|
327
|
+
this.emitStateChanged();
|
|
328
|
+
}
|
|
329
|
+
crossFade(toClipId, duration, options = {}) {
|
|
330
|
+
const fadeDuration = sanitizeDuration(duration);
|
|
331
|
+
const fromStates = this.selectCrossFadeSources(options);
|
|
332
|
+
const fromClipIds = fromStates.map((state) => state.clipId);
|
|
333
|
+
const layer = options.layer ?? options.fromLayer ?? fromStates[0]?.layer;
|
|
334
|
+
const event = {
|
|
335
|
+
fromClipIds,
|
|
336
|
+
toClipId,
|
|
337
|
+
duration: fadeDuration,
|
|
338
|
+
layer
|
|
339
|
+
};
|
|
340
|
+
this.emit("crossFadeStart", event);
|
|
341
|
+
this.emit("crossfadeStart", event);
|
|
342
|
+
this.pendingCrossFades.push(event);
|
|
343
|
+
for (const state of fromStates) {
|
|
344
|
+
state.fade = {
|
|
345
|
+
kind: "out",
|
|
346
|
+
elapsed: 0,
|
|
347
|
+
duration: fadeDuration,
|
|
348
|
+
fromWeight: state.weight,
|
|
349
|
+
toWeight: 0
|
|
350
|
+
};
|
|
351
|
+
state.targetWeight = 0;
|
|
352
|
+
}
|
|
353
|
+
const next = this.play(toClipId, {
|
|
354
|
+
...options,
|
|
355
|
+
exclusive: false,
|
|
356
|
+
restart: true,
|
|
357
|
+
fadeIn: fadeDuration,
|
|
358
|
+
weight: options.weight ?? 1,
|
|
359
|
+
layer: options.layer ?? layer
|
|
360
|
+
});
|
|
361
|
+
if (fadeDuration === 0) {
|
|
362
|
+
this.finishCrossFade(event);
|
|
363
|
+
}
|
|
364
|
+
return next;
|
|
365
|
+
}
|
|
366
|
+
crossfade(toClipId, duration, options = {}) {
|
|
367
|
+
return this.crossFade(toClipId, duration, options);
|
|
368
|
+
}
|
|
369
|
+
setLayerWeight(layer, weight) {
|
|
370
|
+
const cleanLayer = layer.trim() || this.defaultLayer;
|
|
371
|
+
const cleanWeight = sanitizeWeight(weight);
|
|
372
|
+
this.layerWeights.set(cleanLayer, cleanWeight);
|
|
373
|
+
for (const state of this.getInternalStates()) {
|
|
374
|
+
if (state.layer === cleanLayer) {
|
|
375
|
+
state.layerWeight = cleanWeight;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
this.applyRuntimeNodeBindings();
|
|
379
|
+
this.emitStateChanged();
|
|
380
|
+
return this;
|
|
381
|
+
}
|
|
382
|
+
setWeight(weight, layer = this.defaultLayer) {
|
|
383
|
+
return this.setLayerWeight(layer, weight);
|
|
384
|
+
}
|
|
385
|
+
update(dt) {
|
|
386
|
+
if (!Number.isFinite(dt) || dt === 0) {
|
|
387
|
+
return this.snapshot();
|
|
388
|
+
}
|
|
389
|
+
this.clockTime += dt;
|
|
390
|
+
const endedStates = [];
|
|
391
|
+
for (const state of this.getInternalStates()) {
|
|
392
|
+
this.updateFade(state, Math.abs(dt));
|
|
393
|
+
if (state.status !== "playing") {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const advance = advanceState(state, dt);
|
|
397
|
+
this.emitSampledEvents(state);
|
|
398
|
+
if (advance.loopsPassed > 0) {
|
|
399
|
+
this.emit("loop", {
|
|
400
|
+
clipId: state.clipId,
|
|
401
|
+
playbackId: state.id,
|
|
402
|
+
loopCount: state.loopCount,
|
|
403
|
+
loopsPassed: advance.loopsPassed
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (advance.completed) {
|
|
407
|
+
state.status = "completed";
|
|
408
|
+
state.completed = true;
|
|
409
|
+
endedStates.push(state);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const state of endedStates) {
|
|
413
|
+
this.endState(state, "completed");
|
|
414
|
+
}
|
|
415
|
+
this.removeFinishedFadeOuts();
|
|
416
|
+
this.finishCrossFadesIfReady();
|
|
417
|
+
this.applyRuntimeNodeBindings();
|
|
418
|
+
this.emitStateChanged();
|
|
419
|
+
return this.snapshot();
|
|
420
|
+
}
|
|
421
|
+
scrub(clipOrTime, timeOrOptions = {}, maybeOptions = {}) {
|
|
422
|
+
const hasClipId = typeof clipOrTime === "string";
|
|
423
|
+
const time = hasClipId ? Number(timeOrOptions) : clipOrTime;
|
|
424
|
+
const options = hasClipId ? maybeOptions : timeOrOptions;
|
|
425
|
+
const clipId = hasClipId ? clipOrTime : options.clipId ?? this.activeClipId();
|
|
426
|
+
if (!clipId) {
|
|
427
|
+
throw new Error("Cannot scrub without an active clip or clipId.");
|
|
428
|
+
}
|
|
429
|
+
let state = this.findStateByClip(clipId);
|
|
430
|
+
if (!state) {
|
|
431
|
+
if (options.createIfMissing === false) {
|
|
432
|
+
throw new Error(`Cannot scrub inactive animation clip "${clipId}".`);
|
|
433
|
+
}
|
|
434
|
+
this.play(clipId, {
|
|
435
|
+
startTime: time,
|
|
436
|
+
paused: true,
|
|
437
|
+
restart: true,
|
|
438
|
+
layer: options.layer,
|
|
439
|
+
suppressRootMotion: options.suppressRootMotion
|
|
440
|
+
});
|
|
441
|
+
state = this.findStateByClip(clipId);
|
|
442
|
+
}
|
|
443
|
+
if (!state) {
|
|
444
|
+
throw new Error(`Cannot scrub animation clip "${clipId}".`);
|
|
445
|
+
}
|
|
446
|
+
const previousTime = state.localTime;
|
|
447
|
+
state.previousLocalTime = previousTime;
|
|
448
|
+
state.localTime = normalizeStateTime(time, state.duration, state.loopMode);
|
|
449
|
+
state.playhead = state.localTime;
|
|
450
|
+
state.normalizedTime = state.duration > 0 ? state.localTime / state.duration : 0;
|
|
451
|
+
state.completed = false;
|
|
452
|
+
state.status = options.play ? "playing" : "paused";
|
|
453
|
+
if (options.emitEvents) {
|
|
454
|
+
this.emitSampledEvents(state);
|
|
455
|
+
}
|
|
456
|
+
this.emit("scrub", {
|
|
457
|
+
clipId,
|
|
458
|
+
playbackId: state.id,
|
|
459
|
+
fromTime: previousTime,
|
|
460
|
+
toTime: state.localTime
|
|
461
|
+
});
|
|
462
|
+
this.applyRuntimeNodeBindings();
|
|
463
|
+
this.emitStateChanged();
|
|
464
|
+
return this.capturePose({
|
|
465
|
+
clipId,
|
|
466
|
+
emitEvent: false,
|
|
467
|
+
suppressRootMotion: options.suppressRootMotion
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
capturePose(options = {}) {
|
|
471
|
+
const states = options.clipId
|
|
472
|
+
? this.selectStates(options.clipId)
|
|
473
|
+
: this.getInternalStates().filter((state) => effectiveWeight(state) > 0);
|
|
474
|
+
const pose = options.time !== undefined && options.clipId
|
|
475
|
+
? this.sampleSinglePose(this.requireClip(options.clipId), options.time, undefined, options.suppressRootMotion)
|
|
476
|
+
: this.blendStates(states, options.suppressRootMotion);
|
|
477
|
+
const diagnostics = this.diagnostics();
|
|
478
|
+
const snapshot = {
|
|
479
|
+
time: this.clockTime,
|
|
480
|
+
pose,
|
|
481
|
+
clips: states.map(cloneState),
|
|
482
|
+
diagnostics
|
|
483
|
+
};
|
|
484
|
+
if (options.emitEvent ?? true) {
|
|
485
|
+
this.emit("poseCaptured", snapshot);
|
|
486
|
+
}
|
|
487
|
+
return snapshot;
|
|
488
|
+
}
|
|
489
|
+
state(clipId) {
|
|
490
|
+
const state = clipId ? this.findStateByClip(clipId) : this.primaryState();
|
|
491
|
+
return state ? cloneState(state) : undefined;
|
|
492
|
+
}
|
|
493
|
+
snapshot() {
|
|
494
|
+
return {
|
|
495
|
+
time: this.clockTime,
|
|
496
|
+
id: this.id,
|
|
497
|
+
activeClipId: this.activeClipId(),
|
|
498
|
+
clips: this.getInternalStates().map(cloneState),
|
|
499
|
+
layers: Object.fromEntries(this.layerWeights.entries()),
|
|
500
|
+
layerMetadata: Object.fromEntries([...this.layerMetadata.entries()].map(([id, metadata]) => [id, cloneLayerMetadata(metadata)])),
|
|
501
|
+
diagnostics: this.diagnostics(),
|
|
502
|
+
rootMotionSuppressed: this.suppressRootMotion,
|
|
503
|
+
runtimeNodeBindings: this.runtimeNodeBindingSnapshots(),
|
|
504
|
+
retarget: this.retargetSnapshot(),
|
|
505
|
+
embeddedGLB: this.embeddedGLB
|
|
506
|
+
? {
|
|
507
|
+
assetId: this.embeddedGLB.assetId,
|
|
508
|
+
assetName: this.embeddedGLB.assetName,
|
|
509
|
+
clipCount: (this.embeddedGLB.clips ?? this.embeddedGLB.animations ?? []).length,
|
|
510
|
+
skeletonBones: skeletonBoneNames(this.skeleton).size,
|
|
511
|
+
poseBakedFallback: Boolean(this.poseBakedFallback)
|
|
512
|
+
}
|
|
513
|
+
: undefined
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
diagnostics(options = {}) {
|
|
517
|
+
const diagnostics = [];
|
|
518
|
+
const clips = this.listClips();
|
|
519
|
+
const requiredClips = [...this.requiredClips, ...(options.requiredClips ?? [])];
|
|
520
|
+
const requiredBones = [...this.requiredBones, ...(options.requiredBones ?? [])];
|
|
521
|
+
const skeletonBones = skeletonBoneNames(this.skeleton);
|
|
522
|
+
const hasSkeleton = skeletonBones.size > 0;
|
|
523
|
+
const requiresSkeleton = options.requireSkeleton === true || requiredBones.length > 0 || clips.some((clip) => clip.requiredBones.length > 0);
|
|
524
|
+
if (clips.length === 0) {
|
|
525
|
+
diagnostics.push(createDiagnostic("error", "ANIMATION_CLIPS_MISSING", "No animation clips are registered."));
|
|
526
|
+
}
|
|
527
|
+
if (requiresSkeleton && !hasSkeleton) {
|
|
528
|
+
diagnostics.push(createDiagnostic("error", "ANIMATION_SKELETON_MISSING", "Animation controller is missing skeleton metadata."));
|
|
529
|
+
}
|
|
530
|
+
for (const requiredClip of requiredClips) {
|
|
531
|
+
if (!this.clips.has(requiredClip)) {
|
|
532
|
+
diagnostics.push(createDiagnostic("error", "ANIMATION_REQUIRED_CLIP_MISSING", `Required animation clip "${requiredClip}" is missing.`, requiredClip));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
for (const requiredBone of requiredBones) {
|
|
536
|
+
if (hasSkeleton && !skeletonBones.has(requiredBone)) {
|
|
537
|
+
diagnostics.push(createDiagnostic("error", "ANIMATION_REQUIRED_BONE_MISSING", `Required bone "${requiredBone}" is missing.`, undefined, requiredBone));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
diagnostics.push(...this.retargetDiagnostics(options));
|
|
541
|
+
for (const clip of clips) {
|
|
542
|
+
if (clip.tracks.length === 0) {
|
|
543
|
+
diagnostics.push(createDiagnostic(clip.sample ? "info" : "warning", "ANIMATION_CLIP_EMPTY_TRACKS", `Animation clip "${clip.id}" has no tracks${clip.sample ? " and will use a sampler or pose-baked fallback." : "."}`, clip.id));
|
|
544
|
+
}
|
|
545
|
+
for (const requiredBone of clip.requiredBones) {
|
|
546
|
+
if (hasSkeleton && !skeletonBones.has(requiredBone)) {
|
|
547
|
+
diagnostics.push(createDiagnostic("error", "ANIMATION_BONE_MISSING", `Animation clip "${clip.id}" requires missing bone "${requiredBone}".`, clip.id, requiredBone));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
for (const track of clip.tracks) {
|
|
551
|
+
if (!track.keyframes || track.keyframes.length === 0) {
|
|
552
|
+
diagnostics.push(createDiagnostic("warning", "ANIMATION_TRACK_EMPTY", `Animation clip "${clip.id}" has an empty track "${track.id ?? track.target}".`, clip.id, undefined, track.id ?? track.target));
|
|
553
|
+
}
|
|
554
|
+
const bone = boneNameFromTrackTarget(track.target);
|
|
555
|
+
if (bone && hasSkeleton && !skeletonBones.has(bone)) {
|
|
556
|
+
diagnostics.push(createDiagnostic("error", "ANIMATION_BONE_MISSING", `Animation clip "${clip.id}" targets missing bone "${bone}".`, clip.id, bone, track.id ?? track.target));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
for (const binding of this.runtimeNodeBindings.values()) {
|
|
561
|
+
if (typeof binding.node.play !== "function" && typeof binding.node.setAnimation !== "function") {
|
|
562
|
+
diagnostics.push(createDiagnostic("warning", "ANIMATION_RUNTIME_NODE_BINDING_UNSUPPORTED", `Runtime node "${binding.node.id}" is bound to an AnimationController but does not expose play() or setAnimation().`));
|
|
563
|
+
}
|
|
564
|
+
if (binding.options.applyPose !== false && typeof binding.node.setAnimationPose !== "function") {
|
|
565
|
+
diagnostics.push(createDiagnostic("warning", "ANIMATION_RUNTIME_NODE_POSE_BINDING_UNSUPPORTED", `Runtime node "${binding.node.id}" is bound to an AnimationController but does not expose setAnimationPose().`));
|
|
566
|
+
}
|
|
567
|
+
if (binding.options.applyMorphTargets !== false && typeof binding.node.setMorphTargets !== "function") {
|
|
568
|
+
diagnostics.push(createDiagnostic("warning", "ANIMATION_RUNTIME_NODE_MORPH_BINDING_UNSUPPORTED", `Runtime node "${binding.node.id}" is bound to an AnimationController but does not expose setMorphTargets().`));
|
|
569
|
+
}
|
|
570
|
+
if (binding.options.layer && !this.layerWeights.has(binding.options.layer)) {
|
|
571
|
+
diagnostics.push(createDiagnostic("warning", "ANIMATION_RUNTIME_NODE_BINDING_LAYER_MISSING", `Runtime node binding "${binding.id}" targets unknown animation layer "${binding.options.layer}".`));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return diagnostics;
|
|
575
|
+
}
|
|
576
|
+
retargetDiagnostics(options = {}) {
|
|
577
|
+
return this.createRetargetDiagnostics(options);
|
|
578
|
+
}
|
|
579
|
+
diagnose(options) {
|
|
580
|
+
return this.diagnostics(options);
|
|
581
|
+
}
|
|
582
|
+
clipIds() {
|
|
583
|
+
return this.listClips().map((clip) => clip.id);
|
|
584
|
+
}
|
|
585
|
+
listClips() {
|
|
586
|
+
return [...this.clips.values()].sort((a, b) => a.registryIndex - b.registryIndex);
|
|
587
|
+
}
|
|
588
|
+
getClip(clipId) {
|
|
589
|
+
return this.clips.get(clipId);
|
|
590
|
+
}
|
|
591
|
+
requireClip(clipId) {
|
|
592
|
+
const clip = this.getClip(clipId);
|
|
593
|
+
if (!clip) {
|
|
594
|
+
throw new Error(`Animation clip "${clipId}" is not registered.`);
|
|
595
|
+
}
|
|
596
|
+
return clip;
|
|
597
|
+
}
|
|
598
|
+
embeddedGLBClipRegistryMetadata() {
|
|
599
|
+
return this.embeddedGLB;
|
|
600
|
+
}
|
|
601
|
+
dispose() {
|
|
602
|
+
this.states.clear();
|
|
603
|
+
this.listeners.clear();
|
|
604
|
+
for (const binding of this.runtimeNodeBindings.values()) {
|
|
605
|
+
if (typeof binding.node.setAnimationBinding === "function") {
|
|
606
|
+
binding.node.setAnimationBinding(undefined);
|
|
607
|
+
}
|
|
608
|
+
if (typeof binding.node.setAnimationPose === "function") {
|
|
609
|
+
binding.node.setAnimationPose(undefined);
|
|
610
|
+
}
|
|
611
|
+
if (typeof binding.node.setMorphTargets === "function") {
|
|
612
|
+
binding.node.setMorphTargets({});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
this.runtimeNodeBindings.clear();
|
|
616
|
+
this.pendingCrossFades = [];
|
|
617
|
+
}
|
|
618
|
+
createRegisteredClip(definition) {
|
|
619
|
+
if (!definition.id) {
|
|
620
|
+
throw new Error("Animation clips must have a stable id.");
|
|
621
|
+
}
|
|
622
|
+
if (!Number.isFinite(definition.duration) || definition.duration < 0) {
|
|
623
|
+
throw new Error(`Animation clip "${definition.id}" has an invalid duration.`);
|
|
624
|
+
}
|
|
625
|
+
const fallbackPose = clonePose(definition.pose) ??
|
|
626
|
+
clonePose(definition.fallbackPose) ??
|
|
627
|
+
clonePose(extractPoseBakedFallback(definition.metadata?.poseBakedFallback)) ??
|
|
628
|
+
clonePose(this.poseBakedFallback) ??
|
|
629
|
+
createIdentityPose(this.skeleton);
|
|
630
|
+
const tracks = [...(definition.tracks ?? [])];
|
|
631
|
+
const layer = definition.layer ?? definition.metadata?.layer?.id ?? this.defaultLayer;
|
|
632
|
+
const layerMetadata = cloneLayerMetadata(definition.layerMetadata ?? definition.metadata?.layer ?? this.layerMetadata.get(layer) ?? inferLayerMetadata(layer));
|
|
633
|
+
this.registerLayerMetadata(layerMetadata);
|
|
634
|
+
const externalHumanoidLibrary = definition.externalHumanoidLibrary ?? definition.metadata?.externalHumanoidLibrary ?? this.externalHumanoidLibrary;
|
|
635
|
+
const retarget = normalizeRetargetBinding(definition.retarget ?? definition.metadata?.retarget ?? this.retarget, externalHumanoidLibrary);
|
|
636
|
+
const eventSource = definition.eventSource ?? definition.metadata?.eventSource ?? (retarget ? "retargeted-source-clip" : "source-clip");
|
|
637
|
+
const restartFromFrameZero = Boolean(definition.restartFromFrameZero ??
|
|
638
|
+
definition.metadata?.restartFromFrameZero ??
|
|
639
|
+
definition.attack ??
|
|
640
|
+
definition.metadata?.attack ??
|
|
641
|
+
layerMetadata.restartFromFrameZero ??
|
|
642
|
+
definition.tags?.includes("attack"));
|
|
643
|
+
const poseBakedFallbackMetadata = createPoseBakedFallbackRuntimeMetadata(definition.id, definition, this.embeddedGLB, this.poseBakedFallbackMetadata, Boolean(fallbackPose) && !definition.sample);
|
|
644
|
+
const sample = definition.sample ?? createFallbackSampler(definition.id, tracks, fallbackPose, poseBakedFallbackMetadata);
|
|
645
|
+
const metadata = {
|
|
646
|
+
...definition.metadata,
|
|
647
|
+
layer: layerMetadata,
|
|
648
|
+
eventSource,
|
|
649
|
+
restartFromFrameZero,
|
|
650
|
+
attack: definition.attack ?? definition.metadata?.attack,
|
|
651
|
+
retarget,
|
|
652
|
+
externalHumanoidLibrary,
|
|
653
|
+
rootMotion: definition.rootMotion ?? definition.metadata?.rootMotion,
|
|
654
|
+
suppressRootMotion: definition.suppressRootMotion ?? definition.metadata?.suppressRootMotion,
|
|
655
|
+
poseBakedFallback: Boolean(fallbackPose) && !definition.sample,
|
|
656
|
+
poseBakedFallbackMetadata
|
|
657
|
+
};
|
|
658
|
+
const registered = {
|
|
659
|
+
...definition,
|
|
660
|
+
metadata,
|
|
661
|
+
duration: sanitizeDuration(definition.duration),
|
|
662
|
+
loop: definition.loop ?? true,
|
|
663
|
+
tags: [...(definition.tags ?? [])],
|
|
664
|
+
tracks,
|
|
665
|
+
events: [...(definition.events ?? [])].sort((a, b) => a.time - b.time || a.name.localeCompare(b.name)),
|
|
666
|
+
registryIndex: this.nextRegistryIndex,
|
|
667
|
+
layer,
|
|
668
|
+
layerMetadata,
|
|
669
|
+
requiredBones: [...(definition.requiredBones ?? [])],
|
|
670
|
+
bones: [...(definition.bones ?? [])],
|
|
671
|
+
rootMotion: definition.rootMotion ?? definition.metadata?.rootMotion,
|
|
672
|
+
suppressRootMotion: definition.suppressRootMotion ?? definition.metadata?.suppressRootMotion ?? false,
|
|
673
|
+
restartFromFrameZero,
|
|
674
|
+
eventSource,
|
|
675
|
+
retarget,
|
|
676
|
+
externalHumanoidLibrary,
|
|
677
|
+
fallbackPose,
|
|
678
|
+
poseBakedFallback: poseBakedFallbackMetadata,
|
|
679
|
+
sample
|
|
680
|
+
};
|
|
681
|
+
this.nextRegistryIndex += 1;
|
|
682
|
+
if (!this.layerWeights.has(registered.layer)) {
|
|
683
|
+
this.layerWeights.set(registered.layer, 1);
|
|
684
|
+
}
|
|
685
|
+
return registered;
|
|
686
|
+
}
|
|
687
|
+
createPlaybackState(clip, options) {
|
|
688
|
+
const targetWeight = sanitizeWeight(options.weight ?? 1);
|
|
689
|
+
const fadeIn = sanitizeDuration(options.fadeIn ?? 0);
|
|
690
|
+
const loopMode = normalizeLoopMode(options.loop, clip.loop);
|
|
691
|
+
const localTime = normalizeStateTime(options.startTime ?? 0, clip.duration, loopMode);
|
|
692
|
+
const layer = options.layer ?? clip.layer ?? this.defaultLayer;
|
|
693
|
+
const layerMetadata = this.layerMetadata.get(layer) ?? clip.layerMetadata ?? inferLayerMetadata(layer);
|
|
694
|
+
const id = options.id ?? `${clip.id}:${this.nextPlaybackIndex}`;
|
|
695
|
+
this.nextPlaybackIndex += 1;
|
|
696
|
+
return {
|
|
697
|
+
id,
|
|
698
|
+
clip,
|
|
699
|
+
clipId: clip.id,
|
|
700
|
+
status: options.paused ? "paused" : "playing",
|
|
701
|
+
localTime,
|
|
702
|
+
previousLocalTime: localTime,
|
|
703
|
+
normalizedTime: clip.duration > 0 ? localTime / clip.duration : 0,
|
|
704
|
+
duration: clip.duration,
|
|
705
|
+
speed: sanitizeSpeed(options.speed ?? 1),
|
|
706
|
+
weight: fadeIn > 0 ? 0 : targetWeight,
|
|
707
|
+
targetWeight,
|
|
708
|
+
effectiveWeight: 0,
|
|
709
|
+
layer,
|
|
710
|
+
layerMetadata,
|
|
711
|
+
layerWeight: this.layerWeight(layer),
|
|
712
|
+
loopMode,
|
|
713
|
+
loopCount: 0,
|
|
714
|
+
direction: options.direction ?? 1,
|
|
715
|
+
inputDirection: options.direction ?? 1,
|
|
716
|
+
completed: false,
|
|
717
|
+
playhead: localTime,
|
|
718
|
+
rootMotionSuppressed: shouldSuppressRootMotion(options, clip, this.suppressRootMotion),
|
|
719
|
+
restartFromFrameZero: Boolean(options.restartFromFrameZero ?? clip.restartFromFrameZero),
|
|
720
|
+
eventSource: options.eventSource ?? clip.eventSource,
|
|
721
|
+
poseBakedFallback: clip.poseBakedFallback,
|
|
722
|
+
onceEvents: new Set(),
|
|
723
|
+
metadata: options.metadata,
|
|
724
|
+
fade: fadeIn > 0
|
|
725
|
+
? {
|
|
726
|
+
kind: "in",
|
|
727
|
+
elapsed: 0,
|
|
728
|
+
duration: fadeIn,
|
|
729
|
+
fromWeight: 0,
|
|
730
|
+
toWeight: targetWeight
|
|
731
|
+
}
|
|
732
|
+
: undefined
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
emitSampledEvents(state) {
|
|
736
|
+
const invocations = sampleClipEvents({
|
|
737
|
+
id: state.clipId,
|
|
738
|
+
duration: state.duration,
|
|
739
|
+
events: state.clip.events
|
|
740
|
+
}, {
|
|
741
|
+
from: state.previousLocalTime,
|
|
742
|
+
to: state.localTime,
|
|
743
|
+
duration: state.duration,
|
|
744
|
+
loop: state.loopMode !== "once",
|
|
745
|
+
direction: state.direction,
|
|
746
|
+
loopCount: state.loopCount,
|
|
747
|
+
playbackTime: this.clockTime
|
|
748
|
+
});
|
|
749
|
+
for (const invocation of invocations) {
|
|
750
|
+
if (invocation.event.once) {
|
|
751
|
+
const key = animationClipEventKey(invocation.clipId, invocation.event);
|
|
752
|
+
if (state.onceEvents.has(key))
|
|
753
|
+
continue;
|
|
754
|
+
state.onceEvents.add(key);
|
|
755
|
+
}
|
|
756
|
+
this.emit("event", {
|
|
757
|
+
...invocation,
|
|
758
|
+
source: createClipEventSourceMetadata(state, this.clockTime, this.embeddedGLB)
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
updateFade(state, dt) {
|
|
763
|
+
if (!state.fade)
|
|
764
|
+
return;
|
|
765
|
+
if (state.fade.duration === 0) {
|
|
766
|
+
state.weight = state.fade.toWeight;
|
|
767
|
+
state.fade = undefined;
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const elapsed = Math.min(state.fade.duration, state.fade.elapsed + dt);
|
|
771
|
+
const alpha = elapsed / state.fade.duration;
|
|
772
|
+
state.weight = lerp(state.fade.fromWeight, state.fade.toWeight, alpha);
|
|
773
|
+
state.fade = elapsed >= state.fade.duration
|
|
774
|
+
? undefined
|
|
775
|
+
: {
|
|
776
|
+
...state.fade,
|
|
777
|
+
elapsed
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
removeFinishedFadeOuts() {
|
|
781
|
+
for (const state of this.getInternalStates()) {
|
|
782
|
+
if (state.fade || state.weight > 0 || state.targetWeight > 0)
|
|
783
|
+
continue;
|
|
784
|
+
this.states.delete(state.id);
|
|
785
|
+
this.endState(state, "stopped");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
finishCrossFadesIfReady() {
|
|
789
|
+
if (this.pendingCrossFades.length === 0)
|
|
790
|
+
return;
|
|
791
|
+
const hasActiveFade = this.getInternalStates().some((state) => state.fade);
|
|
792
|
+
if (hasActiveFade)
|
|
793
|
+
return;
|
|
794
|
+
for (const event of [...this.pendingCrossFades]) {
|
|
795
|
+
this.finishCrossFade(event);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
finishCrossFade(event) {
|
|
799
|
+
this.pendingCrossFades = this.pendingCrossFades.filter((pending) => pending !== event);
|
|
800
|
+
this.emit("crossFadeEnd", event);
|
|
801
|
+
this.emit("crossfadeEnd", event);
|
|
802
|
+
}
|
|
803
|
+
blendStates(states, suppressRootMotion) {
|
|
804
|
+
const weightedStates = states.filter((state) => effectiveWeight(state) > 0);
|
|
805
|
+
if (weightedStates.length === 0) {
|
|
806
|
+
const fallback = clonePose(this.poseBakedFallback) ?? createIdentityPose(this.skeleton);
|
|
807
|
+
if (fallback) {
|
|
808
|
+
return {
|
|
809
|
+
...fallback,
|
|
810
|
+
metadata: {
|
|
811
|
+
...fallback.metadata,
|
|
812
|
+
poseBakedFallback: true,
|
|
813
|
+
poseBakedFallbackMetadata: this.poseBakedFallbackMetadata
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
return emptyPose({ poseBakedFallback: true, poseBakedFallbackMetadata: this.poseBakedFallbackMetadata });
|
|
818
|
+
}
|
|
819
|
+
const accumulators = new Map();
|
|
820
|
+
const morphTargets = {};
|
|
821
|
+
const rootMotionAccumulator = createBoneAccumulator();
|
|
822
|
+
let hasRootMotion = false;
|
|
823
|
+
let totalWeight = 0;
|
|
824
|
+
let rootMotionSuppressed = false;
|
|
825
|
+
let poseBakedFallback = false;
|
|
826
|
+
let poseBakedFallbackMetadata;
|
|
827
|
+
for (const state of weightedStates) {
|
|
828
|
+
const pose = this.sampleSinglePose(state.clip, state.localTime, state, suppressRootMotion);
|
|
829
|
+
const weight = effectiveWeight(state);
|
|
830
|
+
totalWeight += weight;
|
|
831
|
+
rootMotionSuppressed = rootMotionSuppressed || Boolean(pose.metadata?.rootMotionSuppressed);
|
|
832
|
+
poseBakedFallback = poseBakedFallback || Boolean(pose.metadata?.poseBakedFallback);
|
|
833
|
+
poseBakedFallbackMetadata =
|
|
834
|
+
poseBakedFallbackMetadata ??
|
|
835
|
+
pose.metadata?.poseBakedFallbackMetadata ??
|
|
836
|
+
state.poseBakedFallback;
|
|
837
|
+
for (const [boneName, transform] of Object.entries(pose.bones)) {
|
|
838
|
+
const accumulator = accumulators.get(boneName) ?? createBoneAccumulator();
|
|
839
|
+
accumulateTransform(accumulator, transform, weight);
|
|
840
|
+
accumulators.set(boneName, accumulator);
|
|
841
|
+
}
|
|
842
|
+
for (const [name, value] of Object.entries(pose.morphTargets ?? {})) {
|
|
843
|
+
morphTargets[name] = (morphTargets[name] ?? 0) + value * weight;
|
|
844
|
+
}
|
|
845
|
+
if (pose.rootMotion) {
|
|
846
|
+
hasRootMotion = true;
|
|
847
|
+
accumulateTransform(rootMotionAccumulator, {
|
|
848
|
+
position: pose.rootMotion.translation,
|
|
849
|
+
rotation: pose.rootMotion.rotation
|
|
850
|
+
}, weight);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
const bones = {};
|
|
854
|
+
for (const [boneName, accumulator] of accumulators) {
|
|
855
|
+
bones[boneName] = resolveAccumulator(accumulator);
|
|
856
|
+
}
|
|
857
|
+
if (totalWeight > 0) {
|
|
858
|
+
for (const name of Object.keys(morphTargets)) {
|
|
859
|
+
morphTargets[name] /= totalWeight;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const rootMotionTransform = hasRootMotion ? resolveAccumulator(rootMotionAccumulator) : undefined;
|
|
863
|
+
return {
|
|
864
|
+
bones,
|
|
865
|
+
morphTargets,
|
|
866
|
+
rootMotion: rootMotionTransform
|
|
867
|
+
? {
|
|
868
|
+
translation: rootMotionTransform.position,
|
|
869
|
+
rotation: rootMotionTransform.rotation
|
|
870
|
+
}
|
|
871
|
+
: undefined,
|
|
872
|
+
metadata: {
|
|
873
|
+
poseBakedFallback,
|
|
874
|
+
poseBakedFallbackMetadata,
|
|
875
|
+
rootMotionSuppressed
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
sampleSinglePose(clip, time, state, suppressRootMotion) {
|
|
880
|
+
const localTime = normalizeStateTime(time, clip.duration, clip.loop ? "loop" : "once");
|
|
881
|
+
const sampled = clip.sample?.({
|
|
882
|
+
clip,
|
|
883
|
+
time: localTime,
|
|
884
|
+
normalizedTime: clip.duration > 0 ? localTime / clip.duration : 0,
|
|
885
|
+
playbackState: state ? cloneState(state) : undefined
|
|
886
|
+
});
|
|
887
|
+
const pose = isAnimationPose(sampled) ? sampled : clonePose(clip.fallbackPose) ?? emptyPose({ poseBakedFallback: true });
|
|
888
|
+
const shouldSuppress = suppressRootMotion ?? state?.rootMotionSuppressed ?? clip.suppressRootMotion ?? this.suppressRootMotion;
|
|
889
|
+
return shouldSuppress ? suppressPoseRootMotion(pose, clip.rootMotion ?? this.rootMotion) : clonePose(pose) ?? emptyPose();
|
|
890
|
+
}
|
|
891
|
+
applyRuntimeNodeBindings() {
|
|
892
|
+
for (const binding of this.runtimeNodeBindings.values()) {
|
|
893
|
+
if (binding.options.applyOnUpdate === false)
|
|
894
|
+
continue;
|
|
895
|
+
this.applyRuntimeNodeBinding(binding);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
applyRuntimeNodeBinding(binding) {
|
|
899
|
+
let snapshot = this.createRuntimeNodeBindingSnapshot(binding);
|
|
900
|
+
const animation = createRuntimeNodeAnimationSpec(snapshot, binding.options);
|
|
901
|
+
if (snapshot.appliedClipId) {
|
|
902
|
+
if (typeof binding.node.play === "function") {
|
|
903
|
+
const options = { ...animation };
|
|
904
|
+
delete options.clip;
|
|
905
|
+
binding.node.play(String(snapshot.appliedClipId), options);
|
|
906
|
+
}
|
|
907
|
+
else if (typeof binding.node.setAnimation === "function") {
|
|
908
|
+
binding.node.setAnimation(animation);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (typeof binding.node.setAnimationBinding === "function") {
|
|
912
|
+
binding.node.setAnimationBinding(createRuntimeNodeAnimationBindingMetadata(snapshot));
|
|
913
|
+
}
|
|
914
|
+
if (binding.options.applyPose !== false && snapshot.pose && typeof binding.node.setAnimationPose === "function") {
|
|
915
|
+
binding.node.setAnimationPose(snapshot.pose, createRuntimeNodeAnimationPoseBindingMetadata(snapshot));
|
|
916
|
+
}
|
|
917
|
+
if (binding.options.applyMorphTargets !== false && snapshot.morphTargets && typeof binding.node.setMorphTargets === "function") {
|
|
918
|
+
binding.node.setMorphTargets(snapshot.morphTargets);
|
|
919
|
+
}
|
|
920
|
+
if (binding.options.importedRuntime && binding.options.applyImportedRuntime !== false && snapshot.clipSamples.length > 0) {
|
|
921
|
+
snapshot = {
|
|
922
|
+
...snapshot,
|
|
923
|
+
importedRuntime: applyImportedAnimationRuntime(binding.options.importedRuntime, snapshot.clipSamples)
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
binding.snapshot = snapshot;
|
|
927
|
+
return snapshot;
|
|
928
|
+
}
|
|
929
|
+
createRuntimeNodeBindingSnapshot(binding) {
|
|
930
|
+
const states = this.selectRuntimeNodeBindingStates(binding.options);
|
|
931
|
+
const state = states[0];
|
|
932
|
+
const appliedClipId = state?.clipId ?? binding.options.defaultClipId ?? binding.options.fallbackClipId;
|
|
933
|
+
const clip = appliedClipId ? this.clips.get(appliedClipId) : undefined;
|
|
934
|
+
const layer = state?.layer ?? binding.options.layer ?? clip?.layer;
|
|
935
|
+
const layerMetadata = layer ? this.layerMetadata.get(layer) ?? clip?.layerMetadata ?? inferLayerMetadata(layer) : undefined;
|
|
936
|
+
const pose = this.captureRuntimeNodeBindingPose(binding.options, states, state, clip);
|
|
937
|
+
const morphTargets = pose?.morphTargets ? { ...pose.morphTargets } : undefined;
|
|
938
|
+
const clipSamples = createRuntimeNodeClipSamples(states);
|
|
939
|
+
const poseBakedFallback = Boolean(state?.poseBakedFallback?.enabled ?? clip?.poseBakedFallback?.enabled);
|
|
940
|
+
const retargeted = Boolean(state?.clip.retarget ?? clip?.retarget ?? this.retarget);
|
|
941
|
+
return {
|
|
942
|
+
id: binding.id,
|
|
943
|
+
nodeId: binding.node.id,
|
|
944
|
+
source: "animation-controller",
|
|
945
|
+
controllerId: this.id,
|
|
946
|
+
activeClipId: state?.clipId,
|
|
947
|
+
appliedClipId,
|
|
948
|
+
playbackId: state?.id,
|
|
949
|
+
layer,
|
|
950
|
+
layerMetadata: layerMetadata ? cloneLayerMetadata(layerMetadata) : undefined,
|
|
951
|
+
localTime: state?.localTime,
|
|
952
|
+
captureTime: state?.localTime ?? (appliedClipId ? 0 : undefined),
|
|
953
|
+
loop: state ? state.loopMode !== "once" : clip?.loop,
|
|
954
|
+
speed: state?.speed,
|
|
955
|
+
pose,
|
|
956
|
+
morphTargets,
|
|
957
|
+
boneCount: Object.keys(pose?.bones ?? {}).length,
|
|
958
|
+
morphTargetCount: Object.keys(morphTargets ?? {}).length,
|
|
959
|
+
clipSamples,
|
|
960
|
+
poseBakedFallback,
|
|
961
|
+
retargeted,
|
|
962
|
+
sourceAssetId: clip?.metadata?.assetId ?? this.embeddedGLB?.assetId,
|
|
963
|
+
sourceAssetName: clip?.metadata?.assetName ?? this.embeddedGLB?.assetName,
|
|
964
|
+
metadata: {
|
|
965
|
+
...binding.options.metadata,
|
|
966
|
+
eventSource: state?.eventSource ?? clip?.eventSource,
|
|
967
|
+
restartFromFrameZero: state?.restartFromFrameZero ?? clip?.restartFromFrameZero
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
captureRuntimeNodeBindingPose(options, states, state, clip) {
|
|
972
|
+
if (options.applyPose === false && options.applyMorphTargets === false)
|
|
973
|
+
return undefined;
|
|
974
|
+
if (states.length > 0)
|
|
975
|
+
return clonePose(this.blendStates(states));
|
|
976
|
+
if (state)
|
|
977
|
+
return clonePose(this.sampleSinglePose(state.clip, state.localTime, state, undefined));
|
|
978
|
+
if (clip)
|
|
979
|
+
return clonePose(this.sampleSinglePose(clip, 0, undefined, undefined));
|
|
980
|
+
return undefined;
|
|
981
|
+
}
|
|
982
|
+
selectRuntimeNodeBindingStates(options) {
|
|
983
|
+
const candidates = options.layer
|
|
984
|
+
? this.getInternalStates().filter((state) => state.layer === options.layer)
|
|
985
|
+
: this.getInternalStates();
|
|
986
|
+
return candidates
|
|
987
|
+
.filter((state) => state.status === "playing" || state.status === "paused")
|
|
988
|
+
.filter((state) => effectiveWeight(state) > 0)
|
|
989
|
+
.sort((a, b) => effectiveWeight(b) - effectiveWeight(a));
|
|
990
|
+
}
|
|
991
|
+
selectRuntimeNodeBindingState(options) {
|
|
992
|
+
return this.selectRuntimeNodeBindingStates(options)[0];
|
|
993
|
+
}
|
|
994
|
+
retargetSnapshot() {
|
|
995
|
+
const bindings = this.collectRetargetBindings();
|
|
996
|
+
if (bindings.length === 0 && !this.externalHumanoidLibrary)
|
|
997
|
+
return undefined;
|
|
998
|
+
const primary = bindings[0]?.retarget ?? normalizeRetargetBinding(undefined, this.externalHumanoidLibrary);
|
|
999
|
+
const external = primary?.externalLibrary ?? this.externalHumanoidLibrary;
|
|
1000
|
+
const constraints = normalizeRetargetConstraints(primary?.constraints ?? external?.constraints ?? auraAnimationRetargetDocumentedConstraints);
|
|
1001
|
+
return {
|
|
1002
|
+
enabled: true,
|
|
1003
|
+
externalLibrary: external?.library,
|
|
1004
|
+
sourceAssetId: external?.sourceAssetId ?? this.embeddedGLB?.assetId,
|
|
1005
|
+
sourceAssetName: external?.sourceAssetName ?? this.embeddedGLB?.assetName,
|
|
1006
|
+
sourceClipIds: bindings.map((binding) => binding.clipId).filter((clipId) => Boolean(clipId)),
|
|
1007
|
+
constraints,
|
|
1008
|
+
diagnostics: this.createRetargetDiagnostics({})
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
collectRetargetBindings() {
|
|
1012
|
+
const bindings = [];
|
|
1013
|
+
if (this.retarget)
|
|
1014
|
+
bindings.push({ retarget: this.retarget });
|
|
1015
|
+
for (const clip of this.listClips()) {
|
|
1016
|
+
const retarget = normalizeRetargetBinding(clip.retarget ?? clip.metadata?.retarget, clip.externalHumanoidLibrary ?? clip.metadata?.externalHumanoidLibrary);
|
|
1017
|
+
if (retarget)
|
|
1018
|
+
bindings.push({ retarget, clipId: clip.id });
|
|
1019
|
+
}
|
|
1020
|
+
return bindings;
|
|
1021
|
+
}
|
|
1022
|
+
createRetargetDiagnostics(options) {
|
|
1023
|
+
const diagnostics = [];
|
|
1024
|
+
const bindings = this.collectRetargetBindings();
|
|
1025
|
+
if (bindings.length === 0)
|
|
1026
|
+
return diagnostics;
|
|
1027
|
+
for (const { retarget, clipId } of bindings) {
|
|
1028
|
+
const external = retarget.externalLibrary ?? this.externalHumanoidLibrary;
|
|
1029
|
+
const constraints = normalizeRetargetConstraints(retarget.constraints ?? external?.constraints ?? []);
|
|
1030
|
+
if (constraints.length === 0) {
|
|
1031
|
+
diagnostics.push(createDiagnostic("warning", "ANIMATION_RETARGET_CONSTRAINTS_UNDOCUMENTED", `Retarget metadata${clipId ? ` for clip "${clipId}"` : ""} should document constraints: explicit humanoid bone map, rest pose, uniform scale, root-motion policy, no runtime IK, and no automatic proportion warp.`, clipId));
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
for (const constraint of constraints) {
|
|
1035
|
+
diagnostics.push(createDiagnostic(constraint.severity ?? "info", `ANIMATION_RETARGET_CONSTRAINT_${diagnosticCodeSuffix(constraint.code)}`, constraint.message ?? `Retarget constraint documented: ${constraint.code}.`, clipId));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const mappings = normalizeHumanoidBoneMap(retarget.boneMap ?? external?.boneMap);
|
|
1039
|
+
if (mappings.length === 0) {
|
|
1040
|
+
diagnostics.push(createDiagnostic("error", "ANIMATION_RETARGET_BONE_MAP_MISSING", `Retarget metadata${clipId ? ` for clip "${clipId}"` : ""} is missing an explicit humanoid bone map.`, clipId));
|
|
1041
|
+
}
|
|
1042
|
+
const targetSkeletonBones = skeletonBoneNames(retarget.targetSkeleton ?? this.skeleton);
|
|
1043
|
+
const sourceSkeletonBones = skeletonBoneNames(retarget.sourceSkeleton ?? external?.skeleton);
|
|
1044
|
+
for (const mapping of mappings) {
|
|
1045
|
+
if (mapping.required !== false && targetSkeletonBones.size > 0 && !targetSkeletonBones.has(mapping.target)) {
|
|
1046
|
+
diagnostics.push(createDiagnostic("error", "ANIMATION_RETARGET_TARGET_BONE_MISSING", `Retarget mapping "${mapping.source}" -> "${mapping.target}" targets a missing skeleton bone.`, clipId, mapping.target));
|
|
1047
|
+
}
|
|
1048
|
+
if (mapping.required !== false && sourceSkeletonBones.size > 0 && !sourceSkeletonBones.has(mapping.source)) {
|
|
1049
|
+
diagnostics.push(createDiagnostic("warning", "ANIMATION_RETARGET_SOURCE_BONE_UNVERIFIED", `Retarget mapping source bone "${mapping.source}" is not present in source skeleton metadata.`, clipId, mapping.source));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (!retarget.restPose && !external?.restPose) {
|
|
1053
|
+
diagnostics.push(createDiagnostic("warning", "ANIMATION_RETARGET_REST_POSE_UNDOCUMENTED", `Retarget metadata${clipId ? ` for clip "${clipId}"` : ""} should document source/target rest-pose assumptions.`, clipId));
|
|
1054
|
+
}
|
|
1055
|
+
if (!retarget.scale) {
|
|
1056
|
+
diagnostics.push(createDiagnostic("warning", "ANIMATION_RETARGET_SCALE_UNDOCUMENTED", `Retarget metadata${clipId ? ` for clip "${clipId}"` : ""} should document uniform scale or unit conversion assumptions.`, clipId));
|
|
1057
|
+
}
|
|
1058
|
+
for (const requiredBone of options.requiredBones ?? []) {
|
|
1059
|
+
if (targetSkeletonBones.size > 0 && !targetSkeletonBones.has(requiredBone)) {
|
|
1060
|
+
diagnostics.push(createDiagnostic("error", "ANIMATION_RETARGET_REQUIRED_TARGET_BONE_MISSING", `Retarget target skeleton is missing required bone "${requiredBone}".`, clipId, requiredBone));
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return diagnostics;
|
|
1065
|
+
}
|
|
1066
|
+
selectStates(clipId) {
|
|
1067
|
+
const states = this.getInternalStates();
|
|
1068
|
+
if (!clipId)
|
|
1069
|
+
return states;
|
|
1070
|
+
return states.filter((state) => state.clipId === clipId);
|
|
1071
|
+
}
|
|
1072
|
+
selectCrossFadeSources(options) {
|
|
1073
|
+
if (options.fromClipId)
|
|
1074
|
+
return this.selectStates(options.fromClipId);
|
|
1075
|
+
if (options.fromLayer)
|
|
1076
|
+
return this.getInternalStates().filter((state) => state.layer === options.fromLayer);
|
|
1077
|
+
return this.getInternalStates();
|
|
1078
|
+
}
|
|
1079
|
+
getInternalStates() {
|
|
1080
|
+
return [...this.states.values()];
|
|
1081
|
+
}
|
|
1082
|
+
findStateByClip(clipId) {
|
|
1083
|
+
return this.getInternalStates().find((state) => state.clipId === clipId);
|
|
1084
|
+
}
|
|
1085
|
+
primaryState() {
|
|
1086
|
+
return this.getInternalStates()
|
|
1087
|
+
.filter((state) => state.status === "playing" || state.status === "paused")
|
|
1088
|
+
.sort((a, b) => effectiveWeight(b) - effectiveWeight(a))[0];
|
|
1089
|
+
}
|
|
1090
|
+
activeClipId() {
|
|
1091
|
+
return this.primaryState()?.clipId;
|
|
1092
|
+
}
|
|
1093
|
+
layerWeight(layer) {
|
|
1094
|
+
return this.layerWeights.get(layer) ?? 1;
|
|
1095
|
+
}
|
|
1096
|
+
endState(state, status) {
|
|
1097
|
+
state.status = status;
|
|
1098
|
+
this.emit("end", cloneState(state));
|
|
1099
|
+
}
|
|
1100
|
+
emitStateChanged() {
|
|
1101
|
+
this.emit("stateChanged", this.snapshot());
|
|
1102
|
+
}
|
|
1103
|
+
emit(type, payload) {
|
|
1104
|
+
const listeners = this.listeners.get(type);
|
|
1105
|
+
if (!listeners)
|
|
1106
|
+
return;
|
|
1107
|
+
for (const listener of [...listeners]) {
|
|
1108
|
+
listener(payload);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
export function createAnimationController(options = {}) {
|
|
1113
|
+
return new AnimationController(options);
|
|
1114
|
+
}
|
|
1115
|
+
export const auraAnimationRuntimeMitigationContract = {
|
|
1116
|
+
embeddedClipPlaybackFirst: true,
|
|
1117
|
+
externalHumanoidRetargeting: "diagnostics-gated",
|
|
1118
|
+
retargetingRequiresDocumentedConstraints: true,
|
|
1119
|
+
constraints: auraAnimationRetargetDocumentedConstraints
|
|
1120
|
+
};
|
|
1121
|
+
export function createSourceTestGLBAnimationSwitchHarness(options = {}) {
|
|
1122
|
+
const clipRegistry = options.clipRegistry ?? createDefaultSourceTestGLBClipRegistry();
|
|
1123
|
+
const controller = new AnimationController({
|
|
1124
|
+
id: "source-test-glb-animation-switch",
|
|
1125
|
+
clipRegistry,
|
|
1126
|
+
requiredClips: ["idle", "walk", "lightPunch"],
|
|
1127
|
+
requiredBones: ["hips", "spine", "head", "leftArm", "rightArm", "leftLeg", "rightLeg"],
|
|
1128
|
+
suppressRootMotion: true
|
|
1129
|
+
});
|
|
1130
|
+
const fade = options.crossFadeDuration ?? 0.08;
|
|
1131
|
+
const dt = options.dt ?? 1 / 30;
|
|
1132
|
+
const sequence = [];
|
|
1133
|
+
const capture = (expectedClipId, transition) => {
|
|
1134
|
+
const snapshot = controller.snapshot();
|
|
1135
|
+
const active = snapshot.clips.filter((clip) => clip.status === "playing" || clip.status === "completed");
|
|
1136
|
+
sequence.push({
|
|
1137
|
+
expectedClipId,
|
|
1138
|
+
transition,
|
|
1139
|
+
activeClipId: snapshot.activeClipId,
|
|
1140
|
+
activeClipIds: active.map((clip) => clip.clipId),
|
|
1141
|
+
localTime: controller.state(expectedClipId)?.localTime,
|
|
1142
|
+
rootMotionSuppressed: snapshot.rootMotionSuppressed,
|
|
1143
|
+
poseBakedFallback: Boolean(controller.state(expectedClipId)?.poseBakedFallback)
|
|
1144
|
+
});
|
|
1145
|
+
};
|
|
1146
|
+
controller.play("idle", { restart: true, loop: "loop" });
|
|
1147
|
+
controller.update(dt);
|
|
1148
|
+
capture("idle", "play");
|
|
1149
|
+
controller.crossFade("walk", fade, { restart: true, loop: "loop" });
|
|
1150
|
+
controller.update(fade);
|
|
1151
|
+
capture("walk", "crossFade");
|
|
1152
|
+
controller.crossFade("lightPunch", fade, { restart: true, loop: false, layer: "upper-body", attack: true });
|
|
1153
|
+
controller.update(fade);
|
|
1154
|
+
capture("lightPunch", "crossFade");
|
|
1155
|
+
controller.crossFade("idle", fade, { restart: true, loop: "loop" });
|
|
1156
|
+
controller.update(fade);
|
|
1157
|
+
capture("idle", "crossFade");
|
|
1158
|
+
const diagnostics = controller.diagnostics({
|
|
1159
|
+
requireSkeleton: true,
|
|
1160
|
+
requiredClips: ["idle", "walk", "lightPunch"],
|
|
1161
|
+
requiredBones: ["hips", "spine", "head", "leftArm", "rightArm", "leftLeg", "rightLeg"]
|
|
1162
|
+
});
|
|
1163
|
+
const expectedSequence = ["idle", "walk", "lightPunch", "idle"];
|
|
1164
|
+
return {
|
|
1165
|
+
ok: sequence.map((step) => step.activeClipId).every((clipId, index) => clipId === expectedSequence[index]) &&
|
|
1166
|
+
diagnostics.every((diagnostic) => diagnostic.severity !== "error"),
|
|
1167
|
+
source: "source-test-glb-clip-registry",
|
|
1168
|
+
assetId: clipRegistry.assetId,
|
|
1169
|
+
sequence,
|
|
1170
|
+
diagnostics,
|
|
1171
|
+
mitigation: auraAnimationRuntimeMitigationContract
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function createDefaultSourceTestGLBClipRegistry() {
|
|
1175
|
+
const skeleton = {
|
|
1176
|
+
rootBone: "hips",
|
|
1177
|
+
bones: ["hips", "spine", "head", "leftArm", "rightArm", "leftLeg", "rightLeg"]
|
|
1178
|
+
};
|
|
1179
|
+
return {
|
|
1180
|
+
kind: "aura3d-embedded-glb-animation-registry",
|
|
1181
|
+
assetId: "source-test-fighter-glb",
|
|
1182
|
+
assetName: "Source Test Fighter GLB",
|
|
1183
|
+
skeleton,
|
|
1184
|
+
suppressRootMotion: true,
|
|
1185
|
+
clips: [
|
|
1186
|
+
{
|
|
1187
|
+
id: "idle",
|
|
1188
|
+
duration: 1,
|
|
1189
|
+
loop: true,
|
|
1190
|
+
requiredBones: skeleton.bones,
|
|
1191
|
+
tracks: [
|
|
1192
|
+
{
|
|
1193
|
+
id: "idle-hips",
|
|
1194
|
+
target: "hips.position",
|
|
1195
|
+
property: "translation",
|
|
1196
|
+
keyframes: [
|
|
1197
|
+
{ time: 0, value: { x: 0, y: 0, z: 0 } },
|
|
1198
|
+
{ time: 1, value: { x: 0, y: 0.02, z: 0 } }
|
|
1199
|
+
]
|
|
1200
|
+
}
|
|
1201
|
+
]
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
id: "walk",
|
|
1205
|
+
duration: 1,
|
|
1206
|
+
loop: true,
|
|
1207
|
+
requiredBones: skeleton.bones,
|
|
1208
|
+
rootMotion: { track: "walk-root", bone: "hips", suppress: true, reason: "gameplay kinematic body owns locomotion" },
|
|
1209
|
+
tracks: [
|
|
1210
|
+
{
|
|
1211
|
+
id: "walk-root",
|
|
1212
|
+
target: "hips.position",
|
|
1213
|
+
property: "translation",
|
|
1214
|
+
keyframes: [
|
|
1215
|
+
{ time: 0, value: { x: 0, y: 0, z: 0 } },
|
|
1216
|
+
{ time: 1, value: { x: 0, y: 0, z: 0.35 } }
|
|
1217
|
+
]
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
id: "walk-left-leg",
|
|
1221
|
+
target: "leftLeg.rotation",
|
|
1222
|
+
property: "rotation",
|
|
1223
|
+
keyframes: [
|
|
1224
|
+
{ time: 0, value: { x: 0, y: 0, z: -0.12, w: 0.99 } },
|
|
1225
|
+
{ time: 0.5, value: { x: 0, y: 0, z: 0.12, w: 0.99 } },
|
|
1226
|
+
{ time: 1, value: { x: 0, y: 0, z: -0.12, w: 0.99 } }
|
|
1227
|
+
]
|
|
1228
|
+
}
|
|
1229
|
+
]
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
id: "lightPunch",
|
|
1233
|
+
duration: 0.45,
|
|
1234
|
+
loop: false,
|
|
1235
|
+
attack: true,
|
|
1236
|
+
restartFromFrameZero: true,
|
|
1237
|
+
layer: "upper-body",
|
|
1238
|
+
requiredBones: ["spine", "head", "leftArm", "rightArm"],
|
|
1239
|
+
events: [{ name: "active", type: "hitbox", time: 0.12, once: true, payload: { volume: "light-punch" } }],
|
|
1240
|
+
tracks: [
|
|
1241
|
+
{
|
|
1242
|
+
id: "light-punch-right-arm",
|
|
1243
|
+
target: "rightArm.rotation",
|
|
1244
|
+
property: "rotation",
|
|
1245
|
+
keyframes: [
|
|
1246
|
+
{ time: 0, value: { x: 0, y: 0, z: 0, w: 1 } },
|
|
1247
|
+
{ time: 0.12, value: { x: 0.25, y: 0, z: -0.15, w: 0.96 } },
|
|
1248
|
+
{ time: 0.45, value: { x: 0, y: 0, z: 0, w: 1 } }
|
|
1249
|
+
]
|
|
1250
|
+
}
|
|
1251
|
+
]
|
|
1252
|
+
}
|
|
1253
|
+
]
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function createRuntimeNodeAnimationSpec(snapshot, options) {
|
|
1257
|
+
return compactObject({
|
|
1258
|
+
clip: snapshot.appliedClipId ? String(snapshot.appliedClipId) : undefined,
|
|
1259
|
+
loop: options.syncLoop === false ? undefined : snapshot.loop,
|
|
1260
|
+
speed: options.syncSpeed === false ? undefined : snapshot.speed,
|
|
1261
|
+
startTime: snapshot.metadata?.restartFromFrameZero ? 0 : undefined,
|
|
1262
|
+
duration: undefined,
|
|
1263
|
+
captureTime: options.syncCaptureTime === false ? undefined : snapshot.captureTime,
|
|
1264
|
+
metadata: compactObject({
|
|
1265
|
+
controllerId: snapshot.controllerId,
|
|
1266
|
+
bindingId: snapshot.id,
|
|
1267
|
+
playbackId: snapshot.playbackId,
|
|
1268
|
+
layer: snapshot.layer,
|
|
1269
|
+
layerRole: snapshot.layerMetadata?.role,
|
|
1270
|
+
bodyMask: snapshot.layerMetadata?.bodyMask,
|
|
1271
|
+
poseBakedFallback: snapshot.poseBakedFallback,
|
|
1272
|
+
retargeted: snapshot.retargeted,
|
|
1273
|
+
sourceAssetId: snapshot.sourceAssetId,
|
|
1274
|
+
sourceAssetName: snapshot.sourceAssetName,
|
|
1275
|
+
eventSource: snapshot.metadata?.eventSource,
|
|
1276
|
+
restartFromFrameZero: snapshot.metadata?.restartFromFrameZero
|
|
1277
|
+
})
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
function createRuntimeNodeAnimationBindingMetadata(snapshot) {
|
|
1281
|
+
return compactObject({
|
|
1282
|
+
kind: "aura-runtime-node-animation-binding",
|
|
1283
|
+
controllerId: snapshot.controllerId,
|
|
1284
|
+
bindingId: snapshot.id,
|
|
1285
|
+
activeClipId: snapshot.activeClipId ? String(snapshot.activeClipId) : undefined,
|
|
1286
|
+
playbackId: snapshot.playbackId,
|
|
1287
|
+
layer: snapshot.layer,
|
|
1288
|
+
layerRole: snapshot.layerMetadata?.role,
|
|
1289
|
+
bodyMask: snapshot.layerMetadata?.bodyMask,
|
|
1290
|
+
localTime: snapshot.localTime,
|
|
1291
|
+
captureTime: snapshot.captureTime,
|
|
1292
|
+
loop: snapshot.loop,
|
|
1293
|
+
speed: snapshot.speed,
|
|
1294
|
+
eventSource: typeof snapshot.metadata?.eventSource === "string" ? snapshot.metadata.eventSource : undefined,
|
|
1295
|
+
retargeted: snapshot.retargeted,
|
|
1296
|
+
poseBakedFallback: snapshot.poseBakedFallback,
|
|
1297
|
+
sourceAssetId: snapshot.sourceAssetId,
|
|
1298
|
+
sourceAssetName: snapshot.sourceAssetName,
|
|
1299
|
+
metadata: snapshot.metadata
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
function createRuntimeNodeAnimationPoseBindingMetadata(snapshot) {
|
|
1303
|
+
return compactObject({
|
|
1304
|
+
kind: "aura-runtime-node-animation-pose",
|
|
1305
|
+
controllerId: snapshot.controllerId,
|
|
1306
|
+
bindingId: snapshot.id,
|
|
1307
|
+
activeClipId: snapshot.activeClipId ? String(snapshot.activeClipId) : undefined,
|
|
1308
|
+
playbackId: snapshot.playbackId,
|
|
1309
|
+
localTime: snapshot.localTime,
|
|
1310
|
+
captureTime: snapshot.captureTime,
|
|
1311
|
+
boneCount: snapshot.boneCount,
|
|
1312
|
+
morphTargetCount: snapshot.morphTargetCount,
|
|
1313
|
+
sourceAssetId: snapshot.sourceAssetId,
|
|
1314
|
+
sourceAssetName: snapshot.sourceAssetName,
|
|
1315
|
+
metadata: compactObject({
|
|
1316
|
+
layer: snapshot.layer,
|
|
1317
|
+
layerRole: snapshot.layerMetadata?.role,
|
|
1318
|
+
bodyMask: snapshot.layerMetadata?.bodyMask,
|
|
1319
|
+
poseBakedFallback: snapshot.poseBakedFallback,
|
|
1320
|
+
retargeted: snapshot.retargeted,
|
|
1321
|
+
eventSource: snapshot.metadata?.eventSource,
|
|
1322
|
+
restartFromFrameZero: snapshot.metadata?.restartFromFrameZero
|
|
1323
|
+
})
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
function createRuntimeNodeClipSamples(states) {
|
|
1327
|
+
return states.map((state) => {
|
|
1328
|
+
const additive = Boolean(state.layerMetadata?.additive ?? state.clip.layerMetadata?.additive);
|
|
1329
|
+
return compactObject({
|
|
1330
|
+
clipId: state.clipId,
|
|
1331
|
+
clipName: runtimeClipNameForState(state),
|
|
1332
|
+
localTime: state.localTime,
|
|
1333
|
+
weight: effectiveWeight(state),
|
|
1334
|
+
layer: state.layer,
|
|
1335
|
+
additive: additive ? true : undefined
|
|
1336
|
+
});
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
function applyImportedAnimationRuntime(runtime, samples) {
|
|
1340
|
+
const importedSamples = samples.map((sample) => compactObject({
|
|
1341
|
+
clipName: sample.clipName,
|
|
1342
|
+
time: sample.localTime,
|
|
1343
|
+
weight: sample.weight,
|
|
1344
|
+
additive: sample.additive
|
|
1345
|
+
}));
|
|
1346
|
+
const blended = importedSamples.length > 1;
|
|
1347
|
+
const applyResult = blended && typeof runtime.blendClips === "function"
|
|
1348
|
+
? runtime.blendClips(importedSamples)
|
|
1349
|
+
: blended && typeof runtime.applyClips === "function"
|
|
1350
|
+
? runtime.applyClips(importedSamples)
|
|
1351
|
+
: applySingleImportedClip(runtime, highestWeightRuntimeClipSample(importedSamples));
|
|
1352
|
+
return compactObject({
|
|
1353
|
+
applied: true,
|
|
1354
|
+
blended,
|
|
1355
|
+
sampleCount: importedSamples.length,
|
|
1356
|
+
applyResult,
|
|
1357
|
+
runtimeSnapshot: typeof runtime.snapshot === "function" ? runtime.snapshot() : undefined
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
function applySingleImportedClip(runtime, sample) {
|
|
1361
|
+
if (typeof runtime.applyClip === "function")
|
|
1362
|
+
return runtime.applyClip(sample.clipName, sample.time);
|
|
1363
|
+
if (typeof runtime.applyClipByName === "function")
|
|
1364
|
+
return runtime.applyClipByName(sample.clipName, sample.time);
|
|
1365
|
+
if (typeof runtime.applyClips === "function")
|
|
1366
|
+
return runtime.applyClips([sample]);
|
|
1367
|
+
if (typeof runtime.blendClips === "function")
|
|
1368
|
+
return runtime.blendClips([sample]);
|
|
1369
|
+
throw new Error("Imported animation runtime must expose applyClip(), applyClipByName(), applyClips(), or blendClips().");
|
|
1370
|
+
}
|
|
1371
|
+
function highestWeightRuntimeClipSample(samples) {
|
|
1372
|
+
const [first] = samples;
|
|
1373
|
+
if (!first) {
|
|
1374
|
+
throw new Error("Cannot apply imported animation runtime without an active clip sample.");
|
|
1375
|
+
}
|
|
1376
|
+
return samples.reduce((best, sample) => (sample.weight ?? 1) > (best.weight ?? 1) ? sample : best, first);
|
|
1377
|
+
}
|
|
1378
|
+
function runtimeClipNameForState(state) {
|
|
1379
|
+
return (stringMetadata(state.clip.metadata, "runtimeClipName") ??
|
|
1380
|
+
stringMetadata(state.clip.metadata, "sourceClipName") ??
|
|
1381
|
+
stringMetadata(state.clip.metadata, "sourceClipId") ??
|
|
1382
|
+
stringMetadata(state.clip.metadata, "clipName") ??
|
|
1383
|
+
state.clip.name ??
|
|
1384
|
+
String(state.clipId));
|
|
1385
|
+
}
|
|
1386
|
+
function stringMetadata(metadata, key) {
|
|
1387
|
+
const value = metadata?.[key];
|
|
1388
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
1389
|
+
}
|
|
1390
|
+
function createClipEventSourceMetadata(state, controllerTime, registry) {
|
|
1391
|
+
const poseBakedFallback = Boolean(state.poseBakedFallback?.enabled ?? state.clip.poseBakedFallback?.enabled);
|
|
1392
|
+
const retargeted = Boolean(state.clip.retarget);
|
|
1393
|
+
return {
|
|
1394
|
+
kind: poseBakedFallback ? "pose-baked-fallback" : state.eventSource,
|
|
1395
|
+
semantics: "clip-local-time",
|
|
1396
|
+
clipId: state.clipId,
|
|
1397
|
+
playbackId: state.id,
|
|
1398
|
+
layer: state.layer,
|
|
1399
|
+
localTime: state.localTime,
|
|
1400
|
+
previousLocalTime: state.previousLocalTime,
|
|
1401
|
+
normalizedTime: state.normalizedTime,
|
|
1402
|
+
controllerTime,
|
|
1403
|
+
loopCount: state.loopCount,
|
|
1404
|
+
retargeted,
|
|
1405
|
+
poseBakedFallback,
|
|
1406
|
+
sourceAssetId: state.clip.metadata?.assetId ?? registry?.assetId,
|
|
1407
|
+
sourceAssetName: state.clip.metadata?.assetName ?? registry?.assetName,
|
|
1408
|
+
sourceClipId: state.clip.metadata?.sourceClipId,
|
|
1409
|
+
metadata: {
|
|
1410
|
+
rule: "Clip events are sampled from source clip local time before runtime-node binding or renderer skinning.",
|
|
1411
|
+
layerRole: state.layerMetadata?.role,
|
|
1412
|
+
bodyMask: state.layerMetadata?.bodyMask
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function shouldRestartFromFrameZero(clip, options, layers) {
|
|
1417
|
+
if (options.restartFromFrameZero === true || options.attack === true)
|
|
1418
|
+
return true;
|
|
1419
|
+
const layer = options.layer ?? clip.layer;
|
|
1420
|
+
const layerMetadata = layers.get(layer) ?? clip.layerMetadata;
|
|
1421
|
+
return Boolean(clip.restartFromFrameZero ||
|
|
1422
|
+
clip.metadata?.restartFromFrameZero ||
|
|
1423
|
+
clip.metadata?.attack ||
|
|
1424
|
+
clip.tags.includes("attack") ||
|
|
1425
|
+
layerMetadata?.restartFromFrameZero);
|
|
1426
|
+
}
|
|
1427
|
+
function cloneLayerMetadata(layer) {
|
|
1428
|
+
return {
|
|
1429
|
+
...layer,
|
|
1430
|
+
bones: layer.bones ? [...layer.bones] : undefined,
|
|
1431
|
+
excludedBones: layer.excludedBones ? [...layer.excludedBones] : undefined,
|
|
1432
|
+
metadata: layer.metadata ? { ...layer.metadata } : undefined
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function inferLayerMetadata(layer) {
|
|
1436
|
+
const normalized = layer.trim() || defaultLayerName;
|
|
1437
|
+
if (normalized === "upper-body") {
|
|
1438
|
+
return {
|
|
1439
|
+
id: normalized,
|
|
1440
|
+
role: "upper-body",
|
|
1441
|
+
bodyMask: "upper-body",
|
|
1442
|
+
bones: ["Spine", "Chest", "UpperChest", "Neck", "Head", "LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand", "RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand"],
|
|
1443
|
+
restartFromFrameZero: true,
|
|
1444
|
+
description: "Upper-body overlay layer for attacks, gestures, aim offsets, and hit reactions."
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
if (normalized === "lower-body") {
|
|
1448
|
+
return {
|
|
1449
|
+
id: normalized,
|
|
1450
|
+
role: "lower-body",
|
|
1451
|
+
bodyMask: "lower-body",
|
|
1452
|
+
bones: ["Hips", "LeftUpperLeg", "LeftLowerLeg", "LeftFoot", "LeftToeBase", "RightUpperLeg", "RightLowerLeg", "RightFoot", "RightToeBase"],
|
|
1453
|
+
description: "Lower-body locomotion layer for feet, hips, and grounded movement."
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
return {
|
|
1457
|
+
id: normalized,
|
|
1458
|
+
role: normalized === defaultLayerName ? "base" : "custom",
|
|
1459
|
+
bodyMask: "full-body",
|
|
1460
|
+
description: normalized === defaultLayerName ? "Full-body base animation layer." : "Custom animation layer metadata."
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function normalizeRetargetBinding(retarget, externalLibrary) {
|
|
1464
|
+
if (!retarget && !externalLibrary)
|
|
1465
|
+
return undefined;
|
|
1466
|
+
return {
|
|
1467
|
+
...retarget,
|
|
1468
|
+
kind: retarget?.kind ?? "aura-animation-retarget-binding",
|
|
1469
|
+
source: retarget?.source ?? (externalLibrary ? "external-humanoid-library" : undefined),
|
|
1470
|
+
boneMap: retarget?.boneMap ?? externalLibrary?.boneMap,
|
|
1471
|
+
sourceSkeleton: retarget?.sourceSkeleton ?? externalLibrary?.skeleton,
|
|
1472
|
+
constraints: retarget?.constraints ?? externalLibrary?.constraints,
|
|
1473
|
+
externalLibrary: retarget?.externalLibrary ?? externalLibrary
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function normalizeRetargetConstraints(constraints = []) {
|
|
1477
|
+
return constraints.map((constraint) => {
|
|
1478
|
+
if (typeof constraint === "string") {
|
|
1479
|
+
return documentedRetargetConstraint(constraint);
|
|
1480
|
+
}
|
|
1481
|
+
return {
|
|
1482
|
+
...documentedRetargetConstraint(constraint.code),
|
|
1483
|
+
...constraint
|
|
1484
|
+
};
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
function documentedRetargetConstraint(code) {
|
|
1488
|
+
return auraAnimationRetargetDocumentedConstraints.find((constraint) => constraint.code === code) ?? {
|
|
1489
|
+
code,
|
|
1490
|
+
severity: "info",
|
|
1491
|
+
message: `Retarget constraint documented: ${code}.`
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
function normalizeHumanoidBoneMap(map) {
|
|
1495
|
+
if (!map)
|
|
1496
|
+
return [];
|
|
1497
|
+
if (Array.isArray(map)) {
|
|
1498
|
+
return map
|
|
1499
|
+
.filter((binding) => Boolean(binding.source && binding.target))
|
|
1500
|
+
.map((binding) => ({ ...binding }));
|
|
1501
|
+
}
|
|
1502
|
+
return Object.entries(map)
|
|
1503
|
+
.filter(([source, target]) => Boolean(source && target))
|
|
1504
|
+
.map(([source, target]) => ({ source, target, required: true }));
|
|
1505
|
+
}
|
|
1506
|
+
function diagnosticCodeSuffix(value) {
|
|
1507
|
+
return value.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "DOCUMENTED";
|
|
1508
|
+
}
|
|
1509
|
+
function compactObject(value) {
|
|
1510
|
+
const result = {};
|
|
1511
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1512
|
+
if (entry !== undefined)
|
|
1513
|
+
result[key] = entry;
|
|
1514
|
+
}
|
|
1515
|
+
return result;
|
|
1516
|
+
}
|
|
1517
|
+
export function createEmbeddedGLBAnimationClipRegistryMetadata(source) {
|
|
1518
|
+
const asset = isAnimationAssetLike(source) ? source : undefined;
|
|
1519
|
+
const metadata = asset?.metadata;
|
|
1520
|
+
const nested = metadata?.animation;
|
|
1521
|
+
const registry = (nested ?? source);
|
|
1522
|
+
const clips = registry.clips ?? registry.animations ?? metadata?.animationClips ?? metadata?.animations ?? [];
|
|
1523
|
+
const bones = registry.bones ?? metadata?.bones;
|
|
1524
|
+
const skeleton = registry.skeleton ?? metadata?.skeleton ?? (bones ? { bones } : undefined);
|
|
1525
|
+
const layers = registry.layers ?? metadata?.layers;
|
|
1526
|
+
const externalHumanoidLibrary = registry.externalHumanoidLibrary ?? metadata?.externalHumanoidLibrary;
|
|
1527
|
+
return {
|
|
1528
|
+
kind: registry.kind ?? "aura3d-embedded-glb-animation-registry",
|
|
1529
|
+
assetId: registry.assetId ?? asset?.id,
|
|
1530
|
+
assetName: registry.assetName ?? asset?.name,
|
|
1531
|
+
clips,
|
|
1532
|
+
skeleton,
|
|
1533
|
+
bones,
|
|
1534
|
+
layers,
|
|
1535
|
+
retarget: registry.retarget ?? metadata?.retarget,
|
|
1536
|
+
externalHumanoidLibrary,
|
|
1537
|
+
rootMotion: registry.rootMotion ?? metadata?.rootMotion,
|
|
1538
|
+
suppressRootMotion: registry.suppressRootMotion ?? metadata?.suppressRootMotion,
|
|
1539
|
+
poseBakedFallback: registry.poseBakedFallback ?? metadata?.poseBakedFallback,
|
|
1540
|
+
metadata: {
|
|
1541
|
+
...registry.metadata,
|
|
1542
|
+
url: asset?.url,
|
|
1543
|
+
hash: asset?.hash
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
function embeddedClipToDefinition(input, registry, defaultLayer) {
|
|
1548
|
+
if (typeof input === "string") {
|
|
1549
|
+
const retarget = normalizeRetargetBinding(registry.retarget, registry.externalHumanoidLibrary);
|
|
1550
|
+
return {
|
|
1551
|
+
id: input,
|
|
1552
|
+
name: input,
|
|
1553
|
+
duration: 1,
|
|
1554
|
+
loop: true,
|
|
1555
|
+
layer: defaultLayer,
|
|
1556
|
+
layerMetadata: registry.layers?.find((layer) => layer.id === defaultLayer),
|
|
1557
|
+
eventSource: retarget ? "retargeted-source-clip" : "source-clip",
|
|
1558
|
+
retarget,
|
|
1559
|
+
externalHumanoidLibrary: registry.externalHumanoidLibrary,
|
|
1560
|
+
metadata: {
|
|
1561
|
+
embeddedGLB: true,
|
|
1562
|
+
assetId: registry.assetId,
|
|
1563
|
+
assetName: registry.assetName,
|
|
1564
|
+
source: "embedded-glb-animation-name",
|
|
1565
|
+
durationSource: "defaulted"
|
|
1566
|
+
},
|
|
1567
|
+
fallbackPose: extractPoseBakedFallback(registry.poseBakedFallback)
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
const id = input.id ?? input.name;
|
|
1571
|
+
if (!id) {
|
|
1572
|
+
throw new Error("Embedded GLB animation clips require id or name metadata.");
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
id,
|
|
1576
|
+
name: input.name ?? id,
|
|
1577
|
+
duration: input.duration ?? 1,
|
|
1578
|
+
frameRate: input.frameRate,
|
|
1579
|
+
loop: input.loop ?? true,
|
|
1580
|
+
tags: input.tags,
|
|
1581
|
+
tracks: input.tracks,
|
|
1582
|
+
events: input.events,
|
|
1583
|
+
layer: input.layer ?? defaultLayer,
|
|
1584
|
+
layerMetadata: input.layerMetadata ?? registry.layers?.find((layer) => layer.id === (input.layer ?? defaultLayer)),
|
|
1585
|
+
bones: input.bones,
|
|
1586
|
+
requiredBones: input.requiredBones,
|
|
1587
|
+
rootMotion: input.rootMotion ?? registry.rootMotion,
|
|
1588
|
+
suppressRootMotion: input.suppressRootMotion ?? registry.suppressRootMotion,
|
|
1589
|
+
restartFromFrameZero: input.restartFromFrameZero,
|
|
1590
|
+
attack: input.attack,
|
|
1591
|
+
eventSource: input.eventSource,
|
|
1592
|
+
retarget: input.retarget ?? registry.retarget,
|
|
1593
|
+
externalHumanoidLibrary: input.externalHumanoidLibrary ?? registry.externalHumanoidLibrary,
|
|
1594
|
+
pose: input.pose,
|
|
1595
|
+
fallbackPose: input.fallbackPose ?? extractPoseBakedFallback(registry.poseBakedFallback),
|
|
1596
|
+
metadata: {
|
|
1597
|
+
...input.metadata,
|
|
1598
|
+
embeddedGLB: true,
|
|
1599
|
+
assetId: registry.assetId,
|
|
1600
|
+
assetName: registry.assetName,
|
|
1601
|
+
source: "embedded-glb-clip-registry",
|
|
1602
|
+
durationSource: input.duration === undefined ? "defaulted" : "metadata"
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
function isAnimationAssetLike(value) {
|
|
1607
|
+
return isObject(value) && "metadata" in value;
|
|
1608
|
+
}
|
|
1609
|
+
function createFallbackSampler(clipId, tracks, fallbackPose, poseBakedFallback) {
|
|
1610
|
+
if (tracks.length > 0) {
|
|
1611
|
+
return (context) => samplePoseFromTracks(tracks, context.time, context.clip.duration, fallbackPose, clipId);
|
|
1612
|
+
}
|
|
1613
|
+
if (fallbackPose) {
|
|
1614
|
+
return () => {
|
|
1615
|
+
const pose = clonePose(fallbackPose) ?? emptyPose();
|
|
1616
|
+
return {
|
|
1617
|
+
...pose,
|
|
1618
|
+
bones: pose.bones,
|
|
1619
|
+
metadata: {
|
|
1620
|
+
...fallbackPose.metadata,
|
|
1621
|
+
poseBakedFallback: true,
|
|
1622
|
+
poseBakedFallbackMetadata: poseBakedFallback,
|
|
1623
|
+
sourceClipId: clipId
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
return () => emptyPose({ poseBakedFallback: true, poseBakedFallbackMetadata: poseBakedFallback, emptyTracks: true, sourceClipId: clipId });
|
|
1629
|
+
}
|
|
1630
|
+
function samplePoseFromTracks(tracks, time, duration, fallbackPose, clipId) {
|
|
1631
|
+
const pose = clonePose(fallbackPose) ?? emptyPose();
|
|
1632
|
+
const bones = { ...pose.bones };
|
|
1633
|
+
const morphTargets = { ...(pose.morphTargets ?? {}) };
|
|
1634
|
+
let sampledTracks = 0;
|
|
1635
|
+
for (const track of tracks) {
|
|
1636
|
+
const keyframes = [...(track.keyframes ?? [])].sort((a, b) => a.time - b.time);
|
|
1637
|
+
if (keyframes.length === 0)
|
|
1638
|
+
continue;
|
|
1639
|
+
const value = sampleKeyframes(keyframes, normalizeStateTime(time, duration, "loop"));
|
|
1640
|
+
const property = trackProperty(track);
|
|
1641
|
+
const boneName = boneNameFromTrackTarget(track.target) ?? track.target;
|
|
1642
|
+
if (property === "morph") {
|
|
1643
|
+
const numberValue = toFiniteNumber(value);
|
|
1644
|
+
if (numberValue !== undefined) {
|
|
1645
|
+
morphTargets[track.target] = numberValue;
|
|
1646
|
+
sampledTracks += 1;
|
|
1647
|
+
}
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const current = bones[boneName] ?? {};
|
|
1651
|
+
if (property === "translation" || property === "position") {
|
|
1652
|
+
const vector = toVector3(value);
|
|
1653
|
+
if (vector) {
|
|
1654
|
+
bones[boneName] = { ...current, position: vector };
|
|
1655
|
+
sampledTracks += 1;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
else if (property === "rotation") {
|
|
1659
|
+
const quaternion = toQuaternion(value);
|
|
1660
|
+
if (quaternion) {
|
|
1661
|
+
bones[boneName] = { ...current, rotation: quaternion };
|
|
1662
|
+
sampledTracks += 1;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
else if (property === "scale") {
|
|
1666
|
+
const vector = toVector3(value);
|
|
1667
|
+
if (vector) {
|
|
1668
|
+
bones[boneName] = { ...current, scale: vector };
|
|
1669
|
+
sampledTracks += 1;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return {
|
|
1674
|
+
bones,
|
|
1675
|
+
morphTargets,
|
|
1676
|
+
metadata: {
|
|
1677
|
+
...pose.metadata,
|
|
1678
|
+
poseBakedFallback: sampledTracks === 0,
|
|
1679
|
+
sampledTracks,
|
|
1680
|
+
sourceClipId: clipId
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
function sampleKeyframes(keyframes, time) {
|
|
1685
|
+
if (keyframes.length === 0)
|
|
1686
|
+
return undefined;
|
|
1687
|
+
if (time <= keyframes[0].time)
|
|
1688
|
+
return keyframes[0].value;
|
|
1689
|
+
const last = keyframes[keyframes.length - 1];
|
|
1690
|
+
if (time >= last.time)
|
|
1691
|
+
return last.value;
|
|
1692
|
+
for (let index = 0; index < keyframes.length - 1; index += 1) {
|
|
1693
|
+
const from = keyframes[index];
|
|
1694
|
+
const to = keyframes[index + 1];
|
|
1695
|
+
if (time < from.time || time > to.time)
|
|
1696
|
+
continue;
|
|
1697
|
+
const span = Math.max(0.000001, to.time - from.time);
|
|
1698
|
+
const alpha = (time - from.time) / span;
|
|
1699
|
+
return interpolateValue(from.value, to.value, alpha);
|
|
1700
|
+
}
|
|
1701
|
+
return last.value;
|
|
1702
|
+
}
|
|
1703
|
+
function interpolateValue(from, to, alpha) {
|
|
1704
|
+
const fromNumber = toFiniteNumber(from);
|
|
1705
|
+
const toNumber = toFiniteNumber(to);
|
|
1706
|
+
if (fromNumber !== undefined && toNumber !== undefined)
|
|
1707
|
+
return lerp(fromNumber, toNumber, alpha);
|
|
1708
|
+
const fromVector = toVector3(from);
|
|
1709
|
+
const toVector = toVector3(to);
|
|
1710
|
+
if (fromVector && toVector) {
|
|
1711
|
+
return {
|
|
1712
|
+
x: lerp(fromVector.x, toVector.x, alpha),
|
|
1713
|
+
y: lerp(fromVector.y, toVector.y, alpha),
|
|
1714
|
+
z: lerp(fromVector.z, toVector.z, alpha)
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
const fromQuaternion = toQuaternion(from);
|
|
1718
|
+
const toQuaternionValue = toQuaternion(to);
|
|
1719
|
+
if (fromQuaternion && toQuaternionValue) {
|
|
1720
|
+
return normalizeQuaternion({
|
|
1721
|
+
x: lerp(fromQuaternion.x, toQuaternionValue.x, alpha),
|
|
1722
|
+
y: lerp(fromQuaternion.y, toQuaternionValue.y, alpha),
|
|
1723
|
+
z: lerp(fromQuaternion.z, toQuaternionValue.z, alpha),
|
|
1724
|
+
w: lerp(fromQuaternion.w, toQuaternionValue.w, alpha)
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
return alpha < 0.5 ? from : to;
|
|
1728
|
+
}
|
|
1729
|
+
function advanceState(state, dt) {
|
|
1730
|
+
state.previousLocalTime = state.localTime;
|
|
1731
|
+
if (state.duration <= 0) {
|
|
1732
|
+
state.localTime = 0;
|
|
1733
|
+
state.normalizedTime = 0;
|
|
1734
|
+
return {
|
|
1735
|
+
loopsPassed: 0,
|
|
1736
|
+
completed: state.loopMode === "once"
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
const delta = dt * state.speed * state.inputDirection;
|
|
1740
|
+
state.direction = delta >= 0 ? 1 : -1;
|
|
1741
|
+
if (state.loopMode === "once") {
|
|
1742
|
+
const nextTime = clamp(state.localTime + delta, 0, state.duration);
|
|
1743
|
+
state.localTime = nextTime;
|
|
1744
|
+
state.playhead = nextTime;
|
|
1745
|
+
state.normalizedTime = state.duration > 0 ? state.localTime / state.duration : 0;
|
|
1746
|
+
return {
|
|
1747
|
+
loopsPassed: 0,
|
|
1748
|
+
completed: delta >= 0 ? nextTime >= state.duration : nextTime <= 0
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
const previousPlayhead = state.playhead;
|
|
1752
|
+
state.playhead += delta;
|
|
1753
|
+
const loopsPassed = Math.abs(loopIndex(state.playhead, state.duration) - loopIndex(previousPlayhead, state.duration));
|
|
1754
|
+
if (state.loopMode === "pingpong") {
|
|
1755
|
+
const cycle = loopIndex(state.playhead, state.duration);
|
|
1756
|
+
const local = positiveModulo(state.playhead, state.duration);
|
|
1757
|
+
state.localTime = cycle % 2 === 0 ? local : state.duration - local;
|
|
1758
|
+
state.direction = cycle % 2 === 0 ? state.direction : (state.direction === 1 ? -1 : 1);
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
state.localTime = positiveModulo(state.playhead, state.duration);
|
|
1762
|
+
}
|
|
1763
|
+
state.loopCount += loopsPassed;
|
|
1764
|
+
state.normalizedTime = state.duration > 0 ? state.localTime / state.duration : 0;
|
|
1765
|
+
return {
|
|
1766
|
+
loopsPassed,
|
|
1767
|
+
completed: false
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
function normalizeLoopMode(loop, clipLoop) {
|
|
1771
|
+
if (loop === true)
|
|
1772
|
+
return "loop";
|
|
1773
|
+
if (loop === false)
|
|
1774
|
+
return "once";
|
|
1775
|
+
return loop ?? (clipLoop ? "loop" : "once");
|
|
1776
|
+
}
|
|
1777
|
+
function normalizeStateTime(time, duration, loopMode) {
|
|
1778
|
+
if (!Number.isFinite(time) || duration <= 0)
|
|
1779
|
+
return 0;
|
|
1780
|
+
if (loopMode === "once")
|
|
1781
|
+
return clamp(time, 0, duration);
|
|
1782
|
+
return positiveModulo(time, duration);
|
|
1783
|
+
}
|
|
1784
|
+
function positiveModulo(value, modulus) {
|
|
1785
|
+
if (modulus <= 0)
|
|
1786
|
+
return 0;
|
|
1787
|
+
return ((value % modulus) + modulus) % modulus;
|
|
1788
|
+
}
|
|
1789
|
+
function loopIndex(value, duration) {
|
|
1790
|
+
if (duration <= 0)
|
|
1791
|
+
return 0;
|
|
1792
|
+
return Math.floor(value / duration);
|
|
1793
|
+
}
|
|
1794
|
+
function sanitizeDuration(duration) {
|
|
1795
|
+
if (!Number.isFinite(duration) || duration < 0)
|
|
1796
|
+
return 0;
|
|
1797
|
+
return duration;
|
|
1798
|
+
}
|
|
1799
|
+
function sanitizeSpeed(speed) {
|
|
1800
|
+
if (!Number.isFinite(speed))
|
|
1801
|
+
return 1;
|
|
1802
|
+
return speed;
|
|
1803
|
+
}
|
|
1804
|
+
function sanitizeWeight(weight) {
|
|
1805
|
+
if (!Number.isFinite(weight))
|
|
1806
|
+
return 1;
|
|
1807
|
+
return clamp(weight, 0, 1);
|
|
1808
|
+
}
|
|
1809
|
+
function clamp(value, min, max) {
|
|
1810
|
+
return Math.min(max, Math.max(min, value));
|
|
1811
|
+
}
|
|
1812
|
+
function lerp(from, to, alpha) {
|
|
1813
|
+
return from + (to - from) * alpha;
|
|
1814
|
+
}
|
|
1815
|
+
function cloneState(state) {
|
|
1816
|
+
const layerWeight = state.layerWeight;
|
|
1817
|
+
return {
|
|
1818
|
+
id: state.id,
|
|
1819
|
+
clipId: state.clipId,
|
|
1820
|
+
status: state.status,
|
|
1821
|
+
localTime: state.localTime,
|
|
1822
|
+
previousLocalTime: state.previousLocalTime,
|
|
1823
|
+
normalizedTime: state.normalizedTime,
|
|
1824
|
+
duration: state.duration,
|
|
1825
|
+
speed: state.speed,
|
|
1826
|
+
weight: state.weight,
|
|
1827
|
+
targetWeight: state.targetWeight,
|
|
1828
|
+
effectiveWeight: state.weight * layerWeight,
|
|
1829
|
+
layer: state.layer,
|
|
1830
|
+
layerMetadata: state.layerMetadata ? cloneLayerMetadata(state.layerMetadata) : undefined,
|
|
1831
|
+
layerWeight,
|
|
1832
|
+
loopMode: state.loopMode,
|
|
1833
|
+
loopCount: state.loopCount,
|
|
1834
|
+
direction: state.direction,
|
|
1835
|
+
completed: state.completed,
|
|
1836
|
+
rootMotionSuppressed: state.rootMotionSuppressed,
|
|
1837
|
+
restartFromFrameZero: state.restartFromFrameZero,
|
|
1838
|
+
eventSource: state.eventSource,
|
|
1839
|
+
poseBakedFallback: state.poseBakedFallback ? { ...state.poseBakedFallback } : undefined,
|
|
1840
|
+
metadata: state.metadata ? { ...state.metadata } : undefined,
|
|
1841
|
+
fade: state.fade ? { ...state.fade } : undefined
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
function effectiveWeight(state) {
|
|
1845
|
+
return state.weight * state.layerWeight;
|
|
1846
|
+
}
|
|
1847
|
+
function shouldSuppressRootMotion(options, clip, controllerSuppressRootMotion) {
|
|
1848
|
+
return options.suppressRootMotion ?? clip.suppressRootMotion ?? clip.rootMotion?.suppress ?? controllerSuppressRootMotion;
|
|
1849
|
+
}
|
|
1850
|
+
function suppressPoseRootMotion(pose, metadata) {
|
|
1851
|
+
const cloned = clonePose(pose) ?? emptyPose();
|
|
1852
|
+
return {
|
|
1853
|
+
...cloned,
|
|
1854
|
+
rootMotion: undefined,
|
|
1855
|
+
metadata: {
|
|
1856
|
+
...cloned.metadata,
|
|
1857
|
+
rootMotionSuppressed: true,
|
|
1858
|
+
rootMotionPolicy: "suppressed",
|
|
1859
|
+
rootMotionTrack: metadata?.track,
|
|
1860
|
+
rootMotionBone: metadata?.bone,
|
|
1861
|
+
rootMotionReason: metadata?.reason
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
function isAnimationPose(value) {
|
|
1866
|
+
return isObject(value) && isObject(value.bones);
|
|
1867
|
+
}
|
|
1868
|
+
function emptyPose(metadata = {}) {
|
|
1869
|
+
return {
|
|
1870
|
+
bones: {},
|
|
1871
|
+
morphTargets: {},
|
|
1872
|
+
metadata
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function createIdentityPose(skeleton) {
|
|
1876
|
+
const bones = skeletonBoneNames(skeleton);
|
|
1877
|
+
if (bones.size === 0)
|
|
1878
|
+
return undefined;
|
|
1879
|
+
const poseBones = {};
|
|
1880
|
+
for (const bone of bones) {
|
|
1881
|
+
poseBones[bone] = {
|
|
1882
|
+
position: { x: 0, y: 0, z: 0 },
|
|
1883
|
+
rotation: { x: 0, y: 0, z: 0, w: 1 },
|
|
1884
|
+
scale: { x: 1, y: 1, z: 1 }
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
bones: poseBones,
|
|
1889
|
+
morphTargets: {},
|
|
1890
|
+
metadata: {
|
|
1891
|
+
poseBakedFallback: true,
|
|
1892
|
+
source: "skeleton-bind-pose"
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
function clonePose(pose) {
|
|
1897
|
+
if (!pose)
|
|
1898
|
+
return undefined;
|
|
1899
|
+
const bones = {};
|
|
1900
|
+
for (const [name, transform] of Object.entries(pose.bones ?? {})) {
|
|
1901
|
+
bones[name] = {
|
|
1902
|
+
position: transform.position ? { ...transform.position } : undefined,
|
|
1903
|
+
rotation: transform.rotation ? { ...transform.rotation } : undefined,
|
|
1904
|
+
scale: transform.scale ? { ...transform.scale } : undefined
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
return {
|
|
1908
|
+
bones,
|
|
1909
|
+
morphTargets: pose.morphTargets ? { ...pose.morphTargets } : undefined,
|
|
1910
|
+
rootMotion: pose.rootMotion
|
|
1911
|
+
? {
|
|
1912
|
+
translation: pose.rootMotion.translation ? { ...pose.rootMotion.translation } : undefined,
|
|
1913
|
+
rotation: pose.rootMotion.rotation ? { ...pose.rootMotion.rotation } : undefined
|
|
1914
|
+
}
|
|
1915
|
+
: undefined,
|
|
1916
|
+
metadata: pose.metadata ? { ...pose.metadata } : undefined
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
function extractPoseBakedFallback(value) {
|
|
1920
|
+
if (!value || value === true)
|
|
1921
|
+
return undefined;
|
|
1922
|
+
if (isAnimationPose(value))
|
|
1923
|
+
return value;
|
|
1924
|
+
if (isObject(value) && isAnimationPose(value.pose)) {
|
|
1925
|
+
const pose = clonePose(value.pose);
|
|
1926
|
+
return pose
|
|
1927
|
+
? {
|
|
1928
|
+
...pose,
|
|
1929
|
+
metadata: {
|
|
1930
|
+
...pose.metadata,
|
|
1931
|
+
poseBakedFallback: true,
|
|
1932
|
+
poseBakedFallbackSource: value.source,
|
|
1933
|
+
poseBakedFallbackReason: value.reason
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
: undefined;
|
|
1937
|
+
}
|
|
1938
|
+
return undefined;
|
|
1939
|
+
}
|
|
1940
|
+
function extractPoseBakedFallbackRuntimeMetadata(value, fallback) {
|
|
1941
|
+
if (!value)
|
|
1942
|
+
return undefined;
|
|
1943
|
+
if (value === true || isAnimationPose(value))
|
|
1944
|
+
return fallback;
|
|
1945
|
+
if (isObject(value)) {
|
|
1946
|
+
const metadata = value;
|
|
1947
|
+
return {
|
|
1948
|
+
...fallback,
|
|
1949
|
+
source: metadata.source ?? fallback.source,
|
|
1950
|
+
reason: metadata.reason ?? fallback.reason
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
return fallback;
|
|
1954
|
+
}
|
|
1955
|
+
function createPoseBakedFallbackRuntimeMetadata(clipId, definition, registry, controllerFallback, enabled) {
|
|
1956
|
+
if (!enabled)
|
|
1957
|
+
return undefined;
|
|
1958
|
+
const explicit = extractPoseBakedFallbackRuntimeMetadata(definition.metadata?.poseBakedFallback, {
|
|
1959
|
+
enabled: true,
|
|
1960
|
+
source: "clip-metadata",
|
|
1961
|
+
sourceClipId: clipId,
|
|
1962
|
+
sourceAssetId: registry?.assetId,
|
|
1963
|
+
sourceAssetName: registry?.assetName,
|
|
1964
|
+
fallbackKind: "clip-metadata"
|
|
1965
|
+
});
|
|
1966
|
+
if (explicit)
|
|
1967
|
+
return explicit;
|
|
1968
|
+
if (definition.pose) {
|
|
1969
|
+
return {
|
|
1970
|
+
enabled: true,
|
|
1971
|
+
source: "clip-pose",
|
|
1972
|
+
sourceClipId: clipId,
|
|
1973
|
+
sourceAssetId: registry?.assetId,
|
|
1974
|
+
sourceAssetName: registry?.assetName,
|
|
1975
|
+
fallbackKind: "clip-pose"
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
if (definition.fallbackPose) {
|
|
1979
|
+
return {
|
|
1980
|
+
enabled: true,
|
|
1981
|
+
source: "clip-fallback-pose",
|
|
1982
|
+
sourceClipId: clipId,
|
|
1983
|
+
sourceAssetId: registry?.assetId,
|
|
1984
|
+
sourceAssetName: registry?.assetName,
|
|
1985
|
+
fallbackKind: "clip-fallback-pose"
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
if (controllerFallback) {
|
|
1989
|
+
return {
|
|
1990
|
+
...controllerFallback,
|
|
1991
|
+
enabled: true,
|
|
1992
|
+
sourceClipId: clipId,
|
|
1993
|
+
sourceAssetId: controllerFallback.sourceAssetId ?? registry?.assetId,
|
|
1994
|
+
sourceAssetName: controllerFallback.sourceAssetName ?? registry?.assetName
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
return {
|
|
1998
|
+
enabled: true,
|
|
1999
|
+
source: "skeleton-bind-pose",
|
|
2000
|
+
sourceClipId: clipId,
|
|
2001
|
+
sourceAssetId: registry?.assetId,
|
|
2002
|
+
sourceAssetName: registry?.assetName,
|
|
2003
|
+
fallbackKind: "skeleton-bind-pose"
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
function skeletonBoneNames(skeleton) {
|
|
2007
|
+
const names = new Set();
|
|
2008
|
+
for (const bone of skeleton?.bones ?? []) {
|
|
2009
|
+
if (typeof bone === "string" && bone.trim())
|
|
2010
|
+
names.add(bone);
|
|
2011
|
+
if (isObject(bone) && typeof bone.name === "string" && bone.name.trim())
|
|
2012
|
+
names.add(bone.name);
|
|
2013
|
+
}
|
|
2014
|
+
if (skeleton?.rootBone)
|
|
2015
|
+
names.add(skeleton.rootBone);
|
|
2016
|
+
return names;
|
|
2017
|
+
}
|
|
2018
|
+
function boneNameFromTrackTarget(target) {
|
|
2019
|
+
if (!target.trim())
|
|
2020
|
+
return undefined;
|
|
2021
|
+
const normalized = target.replace(/^\//, "").replace(/\//g, ".");
|
|
2022
|
+
const parts = normalized.split(".").filter(Boolean);
|
|
2023
|
+
if (parts.length === 0)
|
|
2024
|
+
return undefined;
|
|
2025
|
+
if (parts[0] === "materials" || parts[0] === "material" || parts[0] === "morphTargets" || parts[0] === "weights")
|
|
2026
|
+
return undefined;
|
|
2027
|
+
if (parts.length === 1) {
|
|
2028
|
+
return isKnownTransformProperty(parts[0]) ? undefined : parts[0];
|
|
2029
|
+
}
|
|
2030
|
+
if (parts[0] === "bones" || parts[0] === "nodes")
|
|
2031
|
+
return parts[1];
|
|
2032
|
+
const last = parts[parts.length - 1];
|
|
2033
|
+
if (isKnownTransformProperty(last))
|
|
2034
|
+
return parts.slice(0, -1).join(".");
|
|
2035
|
+
return parts[0];
|
|
2036
|
+
}
|
|
2037
|
+
function trackProperty(track) {
|
|
2038
|
+
switch (track.property) {
|
|
2039
|
+
case "translation":
|
|
2040
|
+
return "translation";
|
|
2041
|
+
case "rotation":
|
|
2042
|
+
return "rotation";
|
|
2043
|
+
case "scale":
|
|
2044
|
+
return "scale";
|
|
2045
|
+
case "morph":
|
|
2046
|
+
return "morph";
|
|
2047
|
+
}
|
|
2048
|
+
const target = track.target.toLowerCase();
|
|
2049
|
+
if (target.endsWith(".position"))
|
|
2050
|
+
return "position";
|
|
2051
|
+
if (target.endsWith(".translation"))
|
|
2052
|
+
return "translation";
|
|
2053
|
+
if (target.endsWith(".rotation") || target.endsWith(".quaternion"))
|
|
2054
|
+
return "rotation";
|
|
2055
|
+
if (target.endsWith(".scale"))
|
|
2056
|
+
return "scale";
|
|
2057
|
+
if (target.includes("morph") || target.includes("weights"))
|
|
2058
|
+
return "morph";
|
|
2059
|
+
return "unknown";
|
|
2060
|
+
}
|
|
2061
|
+
function isKnownTransformProperty(value) {
|
|
2062
|
+
return [
|
|
2063
|
+
"translation",
|
|
2064
|
+
"position",
|
|
2065
|
+
"rotation",
|
|
2066
|
+
"quaternion",
|
|
2067
|
+
"scale",
|
|
2068
|
+
"weights",
|
|
2069
|
+
"morph",
|
|
2070
|
+
"visibility",
|
|
2071
|
+
"material"
|
|
2072
|
+
].includes(value);
|
|
2073
|
+
}
|
|
2074
|
+
function createBoneAccumulator() {
|
|
2075
|
+
return {
|
|
2076
|
+
positionWeight: 0,
|
|
2077
|
+
rotationWeight: 0,
|
|
2078
|
+
scaleWeight: 0
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
function accumulateTransform(accumulator, transform, weight) {
|
|
2082
|
+
if (weight <= 0)
|
|
2083
|
+
return;
|
|
2084
|
+
if (transform.position) {
|
|
2085
|
+
accumulator.position = accumulator.position ?? { x: 0, y: 0, z: 0 };
|
|
2086
|
+
accumulator.position.x += transform.position.x * weight;
|
|
2087
|
+
accumulator.position.y += transform.position.y * weight;
|
|
2088
|
+
accumulator.position.z += transform.position.z * weight;
|
|
2089
|
+
accumulator.positionWeight += weight;
|
|
2090
|
+
}
|
|
2091
|
+
if (transform.rotation) {
|
|
2092
|
+
accumulator.rotation = accumulator.rotation ?? { x: 0, y: 0, z: 0, w: 0 };
|
|
2093
|
+
accumulator.rotation.x += transform.rotation.x * weight;
|
|
2094
|
+
accumulator.rotation.y += transform.rotation.y * weight;
|
|
2095
|
+
accumulator.rotation.z += transform.rotation.z * weight;
|
|
2096
|
+
accumulator.rotation.w += transform.rotation.w * weight;
|
|
2097
|
+
accumulator.rotationWeight += weight;
|
|
2098
|
+
}
|
|
2099
|
+
if (transform.scale) {
|
|
2100
|
+
accumulator.scale = accumulator.scale ?? { x: 0, y: 0, z: 0 };
|
|
2101
|
+
accumulator.scale.x += transform.scale.x * weight;
|
|
2102
|
+
accumulator.scale.y += transform.scale.y * weight;
|
|
2103
|
+
accumulator.scale.z += transform.scale.z * weight;
|
|
2104
|
+
accumulator.scaleWeight += weight;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function resolveAccumulator(accumulator) {
|
|
2108
|
+
return {
|
|
2109
|
+
position: accumulator.position && accumulator.positionWeight > 0
|
|
2110
|
+
? {
|
|
2111
|
+
x: accumulator.position.x / accumulator.positionWeight,
|
|
2112
|
+
y: accumulator.position.y / accumulator.positionWeight,
|
|
2113
|
+
z: accumulator.position.z / accumulator.positionWeight
|
|
2114
|
+
}
|
|
2115
|
+
: undefined,
|
|
2116
|
+
rotation: accumulator.rotation && accumulator.rotationWeight > 0
|
|
2117
|
+
? normalizeQuaternion({
|
|
2118
|
+
x: accumulator.rotation.x / accumulator.rotationWeight,
|
|
2119
|
+
y: accumulator.rotation.y / accumulator.rotationWeight,
|
|
2120
|
+
z: accumulator.rotation.z / accumulator.rotationWeight,
|
|
2121
|
+
w: accumulator.rotation.w / accumulator.rotationWeight
|
|
2122
|
+
})
|
|
2123
|
+
: undefined,
|
|
2124
|
+
scale: accumulator.scale && accumulator.scaleWeight > 0
|
|
2125
|
+
? {
|
|
2126
|
+
x: accumulator.scale.x / accumulator.scaleWeight,
|
|
2127
|
+
y: accumulator.scale.y / accumulator.scaleWeight,
|
|
2128
|
+
z: accumulator.scale.z / accumulator.scaleWeight
|
|
2129
|
+
}
|
|
2130
|
+
: undefined
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
function toVector3(value) {
|
|
2134
|
+
if (Array.isArray(value) && value.length >= 3) {
|
|
2135
|
+
const [x, y, z] = value.map(Number);
|
|
2136
|
+
if ([x, y, z].every(Number.isFinite))
|
|
2137
|
+
return { x, y, z };
|
|
2138
|
+
}
|
|
2139
|
+
if (isObject(value)) {
|
|
2140
|
+
const x = Number(value.x);
|
|
2141
|
+
const y = Number(value.y);
|
|
2142
|
+
const z = Number(value.z);
|
|
2143
|
+
if ([x, y, z].every(Number.isFinite))
|
|
2144
|
+
return { x, y, z };
|
|
2145
|
+
}
|
|
2146
|
+
return undefined;
|
|
2147
|
+
}
|
|
2148
|
+
function toQuaternion(value) {
|
|
2149
|
+
if (Array.isArray(value) && value.length >= 4) {
|
|
2150
|
+
const [x, y, z, w] = value.map(Number);
|
|
2151
|
+
if ([x, y, z, w].every(Number.isFinite))
|
|
2152
|
+
return normalizeQuaternion({ x, y, z, w });
|
|
2153
|
+
}
|
|
2154
|
+
if (isObject(value)) {
|
|
2155
|
+
const x = Number(value.x);
|
|
2156
|
+
const y = Number(value.y);
|
|
2157
|
+
const z = Number(value.z);
|
|
2158
|
+
const w = Number(value.w);
|
|
2159
|
+
if ([x, y, z, w].every(Number.isFinite))
|
|
2160
|
+
return normalizeQuaternion({ x, y, z, w });
|
|
2161
|
+
}
|
|
2162
|
+
return undefined;
|
|
2163
|
+
}
|
|
2164
|
+
function normalizeQuaternion(quaternion) {
|
|
2165
|
+
const length = Math.hypot(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
|
|
2166
|
+
if (length <= 0.000001)
|
|
2167
|
+
return { x: 0, y: 0, z: 0, w: 1 };
|
|
2168
|
+
return {
|
|
2169
|
+
x: quaternion.x / length,
|
|
2170
|
+
y: quaternion.y / length,
|
|
2171
|
+
z: quaternion.z / length,
|
|
2172
|
+
w: quaternion.w / length
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
function toFiniteNumber(value) {
|
|
2176
|
+
const number = Number(value);
|
|
2177
|
+
return Number.isFinite(number) ? number : undefined;
|
|
2178
|
+
}
|
|
2179
|
+
function createDiagnostic(severity, code, message, clipId, bone, trackId) {
|
|
2180
|
+
return {
|
|
2181
|
+
severity,
|
|
2182
|
+
code,
|
|
2183
|
+
message,
|
|
2184
|
+
clipId,
|
|
2185
|
+
bone,
|
|
2186
|
+
trackId
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
function isObject(value) {
|
|
2190
|
+
return typeof value === "object" && value !== null;
|
|
2191
|
+
}
|
|
2192
|
+
//# sourceMappingURL=AnimationController.js.map
|