@aura3d/animation 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AnimationClipEvents.d.ts +55 -0
- package/dist/AnimationClipEvents.d.ts.map +1 -1
- package/dist/AnimationClipEvents.js +136 -0
- package/dist/AnimationClipEvents.js.map +1 -1
- package/dist/AnimationClipRegistry.d.ts +9 -3
- package/dist/AnimationClipRegistry.d.ts.map +1 -1
- package/dist/AnimationClipRegistry.js +52 -5
- package/dist/AnimationClipRegistry.js.map +1 -1
- package/dist/AnimationController.d.ts +6 -6
- package/dist/AnimationController.d.ts.map +1 -1
- package/dist/AnimationController.js +3 -3
- package/dist/AnimationController.js.map +1 -1
- package/dist/AnimationMixer.d.ts +10 -0
- package/dist/AnimationMixer.d.ts.map +1 -1
- package/dist/AnimationMixer.js +35 -0
- package/dist/AnimationMixer.js.map +1 -1
- package/dist/AnimationStateGraph.d.ts +14 -5
- package/dist/AnimationStateGraph.d.ts.map +1 -1
- package/dist/AnimationStateGraph.js +25 -2
- package/dist/AnimationStateGraph.js.map +1 -1
- package/dist/AnimationStateMachine.d.ts +26 -0
- package/dist/AnimationStateMachine.d.ts.map +1 -1
- package/dist/AnimationStateMachine.js +32 -0
- package/dist/AnimationStateMachine.js.map +1 -1
- package/dist/Bone.d.ts +4 -0
- package/dist/Bone.d.ts.map +1 -1
- package/dist/Bone.js +3 -0
- package/dist/Bone.js.map +1 -1
- package/dist/FighterAnimationAdapter.d.ts +49 -0
- package/dist/FighterAnimationAdapter.d.ts.map +1 -0
- package/dist/FighterAnimationAdapter.js +71 -0
- package/dist/FighterAnimationAdapter.js.map +1 -0
- package/dist/FootIk.d.ts +77 -0
- package/dist/FootIk.d.ts.map +1 -0
- package/dist/FootIk.js +153 -0
- package/dist/FootIk.js.map +1 -0
- package/dist/HumanoidBoneInference.d.ts +61 -0
- package/dist/HumanoidBoneInference.d.ts.map +1 -0
- package/dist/HumanoidBoneInference.js +239 -0
- package/dist/HumanoidBoneInference.js.map +1 -0
- package/dist/HumanoidRetargeting.d.ts +48 -4
- package/dist/HumanoidRetargeting.d.ts.map +1 -1
- package/dist/HumanoidRetargeting.js +149 -14
- package/dist/HumanoidRetargeting.js.map +1 -1
- package/dist/IK.d.ts +5 -0
- package/dist/IK.d.ts.map +1 -1
- package/dist/IK.js +9 -0
- package/dist/IK.js.map +1 -1
- package/dist/Inertialization.d.ts +65 -0
- package/dist/Inertialization.d.ts.map +1 -0
- package/dist/Inertialization.js +211 -0
- package/dist/Inertialization.js.map +1 -0
- package/dist/LocomotionController.d.ts +5 -0
- package/dist/LocomotionController.d.ts.map +1 -1
- package/dist/LocomotionController.js +3 -0
- package/dist/LocomotionController.js.map +1 -1
- package/dist/LocomotionKit.d.ts +52 -0
- package/dist/LocomotionKit.d.ts.map +1 -0
- package/dist/LocomotionKit.js +38 -0
- package/dist/LocomotionKit.js.map +1 -0
- package/dist/MotionQuality.d.ts +3 -3
- package/dist/MotionQuality.d.ts.map +1 -1
- package/dist/MotionQuality.js +2 -2
- package/dist/MotionQuality.js.map +1 -1
- package/dist/RigQuality.d.ts +31 -0
- package/dist/RigQuality.d.ts.map +1 -0
- package/dist/RigQuality.js +70 -0
- package/dist/RigQuality.js.map +1 -0
- package/dist/SecondaryAnimationFixtures.d.ts.map +1 -1
- package/dist/SecondaryAnimationFixtures.js +1 -3
- package/dist/SecondaryAnimationFixtures.js.map +1 -1
- package/dist/Skeleton.d.ts +16 -1
- package/dist/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton.js +42 -0
- package/dist/Skeleton.js.map +1 -1
- package/dist/SpringBones.d.ts +60 -0
- package/dist/SpringBones.d.ts.map +1 -0
- package/dist/SpringBones.js +173 -0
- package/dist/SpringBones.js.map +1 -0
- package/dist/browser-index.d.ts +12 -3
- package/dist/browser-index.d.ts.map +1 -1
- package/dist/browser-index.js +11 -2
- package/dist/browser-index.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/library/performanceStateGraph.d.ts +42 -0
- package/dist/library/performanceStateGraph.d.ts.map +1 -0
- package/dist/library/performanceStateGraph.js +101 -0
- package/dist/library/performanceStateGraph.js.map +1 -0
- package/dist/library/standardHumanoidClips.d.ts +27 -0
- package/dist/library/standardHumanoidClips.d.ts.map +1 -0
- package/dist/library/standardHumanoidClips.js +357 -0
- package/dist/library/standardHumanoidClips.js.map +1 -0
- package/package.json +3 -3
- package/src/AnimationClipEvents.ts +181 -0
- package/src/AnimationClipRegistry.ts +65 -9
- package/src/AnimationController.ts +11 -11
- package/src/AnimationMixer.ts +36 -0
- package/src/AnimationStateGraph.ts +34 -5
- package/src/AnimationStateMachine.ts +48 -0
- package/src/Bone.ts +5 -0
- package/src/FighterAnimationAdapter.ts +124 -0
- package/src/FootIk.ts +251 -0
- package/src/HumanoidBoneInference.ts +323 -0
- package/src/HumanoidRetargeting.ts +180 -19
- package/src/IK.ts +10 -0
- package/src/Inertialization.ts +269 -0
- package/src/LocomotionController.ts +6 -0
- package/src/LocomotionKit.ts +89 -0
- package/src/MotionQuality.ts +5 -5
- package/src/RigQuality.ts +101 -0
- package/src/SecondaryAnimationFixtures.ts +1 -3
- package/src/Skeleton.ts +45 -1
- package/src/SpringBones.ts +243 -0
- package/src/browser-index.ts +13 -4
- package/src/index.ts +12 -4
- package/src/library/performanceStateGraph.ts +117 -0
- package/src/library/standardHumanoidClips.ts +442 -0
- package/src/Retargeting.ts +0 -29
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { BlendTree1D, type BlendTreeWeight } from "./BlendTree.js";
|
|
2
|
+
import { createLocomotionAnimationStateGraph } from "./AnimationStateGraph.js";
|
|
3
|
+
import type { AnimationStateBlend, AnimationStateMachine } from "./AnimationStateMachine.js";
|
|
4
|
+
import type { FootIkRig } from "./FootIk.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Composable locomotion kit: maps a scalar movement speed into a discrete locomotion state
|
|
8
|
+
* (idle/walk/run) plus blended clip weights, using the real `BlendTree1D` and
|
|
9
|
+
* `createLocomotionAnimationStateGraph` primitives. This is the deterministic, browser-free
|
|
10
|
+
* core a `character-controller` template wires to a physics `CharacterController`, input, and
|
|
11
|
+
* an `AnimationController` crossfade.
|
|
12
|
+
*/
|
|
13
|
+
export interface LocomotionKitOptions<TClipId extends string = string> {
|
|
14
|
+
readonly idleClip: TClipId;
|
|
15
|
+
readonly walkClip: TClipId;
|
|
16
|
+
readonly runClip: TClipId;
|
|
17
|
+
/** Speed at which the walk clip reaches full weight (default 1). */
|
|
18
|
+
readonly walkSpeed?: number;
|
|
19
|
+
/** Speed at which the run clip reaches full weight (default 4). */
|
|
20
|
+
readonly runSpeed?: number;
|
|
21
|
+
/** Speed above which the character is considered moving (default 0.05). */
|
|
22
|
+
readonly movingThreshold?: number;
|
|
23
|
+
/** Speed at/above which the character is considered running (default = runSpeed, so state stays coherent with the blend). */
|
|
24
|
+
readonly runningThreshold?: number;
|
|
25
|
+
/** Optional foot-IK rig so locomotion can ground feet on uneven terrain (no foot sliding). */
|
|
26
|
+
readonly footIkRig?: FootIkRig;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LocomotionKitSample<TClipId extends string = string> {
|
|
30
|
+
readonly speed: number;
|
|
31
|
+
readonly state: string;
|
|
32
|
+
readonly moving: boolean;
|
|
33
|
+
readonly running: boolean;
|
|
34
|
+
readonly clipWeights: ReadonlyArray<{ readonly clip: TClipId; readonly weight: number }>;
|
|
35
|
+
/**
|
|
36
|
+
* Inertialized blend between the previously-active discrete state and the current one. When the
|
|
37
|
+
* state graph switches (e.g. walk→run), the previous state's clip fades out on a critically-damped
|
|
38
|
+
* curve instead of snapping — momentum-preserving idle↔walk↔run transitions.
|
|
39
|
+
*/
|
|
40
|
+
readonly stateTransition: AnimationStateBlend;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LocomotionKit<TClipId extends string = string> {
|
|
44
|
+
readonly graph: AnimationStateMachine;
|
|
45
|
+
/** Optional foot-IK rig hook; call `footIk.solveFootPlacement(...)` to ground feet. */
|
|
46
|
+
readonly footIk: FootIkRig | undefined;
|
|
47
|
+
/** Blend clip weights for a speed (sum ~= 1), independent of the discrete state graph. */
|
|
48
|
+
blendWeights(speed: number): ReadonlyArray<BlendTreeWeight<TClipId>>;
|
|
49
|
+
/** Advance the state graph and produce the full locomotion sample for a speed. */
|
|
50
|
+
sample(speed: number, delta?: number): LocomotionKitSample<TClipId>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createLocomotionKit<TClipId extends string = string>(options: LocomotionKitOptions<TClipId>): LocomotionKit<TClipId> {
|
|
54
|
+
const walkSpeed = options.walkSpeed ?? 1;
|
|
55
|
+
const runSpeed = options.runSpeed ?? 4;
|
|
56
|
+
const movingThreshold = options.movingThreshold ?? 0.05;
|
|
57
|
+
const runningThreshold = options.runningThreshold ?? runSpeed;
|
|
58
|
+
|
|
59
|
+
const tree = new BlendTree1D<TClipId>([
|
|
60
|
+
{ value: options.idleClip, threshold: 0 },
|
|
61
|
+
{ value: options.walkClip, threshold: walkSpeed },
|
|
62
|
+
{ value: options.runClip, threshold: runSpeed }
|
|
63
|
+
]);
|
|
64
|
+
const graph = createLocomotionAnimationStateGraph();
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
graph,
|
|
68
|
+
footIk: options.footIkRig,
|
|
69
|
+
blendWeights(speed: number) {
|
|
70
|
+
return tree.weights(Math.max(0, speed));
|
|
71
|
+
},
|
|
72
|
+
sample(speed: number, delta = 1 / 30): LocomotionKitSample<TClipId> {
|
|
73
|
+
const clamped = Math.max(0, speed);
|
|
74
|
+
const moving = clamped > movingThreshold;
|
|
75
|
+
const running = clamped >= runningThreshold;
|
|
76
|
+
graph.setParameter("isMoving", moving);
|
|
77
|
+
graph.setParameter("isRunning", running);
|
|
78
|
+
const state = graph.update(delta);
|
|
79
|
+
return {
|
|
80
|
+
speed: clamped,
|
|
81
|
+
state,
|
|
82
|
+
moving,
|
|
83
|
+
running,
|
|
84
|
+
clipWeights: tree.weights(clamped).map((weight) => ({ clip: weight.value, weight: weight.weight })),
|
|
85
|
+
stateTransition: graph.stateBlend()
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
package/src/MotionQuality.ts
CHANGED
|
@@ -17,8 +17,8 @@ export interface AnimationMotionQualityReport {
|
|
|
17
17
|
readonly healthy: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export interface
|
|
21
|
-
readonly kind: "
|
|
20
|
+
export interface AnimationAnimationMotionQualityReport extends AnimationMotionQualityReport {
|
|
21
|
+
readonly kind: "animation-animation-motion-quality";
|
|
22
22
|
readonly staticPoseRejected: boolean;
|
|
23
23
|
readonly issues: readonly string[];
|
|
24
24
|
}
|
|
@@ -103,10 +103,10 @@ export function summarizeAnimationMotion(
|
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
export function
|
|
106
|
+
export function summarizeAnimationAnimationMotion(
|
|
107
107
|
samples: readonly AnimationMotionSample[],
|
|
108
108
|
options: Pick<AnimationMotionQualityOptions, "minimumSamples" | "minimumTimeRangeSeconds" | "minimumPoseDiversityScore"> = {}
|
|
109
|
-
):
|
|
109
|
+
): AnimationAnimationMotionQualityReport {
|
|
110
110
|
const base = summarizeAnimationMotion(samples, options);
|
|
111
111
|
const issues = [
|
|
112
112
|
base.sampleCount < (options.minimumSamples ?? 8) ? "not enough motion samples" : undefined,
|
|
@@ -116,7 +116,7 @@ export function summarizeCartoonAnimationMotion(
|
|
|
116
116
|
].filter((issue): issue is string => Boolean(issue));
|
|
117
117
|
return {
|
|
118
118
|
...base,
|
|
119
|
-
kind: "
|
|
119
|
+
kind: "animation-animation-motion-quality",
|
|
120
120
|
staticPoseRejected: issues.length > 0,
|
|
121
121
|
issues,
|
|
122
122
|
healthy: issues.length === 0
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HUMANOID_BONES,
|
|
3
|
+
type HumanoidBoneName,
|
|
4
|
+
type HumanoidRigDefinition
|
|
5
|
+
} from "./HumanoidRetargeting.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Body-acting suitability grade for an (inferred) humanoid rig.
|
|
9
|
+
*
|
|
10
|
+
* - **A** — full humanoid: spine/neck/head, both arms with forearms + hands, both legs with lower
|
|
11
|
+
* legs + feet. Safe for the full performance vocabulary (gesture, point, walk/run, foot work).
|
|
12
|
+
* - **B** — partial humanoid: enough of the body to act (spine/head + both arms or both legs) but
|
|
13
|
+
* missing limbs/extremities; the director should avoid the motions that need the missing chain.
|
|
14
|
+
* - **C** — mascot / sparse (~8 mappable nodes): a torso + head + stub arms; head/torso acting only,
|
|
15
|
+
* no real limb motion.
|
|
16
|
+
* - **D** — not suitable for body acting: no usable skeleton (root/props only).
|
|
17
|
+
*/
|
|
18
|
+
export type RigGrade = "A" | "B" | "C" | "D";
|
|
19
|
+
|
|
20
|
+
export interface RigQualityReport {
|
|
21
|
+
readonly grade: RigGrade;
|
|
22
|
+
/** Human-readable justifications for the grade (what was present / missing). */
|
|
23
|
+
readonly reasons: string[];
|
|
24
|
+
readonly hasLegs: boolean;
|
|
25
|
+
readonly hasKnees: boolean;
|
|
26
|
+
readonly hasAnkles: boolean;
|
|
27
|
+
readonly hasFeet: boolean;
|
|
28
|
+
/** Count of canonical humanoid bones that resolve to a real node. */
|
|
29
|
+
readonly mappedBoneCount: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A bone counts as mapped when it resolves to a non-empty target node name. */
|
|
33
|
+
function isMapped(rig: HumanoidRigDefinition, bone: HumanoidBoneName): boolean {
|
|
34
|
+
const binding = rig.bones[bone];
|
|
35
|
+
return Boolean(binding && typeof binding.name === "string" && binding.name.length > 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Grade a rig for body-acting suitability. Pure + deterministic — driven only by which canonical
|
|
40
|
+
* {@link HUMANOID_BONES} resolve to nodes, so it works on the output of `inferHumanoidRig` for any
|
|
41
|
+
* skeleton naming convention (Mixamo / Blender / UE / sparse mascot / rich humanoid).
|
|
42
|
+
*/
|
|
43
|
+
export function gradeRig(rig: HumanoidRigDefinition): RigQualityReport {
|
|
44
|
+
const has = (bone: HumanoidBoneName): boolean => isMapped(rig, bone);
|
|
45
|
+
const mappedBoneCount = HUMANOID_BONES.reduce((n, bone) => (has(bone) ? n + 1 : n), 0);
|
|
46
|
+
|
|
47
|
+
// Limb-chain capability (mirrors FootIk's notion of a usable leg chain).
|
|
48
|
+
const hasLegs = (has("leftUpperLeg") || has("leftLowerLeg")) && (has("rightUpperLeg") || has("rightLowerLeg"));
|
|
49
|
+
const hasKnees = has("leftLowerLeg") && has("rightLowerLeg");
|
|
50
|
+
const hasAnkles = has("leftFoot") && has("rightFoot");
|
|
51
|
+
const hasFeet = hasAnkles; // foot node == ankle/foot joint in the canonical skeleton.
|
|
52
|
+
|
|
53
|
+
// Spine / head axis.
|
|
54
|
+
const hasSpine = has("spine") || has("chest") || has("upperChest");
|
|
55
|
+
const hasHead = has("head");
|
|
56
|
+
const hasNeck = has("neck");
|
|
57
|
+
|
|
58
|
+
// Arm chains.
|
|
59
|
+
const hasBothUpperArms = has("leftUpperArm") && has("rightUpperArm");
|
|
60
|
+
const hasBothForearms = has("leftLowerArm") && has("rightLowerArm");
|
|
61
|
+
const hasBothHands = has("leftHand") && has("rightHand");
|
|
62
|
+
const hasBothLowerLegs = has("leftLowerLeg") && has("rightLowerLeg");
|
|
63
|
+
|
|
64
|
+
const reasons: string[] = [];
|
|
65
|
+
reasons.push(`${mappedBoneCount} of ${HUMANOID_BONES.length} canonical humanoid bones mapped`);
|
|
66
|
+
|
|
67
|
+
// --- D: no usable skeleton --------------------------------------------------------------------
|
|
68
|
+
// Without a spine/head axis OR both upper arms there is nothing to act with.
|
|
69
|
+
if (mappedBoneCount < 5 || (!hasSpine && !hasHead) || (!hasBothUpperArms && !hasLegs)) {
|
|
70
|
+
reasons.push("not enough body structure to drive body acting (D)");
|
|
71
|
+
if (!hasSpine && !hasHead) reasons.push("missing spine/head axis");
|
|
72
|
+
if (!hasBothUpperArms) reasons.push("missing a pair of upper arms");
|
|
73
|
+
if (!hasLegs) reasons.push("missing a usable pair of legs");
|
|
74
|
+
return { grade: "D", reasons, hasLegs, hasKnees, hasAnkles, hasFeet, mappedBoneCount };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- A: full humanoid -------------------------------------------------------------------------
|
|
78
|
+
const fullUpperBody = hasSpine && hasNeck && hasHead && hasBothUpperArms && hasBothForearms && hasBothHands;
|
|
79
|
+
const fullLowerBody = hasLegs && hasBothLowerLegs && hasFeet;
|
|
80
|
+
if (mappedBoneCount >= 18 && fullUpperBody && fullLowerBody) {
|
|
81
|
+
reasons.push("full humanoid: spine/neck/head + both arms (forearms+hands) + both legs (lower legs+feet) (A)");
|
|
82
|
+
return { grade: "A", reasons, hasLegs, hasKnees, hasAnkles, hasFeet, mappedBoneCount };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- B: partial humanoid ----------------------------------------------------------------------
|
|
86
|
+
// Enough body to act: a spine/head axis plus at least a full pair of arms OR a full pair of legs.
|
|
87
|
+
const usableArms = hasBothUpperArms && hasBothForearms;
|
|
88
|
+
const usableLegs = hasLegs && hasBothLowerLegs;
|
|
89
|
+
if ((hasSpine || hasHead) && (usableArms || usableLegs) && mappedBoneCount >= 10) {
|
|
90
|
+
reasons.push("partial humanoid: body acting possible but a limb chain or extremities are missing (B)");
|
|
91
|
+
if (!fullUpperBody) reasons.push("incomplete upper body (forearms/hands/neck)");
|
|
92
|
+
if (!fullLowerBody) reasons.push("incomplete lower body (lower legs/feet)");
|
|
93
|
+
return { grade: "B", reasons, hasLegs, hasKnees, hasAnkles, hasFeet, mappedBoneCount };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- C: mascot / sparse -----------------------------------------------------------------------
|
|
97
|
+
reasons.push("mascot/sparse rig: head + torso acting only, no real limb chains (C)");
|
|
98
|
+
if (!usableArms) reasons.push("no usable forearm chain on both arms");
|
|
99
|
+
if (!usableLegs) reasons.push("no usable lower-leg chain on both legs");
|
|
100
|
+
return { grade: "C", reasons, hasLegs, hasKnees, hasAnkles, hasFeet, mappedBoneCount };
|
|
101
|
+
}
|
|
@@ -88,12 +88,10 @@ export function sampleSecondaryAnimationFixture(options: SecondaryAnimationFixtu
|
|
|
88
88
|
deterministicReplay: true
|
|
89
89
|
},
|
|
90
90
|
hash: "",
|
|
91
|
-
claimBoundary: "Secondary animation fixture
|
|
91
|
+
claimBoundary: "Secondary animation fixture provides the deterministic FootIkSample/SpringBoneSample telemetry shapes and oracle values; the real runtimes now live in FootIk.ts (two-bone foot IK + foot-lock on solveTwoBoneIk + a ground query) and SpringBones.ts (integrated spring chain). The fixture still does not claim retargeting, general full-body IK, cloth/hair simulation, Unity Animation Rigging parity, or Unreal Control Rig parity.",
|
|
92
92
|
blockedClaims: [
|
|
93
|
-
"production foot locking parity",
|
|
94
93
|
"full-body IK solver parity",
|
|
95
94
|
"runtime retargeting parity",
|
|
96
|
-
"production spring-bone collision parity",
|
|
97
95
|
"Unity Animation Rigging parity",
|
|
98
96
|
"Unreal Control Rig parity"
|
|
99
97
|
] as const
|
package/src/Skeleton.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Bone, type BoneDescriptor } from "./Bone.js";
|
|
2
|
-
import { multiplyMat4, type Mat4 } from "./Keyframe.js";
|
|
2
|
+
import { multiplyMat4, type Mat4, type Vec3 } from "./Keyframe.js";
|
|
3
3
|
|
|
4
4
|
export class Skeleton {
|
|
5
5
|
readonly bones: readonly Bone[];
|
|
@@ -31,4 +31,48 @@ export class Skeleton {
|
|
|
31
31
|
const worlds = this.worldMatrices();
|
|
32
32
|
return this.bones.map((bone, index) => multiplyMat4(worlds[index]!, bone.inverseBindMatrix));
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/** World-space positions (translation component of each bone's world matrix). */
|
|
36
|
+
boneWorldPositions(): readonly Vec3[] {
|
|
37
|
+
return this.worldMatrices().map((m) => [m[12]!, m[13]!, m[14]!] as Vec3);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ordered descendant chain from a bone (by name or index), following the first child at each
|
|
42
|
+
* level. Returns bone indices root-first — the input to {@link createSpringChain} (use the
|
|
43
|
+
* matching {@link boneWorldPositions} as the rest pose). Members tagged `springChain` are honored;
|
|
44
|
+
* the walk stops at the first leaf or when a child is no longer part of the chain.
|
|
45
|
+
*/
|
|
46
|
+
springChainIndices(root: string | number): readonly number[] {
|
|
47
|
+
const rootIndex = typeof root === "number" ? root : this.bones.findIndex((b) => b.name === root);
|
|
48
|
+
if (rootIndex < 0 || rootIndex >= this.bones.length) {
|
|
49
|
+
throw new Error(`Spring chain root ${String(root)} not found in skeleton.`);
|
|
50
|
+
}
|
|
51
|
+
const chain: number[] = [rootIndex];
|
|
52
|
+
let current = rootIndex;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const childIndex = this.bones.findIndex((b, i) => i > current && b.parentIndex === current);
|
|
55
|
+
if (childIndex < 0) break;
|
|
56
|
+
chain.push(childIndex);
|
|
57
|
+
current = childIndex;
|
|
58
|
+
}
|
|
59
|
+
return chain;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Write solved spring-chain world positions back onto the bones as local translations (relative
|
|
64
|
+
* to each bone's solved parent), so the deformed chain renders. `chainIndices` and `positions`
|
|
65
|
+
* are root-first and aligned.
|
|
66
|
+
*/
|
|
67
|
+
writeSpringChainBack(chainIndices: readonly number[], positions: readonly Vec3[]): void {
|
|
68
|
+
if (chainIndices.length !== positions.length) {
|
|
69
|
+
throw new Error("Spring chain indices and positions must align.");
|
|
70
|
+
}
|
|
71
|
+
for (let i = 1; i < chainIndices.length; i += 1) {
|
|
72
|
+
const boneIndex = chainIndices[i]!;
|
|
73
|
+
const parentPos = positions[i - 1]!;
|
|
74
|
+
const pos = positions[i]!;
|
|
75
|
+
this.bones[boneIndex]!.translation = [pos[0] - parentPos[0], pos[1] - parentPos[1], pos[2] - parentPos[2]];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
34
78
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Spring bones: secondary dynamics for hair, cloth flaps, tails, and accessories. A chain of
|
|
2
|
+
// particles hangs off a kinematic root (driven by the character's animation). Each particle is
|
|
3
|
+
// pulled toward its rest pose by a spring, pulled down by gravity, damped, kept at its bone length
|
|
4
|
+
// by a distance constraint, and pushed out of optional sphere/capsule colliders. The result
|
|
5
|
+
// composes on top of any clip for free — it just reacts to how the root moves.
|
|
6
|
+
//
|
|
7
|
+
// Integration is semi-implicit (symplectic) Euler with a fixed timestep (subdivided into substeps
|
|
8
|
+
// for stiffness stability), so it is deterministic: the same root-motion stream and the same `dt`
|
|
9
|
+
// always produce the same chain. Pure: no `Date.now()` / `Math.random()`, no engine/render import.
|
|
10
|
+
//
|
|
11
|
+
// The deterministic `SpringBoneSample` / `springSample()` in SecondaryAnimationFixtures remain the
|
|
12
|
+
// telemetry oracle/data-shape; this runtime produces the same invariants via `telemetry()`.
|
|
13
|
+
|
|
14
|
+
import type { Quat, Vec3 } from "./Keyframe.js";
|
|
15
|
+
|
|
16
|
+
export interface SpringCollider {
|
|
17
|
+
readonly kind: "sphere" | "capsule";
|
|
18
|
+
readonly center: Vec3;
|
|
19
|
+
/** Capsule second endpoint (ignored for spheres). */
|
|
20
|
+
readonly tail?: Vec3;
|
|
21
|
+
readonly radius: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SpringChainOptions {
|
|
25
|
+
/** Rest-pose world positions of the chain, root first (index 0 is the kinematic root). */
|
|
26
|
+
readonly bones: readonly Vec3[];
|
|
27
|
+
/** Spring stiffness pulling each particle to its rest pose (default 40). */
|
|
28
|
+
readonly stiffness?: number;
|
|
29
|
+
/** Velocity damping (default 4). Higher = settles faster. */
|
|
30
|
+
readonly damping?: number;
|
|
31
|
+
/** Gravity acceleration (default [0,-9.81,0]). */
|
|
32
|
+
readonly gravity?: Vec3;
|
|
33
|
+
/** Optional colliders the chain is pushed out of. */
|
|
34
|
+
readonly colliders?: readonly SpringCollider[];
|
|
35
|
+
/** Integration substeps per `integrate` call (default 2) for stiffness stability. */
|
|
36
|
+
readonly substeps?: number;
|
|
37
|
+
/** Optional chain label (telemetry). */
|
|
38
|
+
readonly name?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Root transform driving the chain each frame. */
|
|
42
|
+
export interface SpringRootTransform {
|
|
43
|
+
readonly position: Vec3;
|
|
44
|
+
/** Optional rotation so the rest offsets follow the character's facing. */
|
|
45
|
+
readonly rotation?: Quat;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SpringChainTelemetry {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly boneCount: number;
|
|
51
|
+
readonly stiffness: number;
|
|
52
|
+
readonly damping: number;
|
|
53
|
+
readonly rootPosition: Vec3;
|
|
54
|
+
readonly tipPosition: Vec3;
|
|
55
|
+
/** Distance from root to tip (how far the chain swings out). */
|
|
56
|
+
readonly maxDisplacement: number;
|
|
57
|
+
/** Number of particles currently resolved against a collider this step. */
|
|
58
|
+
readonly collisionContacts: number;
|
|
59
|
+
/** Total kinetic energy (Σ½|v|²); used to verify damping decay. */
|
|
60
|
+
readonly kineticEnergy: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SpringChain {
|
|
64
|
+
/** Advance the chain by `dt` seconds with the root at `rootTransform`. Deterministic. */
|
|
65
|
+
integrate(dt: number, rootTransform: SpringRootTransform): void;
|
|
66
|
+
/** Current solved world positions (index 0 is the root). */
|
|
67
|
+
positions(): readonly Vec3[];
|
|
68
|
+
/** Telemetry for tests/gates and matching the fixture oracle's invariants. */
|
|
69
|
+
telemetry(): SpringChainTelemetry;
|
|
70
|
+
/** Reset the chain to its rest pose at a root transform (zero velocity). */
|
|
71
|
+
reset(rootTransform?: SpringRootTransform): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a spring-bone chain. Bind it to a bone subtree (see {@link tagSpringChain}) and call
|
|
76
|
+
* {@link SpringChain.integrate} each frame; write {@link SpringChain.positions} back onto the bones.
|
|
77
|
+
*/
|
|
78
|
+
export function createSpringChain(options: SpringChainOptions): SpringChain {
|
|
79
|
+
if (options.bones.length < 2) {
|
|
80
|
+
throw new Error("Spring chain requires at least a root and one child bone.");
|
|
81
|
+
}
|
|
82
|
+
const stiffness = options.stiffness ?? 40;
|
|
83
|
+
const damping = options.damping ?? 4;
|
|
84
|
+
const gravity = options.gravity ?? [0, -9.81, 0];
|
|
85
|
+
const colliders = options.colliders ?? [];
|
|
86
|
+
const substeps = Math.max(1, Math.floor(options.substeps ?? 2));
|
|
87
|
+
const name = options.name ?? "spring-chain";
|
|
88
|
+
|
|
89
|
+
// Rest offset of each particle relative to its parent, in the root's initial frame.
|
|
90
|
+
const restOffsets: Vec3[] = options.bones.map((bone, i) =>
|
|
91
|
+
i === 0 ? [0, 0, 0] : sub(bone, options.bones[i - 1]!)
|
|
92
|
+
);
|
|
93
|
+
const restLengths: number[] = restOffsets.map((o) => len(o));
|
|
94
|
+
|
|
95
|
+
const positions: Vec3[] = options.bones.map((b) => [...b] as Vec3);
|
|
96
|
+
const velocities: Vec3[] = options.bones.map(() => [0, 0, 0] as Vec3);
|
|
97
|
+
let contacts = 0;
|
|
98
|
+
|
|
99
|
+
function rotatedRestOffset(i: number, rotation: Quat | undefined): Vec3 {
|
|
100
|
+
return rotation ? rotateVec3(restOffsets[i]!, rotation) : restOffsets[i]!;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function step(dt: number, root: SpringRootTransform): void {
|
|
104
|
+
contacts = 0;
|
|
105
|
+
// Root particle is kinematic.
|
|
106
|
+
positions[0] = [...root.position] as Vec3;
|
|
107
|
+
velocities[0] = [0, 0, 0];
|
|
108
|
+
for (let i = 1; i < positions.length; i += 1) {
|
|
109
|
+
const parent = positions[i - 1]!;
|
|
110
|
+
const rest = add(parent, rotatedRestOffset(i, root.rotation));
|
|
111
|
+
const p = positions[i]!;
|
|
112
|
+
const v = velocities[i]!;
|
|
113
|
+
// a = stiffness*(rest - p) + gravity - damping*v (semi-implicit: update v, then p)
|
|
114
|
+
const ax = stiffness * (rest[0] - p[0]) + gravity[0] - damping * v[0];
|
|
115
|
+
const ay = stiffness * (rest[1] - p[1]) + gravity[1] - damping * v[1];
|
|
116
|
+
const az = stiffness * (rest[2] - p[2]) + gravity[2] - damping * v[2];
|
|
117
|
+
const nv: [number, number, number] = [v[0] + ax * dt, v[1] + ay * dt, v[2] + az * dt];
|
|
118
|
+
let np: Vec3 = [p[0] + nv[0] * dt, p[1] + nv[1] * dt, p[2] + nv[2] * dt];
|
|
119
|
+
|
|
120
|
+
// Distance constraint: keep the bone length to the parent.
|
|
121
|
+
np = enforceLength(parent, np, restLengths[i]!);
|
|
122
|
+
|
|
123
|
+
// Collider push-out.
|
|
124
|
+
for (const collider of colliders) {
|
|
125
|
+
const resolved = pushOut(np, collider);
|
|
126
|
+
if (resolved) {
|
|
127
|
+
np = resolved.position;
|
|
128
|
+
// Remove the inward velocity component so it doesn't tunnel back in.
|
|
129
|
+
const dot = nv[0] * resolved.normal[0] + nv[1] * resolved.normal[1] + nv[2] * resolved.normal[2];
|
|
130
|
+
if (dot < 0) {
|
|
131
|
+
nv[0] -= dot * resolved.normal[0];
|
|
132
|
+
nv[1] -= dot * resolved.normal[1];
|
|
133
|
+
nv[2] -= dot * resolved.normal[2];
|
|
134
|
+
}
|
|
135
|
+
contacts += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Recompute velocity from the constrained position so energy bookkeeping stays consistent.
|
|
140
|
+
velocities[i] = [(np[0] - p[0]) / dt, (np[1] - p[1]) / dt, (np[2] - p[2]) / dt];
|
|
141
|
+
positions[i] = np;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
integrate(dt, rootTransform) {
|
|
147
|
+
if (!Number.isFinite(dt) || dt <= 0) throw new Error("Spring chain dt must be finite and positive.");
|
|
148
|
+
const sub = dt / substeps;
|
|
149
|
+
for (let s = 0; s < substeps; s += 1) step(sub, rootTransform);
|
|
150
|
+
},
|
|
151
|
+
positions() {
|
|
152
|
+
return positions.map((p) => [...p] as Vec3);
|
|
153
|
+
},
|
|
154
|
+
telemetry() {
|
|
155
|
+
const tip = positions[positions.length - 1]!;
|
|
156
|
+
const root = positions[0]!;
|
|
157
|
+
let ke = 0;
|
|
158
|
+
for (const v of velocities) ke += 0.5 * (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
159
|
+
return {
|
|
160
|
+
name,
|
|
161
|
+
boneCount: positions.length,
|
|
162
|
+
stiffness,
|
|
163
|
+
damping,
|
|
164
|
+
rootPosition: [...root] as Vec3,
|
|
165
|
+
tipPosition: [...tip] as Vec3,
|
|
166
|
+
maxDisplacement: dist(root, tip),
|
|
167
|
+
collisionContacts: contacts,
|
|
168
|
+
kineticEnergy: ke
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
reset(rootTransform) {
|
|
172
|
+
const rootPos = rootTransform?.position ?? options.bones[0]!;
|
|
173
|
+
const rotation = rootTransform?.rotation;
|
|
174
|
+
positions[0] = [...rootPos] as Vec3;
|
|
175
|
+
velocities[0] = [0, 0, 0];
|
|
176
|
+
for (let i = 1; i < positions.length; i += 1) {
|
|
177
|
+
positions[i] = add(positions[i - 1]!, rotatedRestOffset(i, rotation));
|
|
178
|
+
velocities[i] = [0, 0, 0];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function enforceLength(anchor: Vec3, point: Vec3, length: number): Vec3 {
|
|
185
|
+
const d = sub(point, anchor);
|
|
186
|
+
const l = len(d);
|
|
187
|
+
if (l <= 1e-8) return [anchor[0], anchor[1] + length, anchor[2]];
|
|
188
|
+
const s = length / l;
|
|
189
|
+
return [anchor[0] + d[0] * s, anchor[1] + d[1] * s, anchor[2] + d[2] * s];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function pushOut(point: Vec3, collider: SpringCollider): { position: Vec3; normal: Vec3 } | undefined {
|
|
193
|
+
const closest = collider.kind === "capsule" && collider.tail
|
|
194
|
+
? closestPointOnSegment(point, collider.center, collider.tail)
|
|
195
|
+
: collider.center;
|
|
196
|
+
const d = sub(point, closest);
|
|
197
|
+
const l = len(d);
|
|
198
|
+
if (l >= collider.radius) return undefined;
|
|
199
|
+
const normal: Vec3 = l <= 1e-8 ? [0, 1, 0] : [d[0] / l, d[1] / l, d[2] / l];
|
|
200
|
+
return {
|
|
201
|
+
position: [closest[0] + normal[0] * collider.radius, closest[1] + normal[1] * collider.radius, closest[2] + normal[2] * collider.radius],
|
|
202
|
+
normal
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function closestPointOnSegment(p: Vec3, a: Vec3, b: Vec3): Vec3 {
|
|
207
|
+
const ab = sub(b, a);
|
|
208
|
+
const t = clamp(dot(sub(p, a), ab) / Math.max(1e-8, dot(ab, ab)), 0, 1);
|
|
209
|
+
return [a[0] + ab[0] * t, a[1] + ab[1] * t, a[2] + ab[2] * t];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function add(a: Vec3, b: Vec3): Vec3 {
|
|
213
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
214
|
+
}
|
|
215
|
+
function sub(a: Vec3, b: Vec3): Vec3 {
|
|
216
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
217
|
+
}
|
|
218
|
+
function dot(a: Vec3, b: Vec3): number {
|
|
219
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
220
|
+
}
|
|
221
|
+
function len(a: Vec3): number {
|
|
222
|
+
return Math.hypot(a[0], a[1], a[2]);
|
|
223
|
+
}
|
|
224
|
+
function dist(a: Vec3, b: Vec3): number {
|
|
225
|
+
return len(sub(a, b));
|
|
226
|
+
}
|
|
227
|
+
function clamp(value: number, min: number, max: number): number {
|
|
228
|
+
return Math.max(min, Math.min(max, value));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function rotateVec3(v: Vec3, q: Quat): Vec3 {
|
|
232
|
+
// v' = q * v * q⁻¹ (unit quaternion).
|
|
233
|
+
const [x, y, z, w] = q;
|
|
234
|
+
const ix = w * v[0] + y * v[2] - z * v[1];
|
|
235
|
+
const iy = w * v[1] + z * v[0] - x * v[2];
|
|
236
|
+
const iz = w * v[2] + x * v[1] - y * v[0];
|
|
237
|
+
const iw = -x * v[0] - y * v[1] - z * v[2];
|
|
238
|
+
return [
|
|
239
|
+
ix * w + iw * -x + iy * -z - iz * -y,
|
|
240
|
+
iy * w + iw * -y + iz * -x - ix * -z,
|
|
241
|
+
iz * w + iw * -z + ix * -y - iy * -x
|
|
242
|
+
];
|
|
243
|
+
}
|
package/src/browser-index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./Bone.js";
|
|
|
9
9
|
export * from "./Skeleton.js";
|
|
10
10
|
export * from "./Skinning.js";
|
|
11
11
|
export * from "./BlendTree.js";
|
|
12
|
+
export * from "./Inertialization.js";
|
|
12
13
|
export * from "./AnimationStateMachine.js";
|
|
13
14
|
export * from "./RootMotion.js";
|
|
14
15
|
export * from "./MotionQuality.js";
|
|
@@ -16,12 +17,16 @@ export * from "./LocomotionController.js";
|
|
|
16
17
|
export * from "./SceneAnimationBridge.js";
|
|
17
18
|
export * from "./ECSAnimationBridge.js";
|
|
18
19
|
export * from "./IK.js";
|
|
20
|
+
export * from "./FootIk.js";
|
|
21
|
+
export * from "./SpringBones.js";
|
|
19
22
|
export * from "./MotionMatchingFixtures.js";
|
|
20
23
|
export * from "./SecondaryAnimationFixtures.js";
|
|
21
24
|
export * from "./CrowdAnimation.js";
|
|
22
|
-
export * from "./Retargeting.js";
|
|
23
25
|
export * from "./AnimationClipEvents.js";
|
|
24
|
-
export { AnimationClipRegistry,
|
|
26
|
+
export { AnimationClipRegistry, createAnimationClipRegistry, validateAnimationClipMap, validateAnimationStudioClipMap } from "./AnimationClipRegistry.js";
|
|
27
|
+
export { createAnimationAnimationStateGraph, createLocomotionAnimationStateGraph, sampleAnimationAnimationStateGraph } from "./AnimationStateGraph.js";
|
|
28
|
+
export * from "./LocomotionKit.js";
|
|
29
|
+
export * from "./FighterAnimationAdapter.js";
|
|
25
30
|
export type {
|
|
26
31
|
AnimationClipDefinition,
|
|
27
32
|
AnimationClipId,
|
|
@@ -34,12 +39,16 @@ export type {
|
|
|
34
39
|
AnimationKeyframe,
|
|
35
40
|
AnimationTrack as AnimationClipTrack,
|
|
36
41
|
AnimationTrackTarget,
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
AnimationClipMapReadiness,
|
|
43
|
+
AnimationClipMapReadinessOptions,
|
|
39
44
|
RegisteredAnimationClip
|
|
40
45
|
} from "./AnimationClipRegistry.js";
|
|
41
46
|
export * from "./AnimationController.js";
|
|
42
47
|
export * from "./HumanoidRetargeting.js";
|
|
48
|
+
export * from "./HumanoidBoneInference.js";
|
|
49
|
+
export * from "./RigQuality.js";
|
|
50
|
+
export * from "./library/standardHumanoidClips.js";
|
|
51
|
+
export * from "./library/performanceStateGraph.js";
|
|
43
52
|
export { AnimationClipThreeCompat } from "./threejs-compatibility/AnimationClip.js";
|
|
44
53
|
export type { ThreeCompatKeyframeTrack, ThreeCompatLoopMode } from "./threejs-compatibility/AnimationClip.js";
|
|
45
54
|
export { AnimationActionThreeCompat } from "./threejs-compatibility/AnimationAction.js";
|
package/src/index.ts
CHANGED
|
@@ -9,20 +9,24 @@ export * from "./Bone.js";
|
|
|
9
9
|
export * from "./Skeleton.js";
|
|
10
10
|
export * from "./Skinning.js";
|
|
11
11
|
export * from "./BlendTree.js";
|
|
12
|
+
export * from "./Inertialization.js";
|
|
12
13
|
export * from "./AnimationStateMachine.js";
|
|
13
14
|
export * from "./AnimationStateGraph.js";
|
|
15
|
+
export * from "./LocomotionKit.js";
|
|
16
|
+
export * from "./FighterAnimationAdapter.js";
|
|
14
17
|
export * from "./RootMotion.js";
|
|
15
18
|
export * from "./MotionQuality.js";
|
|
16
19
|
export * from "./LocomotionController.js";
|
|
17
20
|
export * from "./SceneAnimationBridge.js";
|
|
18
21
|
export * from "./ECSAnimationBridge.js";
|
|
19
22
|
export * from "./IK.js";
|
|
23
|
+
export * from "./FootIk.js";
|
|
24
|
+
export * from "./SpringBones.js";
|
|
20
25
|
export * from "./MotionMatchingFixtures.js";
|
|
21
26
|
export * from "./SecondaryAnimationFixtures.js";
|
|
22
27
|
export * from "./CrowdAnimation.js";
|
|
23
|
-
export * from "./Retargeting.js";
|
|
24
28
|
export * from "./AnimationClipEvents.js";
|
|
25
|
-
export { AnimationClipRegistry,
|
|
29
|
+
export { AnimationClipRegistry, createAnimationClipRegistry, validateAnimationClipMap, validateAnimationStudioClipMap } from "./AnimationClipRegistry.js";
|
|
26
30
|
export type {
|
|
27
31
|
AnimationClipDefinition,
|
|
28
32
|
AnimationClipId,
|
|
@@ -35,12 +39,16 @@ export type {
|
|
|
35
39
|
AnimationKeyframe,
|
|
36
40
|
AnimationTrack as AnimationClipTrack,
|
|
37
41
|
AnimationTrackTarget,
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
AnimationClipMapReadiness,
|
|
43
|
+
AnimationClipMapReadinessOptions,
|
|
40
44
|
RegisteredAnimationClip
|
|
41
45
|
} from "./AnimationClipRegistry.js";
|
|
42
46
|
export * from "./AnimationController.js";
|
|
43
47
|
export * from "./HumanoidRetargeting.js";
|
|
48
|
+
export * from "./HumanoidBoneInference.js";
|
|
49
|
+
export * from "./RigQuality.js";
|
|
50
|
+
export * from "./library/standardHumanoidClips.js";
|
|
51
|
+
export * from "./library/performanceStateGraph.js";
|
|
44
52
|
export { AnimationClipThreeCompat } from "./threejs-compatibility/AnimationClip.js";
|
|
45
53
|
export type { ThreeCompatKeyframeTrack, ThreeCompatLoopMode } from "./threejs-compatibility/AnimationClip.js";
|
|
46
54
|
export { AnimationActionThreeCompat } from "./threejs-compatibility/AnimationAction.js";
|