@aura3d/animation 1.4.0 → 1.4.2
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/package.json +36 -3
- package/src/AnimationAction.ts +0 -166
- package/src/AnimationClip.ts +0 -63
- package/src/AnimationClipEvents.ts +0 -501
- package/src/AnimationClipRegistry.ts +0 -404
- package/src/AnimationController.ts +0 -1017
- package/src/AnimationEvents.ts +0 -34
- package/src/AnimationLayer.ts +0 -73
- package/src/AnimationMixer.ts +0 -402
- package/src/AnimationStateGraph.ts +0 -105
- package/src/AnimationStateMachine.ts +0 -265
- package/src/AnimationTrack.ts +0 -176
- package/src/BlendTree.ts +0 -89
- package/src/Bone.ts +0 -40
- package/src/CrowdAnimation.ts +0 -25
- package/src/ECSAnimationBridge.ts +0 -30
- package/src/FighterAnimationAdapter.ts +0 -124
- package/src/FootIk.ts +0 -251
- package/src/HumanoidBoneInference.ts +0 -323
- package/src/HumanoidRetargeting.ts +0 -700
- package/src/IK.ts +0 -117
- package/src/Inertialization.ts +0 -269
- package/src/Keyframe.ts +0 -159
- package/src/LocomotionController.ts +0 -155
- package/src/LocomotionKit.ts +0 -89
- package/src/MotionMatchingFixtures.ts +0 -254
- package/src/MotionQuality.ts +0 -182
- package/src/RigQuality.ts +0 -101
- package/src/RootMotion.ts +0 -124
- package/src/SceneAnimationBridge.ts +0 -80
- package/src/SecondaryAnimationFixtures.ts +0 -210
- package/src/Skeleton.ts +0 -78
- package/src/Skinning.ts +0 -18
- package/src/SpringBones.ts +0 -243
- package/src/browser-index.ts +0 -66
- package/src/index.ts +0 -66
- package/src/library/performanceStateGraph.ts +0 -173
- package/src/library/standardHumanoidClips.ts +0 -659
- package/src/threejs-compatibility/AnimationAction.ts +0 -27
- package/src/threejs-compatibility/AnimationClip.ts +0 -16
- package/src/threejs-compatibility/AnimationDiagnostics.ts +0 -42
- package/src/threejs-compatibility/AnimationMixer.ts +0 -24
- package/src/threejs-compatibility/MorphTargetMixer.ts +0 -16
- package/src/threejs-compatibility/Skeleton.ts +0 -9
- package/src/threejs-compatibility/SkinnedMesh.ts +0 -12
- package/src/threejs-compatibility/index.ts +0 -11
package/src/RootMotion.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import type { AnimationClip } from "./AnimationClip.js";
|
|
2
|
-
import type { AnimationTrack } from "./AnimationTrack.js";
|
|
3
|
-
import type { Vec3 } from "./Keyframe.js";
|
|
4
|
-
|
|
5
|
-
export type RootMotionExtractionDescriptor = {
|
|
6
|
-
readonly target?: string;
|
|
7
|
-
readonly fromTime: number;
|
|
8
|
-
readonly toTime: number;
|
|
9
|
-
readonly loop?: boolean;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type RootMotionSample = {
|
|
13
|
-
readonly target: string;
|
|
14
|
-
readonly fromTime: number;
|
|
15
|
-
readonly toTime: number;
|
|
16
|
-
readonly looped: boolean;
|
|
17
|
-
readonly delta: Vec3;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type RootMotionTarget = {
|
|
21
|
-
position: [number, number, number];
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const defaultRootMotionTarget = "root.position";
|
|
25
|
-
|
|
26
|
-
export function extractRootMotion(clip: AnimationClip, descriptor: RootMotionExtractionDescriptor): RootMotionSample {
|
|
27
|
-
const target = descriptor.target ?? defaultRootMotionTarget;
|
|
28
|
-
if (!Number.isFinite(descriptor.fromTime) || !Number.isFinite(descriptor.toTime)) {
|
|
29
|
-
throw new Error("Root motion times must be finite.");
|
|
30
|
-
}
|
|
31
|
-
const track = clip.tracks.find((candidate) => candidate.target === target);
|
|
32
|
-
if (!track) {
|
|
33
|
-
throw new Error(`Root motion track "${target}" was not found.`);
|
|
34
|
-
}
|
|
35
|
-
if (track.valueType !== "vector3") {
|
|
36
|
-
throw new Error(`Root motion track "${target}" must be a vector3 track.`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const loop = descriptor.loop ?? false;
|
|
40
|
-
const delta = loop
|
|
41
|
-
? extractLoopingDelta(track, clip.duration, descriptor.fromTime, descriptor.toTime)
|
|
42
|
-
: subtractVec3(
|
|
43
|
-
sampleVec3(track, clampTime(descriptor.toTime, clip.duration)),
|
|
44
|
-
sampleVec3(track, clampTime(descriptor.fromTime, clip.duration))
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
target,
|
|
49
|
-
fromTime: descriptor.fromTime,
|
|
50
|
-
toTime: descriptor.toTime,
|
|
51
|
-
looped: loop,
|
|
52
|
-
delta
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function applyRootMotion(target: RootMotionTarget, sample: RootMotionSample, scale = 1): RootMotionTarget {
|
|
57
|
-
if (!Number.isFinite(scale)) {
|
|
58
|
-
throw new Error("Root motion scale must be finite.");
|
|
59
|
-
}
|
|
60
|
-
target.position = [
|
|
61
|
-
target.position[0] + sample.delta[0] * scale,
|
|
62
|
-
target.position[1] + sample.delta[1] * scale,
|
|
63
|
-
target.position[2] + sample.delta[2] * scale
|
|
64
|
-
];
|
|
65
|
-
return target;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function extractLoopingDelta(track: AnimationTrack, duration: number, fromTime: number, toTime: number): Vec3 {
|
|
69
|
-
if (!Number.isFinite(duration) || duration <= 0) {
|
|
70
|
-
throw new Error("Looping root motion requires a positive clip duration.");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let effectiveTo = toTime;
|
|
74
|
-
if (effectiveTo < fromTime) {
|
|
75
|
-
effectiveTo += Math.ceil((fromTime - effectiveTo) / duration) * duration;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const fromCycle = Math.floor(fromTime / duration);
|
|
79
|
-
const toCycle = Math.floor(effectiveTo / duration);
|
|
80
|
-
const fromWrapped = wrapTime(fromTime, duration);
|
|
81
|
-
const toWrapped = wrapTime(effectiveTo, duration);
|
|
82
|
-
|
|
83
|
-
if (fromCycle === toCycle) {
|
|
84
|
-
return subtractVec3(sampleVec3(track, toWrapped), sampleVec3(track, fromWrapped));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const cycleDelta = subtractVec3(sampleVec3(track, duration), sampleVec3(track, 0));
|
|
88
|
-
const firstPartial = subtractVec3(sampleVec3(track, duration), sampleVec3(track, fromWrapped));
|
|
89
|
-
const wholeCycles = Math.max(0, toCycle - fromCycle - 1);
|
|
90
|
-
const finalPartial = subtractVec3(sampleVec3(track, toWrapped), sampleVec3(track, 0));
|
|
91
|
-
return addVec3(addVec3(firstPartial, scaleVec3(cycleDelta, wholeCycles)), finalPartial);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function clampTime(time: number, duration: number): number {
|
|
95
|
-
if (!Number.isFinite(duration) || duration < 0) {
|
|
96
|
-
throw new Error("Root motion clip duration must be finite and non-negative.");
|
|
97
|
-
}
|
|
98
|
-
return Math.max(0, Math.min(duration, time));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function wrapTime(time: number, duration: number): number {
|
|
102
|
-
const wrapped = time % duration;
|
|
103
|
-
return wrapped < 0 ? wrapped + duration : wrapped;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function sampleVec3(track: AnimationTrack, time: number): Vec3 {
|
|
107
|
-
const value = track.sample(time);
|
|
108
|
-
if (!Array.isArray(value) || value.length !== 3 || value.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
|
|
109
|
-
throw new Error(`Root motion track "${track.target}" sampled an invalid vector3 value.`);
|
|
110
|
-
}
|
|
111
|
-
return [value[0], value[1], value[2]];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function addVec3(a: Vec3, b: Vec3): Vec3 {
|
|
115
|
-
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function subtractVec3(a: Vec3, b: Vec3): Vec3 {
|
|
119
|
-
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function scaleVec3(a: Vec3, scale: number): Vec3 {
|
|
123
|
-
return [a[0] * scale, a[1] * scale, a[2] * scale];
|
|
124
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { AnimationValue } from "./Keyframe.js";
|
|
2
|
-
|
|
3
|
-
export type SceneAnimationNode = {
|
|
4
|
-
name?: string;
|
|
5
|
-
position?: [number, number, number];
|
|
6
|
-
rotation?: [number, number, number, number];
|
|
7
|
-
scale?: [number, number, number];
|
|
8
|
-
weights?: number[];
|
|
9
|
-
setPosition?: (value: readonly [number, number, number]) => void;
|
|
10
|
-
setRotation?: (value: readonly [number, number, number, number]) => void;
|
|
11
|
-
setScale?: (value: readonly [number, number, number]) => void;
|
|
12
|
-
setWeights?: (value: readonly number[]) => void;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export class SceneAnimationBridge {
|
|
16
|
-
private readonly nodes = new Map<string, SceneAnimationNode>();
|
|
17
|
-
|
|
18
|
-
register(name: string, node: SceneAnimationNode): void {
|
|
19
|
-
if (name.trim().length === 0) {
|
|
20
|
-
throw new Error("Scene animation target name cannot be empty.");
|
|
21
|
-
}
|
|
22
|
-
this.nodes.set(name, node);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
setAnimationValue(target: string, value: AnimationValue): void {
|
|
26
|
-
const [nodeName, property] = target.split(".");
|
|
27
|
-
if (!nodeName || !property) {
|
|
28
|
-
throw new Error(`Invalid scene animation target ${target}.`);
|
|
29
|
-
}
|
|
30
|
-
const node = this.nodes.get(nodeName);
|
|
31
|
-
if (!node) {
|
|
32
|
-
throw new Error(`Missing scene animation target ${nodeName}.`);
|
|
33
|
-
}
|
|
34
|
-
if (property === "position" || property === "translation") {
|
|
35
|
-
writeVec3(node, "position", "setPosition", value);
|
|
36
|
-
} else if (property === "rotation") {
|
|
37
|
-
writeQuat(node, value);
|
|
38
|
-
} else if (property === "scale") {
|
|
39
|
-
writeVec3(node, "scale", "setScale", value);
|
|
40
|
-
} else if (property === "weights") {
|
|
41
|
-
writeWeights(node, value);
|
|
42
|
-
} else {
|
|
43
|
-
throw new Error(`Unsupported scene animation property ${property}.`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function writeWeights(node: SceneAnimationNode, value: AnimationValue): void {
|
|
49
|
-
if (!Array.isArray(value) || value.some((weight) => typeof weight !== "number" || !Number.isFinite(weight))) {
|
|
50
|
-
throw new Error("weights animation value must be a finite number array.");
|
|
51
|
-
}
|
|
52
|
-
const weights = value.map((weight) => Math.max(0, Math.min(1, weight)));
|
|
53
|
-
if (node.setWeights) {
|
|
54
|
-
node.setWeights(weights);
|
|
55
|
-
} else {
|
|
56
|
-
node.weights = weights;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function writeVec3(node: SceneAnimationNode, property: "position" | "scale", setter: "setPosition" | "setScale", value: AnimationValue): void {
|
|
61
|
-
if (!Array.isArray(value) || value.length !== 3) {
|
|
62
|
-
throw new Error(`${property} animation value must be a vec3.`);
|
|
63
|
-
}
|
|
64
|
-
if (node[setter]) {
|
|
65
|
-
node[setter](value as [number, number, number]);
|
|
66
|
-
} else {
|
|
67
|
-
node[property] = [value[0], value[1], value[2]];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function writeQuat(node: SceneAnimationNode, value: AnimationValue): void {
|
|
72
|
-
if (!Array.isArray(value) || value.length !== 4) {
|
|
73
|
-
throw new Error("rotation animation value must be a quaternion.");
|
|
74
|
-
}
|
|
75
|
-
if (node.setRotation) {
|
|
76
|
-
node.setRotation(value as [number, number, number, number]);
|
|
77
|
-
} else {
|
|
78
|
-
node.rotation = [value[0], value[1], value[2], value[3]];
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import type { Vec3 } from "./Keyframe";
|
|
2
|
-
|
|
3
|
-
export interface SecondaryAnimationFixtureOptions {
|
|
4
|
-
readonly stridePhase?: number;
|
|
5
|
-
readonly rootHeight?: number;
|
|
6
|
-
readonly velocity?: Vec3;
|
|
7
|
-
readonly terrainSlope?: number;
|
|
8
|
-
readonly deltaSeconds?: number;
|
|
9
|
-
readonly seed?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface FootIkSample {
|
|
13
|
-
readonly side: "left" | "right";
|
|
14
|
-
readonly sourceFoot: Vec3;
|
|
15
|
-
readonly plantedFoot: Vec3;
|
|
16
|
-
readonly groundNormal: Vec3;
|
|
17
|
-
readonly grounded: boolean;
|
|
18
|
-
readonly verticalCorrection: number;
|
|
19
|
-
readonly targetError: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface SpringBoneSample {
|
|
23
|
-
readonly chainName: "ponytail";
|
|
24
|
-
readonly boneCount: number;
|
|
25
|
-
readonly stiffness: number;
|
|
26
|
-
readonly damping: number;
|
|
27
|
-
readonly collisionRadius: number;
|
|
28
|
-
readonly substeps: number;
|
|
29
|
-
readonly gravity: Vec3;
|
|
30
|
-
readonly wind: Vec3;
|
|
31
|
-
readonly rootPosition: Vec3;
|
|
32
|
-
readonly tipPosition: Vec3;
|
|
33
|
-
readonly maxDisplacement: number;
|
|
34
|
-
readonly collisionContacts: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface SecondaryAnimationFixture {
|
|
38
|
-
readonly source: "origin-master-foot-ik-spring-bone-adapted";
|
|
39
|
-
readonly footIk: {
|
|
40
|
-
readonly feet: readonly FootIkSample[];
|
|
41
|
-
readonly hipOffset: number;
|
|
42
|
-
readonly averageTargetError: number;
|
|
43
|
-
readonly groundedFeet: number;
|
|
44
|
-
readonly terrainSlope: number;
|
|
45
|
-
};
|
|
46
|
-
readonly springBone: SpringBoneSample;
|
|
47
|
-
readonly productionReadiness: {
|
|
48
|
-
readonly footPlacementTelemetry: boolean;
|
|
49
|
-
readonly hipAdjustmentTelemetry: boolean;
|
|
50
|
-
readonly springChainTelemetry: boolean;
|
|
51
|
-
readonly collisionTelemetry: boolean;
|
|
52
|
-
readonly deterministicReplay: boolean;
|
|
53
|
-
};
|
|
54
|
-
readonly hash: string;
|
|
55
|
-
readonly claimBoundary: string;
|
|
56
|
-
readonly blockedClaims: readonly string[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function sampleSecondaryAnimationFixture(options: SecondaryAnimationFixtureOptions = {}): SecondaryAnimationFixture {
|
|
60
|
-
const stridePhase = wrap01(options.stridePhase ?? 0.35);
|
|
61
|
-
const rootHeight = clamp(options.rootHeight ?? 1.08, 0.4, 2.4);
|
|
62
|
-
const velocity = options.velocity ?? [0.9, 0, 0.22];
|
|
63
|
-
const terrainSlope = clamp(options.terrainSlope ?? 0.18, -0.7, 0.7);
|
|
64
|
-
const deltaSeconds = clamp(options.deltaSeconds ?? 1 / 60, 1 / 240, 1 / 10);
|
|
65
|
-
const seed = integerSeed(options.seed ?? 0x3d2025);
|
|
66
|
-
const groundNormal = normalize([-terrainSlope, 1, 0.08]);
|
|
67
|
-
const left = footSample("left", stridePhase, rootHeight, terrainSlope, groundNormal, velocity, seed);
|
|
68
|
-
const right = footSample("right", wrap01(stridePhase + 0.5), rootHeight, terrainSlope, groundNormal, velocity, seed + 17);
|
|
69
|
-
const feet = [left, right] as const;
|
|
70
|
-
const averageTargetError = round(feet.reduce((sum, foot) => sum + foot.targetError, 0) / feet.length);
|
|
71
|
-
const hipOffset = round(-Math.max(...feet.map((foot) => Math.max(0, foot.verticalCorrection))) * 0.72);
|
|
72
|
-
const springBone = springSample(rootHeight, velocity, deltaSeconds, seed);
|
|
73
|
-
const fixture = {
|
|
74
|
-
source: "origin-master-foot-ik-spring-bone-adapted" as const,
|
|
75
|
-
footIk: {
|
|
76
|
-
feet,
|
|
77
|
-
hipOffset,
|
|
78
|
-
averageTargetError,
|
|
79
|
-
groundedFeet: feet.filter((foot) => foot.grounded).length,
|
|
80
|
-
terrainSlope: round(terrainSlope)
|
|
81
|
-
},
|
|
82
|
-
springBone,
|
|
83
|
-
productionReadiness: {
|
|
84
|
-
footPlacementTelemetry: feet.every((foot) => foot.grounded && foot.targetError <= 0.015),
|
|
85
|
-
hipAdjustmentTelemetry: hipOffset < 0,
|
|
86
|
-
springChainTelemetry: springBone.boneCount >= 4 && springBone.maxDisplacement > 0,
|
|
87
|
-
collisionTelemetry: springBone.collisionContacts > 0,
|
|
88
|
-
deterministicReplay: true
|
|
89
|
-
},
|
|
90
|
-
hash: "",
|
|
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
|
-
blockedClaims: [
|
|
93
|
-
"full-body IK solver parity",
|
|
94
|
-
"runtime retargeting parity",
|
|
95
|
-
"Unity Animation Rigging parity",
|
|
96
|
-
"Unreal Control Rig parity"
|
|
97
|
-
] as const
|
|
98
|
-
};
|
|
99
|
-
return {
|
|
100
|
-
...fixture,
|
|
101
|
-
hash: stableHash([
|
|
102
|
-
fixture.footIk.groundedFeet,
|
|
103
|
-
fixture.footIk.hipOffset,
|
|
104
|
-
fixture.footIk.averageTargetError,
|
|
105
|
-
fixture.springBone.maxDisplacement,
|
|
106
|
-
fixture.springBone.collisionContacts,
|
|
107
|
-
fixture.springBone.tipPosition.join(",")
|
|
108
|
-
].join("|"))
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function footSample(side: "left" | "right", phase: number, rootHeight: number, terrainSlope: number, normal: Vec3, velocity: Vec3, seed: number): FootIkSample {
|
|
113
|
-
const lateral = side === "left" ? -0.18 : 0.18;
|
|
114
|
-
const stride = Math.sin(phase * Math.PI * 2) * 0.22 + velocity[0] * 0.06;
|
|
115
|
-
const lift = Math.max(0, Math.sin(phase * Math.PI * 2)) * 0.08;
|
|
116
|
-
const jitter = (noise(seed) - 0.5) * 0.01;
|
|
117
|
-
const x = round(stride + jitter);
|
|
118
|
-
const z = round(lateral + velocity[2] * 0.04);
|
|
119
|
-
const sourceY = round(rootHeight - 1.05 + lift);
|
|
120
|
-
const groundY = round(x * terrainSlope - Math.abs(lateral) * 0.04);
|
|
121
|
-
const planted: Vec3 = [x, groundY + 0.035, z];
|
|
122
|
-
const source: Vec3 = [x, sourceY, z];
|
|
123
|
-
const verticalCorrection = round(sourceY - planted[1]);
|
|
124
|
-
return {
|
|
125
|
-
side,
|
|
126
|
-
sourceFoot: source,
|
|
127
|
-
plantedFoot: planted,
|
|
128
|
-
groundNormal: normal,
|
|
129
|
-
grounded: true,
|
|
130
|
-
verticalCorrection,
|
|
131
|
-
targetError: round(Math.abs(planted[1] - (groundY + 0.035)))
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function springSample(rootHeight: number, velocity: Vec3, deltaSeconds: number, seed: number): SpringBoneSample {
|
|
136
|
-
const stiffness = 0.62;
|
|
137
|
-
const damping = 0.38;
|
|
138
|
-
const collisionRadius = 0.08;
|
|
139
|
-
const substeps = Math.max(1, Math.min(4, Math.ceil(deltaSeconds / (1 / 120))));
|
|
140
|
-
const rootPosition: Vec3 = [0, round(rootHeight + 0.32), -0.08];
|
|
141
|
-
const wind: Vec3 = [round(velocity[0] * 0.18 + noise(seed + 9) * 0.05), 0, round(0.1 + velocity[2] * 0.08)];
|
|
142
|
-
const gravity: Vec3 = [0, -9.81, 0];
|
|
143
|
-
const displacement = round(Math.hypot(wind[0], wind[2]) * (1 - damping) + 0.055);
|
|
144
|
-
const rawTip: Vec3 = [
|
|
145
|
-
round(rootPosition[0] + wind[0] * 0.38),
|
|
146
|
-
round(rootPosition[1] - 0.42 - displacement * 0.35),
|
|
147
|
-
round(rootPosition[2] + wind[2] * 0.42)
|
|
148
|
-
];
|
|
149
|
-
const collisionFloor = rootHeight - 0.22;
|
|
150
|
-
const collisionContacts = rawTip[1] < collisionFloor + collisionRadius ? 1 : 0;
|
|
151
|
-
const tipPosition: Vec3 = collisionContacts > 0 ? [rawTip[0], round(collisionFloor + collisionRadius), rawTip[2]] : rawTip;
|
|
152
|
-
return {
|
|
153
|
-
chainName: "ponytail",
|
|
154
|
-
boneCount: 5,
|
|
155
|
-
stiffness,
|
|
156
|
-
damping,
|
|
157
|
-
collisionRadius,
|
|
158
|
-
substeps,
|
|
159
|
-
gravity,
|
|
160
|
-
wind,
|
|
161
|
-
rootPosition,
|
|
162
|
-
tipPosition,
|
|
163
|
-
maxDisplacement: round(distance(rootPosition, tipPosition)),
|
|
164
|
-
collisionContacts
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function wrap01(value: number): number {
|
|
169
|
-
const wrapped = value - Math.floor(value);
|
|
170
|
-
return Number.isFinite(wrapped) ? wrapped : 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function integerSeed(value: number): number {
|
|
174
|
-
if (!Number.isInteger(value)) throw new Error("Secondary animation fixture seed must be an integer.");
|
|
175
|
-
return value;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function normalize(value: Vec3): Vec3 {
|
|
179
|
-
const length = Math.hypot(value[0], value[1], value[2]);
|
|
180
|
-
if (length <= 1e-6) return [0, 1, 0];
|
|
181
|
-
return [round(value[0] / length), round(value[1] / length), round(value[2] / length)];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function distance(left: Vec3, right: Vec3): number {
|
|
185
|
-
return Math.hypot(left[0] - right[0], left[1] - right[1], left[2] - right[2]);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function noise(seed: number): number {
|
|
189
|
-
let value = Math.imul(seed ^ 0x9e3779b9, 0x85ebca6b);
|
|
190
|
-
value ^= value >>> 13;
|
|
191
|
-
value = Math.imul(value, 0xc2b2ae35);
|
|
192
|
-
return ((value ^ (value >>> 16)) >>> 0) / 0xffffffff;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function clamp(value: number, min: number, max: number): number {
|
|
196
|
-
return Math.max(min, Math.min(max, Number.isFinite(value) ? value : min));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function round(value: number): number {
|
|
200
|
-
return Number(value.toFixed(4));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function stableHash(value: string): string {
|
|
204
|
-
let hash = 0x811c9dc5;
|
|
205
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
206
|
-
hash ^= value.charCodeAt(index);
|
|
207
|
-
hash = Math.imul(hash, 0x01000193);
|
|
208
|
-
}
|
|
209
|
-
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
210
|
-
}
|
package/src/Skeleton.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Bone, type BoneDescriptor } from "./Bone.js";
|
|
2
|
-
import { multiplyMat4, type Mat4, type Vec3 } from "./Keyframe.js";
|
|
3
|
-
|
|
4
|
-
export class Skeleton {
|
|
5
|
-
readonly bones: readonly Bone[];
|
|
6
|
-
|
|
7
|
-
constructor(bones: readonly (Bone | BoneDescriptor)[]) {
|
|
8
|
-
if (bones.length === 0) {
|
|
9
|
-
throw new Error("Skeleton requires at least one bone.");
|
|
10
|
-
}
|
|
11
|
-
this.bones = bones.map((bone) => bone instanceof Bone ? bone : new Bone(bone));
|
|
12
|
-
for (let index = 0; index < this.bones.length; index += 1) {
|
|
13
|
-
const parent = this.bones[index]!.parentIndex;
|
|
14
|
-
if (parent >= index || parent < -1) {
|
|
15
|
-
throw new Error(`Bone ${this.bones[index]!.name} has invalid parent index ${parent}.`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
worldMatrices(): readonly Mat4[] {
|
|
21
|
-
const matrices: Mat4[] = [];
|
|
22
|
-
for (let index = 0; index < this.bones.length; index += 1) {
|
|
23
|
-
const bone = this.bones[index]!;
|
|
24
|
-
const local = bone.localMatrix();
|
|
25
|
-
matrices[index] = bone.parentIndex >= 0 ? multiplyMat4(matrices[bone.parentIndex]!, local) : local;
|
|
26
|
-
}
|
|
27
|
-
return matrices;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
matrixPalette(): readonly Mat4[] {
|
|
31
|
-
const worlds = this.worldMatrices();
|
|
32
|
-
return this.bones.map((bone, index) => multiplyMat4(worlds[index]!, bone.inverseBindMatrix));
|
|
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
|
-
}
|
|
78
|
-
}
|
package/src/Skinning.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { Skeleton } from "./Skeleton.js";
|
|
2
|
-
|
|
3
|
-
export type SkinningPalette = {
|
|
4
|
-
readonly jointCount: number;
|
|
5
|
-
readonly matrices: Float32Array;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export function buildSkinningPalette(skeleton: Skeleton, maxJoints = 256): SkinningPalette {
|
|
9
|
-
const palette = skeleton.matrixPalette();
|
|
10
|
-
if (palette.length > maxJoints) {
|
|
11
|
-
throw new Error(`Skeleton has ${palette.length} joints, exceeding max ${maxJoints}.`);
|
|
12
|
-
}
|
|
13
|
-
const matrices = new Float32Array(palette.length * 16);
|
|
14
|
-
for (let index = 0; index < palette.length; index += 1) {
|
|
15
|
-
matrices.set(palette[index]!, index * 16);
|
|
16
|
-
}
|
|
17
|
-
return { jointCount: palette.length, matrices };
|
|
18
|
-
}
|