@aura3d/animation 3.0.0 → 4.0.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/AnimationController.d.ts +8 -0
- package/dist/AnimationController.d.ts.map +1 -1
- package/dist/AnimationController.js +93 -0
- package/dist/AnimationController.js.map +1 -1
- package/dist/AnimationMixer.d.ts +4 -1
- package/dist/AnimationMixer.d.ts.map +1 -1
- package/dist/AnimationMixer.js +47 -29
- package/dist/AnimationMixer.js.map +1 -1
- package/dist/FootIk.d.ts +50 -0
- package/dist/FootIk.d.ts.map +1 -1
- package/dist/FootIk.js +224 -8
- package/dist/FootIk.js.map +1 -1
- package/dist/HumanoidRetargeting.d.ts +6 -2
- package/dist/HumanoidRetargeting.d.ts.map +1 -1
- package/dist/HumanoidRetargeting.js +18 -8
- package/dist/HumanoidRetargeting.js.map +1 -1
- package/dist/IK.d.ts +26 -0
- package/dist/IK.d.ts.map +1 -1
- package/dist/IK.js +515 -0
- package/dist/IK.js.map +1 -1
- package/dist/Keyframe.d.ts.map +1 -1
- package/dist/Keyframe.js +13 -12
- package/dist/Keyframe.js.map +1 -1
- package/dist/LocomotionEvidence.d.ts +34 -0
- package/dist/LocomotionEvidence.d.ts.map +1 -0
- package/dist/LocomotionEvidence.js +48 -0
- package/dist/LocomotionEvidence.js.map +1 -0
- package/dist/RootMotion.d.ts +13 -4
- package/dist/RootMotion.d.ts.map +1 -1
- package/dist/RootMotion.js +21 -1
- package/dist/RootMotion.js.map +1 -1
- package/dist/SpringBones.d.ts +80 -0
- package/dist/SpringBones.d.ts.map +1 -1
- package/dist/SpringBones.js +593 -49
- package/dist/SpringBones.js.map +1 -1
- package/dist/browser-index.d.ts +1 -1
- package/dist/browser-index.d.ts.map +1 -1
- package/dist/browser-index.js +1 -1
- package/dist/browser-index.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +7 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/pose.d.ts +25 -0
- package/dist/contracts/pose.d.ts.map +1 -0
- package/dist/contracts/pose.js +41 -0
- package/dist/contracts/pose.js.map +1 -0
- package/dist/experimental/CrowdAnimation.d.ts +13 -0
- package/dist/experimental/CrowdAnimation.d.ts.map +1 -0
- package/dist/experimental/CrowdAnimation.js +14 -0
- package/dist/experimental/CrowdAnimation.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lanes/index.d.ts +17 -0
- package/dist/lanes/index.d.ts.map +1 -0
- package/dist/lanes/index.js +17 -0
- package/dist/lanes/index.js.map +1 -0
- package/dist/lanes/prd01.d.ts +6 -0
- package/dist/lanes/prd01.d.ts.map +1 -0
- package/dist/lanes/prd01.js +6 -0
- package/dist/lanes/prd01.js.map +1 -0
- package/dist/lanes/prd02.d.ts +6 -0
- package/dist/lanes/prd02.d.ts.map +1 -0
- package/dist/lanes/prd02.js +6 -0
- package/dist/lanes/prd02.js.map +1 -0
- package/dist/lanes/prd03.d.ts +6 -0
- package/dist/lanes/prd03.d.ts.map +1 -0
- package/dist/lanes/prd03.js +6 -0
- package/dist/lanes/prd03.js.map +1 -0
- package/dist/lanes/prd04.d.ts +6 -0
- package/dist/lanes/prd04.d.ts.map +1 -0
- package/dist/lanes/prd04.js +6 -0
- package/dist/lanes/prd04.js.map +1 -0
- package/dist/lanes/prd05.d.ts +6 -0
- package/dist/lanes/prd05.d.ts.map +1 -0
- package/dist/lanes/prd05.js +6 -0
- package/dist/lanes/prd05.js.map +1 -0
- package/dist/lanes/prd06.d.ts +37 -0
- package/dist/lanes/prd06.d.ts.map +1 -0
- package/dist/lanes/prd06.js +26 -0
- package/dist/lanes/prd06.js.map +1 -0
- package/dist/lanes/prd07.d.ts +6 -0
- package/dist/lanes/prd07.d.ts.map +1 -0
- package/dist/lanes/prd07.js +6 -0
- package/dist/lanes/prd07.js.map +1 -0
- package/dist/lanes/prd08.d.ts +6 -0
- package/dist/lanes/prd08.d.ts.map +1 -0
- package/dist/lanes/prd08.js +6 -0
- package/dist/lanes/prd08.js.map +1 -0
- package/dist/lanes/prd09.d.ts +6 -0
- package/dist/lanes/prd09.d.ts.map +1 -0
- package/dist/lanes/prd09.js +6 -0
- package/dist/lanes/prd09.js.map +1 -0
- package/dist/lanes/prd10.d.ts +6 -0
- package/dist/lanes/prd10.d.ts.map +1 -0
- package/dist/lanes/prd10.js +6 -0
- package/dist/lanes/prd10.js.map +1 -0
- package/dist/lanes/prd11.d.ts +6 -0
- package/dist/lanes/prd11.d.ts.map +1 -0
- package/dist/lanes/prd11.js +6 -0
- package/dist/lanes/prd11.js.map +1 -0
- package/dist/lanes/prd12.d.ts +6 -0
- package/dist/lanes/prd12.d.ts.map +1 -0
- package/dist/lanes/prd12.js +6 -0
- package/dist/lanes/prd12.js.map +1 -0
- package/dist/lanes/prd13.d.ts +6 -0
- package/dist/lanes/prd13.d.ts.map +1 -0
- package/dist/lanes/prd13.js +6 -0
- package/dist/lanes/prd13.js.map +1 -0
- package/dist/lanes/prd14.d.ts +6 -0
- package/dist/lanes/prd14.d.ts.map +1 -0
- package/dist/lanes/prd14.js +6 -0
- package/dist/lanes/prd14.js.map +1 -0
- package/dist/lanes/prd15.d.ts +6 -0
- package/dist/lanes/prd15.d.ts.map +1 -0
- package/dist/lanes/prd15.js +6 -0
- package/dist/lanes/prd15.js.map +1 -0
- package/dist/pose/BoneMask.d.ts +39 -0
- package/dist/pose/BoneMask.d.ts.map +1 -0
- package/dist/pose/BoneMask.js +160 -0
- package/dist/pose/BoneMask.js.map +1 -0
- package/dist/pose/CcdIkConstraint.d.ts +38 -0
- package/dist/pose/CcdIkConstraint.d.ts.map +1 -0
- package/dist/pose/CcdIkConstraint.js +198 -0
- package/dist/pose/CcdIkConstraint.js.map +1 -0
- package/dist/pose/CompiledClip.d.ts +48 -0
- package/dist/pose/CompiledClip.d.ts.map +1 -0
- package/dist/pose/CompiledClip.js +209 -0
- package/dist/pose/CompiledClip.js.map +1 -0
- package/dist/pose/LookAtConstraint.d.ts +48 -0
- package/dist/pose/LookAtConstraint.d.ts.map +1 -0
- package/dist/pose/LookAtConstraint.js +251 -0
- package/dist/pose/LookAtConstraint.js.map +1 -0
- package/dist/pose/MotionMetrics.d.ts +136 -0
- package/dist/pose/MotionMetrics.d.ts.map +1 -0
- package/dist/pose/MotionMetrics.js +253 -0
- package/dist/pose/MotionMetrics.js.map +1 -0
- package/dist/pose/PoseBuffer.d.ts +23 -0
- package/dist/pose/PoseBuffer.d.ts.map +1 -0
- package/dist/pose/PoseBuffer.js +48 -0
- package/dist/pose/PoseBuffer.js.map +1 -0
- package/dist/pose/PoseInertializer.d.ts +38 -0
- package/dist/pose/PoseInertializer.d.ts.map +1 -0
- package/dist/pose/PoseInertializer.js +208 -0
- package/dist/pose/PoseInertializer.js.map +1 -0
- package/dist/pose/PoseMixer.d.ts +256 -0
- package/dist/pose/PoseMixer.d.ts.map +1 -0
- package/dist/pose/PoseMixer.js +848 -0
- package/dist/pose/PoseMixer.js.map +1 -0
- package/dist/pose/Retarget.d.ts +77 -0
- package/dist/pose/Retarget.d.ts.map +1 -0
- package/dist/pose/Retarget.js +378 -0
- package/dist/pose/Retarget.js.map +1 -0
- package/dist/pose/RetargetCache.d.ts +21 -0
- package/dist/pose/RetargetCache.d.ts.map +1 -0
- package/dist/pose/RetargetCache.js +102 -0
- package/dist/pose/RetargetCache.js.map +1 -0
- package/dist/pose/RetargetWorker.d.ts +21 -0
- package/dist/pose/RetargetWorker.d.ts.map +1 -0
- package/dist/pose/RetargetWorker.js +56 -0
- package/dist/pose/RetargetWorker.js.map +1 -0
- package/dist/pose/SkeletonBinding.d.ts +55 -0
- package/dist/pose/SkeletonBinding.d.ts.map +1 -0
- package/dist/pose/SkeletonBinding.js +62 -0
- package/dist/pose/SkeletonBinding.js.map +1 -0
- package/dist/pose/blendKernels.d.ts +32 -0
- package/dist/pose/blendKernels.d.ts.map +1 -0
- package/dist/pose/blendKernels.js +101 -0
- package/dist/pose/blendKernels.js.map +1 -0
- package/dist/pose/makeClipAdditive.d.ts +22 -0
- package/dist/pose/makeClipAdditive.d.ts.map +1 -0
- package/dist/pose/makeClipAdditive.js +101 -0
- package/dist/pose/makeClipAdditive.js.map +1 -0
- package/dist/pose/poseMixerFlags.d.ts +18 -0
- package/dist/pose/poseMixerFlags.d.ts.map +1 -0
- package/dist/pose/poseMixerFlags.js +30 -0
- package/dist/pose/poseMixerFlags.js.map +1 -0
- package/dist/pose/quatFlat.d.ts +16 -0
- package/dist/pose/quatFlat.d.ts.map +1 -0
- package/dist/pose/quatFlat.js +88 -0
- package/dist/pose/quatFlat.js.map +1 -0
- package/dist/pose/retarget.worker.d.ts +25 -0
- package/dist/pose/retarget.worker.d.ts.map +1 -0
- package/dist/pose/retarget.worker.js +22 -0
- package/dist/pose/retarget.worker.js.map +1 -0
- package/package.json +14 -2
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
import { sampleClipEvents } from "../AnimationClipEvents.js";
|
|
2
|
+
import { compileClip, createTrackCursors, sampleTrackInto } from "./CompiledClip.js";
|
|
3
|
+
import { lerpVec3Flat, multiplyQuatFlat, slerpQuatFlat } from "./quatFlat.js";
|
|
4
|
+
import { isClipAdditive } from "./makeClipAdditive.js";
|
|
5
|
+
export class PoseAction {
|
|
6
|
+
mixer;
|
|
7
|
+
clipName;
|
|
8
|
+
clip;
|
|
9
|
+
bindings;
|
|
10
|
+
cursors;
|
|
11
|
+
additive;
|
|
12
|
+
weight = 1;
|
|
13
|
+
timeScale = 1;
|
|
14
|
+
time = 0;
|
|
15
|
+
playing = false;
|
|
16
|
+
paused = false;
|
|
17
|
+
enabled = true;
|
|
18
|
+
loopMode = "repeat";
|
|
19
|
+
repetitions = Infinity;
|
|
20
|
+
clampWhenFinished = false;
|
|
21
|
+
syncGroupName = null;
|
|
22
|
+
pingPongForward = true;
|
|
23
|
+
loopCount = -1;
|
|
24
|
+
fade = null;
|
|
25
|
+
warpState = null;
|
|
26
|
+
restoreTimeScale = null;
|
|
27
|
+
effectiveWeight = 0;
|
|
28
|
+
effectiveTimeScale = 0;
|
|
29
|
+
constructor(mixer, clipName, clip, options = {}) {
|
|
30
|
+
this.mixer = mixer;
|
|
31
|
+
this.clipName = clipName;
|
|
32
|
+
this.clip = clip;
|
|
33
|
+
this.bindings = mixer.resolveBindings(clip);
|
|
34
|
+
this.cursors = createTrackCursors(clip);
|
|
35
|
+
const source = mixer.sourceClip(clipName);
|
|
36
|
+
this.additive = options.additive ?? (source !== undefined && isClipAdditive(source));
|
|
37
|
+
this.weight = options.weight ?? 1;
|
|
38
|
+
this.timeScale = options.timeScale ?? 1;
|
|
39
|
+
if (options.loopMode)
|
|
40
|
+
this.loopMode = options.loopMode;
|
|
41
|
+
if (options.repetitions !== undefined)
|
|
42
|
+
this.repetitions = options.repetitions;
|
|
43
|
+
this.clampWhenFinished = options.clampWhenFinished ?? false;
|
|
44
|
+
this.syncGroupName = options.syncGroup ?? null;
|
|
45
|
+
}
|
|
46
|
+
play() {
|
|
47
|
+
this.playing = true;
|
|
48
|
+
this.paused = false;
|
|
49
|
+
this.enabled = true;
|
|
50
|
+
this.mixer.activate(this);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
stop() {
|
|
54
|
+
this.playing = false;
|
|
55
|
+
this.paused = false;
|
|
56
|
+
this.time = 0;
|
|
57
|
+
this.loopCount = -1;
|
|
58
|
+
this.pingPongForward = true;
|
|
59
|
+
this.mixer.deactivate(this);
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
pause() {
|
|
63
|
+
this.paused = true;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
setEffectiveWeight(weight) {
|
|
67
|
+
this.weight = weight;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
setEffectiveTimeScale(timeScale) {
|
|
71
|
+
this.timeScale = timeScale;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
setLoop(mode, repetitions = Infinity) {
|
|
75
|
+
this.loopMode = mode;
|
|
76
|
+
this.repetitions = repetitions;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
/** three r185 syncWith: copy time + timeScale and stop any warping. */
|
|
80
|
+
syncWith(other) {
|
|
81
|
+
this.time = other.time;
|
|
82
|
+
this.timeScale = other.timeScale;
|
|
83
|
+
this.stopWarping();
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
fadeIn(duration) {
|
|
87
|
+
return this.scheduleFade(duration, 0, 1);
|
|
88
|
+
}
|
|
89
|
+
fadeOut(duration) {
|
|
90
|
+
return this.scheduleFade(duration, 1, 0);
|
|
91
|
+
}
|
|
92
|
+
fadeTo(weight, duration) {
|
|
93
|
+
return this.scheduleFade(duration, this.weight, weight);
|
|
94
|
+
}
|
|
95
|
+
/** three r185 warp: multiplicative timescale interpolant over mixer time (values = start/base → end/base). */
|
|
96
|
+
warp(startTimeScale, endTimeScale, duration) {
|
|
97
|
+
const now = this.mixer.time;
|
|
98
|
+
const base = this.timeScale;
|
|
99
|
+
this.warpState = { startTime: now, duration, from: startTimeScale / base, to: endTimeScale / base };
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
stopWarping() {
|
|
103
|
+
this.warpState = null;
|
|
104
|
+
this.restoreTimeScale = null;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
stopFading() {
|
|
108
|
+
this.fade = null;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
/** three r185 crossFadeFrom(this=fadeIn, fadeOut, duration, warp). */
|
|
112
|
+
crossFadeFrom(fadeOutAction, duration, warp = false) {
|
|
113
|
+
fadeOutAction.fadeOut(duration);
|
|
114
|
+
this.fadeIn(duration);
|
|
115
|
+
if (warp) {
|
|
116
|
+
const fadeInDuration = this.clip.duration;
|
|
117
|
+
const fadeOutDuration = fadeOutAction.clip.duration;
|
|
118
|
+
const startEndRatio = fadeInDuration === 0 ? 1 : fadeOutDuration / fadeInDuration;
|
|
119
|
+
const endStartRatio = fadeOutDuration === 0 ? 1 : fadeInDuration / fadeOutDuration;
|
|
120
|
+
fadeOutAction.restoreTimeScale = fadeOutAction.timeScale;
|
|
121
|
+
this.restoreTimeScale = this.timeScale;
|
|
122
|
+
fadeOutAction.warp(1.0, startEndRatio, duration);
|
|
123
|
+
this.warp(endStartRatio, 1.0, duration);
|
|
124
|
+
}
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
crossFadeTo(fadeInAction, duration, warp = false) {
|
|
128
|
+
return fadeInAction.crossFadeFrom(this, duration, warp);
|
|
129
|
+
}
|
|
130
|
+
get normalizedPhase() {
|
|
131
|
+
return this.clip.duration > 0 ? this.time / this.clip.duration : 0;
|
|
132
|
+
}
|
|
133
|
+
scheduleFade(duration, weightNow, weightThen) {
|
|
134
|
+
const now = this.mixer.time;
|
|
135
|
+
if (duration <= 0) {
|
|
136
|
+
this.fade = null;
|
|
137
|
+
this.weight = weightThen;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
this.fade = { startTime: now, duration, from: weightNow, to: weightThen };
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
/** Effective weight at mixer.time, per r185 `_updateWeight`. */
|
|
144
|
+
updateWeight(now) {
|
|
145
|
+
let weight = 0;
|
|
146
|
+
if (this.enabled) {
|
|
147
|
+
weight = this.weight;
|
|
148
|
+
const fade = this.fade;
|
|
149
|
+
if (fade !== null) {
|
|
150
|
+
const end = fade.startTime + fade.duration;
|
|
151
|
+
const t = Math.min(Math.max((now - fade.startTime) / fade.duration, 0), 1);
|
|
152
|
+
const interpolant = fade.from + (fade.to - fade.from) * t;
|
|
153
|
+
weight *= interpolant;
|
|
154
|
+
if (now > end) {
|
|
155
|
+
this.fade = null;
|
|
156
|
+
if (interpolant === 0)
|
|
157
|
+
this.enabled = false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.effectiveWeight = weight;
|
|
162
|
+
return weight;
|
|
163
|
+
}
|
|
164
|
+
/** Effective timescale at mixer.time, per r185 `_updateTimeScale`. */
|
|
165
|
+
updateTimeScale(now) {
|
|
166
|
+
let timeScale = 0;
|
|
167
|
+
if (!this.paused) {
|
|
168
|
+
timeScale = this.timeScale;
|
|
169
|
+
const warp = this.warpState;
|
|
170
|
+
if (warp !== null) {
|
|
171
|
+
const end = warp.startTime + warp.duration;
|
|
172
|
+
const t = Math.min(Math.max((now - warp.startTime) / warp.duration, 0), 1);
|
|
173
|
+
timeScale *= warp.from + (warp.to - warp.from) * t;
|
|
174
|
+
if (now > end) {
|
|
175
|
+
if (timeScale === 0) {
|
|
176
|
+
this.paused = true;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
if (this.restoreTimeScale !== null)
|
|
180
|
+
timeScale = this.restoreTimeScale;
|
|
181
|
+
this.timeScale = timeScale;
|
|
182
|
+
}
|
|
183
|
+
this.warpState = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this.effectiveTimeScale = timeScale;
|
|
188
|
+
return timeScale;
|
|
189
|
+
}
|
|
190
|
+
/** Advance clip time per r185 `_updateTime`; returns {wrapped, finished}. */
|
|
191
|
+
updateTime(deltaTime) {
|
|
192
|
+
const duration = this.clip.duration;
|
|
193
|
+
const previous = this.time;
|
|
194
|
+
let time = previous + deltaTime;
|
|
195
|
+
let wrapped = false;
|
|
196
|
+
let finished = false;
|
|
197
|
+
if (this.loopMode === "once") {
|
|
198
|
+
if (this.loopCount === -1)
|
|
199
|
+
this.loopCount = 0;
|
|
200
|
+
if (time >= duration) {
|
|
201
|
+
time = duration;
|
|
202
|
+
finished = true;
|
|
203
|
+
}
|
|
204
|
+
else if (time < 0) {
|
|
205
|
+
time = 0;
|
|
206
|
+
finished = true;
|
|
207
|
+
}
|
|
208
|
+
if (finished) {
|
|
209
|
+
if (this.clampWhenFinished)
|
|
210
|
+
this.paused = true;
|
|
211
|
+
else
|
|
212
|
+
this.enabled = false;
|
|
213
|
+
this.time = time;
|
|
214
|
+
return { wrapped, finished, previous };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// repeat / pingpong
|
|
219
|
+
if (this.loopCount === -1) {
|
|
220
|
+
if (deltaTime >= 0) {
|
|
221
|
+
this.loopCount = 0;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (duration > 0 && (time >= duration || time < 0)) {
|
|
225
|
+
const loopDelta = Math.floor(time / duration);
|
|
226
|
+
time -= duration * loopDelta;
|
|
227
|
+
this.loopCount += Math.abs(loopDelta);
|
|
228
|
+
wrapped = true;
|
|
229
|
+
const pending = this.repetitions - this.loopCount;
|
|
230
|
+
if (pending <= 0) {
|
|
231
|
+
if (this.clampWhenFinished)
|
|
232
|
+
this.paused = true;
|
|
233
|
+
else
|
|
234
|
+
this.enabled = false;
|
|
235
|
+
this.time = deltaTime > 0 ? duration : 0;
|
|
236
|
+
return { wrapped, finished: true, previous };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (this.loopMode === "pingpong") {
|
|
240
|
+
if ((this.loopCount & 1) === 1)
|
|
241
|
+
this.pingPongForward = false;
|
|
242
|
+
else
|
|
243
|
+
this.pingPongForward = true;
|
|
244
|
+
if (!this.pingPongForward)
|
|
245
|
+
time = duration - time;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
this.time = time;
|
|
249
|
+
return { wrapped, finished, previous };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export class PoseMixer {
|
|
253
|
+
skeleton;
|
|
254
|
+
inertializer;
|
|
255
|
+
time = 0;
|
|
256
|
+
timeScale = 1;
|
|
257
|
+
clips = new Map();
|
|
258
|
+
actions = [];
|
|
259
|
+
layers = [];
|
|
260
|
+
layerByName = new Map();
|
|
261
|
+
eventListeners = new Set();
|
|
262
|
+
latestPose = null;
|
|
263
|
+
previousPose = null;
|
|
264
|
+
inertializerElapsed = Infinity;
|
|
265
|
+
inertializerHalfLife = 0.15;
|
|
266
|
+
scratchClip = new Float32Array(4);
|
|
267
|
+
// §13 micro-budget — the accumulate paths must run in single-digit µs for a
|
|
268
|
+
// 191-joint rig, so per-(bone,channel) state lives in flat arrays + touched
|
|
269
|
+
// lists, not Maps: a Uint32Array push is ~1ns where Map.get/set ~50ns each
|
|
270
|
+
// was the whole 60 µs budget on its own.
|
|
271
|
+
accWeight;
|
|
272
|
+
addWeight;
|
|
273
|
+
addQuat;
|
|
274
|
+
addVec;
|
|
275
|
+
accTouched;
|
|
276
|
+
addTouched;
|
|
277
|
+
accTouchedCount = 0;
|
|
278
|
+
addTouchedCount = 0;
|
|
279
|
+
constructor(options) {
|
|
280
|
+
this.skeleton = options.skeleton;
|
|
281
|
+
this.inertializer = options.inertializer ?? null;
|
|
282
|
+
const slots = this.skeleton.boneCount * 3;
|
|
283
|
+
this.accWeight = new Float32Array(slots);
|
|
284
|
+
this.addWeight = new Float32Array(slots);
|
|
285
|
+
this.addQuat = new Float32Array(slots * 4);
|
|
286
|
+
this.addVec = new Float32Array(slots * 3);
|
|
287
|
+
this.accTouched = new Uint32Array(slots);
|
|
288
|
+
this.addTouched = new Uint32Array(slots);
|
|
289
|
+
}
|
|
290
|
+
resetAccumulators() {
|
|
291
|
+
for (let i = 0; i < this.accTouchedCount; i += 1)
|
|
292
|
+
this.accWeight[this.accTouched[i]] = 0;
|
|
293
|
+
for (let i = 0; i < this.addTouchedCount; i += 1)
|
|
294
|
+
this.addWeight[this.addTouched[i]] = 0;
|
|
295
|
+
this.accTouchedCount = 0;
|
|
296
|
+
this.addTouchedCount = 0;
|
|
297
|
+
}
|
|
298
|
+
setEffectiveTimeScale(timeScale) {
|
|
299
|
+
this.timeScale = timeScale;
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
get effectiveTimeScale() {
|
|
303
|
+
return this.timeScale;
|
|
304
|
+
}
|
|
305
|
+
/** Register a clip under a name (compiles it for flat sampling). */
|
|
306
|
+
addClip(name, clip) {
|
|
307
|
+
this.clips.set(name, { source: clip, compiled: compileClip(clip) });
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
310
|
+
/** Register a pre-compiled clip (returns undefined if unknown). */
|
|
311
|
+
addCompiledClip(name, compiled, source) {
|
|
312
|
+
this.clips.set(name, { source, compiled });
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
sourceClip(name) {
|
|
316
|
+
return this.clips.get(name)?.source;
|
|
317
|
+
}
|
|
318
|
+
clipAction(name, options = {}) {
|
|
319
|
+
const entry = this.clips.get(name);
|
|
320
|
+
if (!entry) {
|
|
321
|
+
throw new Error(`ANIMATION_CLIP_NOT_FOUND: clip "${name}" is not registered. Available: ${[...this.clips.keys()].join(", ")}`);
|
|
322
|
+
}
|
|
323
|
+
return new PoseAction(this, name, entry.compiled, options);
|
|
324
|
+
}
|
|
325
|
+
/** three r185 play: activate + begin (starts faded-in when `crossFade`). */
|
|
326
|
+
play(name, options = {}) {
|
|
327
|
+
const action = this.clipAction(name, options);
|
|
328
|
+
action.play();
|
|
329
|
+
return action;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Every currently-playing, enabled action across the base layer and named
|
|
333
|
+
* layers — the set `update`/`evaluate` will contribute this frame (T1.9
|
|
334
|
+
* uses it to sample non-pose tracks at each action's own clock/weight and
|
|
335
|
+
* to compute covered-bone sets for `restPoseReset: false`).
|
|
336
|
+
*/
|
|
337
|
+
activeActions() {
|
|
338
|
+
const all = [];
|
|
339
|
+
for (const action of this.actions) {
|
|
340
|
+
if (action.playing && action.enabled)
|
|
341
|
+
all.push(action);
|
|
342
|
+
}
|
|
343
|
+
for (const layer of this.layers) {
|
|
344
|
+
for (const action of layer.actions) {
|
|
345
|
+
if (action.playing && action.enabled)
|
|
346
|
+
all.push(action);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return all;
|
|
350
|
+
}
|
|
351
|
+
/** The active base (non-additive, base-layer) action, or null when none plays. */
|
|
352
|
+
baseAction() {
|
|
353
|
+
return this.activeBaseAction();
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Same active set as {@link activeActions} annotated with the owning layer
|
|
357
|
+
* name — `""` for the base layer — for `animationState()` snapshots (C-37).
|
|
358
|
+
*/
|
|
359
|
+
activeActionEntries() {
|
|
360
|
+
const all = [];
|
|
361
|
+
for (const action of this.actions) {
|
|
362
|
+
if (action.playing && action.enabled)
|
|
363
|
+
all.push({ action, layer: "" });
|
|
364
|
+
}
|
|
365
|
+
for (const layer of this.layers) {
|
|
366
|
+
for (const action of layer.actions) {
|
|
367
|
+
if (action.playing && action.enabled)
|
|
368
|
+
all.push({ action, layer: layer.name });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return all;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* `crossFadeTo(clip, seconds, options)` — mirrors `node.play` semantics:
|
|
375
|
+
* the current active base action fades out over `seconds` (default driven by
|
|
376
|
+
* caller), the new one fades in; `warp` warps timescales by the duration
|
|
377
|
+
* ratio; `transition: "inertialize"` captures pose offsets instead.
|
|
378
|
+
*/
|
|
379
|
+
crossFadeTo(name, seconds, options = {}) {
|
|
380
|
+
const transition = options.transition ?? "crossfade";
|
|
381
|
+
const fadeIn = this.clipAction(name, {
|
|
382
|
+
syncGroup: options.syncGroup,
|
|
383
|
+
weight: options.weight,
|
|
384
|
+
additive: options.additive
|
|
385
|
+
});
|
|
386
|
+
const current = this.activeBaseAction();
|
|
387
|
+
if (transition === "inertialize" && this.inertializer !== null && this.latestPose !== null && this.previousPose !== null) {
|
|
388
|
+
const dt = this.lastDt > 0 ? this.lastDt : 1 / 60;
|
|
389
|
+
this.inertializer.transition(this.latestPose, this.previousPose, dt, options.halfLife ?? 0.15);
|
|
390
|
+
this.inertializerElapsed = 0;
|
|
391
|
+
this.inertializerHalfLife = options.halfLife ?? 0.15;
|
|
392
|
+
if (current !== null)
|
|
393
|
+
current.stop();
|
|
394
|
+
fadeIn.play();
|
|
395
|
+
return fadeIn;
|
|
396
|
+
}
|
|
397
|
+
if (current === null) {
|
|
398
|
+
fadeIn.fadeIn(seconds);
|
|
399
|
+
fadeIn.play();
|
|
400
|
+
return fadeIn;
|
|
401
|
+
}
|
|
402
|
+
fadeIn.play();
|
|
403
|
+
current.crossFadeTo(fadeIn, seconds, options.warp === true);
|
|
404
|
+
return fadeIn;
|
|
405
|
+
}
|
|
406
|
+
activeBaseAction() {
|
|
407
|
+
for (let i = this.actions.length - 1; i >= 0; i -= 1) {
|
|
408
|
+
const action = this.actions[i];
|
|
409
|
+
if (!action.additive && action.playing)
|
|
410
|
+
return action;
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
/** Layer API (C-19 playLayer/stopLayer groundwork). */
|
|
415
|
+
playLayer(layerName, clipName, options = {}) {
|
|
416
|
+
let layer = this.layerByName.get(layerName);
|
|
417
|
+
if (!layer) {
|
|
418
|
+
layer = { name: layerName, blendMode: options.blendMode ?? "override", mask: options.mask ?? null, weight: options.weight ?? 1, actions: [] };
|
|
419
|
+
this.layerByName.set(layerName, layer);
|
|
420
|
+
this.layers.push(layer);
|
|
421
|
+
}
|
|
422
|
+
const action = this.clipAction(clipName, options);
|
|
423
|
+
action.play();
|
|
424
|
+
layer.actions.push(action);
|
|
425
|
+
return action;
|
|
426
|
+
}
|
|
427
|
+
stopLayer(layerName, fadeSeconds = 0) {
|
|
428
|
+
const layer = this.layerByName.get(layerName);
|
|
429
|
+
if (!layer)
|
|
430
|
+
return;
|
|
431
|
+
for (const action of layer.actions) {
|
|
432
|
+
if (fadeSeconds > 0)
|
|
433
|
+
action.fadeOut(fadeSeconds);
|
|
434
|
+
else
|
|
435
|
+
action.stop();
|
|
436
|
+
}
|
|
437
|
+
if (fadeSeconds <= 0) {
|
|
438
|
+
this.layerByName.delete(layerName);
|
|
439
|
+
this.layers.splice(this.layers.indexOf(layer), 1);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
onEvent(listener) {
|
|
443
|
+
this.eventListeners.add(listener);
|
|
444
|
+
return () => this.eventListeners.delete(listener);
|
|
445
|
+
}
|
|
446
|
+
activate(action) {
|
|
447
|
+
if (!this.actions.includes(action))
|
|
448
|
+
this.actions.push(action);
|
|
449
|
+
}
|
|
450
|
+
deactivate(action) {
|
|
451
|
+
const index = this.actions.indexOf(action);
|
|
452
|
+
if (index >= 0)
|
|
453
|
+
this.actions.splice(index, 1);
|
|
454
|
+
}
|
|
455
|
+
/** Resolve `CompiledClip` node-path targets to skeleton bone indices. */
|
|
456
|
+
resolveBindings(clip) {
|
|
457
|
+
const bindings = [];
|
|
458
|
+
for (let i = 0; i < clip.tracks.length; i += 1) {
|
|
459
|
+
const track = clip.tracks[i];
|
|
460
|
+
const parsed = parseTrackTarget(track.target);
|
|
461
|
+
if (parsed === null)
|
|
462
|
+
continue;
|
|
463
|
+
const indices = this.skeleton.jointIndicesByName.get(parsed.nodeName);
|
|
464
|
+
if (!indices || indices.length === 0)
|
|
465
|
+
continue;
|
|
466
|
+
bindings.push({ trackIndex: i, boneIndex: indices[0], channel: parsed.channel });
|
|
467
|
+
}
|
|
468
|
+
return bindings;
|
|
469
|
+
}
|
|
470
|
+
statelessBindings = new Map();
|
|
471
|
+
statelessCursors = new Map();
|
|
472
|
+
statelessBindingsFor(clip) {
|
|
473
|
+
let bindings = this.statelessBindings.get(clip);
|
|
474
|
+
if (bindings === undefined) {
|
|
475
|
+
bindings = this.resolveBindings(clip);
|
|
476
|
+
this.statelessBindings.set(clip, bindings);
|
|
477
|
+
}
|
|
478
|
+
return bindings;
|
|
479
|
+
}
|
|
480
|
+
statelessCursorsFor(clip) {
|
|
481
|
+
let cursors = this.statelessCursors.get(clip);
|
|
482
|
+
if (cursors === undefined) {
|
|
483
|
+
cursors = createTrackCursors(clip);
|
|
484
|
+
this.statelessCursors.set(clip, cursors);
|
|
485
|
+
}
|
|
486
|
+
return cursors;
|
|
487
|
+
}
|
|
488
|
+
lastDt = 0;
|
|
489
|
+
/**
|
|
490
|
+
* Advance mixer time by `dt` (× mixer timescale), update all actions'
|
|
491
|
+
* times/weights, and emit clip events between old and new action times.
|
|
492
|
+
*/
|
|
493
|
+
update(dt) {
|
|
494
|
+
this.lastDt = dt;
|
|
495
|
+
this.time += dt;
|
|
496
|
+
const scaledDt = dt * this.timeScale;
|
|
497
|
+
const events = [];
|
|
498
|
+
const allActions = [...this.actions];
|
|
499
|
+
for (const layer of this.layers)
|
|
500
|
+
allActions.push(...layer.actions);
|
|
501
|
+
for (const action of allActions) {
|
|
502
|
+
if (!action.playing && !action.enabled)
|
|
503
|
+
continue;
|
|
504
|
+
const effectiveTimeScale = action.updateTimeScale(this.time);
|
|
505
|
+
const clipDelta = scaledDt * effectiveTimeScale;
|
|
506
|
+
const { previous, wrapped } = action.updateTime(clipDelta);
|
|
507
|
+
const weight = action.updateWeight(this.time);
|
|
508
|
+
if (!action.enabled && !action.playing)
|
|
509
|
+
this.deactivate(action);
|
|
510
|
+
if (!action.enabled)
|
|
511
|
+
continue;
|
|
512
|
+
const source = this.clips.get(action.clipName);
|
|
513
|
+
if (source !== undefined && source.source.events.length > 0 && weight > 0) {
|
|
514
|
+
const to = action.time;
|
|
515
|
+
const invocations = sampleClipEvents({ id: action.clipName, duration: source.compiled.duration, events: source.source.events }, {
|
|
516
|
+
from: previous,
|
|
517
|
+
to,
|
|
518
|
+
duration: source.compiled.duration,
|
|
519
|
+
loop: wrapped,
|
|
520
|
+
direction: to >= previous ? 1 : -1,
|
|
521
|
+
playbackTime: this.time
|
|
522
|
+
});
|
|
523
|
+
events.push(...invocations);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Aura extension: sync groups keep normalized phase aligned to the leader
|
|
527
|
+
// (first activated member) — three r185 has no phase sync. Only enabled
|
|
528
|
+
// members take part: a faded-out action stays `playing` (enabled=false, as
|
|
529
|
+
// in r185) and must not keep leading the group — it would slave the
|
|
530
|
+
// incoming clip to its own (restored) rate after the crossfade.
|
|
531
|
+
const groups = new Map();
|
|
532
|
+
for (const action of allActions) {
|
|
533
|
+
if (action.syncGroupName === null || !action.playing || !action.enabled)
|
|
534
|
+
continue;
|
|
535
|
+
const group = groups.get(action.syncGroupName) ?? [];
|
|
536
|
+
group.push(action);
|
|
537
|
+
groups.set(action.syncGroupName, group);
|
|
538
|
+
}
|
|
539
|
+
for (const group of groups.values()) {
|
|
540
|
+
if (group.length < 2)
|
|
541
|
+
continue;
|
|
542
|
+
const leader = group[0];
|
|
543
|
+
const phase = leader.normalizedPhase;
|
|
544
|
+
for (let i = 1; i < group.length; i += 1) {
|
|
545
|
+
const member = group[i];
|
|
546
|
+
member.time = phase * member.clip.duration;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
for (const invocation of events) {
|
|
550
|
+
for (const listener of this.eventListeners)
|
|
551
|
+
listener(invocation);
|
|
552
|
+
}
|
|
553
|
+
if (this.inertializer !== null && this.inertializerElapsed !== Infinity) {
|
|
554
|
+
this.inertializerElapsed += scaledDt;
|
|
555
|
+
}
|
|
556
|
+
// Sweep layers whose actions have all faded out.
|
|
557
|
+
for (let i = this.layers.length - 1; i >= 0; i -= 1) {
|
|
558
|
+
const layer = this.layers[i];
|
|
559
|
+
if (layer.actions.length > 0 && layer.actions.every((a) => !a.enabled || !a.playing)) {
|
|
560
|
+
for (const action of layer.actions)
|
|
561
|
+
action.stop();
|
|
562
|
+
this.layers.splice(i, 1);
|
|
563
|
+
this.layerByName.delete(layer.name);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return events;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Evaluate the final pose into `out` (PoseBuffer with skeleton.boneCount
|
|
570
|
+
* bones). Starts from `skeleton.restPose`, applies the base layer
|
|
571
|
+
* (incremental-weighted, rest fills below Σw=1), then each layer in order.
|
|
572
|
+
*/
|
|
573
|
+
evaluate(out) {
|
|
574
|
+
const n = this.skeleton.boneCount;
|
|
575
|
+
const rest = this.skeleton.restPose;
|
|
576
|
+
this.previousPose = this.latestPose;
|
|
577
|
+
// Start at rest — bones untouched by any action keep rest TRS.
|
|
578
|
+
out.positions.set(rest.positions);
|
|
579
|
+
out.rotations.set(rest.rotations);
|
|
580
|
+
out.scales.set(rest.scales);
|
|
581
|
+
this.resetAccumulators();
|
|
582
|
+
this.applyBase(out);
|
|
583
|
+
this.applyRestFill(out);
|
|
584
|
+
this.applyAdditiveAccumulators(out);
|
|
585
|
+
for (const layer of this.layers) {
|
|
586
|
+
this.applyLayer(out, layer);
|
|
587
|
+
}
|
|
588
|
+
if (this.inertializer !== null && this.inertializerElapsed !== Infinity && Number.isFinite(this.inertializerElapsed)) {
|
|
589
|
+
this.inertializer.apply(out, out, this.inertializerElapsed);
|
|
590
|
+
}
|
|
591
|
+
// §13: the latestPose/previousPose snapshots only feed the inertialize
|
|
592
|
+
// transition — skip the 4·boneCount store clone when no inertializer
|
|
593
|
+
// exists (the common path, ~1-2µs on a 191-bone rig).
|
|
594
|
+
if (this.inertializer !== null) {
|
|
595
|
+
this.latestPose = clonePoseInto(out, this.latestPose, n);
|
|
596
|
+
}
|
|
597
|
+
this.resetAccumulators();
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Stateless one-shot blend (T1.7): evaluate the given clips at explicit
|
|
601
|
+
* times with explicit weights and write the result into `out`. No
|
|
602
|
+
* PoseAction is created or mutated; action/layer/inertializer state is
|
|
603
|
+
* untouched. Same accumulate + rest-fill + additive pipeline as
|
|
604
|
+
* `evaluate`, so the two paths stay within the 1e-4 parity bar.
|
|
605
|
+
*/
|
|
606
|
+
evaluateSamples(samples, out) {
|
|
607
|
+
const rest = this.skeleton.restPose;
|
|
608
|
+
out.positions.set(rest.positions);
|
|
609
|
+
out.rotations.set(rest.rotations);
|
|
610
|
+
out.scales.set(rest.scales);
|
|
611
|
+
this.resetAccumulators();
|
|
612
|
+
const scratch = this.scratchClip;
|
|
613
|
+
for (const sample of samples) {
|
|
614
|
+
const entry = this.clips.get(sample.clipName);
|
|
615
|
+
if (entry === undefined) {
|
|
616
|
+
throw new Error(`ANIMATION_CLIP_NOT_FOUND: clip "${sample.clipName}" is not registered. Available: ${[...this.clips.keys()].join(", ")}`);
|
|
617
|
+
}
|
|
618
|
+
const clip = entry.compiled;
|
|
619
|
+
const weight = sample.weight ?? 1;
|
|
620
|
+
if (weight <= 0)
|
|
621
|
+
continue;
|
|
622
|
+
const additive = sample.additive ?? isClipAdditive(entry.source);
|
|
623
|
+
const bindings = this.statelessBindingsFor(clip);
|
|
624
|
+
const cursors = this.statelessCursorsFor(clip);
|
|
625
|
+
for (const binding of bindings) {
|
|
626
|
+
const boneWeight = sample.mask == null ? weight : weight * sample.mask[binding.boneIndex];
|
|
627
|
+
if (boneWeight <= 0)
|
|
628
|
+
continue;
|
|
629
|
+
const track = clip.tracks[binding.trackIndex];
|
|
630
|
+
sampleTrackInto(track, sample.time, cursors, binding.trackIndex, scratch, 0);
|
|
631
|
+
this.accumulate(out, binding, track, scratch, boneWeight, additive);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
this.applyRestFill(out);
|
|
635
|
+
this.applyAdditiveAccumulators(out);
|
|
636
|
+
this.resetAccumulators();
|
|
637
|
+
}
|
|
638
|
+
applyBase(out) {
|
|
639
|
+
const scratch = this.scratchClip;
|
|
640
|
+
for (const action of this.actions) {
|
|
641
|
+
const weight = action.effectiveWeight;
|
|
642
|
+
if (weight <= 0 || !action.enabled)
|
|
643
|
+
continue;
|
|
644
|
+
for (const binding of action.bindings) {
|
|
645
|
+
const track = action.clip.tracks[binding.trackIndex];
|
|
646
|
+
sampleTrackInto(track, action.time, action.cursors, binding.trackIndex, scratch, 0);
|
|
647
|
+
this.accumulate(out, binding, track, scratch, weight, action.additive);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
applyLayer(out, layer) {
|
|
652
|
+
const scratch = this.scratchClip;
|
|
653
|
+
for (const action of layer.actions) {
|
|
654
|
+
const weight = action.effectiveWeight * layer.weight;
|
|
655
|
+
if (weight <= 0 || !action.enabled)
|
|
656
|
+
continue;
|
|
657
|
+
for (const binding of action.bindings) {
|
|
658
|
+
const maskWeight = layer.mask === null ? weight : weight * layer.mask[binding.boneIndex];
|
|
659
|
+
if (maskWeight <= 0)
|
|
660
|
+
continue;
|
|
661
|
+
const track = action.clip.tracks[binding.trackIndex];
|
|
662
|
+
sampleTrackInto(track, action.time, action.cursors, binding.trackIndex, scratch, 0);
|
|
663
|
+
if (layer.blendMode === "additive" || action.additive) {
|
|
664
|
+
this.accumulateAdditive(out, binding, track, scratch, maskWeight);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
this.applyOverride(out, binding, track, scratch, maskWeight);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
accuKey(boneIndex, channel) {
|
|
673
|
+
return boneIndex * 3 + (channel === "position" ? 0 : channel === "rotation" ? 1 : 2);
|
|
674
|
+
}
|
|
675
|
+
/** Base-layer incremental accumulate: `acc = mix(acc, v, w/(Σ+w))`, then rest. */
|
|
676
|
+
accumulate(out, binding, track, sample, weight, additive) {
|
|
677
|
+
const b = binding.boneIndex;
|
|
678
|
+
const key = this.accuKey(b, binding.channel);
|
|
679
|
+
if (additive) {
|
|
680
|
+
this.accumulateAdditiveAccu(binding, sample, weight);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const prevWeight = this.accWeight[key];
|
|
684
|
+
if (prevWeight === 0) {
|
|
685
|
+
this.accTouched[this.accTouchedCount++] = key;
|
|
686
|
+
this.writeSample(out, b, binding.channel, sample);
|
|
687
|
+
this.accWeight[key] = weight;
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const cumulative = prevWeight + weight;
|
|
691
|
+
const mix = weight / cumulative;
|
|
692
|
+
this.mixInPlace(out, b, binding.channel, sample, mix);
|
|
693
|
+
this.accWeight[key] = cumulative;
|
|
694
|
+
}
|
|
695
|
+
/** r185 `accumulateAdditive`: quats chain-multiply `add = slerp(add, add·delta, w)`; vecs `add += delta·w`. */
|
|
696
|
+
accumulateAdditiveAccu(binding, delta, weight) {
|
|
697
|
+
const key = this.accuKey(binding.boneIndex, binding.channel);
|
|
698
|
+
const prev = this.addWeight[key];
|
|
699
|
+
if (prev === 0)
|
|
700
|
+
this.addTouched[this.addTouchedCount++] = key;
|
|
701
|
+
if (binding.channel === "rotation") {
|
|
702
|
+
const off = key * 4;
|
|
703
|
+
if (prev === 0) {
|
|
704
|
+
this.addQuat[off] = 0;
|
|
705
|
+
this.addQuat[off + 1] = 0;
|
|
706
|
+
this.addQuat[off + 2] = 0;
|
|
707
|
+
this.addQuat[off + 3] = 1;
|
|
708
|
+
}
|
|
709
|
+
const work = scratchMul;
|
|
710
|
+
multiplyQuatFlat(work, 0, this.addQuat, off, delta, 0);
|
|
711
|
+
slerpQuatFlat(this.addQuat, off, this.addQuat, off, work, 0, weight);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
const off = key * 3;
|
|
715
|
+
if (prev === 0) {
|
|
716
|
+
this.addVec[off] = 0;
|
|
717
|
+
this.addVec[off + 1] = 0;
|
|
718
|
+
this.addVec[off + 2] = 0;
|
|
719
|
+
}
|
|
720
|
+
this.addVec[off] += delta[0] * weight;
|
|
721
|
+
this.addVec[off + 1] += delta[1] * weight;
|
|
722
|
+
this.addVec[off + 2] += delta[2] * weight;
|
|
723
|
+
}
|
|
724
|
+
this.addWeight[key] = prev + weight;
|
|
725
|
+
}
|
|
726
|
+
/** r185 `apply` additive stage: `accu = slerp(accu, accu·add, 1)` / `accu += add`. */
|
|
727
|
+
applyAdditiveAccumulators(out) {
|
|
728
|
+
for (let t = 0; t < this.addTouchedCount; t += 1) {
|
|
729
|
+
const key = this.addTouched[t];
|
|
730
|
+
const boneIndex = (key / 3) | 0;
|
|
731
|
+
const channel = key % 3 === 0 ? "position" : key % 3 === 1 ? "rotation" : "scale";
|
|
732
|
+
if (channel === "rotation") {
|
|
733
|
+
multiplyQuatFlat(scratchMul, 0, out.rotations, boneIndex * 4, this.addQuat, key * 4);
|
|
734
|
+
slerpQuatFlat(out.rotations, boneIndex * 4, out.rotations, boneIndex * 4, scratchMul, 0, 1);
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
const off = boneIndex * 3;
|
|
738
|
+
const src = key * 3;
|
|
739
|
+
const dst = channel === "position" ? out.positions : out.scales;
|
|
740
|
+
for (let k = 0; k < 3; k += 1)
|
|
741
|
+
dst[off + k] += this.addVec[src + k];
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/** After base accumulate: fill below-1 remainder from rest pose. Call once per channel per evaluate. */
|
|
746
|
+
applyRestFill(out) {
|
|
747
|
+
const rest = this.skeleton.restPose;
|
|
748
|
+
for (let t = 0; t < this.accTouchedCount; t += 1) {
|
|
749
|
+
const key = this.accTouched[t];
|
|
750
|
+
const weight = this.accWeight[key];
|
|
751
|
+
if (weight >= 1)
|
|
752
|
+
continue;
|
|
753
|
+
const boneIndex = Math.floor(key / 3);
|
|
754
|
+
const channel = key % 3 === 0 ? "position" : key % 3 === 1 ? "rotation" : "scale";
|
|
755
|
+
const remainder = 1 - weight;
|
|
756
|
+
if (channel === "rotation") {
|
|
757
|
+
slerpQuatFlat(out.rotations, boneIndex * 4, out.rotations, boneIndex * 4, rest.rotations, boneIndex * 4, remainder);
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
const off = boneIndex * 3;
|
|
761
|
+
const dst = channel === "position" ? out.positions : out.scales;
|
|
762
|
+
const src = channel === "position" ? rest.positions : rest.scales;
|
|
763
|
+
lerpVec3Flat(dst, off, dst, off, src, off, remainder);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
writeSample(out, boneIndex, channel, sample) {
|
|
768
|
+
if (channel === "rotation") {
|
|
769
|
+
const off = boneIndex * 4;
|
|
770
|
+
out.rotations[off] = sample[0];
|
|
771
|
+
out.rotations[off + 1] = sample[1];
|
|
772
|
+
out.rotations[off + 2] = sample[2];
|
|
773
|
+
out.rotations[off + 3] = sample[3];
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
const off = boneIndex * 3;
|
|
777
|
+
const dst = channel === "position" ? out.positions : out.scales;
|
|
778
|
+
dst[off] = sample[0];
|
|
779
|
+
dst[off + 1] = sample[1];
|
|
780
|
+
dst[off + 2] = sample[2];
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
mixInPlace(out, boneIndex, channel, sample, t) {
|
|
784
|
+
const scratchB = scratchSampleB;
|
|
785
|
+
if (channel === "rotation") {
|
|
786
|
+
slerpQuatFlat(out.rotations, boneIndex * 4, out.rotations, boneIndex * 4, sample, 0, t);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
const off = boneIndex * 3;
|
|
790
|
+
const dst = channel === "position" ? out.positions : out.scales;
|
|
791
|
+
lerpVec3Flat(dst, off, dst, off, sample, 0, t);
|
|
792
|
+
void scratchB;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Override layer: `out[b] = slerp(out[b], layer[b], w)` — applied to whatever
|
|
797
|
+
* the base produced (which may itself be rest-blended).
|
|
798
|
+
*/
|
|
799
|
+
applyOverride(out, binding, track, sample, weight) {
|
|
800
|
+
this.mixInPlace(out, binding.boneIndex, binding.channel, sample, weight);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Additive: quats `out = slerp(out, out·delta, w)`; positions and scales
|
|
804
|
+
* `out += delta·w` (r185 `_slerpAdditive`/`_lerpAdditive`).
|
|
805
|
+
*/
|
|
806
|
+
accumulateAdditive(out, binding, track, delta, weight) {
|
|
807
|
+
const b = binding.boneIndex;
|
|
808
|
+
if (binding.channel === "rotation") {
|
|
809
|
+
multiplyQuatFlat(scratchMul, 0, out.rotations, b * 4, delta, 0);
|
|
810
|
+
slerpQuatFlat(out.rotations, b * 4, out.rotations, b * 4, scratchMul, 0, weight);
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
const off = b * 3;
|
|
814
|
+
const dst = binding.channel === "position" ? out.positions : out.scales;
|
|
815
|
+
for (let k = 0; k < 3; k += 1)
|
|
816
|
+
dst[off + k] += delta[k] * weight;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const scratchMul = new Float32Array(4);
|
|
821
|
+
const scratchSampleB = new Float32Array(4);
|
|
822
|
+
function clonePoseInto(source, target, boneCount) {
|
|
823
|
+
const pose = target ?? { boneCount, positions: new Float32Array(boneCount * 3), rotations: new Float32Array(boneCount * 4), scales: new Float32Array(boneCount * 3) };
|
|
824
|
+
pose.positions.set(source.positions);
|
|
825
|
+
pose.rotations.set(source.rotations);
|
|
826
|
+
pose.scales.set(source.scales);
|
|
827
|
+
return pose;
|
|
828
|
+
}
|
|
829
|
+
const CHANNELS = {
|
|
830
|
+
translation: "position",
|
|
831
|
+
position: "position",
|
|
832
|
+
rotation: "rotation",
|
|
833
|
+
quaternion: "rotation",
|
|
834
|
+
scale: "scale"
|
|
835
|
+
};
|
|
836
|
+
/** Parse `<nodeName>.<path>` — mirrors glTF runtime targets (weights/material/light are out of pose scope). */
|
|
837
|
+
function parseTrackTarget(target) {
|
|
838
|
+
if (target.startsWith("material:") || target.startsWith("light:"))
|
|
839
|
+
return null;
|
|
840
|
+
const separator = target.lastIndexOf(".");
|
|
841
|
+
if (separator <= 0 || separator === target.length - 1)
|
|
842
|
+
return null;
|
|
843
|
+
const channel = CHANNELS[target.slice(separator + 1)];
|
|
844
|
+
if (channel === undefined)
|
|
845
|
+
return null;
|
|
846
|
+
return { nodeName: target.slice(0, separator), channel };
|
|
847
|
+
}
|
|
848
|
+
//# sourceMappingURL=PoseMixer.js.map
|