@zephyr3d/scene 0.9.8 → 0.9.10
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/animation/animation.js +87 -0
- package/dist/animation/animation.js.map +1 -1
- package/dist/animation/animationcontroller.js +503 -0
- package/dist/animation/animationcontroller.js.map +1 -0
- package/dist/animation/animationset.js +643 -36
- package/dist/animation/animationset.js.map +1 -1
- package/dist/animation/animationtimeline.js +974 -0
- package/dist/animation/animationtimeline.js.map +1 -0
- package/dist/app/scriptregistry.js +1 -1
- package/dist/app/scriptregistry.js.map +1 -1
- package/dist/asset/model.js +2 -1
- package/dist/asset/model.js.map +1 -1
- package/dist/index.d.ts +1263 -23
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/shadow/esm.js.map +1 -1
- package/dist/utility/serialization/manager.js +1 -0
- package/dist/utility/serialization/manager.js.map +1 -1
- package/dist/utility/serialization/scene/animation.js +4 -3
- package/dist/utility/serialization/scene/animation.js.map +1 -1
- package/dist/utility/serialization/scene/node.js +3 -2
- package/dist/utility/serialization/scene/node.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Disposable, weightedAverage, Quaternion, Interpolator, Vector3 } from '@zephyr3d/base';
|
|
1
|
+
import { makeObservable, Disposable, weightedAverage, Quaternion, Observable, Interpolator, Vector3 } from '@zephyr3d/base';
|
|
2
2
|
import { AnimationClip } from './animation.js';
|
|
3
3
|
import { NodeRotationTrack } from './rotationtrack.js';
|
|
4
4
|
import { NodeEulerRotationTrack } from './eulerrotationtrack.js';
|
|
@@ -7,6 +7,224 @@ import { NodeScaleTrack } from './scaletrack.js';
|
|
|
7
7
|
import { HumanoidBodyRig } from './skeleton.js';
|
|
8
8
|
import { createSkeletalMaskedAnimationClip } from './animationmask.js';
|
|
9
9
|
|
|
10
|
+
let nextAnimationPlaybackId = 1;
|
|
11
|
+
/**
|
|
12
|
+
* Runtime handle for one playback of an AnimationClip.
|
|
13
|
+
* @public
|
|
14
|
+
*/ class AnimationPlayback extends Observable {
|
|
15
|
+
/** @internal */ _animationSet;
|
|
16
|
+
/** @internal */ _clip;
|
|
17
|
+
/** @internal */ _options;
|
|
18
|
+
/** @internal */ _id;
|
|
19
|
+
/** @internal */ _frameWaiters;
|
|
20
|
+
/** @internal */ _state;
|
|
21
|
+
/** @internal */ _time;
|
|
22
|
+
/** @internal */ _weight;
|
|
23
|
+
/** @internal */ _speedRatio;
|
|
24
|
+
/** @internal */ _layer;
|
|
25
|
+
/** @internal */ _priority;
|
|
26
|
+
/** @internal */ _interruptible;
|
|
27
|
+
constructor(animationSet, clip, options){
|
|
28
|
+
super();
|
|
29
|
+
this._animationSet = animationSet;
|
|
30
|
+
this._clip = clip;
|
|
31
|
+
this._options = {
|
|
32
|
+
...options ?? {}
|
|
33
|
+
};
|
|
34
|
+
this._id = this._options.id ?? `animation-playback-${nextAnimationPlaybackId++}`;
|
|
35
|
+
this._state = 'scheduled';
|
|
36
|
+
this._speedRatio = this._options.speedRatio ?? 1;
|
|
37
|
+
this._weight = this._options.weight ?? clip.weight ?? 1;
|
|
38
|
+
this._time = this._speedRatio < 0 ? clip.timeDuration : 0;
|
|
39
|
+
this._layer = this._options.layer ?? 'default';
|
|
40
|
+
this._priority = this._options.priority ?? 0;
|
|
41
|
+
this._interruptible = this._options.interruptible ?? true;
|
|
42
|
+
this._frameWaiters = new Map();
|
|
43
|
+
}
|
|
44
|
+
get id() {
|
|
45
|
+
return this._id;
|
|
46
|
+
}
|
|
47
|
+
get animationSet() {
|
|
48
|
+
return this._animationSet;
|
|
49
|
+
}
|
|
50
|
+
get clip() {
|
|
51
|
+
return this._clip;
|
|
52
|
+
}
|
|
53
|
+
get state() {
|
|
54
|
+
return this._state;
|
|
55
|
+
}
|
|
56
|
+
get time() {
|
|
57
|
+
return this._time;
|
|
58
|
+
}
|
|
59
|
+
set time(value) {
|
|
60
|
+
this.seek(value);
|
|
61
|
+
}
|
|
62
|
+
get normalizedTime() {
|
|
63
|
+
return this._clip.timeDuration > 0 ? this._time / this._clip.timeDuration : 0;
|
|
64
|
+
}
|
|
65
|
+
set normalizedTime(value) {
|
|
66
|
+
this.seek(value * this._clip.timeDuration);
|
|
67
|
+
}
|
|
68
|
+
get weight() {
|
|
69
|
+
return this._weight;
|
|
70
|
+
}
|
|
71
|
+
set weight(value) {
|
|
72
|
+
this._animationSet.setPlaybackWeight(this, value);
|
|
73
|
+
}
|
|
74
|
+
get speedRatio() {
|
|
75
|
+
return this._speedRatio;
|
|
76
|
+
}
|
|
77
|
+
set speedRatio(value) {
|
|
78
|
+
this._animationSet.setPlaybackSpeedRatio(this, value);
|
|
79
|
+
}
|
|
80
|
+
get layer() {
|
|
81
|
+
return this._layer;
|
|
82
|
+
}
|
|
83
|
+
get priority() {
|
|
84
|
+
return this._priority;
|
|
85
|
+
}
|
|
86
|
+
get interruptible() {
|
|
87
|
+
return this._interruptible;
|
|
88
|
+
}
|
|
89
|
+
play() {
|
|
90
|
+
this._animationSet.startPlayback(this);
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
pause() {
|
|
94
|
+
this._animationSet.pausePlayback(this);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
resume() {
|
|
98
|
+
this._animationSet.resumePlayback(this);
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
stop(options) {
|
|
102
|
+
this._animationSet.stopPlayback(this, options);
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
seek(time, options) {
|
|
106
|
+
this._animationSet.seekPlayback(this, time, options);
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
fadeTo(weight, duration) {
|
|
110
|
+
this._animationSet.fadePlaybackTo(this, weight, duration);
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
crossFadeTo(name, options) {
|
|
114
|
+
const duration = Math.max(options?.duration ?? 0, 0);
|
|
115
|
+
this.stop({
|
|
116
|
+
fadeOut: duration,
|
|
117
|
+
reason: 'interrupted'
|
|
118
|
+
});
|
|
119
|
+
return this._animationSet.play(name, {
|
|
120
|
+
...options,
|
|
121
|
+
fadeIn: duration
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
waitForComplete() {
|
|
125
|
+
return new Promise((resolve)=>{
|
|
126
|
+
this.once('complete', ()=>resolve(this));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
waitForMarker(idOrName) {
|
|
130
|
+
if (this._state === 'stopped' || this._state === 'completed') {
|
|
131
|
+
return Promise.resolve(undefined);
|
|
132
|
+
}
|
|
133
|
+
return new Promise((resolve)=>{
|
|
134
|
+
const handler = (event)=>{
|
|
135
|
+
if (event.marker.id === idOrName || event.marker.name === idOrName) {
|
|
136
|
+
this.off('marker', handler);
|
|
137
|
+
this.off('stop', stop);
|
|
138
|
+
resolve(event);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const stop = ()=>{
|
|
142
|
+
this.off('marker', handler);
|
|
143
|
+
this.off('stop', stop);
|
|
144
|
+
resolve(undefined);
|
|
145
|
+
};
|
|
146
|
+
this.on('marker', handler);
|
|
147
|
+
this.on('stop', stop);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
waitForFrame(frame) {
|
|
151
|
+
if (this._state === 'stopped' || this._state === 'completed') {
|
|
152
|
+
return Promise.resolve(undefined);
|
|
153
|
+
}
|
|
154
|
+
return new Promise((resolve)=>{
|
|
155
|
+
const waiters = this._frameWaiters.get(frame) ?? [];
|
|
156
|
+
let settled = false;
|
|
157
|
+
let stop;
|
|
158
|
+
const wrappedResolve = (event)=>{
|
|
159
|
+
if (settled) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
settled = true;
|
|
163
|
+
this.off('stop', stop);
|
|
164
|
+
resolve(event);
|
|
165
|
+
};
|
|
166
|
+
stop = ()=>{
|
|
167
|
+
const current = this._frameWaiters.get(frame);
|
|
168
|
+
if (current) {
|
|
169
|
+
this._frameWaiters.set(frame, current.filter((item)=>item !== wrappedResolve));
|
|
170
|
+
}
|
|
171
|
+
wrappedResolve(undefined);
|
|
172
|
+
};
|
|
173
|
+
waiters.push(wrappedResolve);
|
|
174
|
+
this._frameWaiters.set(frame, waiters);
|
|
175
|
+
this.on('stop', stop);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** @internal */ _getOptions() {
|
|
179
|
+
return this._options;
|
|
180
|
+
}
|
|
181
|
+
/** @internal */ _setState(state) {
|
|
182
|
+
this._state = state;
|
|
183
|
+
}
|
|
184
|
+
/** @internal */ _setTime(time) {
|
|
185
|
+
this._time = time;
|
|
186
|
+
}
|
|
187
|
+
/** @internal */ _setWeight(weight) {
|
|
188
|
+
this._weight = weight;
|
|
189
|
+
}
|
|
190
|
+
/** @internal */ _setSpeedRatio(speedRatio) {
|
|
191
|
+
this._speedRatio = speedRatio;
|
|
192
|
+
}
|
|
193
|
+
/** @internal */ _getWatchedFrames() {
|
|
194
|
+
return [
|
|
195
|
+
...this._frameWaiters.keys()
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
/** @internal */ _emitStart(event) {
|
|
199
|
+
this.dispatchEvent('start', event);
|
|
200
|
+
}
|
|
201
|
+
/** @internal */ _emitLoop(event) {
|
|
202
|
+
this.dispatchEvent('loop', event);
|
|
203
|
+
}
|
|
204
|
+
/** @internal */ _emitMarker(event) {
|
|
205
|
+
this.dispatchEvent('marker', event);
|
|
206
|
+
}
|
|
207
|
+
/** @internal */ _emitFrame(event) {
|
|
208
|
+
this.dispatchEvent('frame', event);
|
|
209
|
+
const waiters = this._frameWaiters.get(event.frame);
|
|
210
|
+
if (waiters) {
|
|
211
|
+
this._frameWaiters.delete(event.frame);
|
|
212
|
+
waiters.forEach((resolve)=>resolve(event));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** @internal */ _emitComplete(event) {
|
|
216
|
+
this.dispatchEvent('complete', event);
|
|
217
|
+
}
|
|
218
|
+
/** @internal */ _emitStop(event) {
|
|
219
|
+
this.dispatchEvent('stop', event);
|
|
220
|
+
}
|
|
221
|
+
/** @internal */ _emitPause(event) {
|
|
222
|
+
this.dispatchEvent('pause', event);
|
|
223
|
+
}
|
|
224
|
+
/** @internal */ _emitResume(event) {
|
|
225
|
+
this.dispatchEvent('resume', event);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
10
228
|
function cloneInterpolator(src) {
|
|
11
229
|
return new Interpolator(src.mode, src.target, src.inputs instanceof Float32Array ? new Float32Array(src.inputs) : [
|
|
12
230
|
...src.inputs
|
|
@@ -388,26 +606,7 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
388
606
|
dstClip.addTrack(remap.dstNode, track);
|
|
389
607
|
}
|
|
390
608
|
}
|
|
391
|
-
|
|
392
|
-
* Animation set
|
|
393
|
-
*
|
|
394
|
-
* Manages a collection of named animation clips for a model and orchestrates:
|
|
395
|
-
* - Playback state (time, loops, speed, weights, fade-in/out).
|
|
396
|
-
* - Blending across multiple tracks targeting the same property via weighted averages.
|
|
397
|
-
* - Skeleton usage and application for clips that drive skeletal animation.
|
|
398
|
-
* - Active track registration and cleanup as clips start/stop.
|
|
399
|
-
*
|
|
400
|
-
* Usage:
|
|
401
|
-
* - Create or retrieve `AnimationClip`s by name.
|
|
402
|
-
* - Start playback with `playAnimation(name, options)`.
|
|
403
|
-
* - Advance animation with `update(deltaSeconds)`.
|
|
404
|
-
* - Optionally adjust weight while playing with `setAnimationWeight(name, weight)`.
|
|
405
|
-
*
|
|
406
|
-
* Lifetime:
|
|
407
|
-
* - Disposing the set releases references to the model, clips, and clears active state.
|
|
408
|
-
*
|
|
409
|
-
* @public
|
|
410
|
-
*/ class AnimationSet extends Disposable {
|
|
609
|
+
class AnimationSet extends makeObservable(Disposable)() {
|
|
411
610
|
/** @internal */ _model;
|
|
412
611
|
/** @internal */ _animations;
|
|
413
612
|
/** @internal */ _skeletons;
|
|
@@ -417,6 +616,14 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
417
616
|
/** @internal */ _activeRigs;
|
|
418
617
|
/** @internal */ _activeAnimations;
|
|
419
618
|
/**
|
|
619
|
+
* Callbacks driven by the same logical clock as the animations (see {@link update}).
|
|
620
|
+
*
|
|
621
|
+
* Used by higher-level constructs (e.g. timeline runners) that need to advance
|
|
622
|
+
* wall-clock-independent timers in sync with playback, so they pause/scale with the
|
|
623
|
+
* game clock instead of running on real time.
|
|
624
|
+
* @internal
|
|
625
|
+
*/ _timelineTickers;
|
|
626
|
+
/**
|
|
420
627
|
* Create an AnimationSet controlling the provided model.
|
|
421
628
|
*
|
|
422
629
|
* @param model - The SceneNode (model root) controlled by this animation set.
|
|
@@ -430,6 +637,19 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
430
637
|
this._activeAnimations = new Map();
|
|
431
638
|
this._skeletons = [];
|
|
432
639
|
this._rigs = [];
|
|
640
|
+
this._timelineTickers = new Set();
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Register a callback advanced by {@link update} using the same delta time as the animations.
|
|
644
|
+
* @internal
|
|
645
|
+
*/ _registerTimelineTicker(ticker) {
|
|
646
|
+
this._timelineTickers.add(ticker);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Unregister a callback previously registered with {@link _registerTimelineTicker}.
|
|
650
|
+
* @internal
|
|
651
|
+
*/ _unregisterTimelineTicker(ticker) {
|
|
652
|
+
this._timelineTickers.delete(ticker);
|
|
433
653
|
}
|
|
434
654
|
/**
|
|
435
655
|
* The model (SceneNode) controlled by this animation set.
|
|
@@ -533,30 +753,99 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
533
753
|
* @param deltaInSeconds - Time step in seconds since last update.
|
|
534
754
|
*/ update(deltaInSeconds) {
|
|
535
755
|
this._activeAnimations.forEach((v, k)=>{
|
|
756
|
+
if (!v.started) {
|
|
757
|
+
v.started = true;
|
|
758
|
+
const event = this.createPlaybackEvent(v, k);
|
|
759
|
+
v.playback._emitStart(event);
|
|
760
|
+
this.dispatchEvent('playbackstart', event);
|
|
761
|
+
}
|
|
762
|
+
if (v.paused) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
536
765
|
if (v.fadeOut > 0 && v.fadeOutStart < 0) {
|
|
537
766
|
v.fadeOutStart = v.animateTime;
|
|
538
767
|
}
|
|
768
|
+
if (v.completed) {
|
|
769
|
+
v.animateTime += Math.abs(deltaInSeconds * v.speedRatio);
|
|
770
|
+
if (v.fadeOut > 0 && v.animateTime - v.fadeOutStart >= v.fadeOut) {
|
|
771
|
+
this.stopAnimation(k.name, {
|
|
772
|
+
reason: v.stopReason
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
539
777
|
// Update animation time
|
|
540
778
|
if (v.firstFrame) {
|
|
541
779
|
v.firstFrame = false;
|
|
542
780
|
} else {
|
|
781
|
+
const previousTime = v.currentTime;
|
|
543
782
|
const timeAdvance = deltaInSeconds * v.speedRatio;
|
|
544
783
|
v.currentTime += timeAdvance;
|
|
545
784
|
v.animateTime += timeAdvance;
|
|
785
|
+
const direction = timeAdvance >= 0 ? 1 : -1;
|
|
546
786
|
if (k.timeDuration > 0) {
|
|
547
|
-
if (v.currentTime
|
|
548
|
-
v.
|
|
787
|
+
if (v.rangeEnd !== null && v.speedRatio >= 0 && v.currentTime >= v.rangeEnd) {
|
|
788
|
+
v.currentTime = v.rangeEnd;
|
|
789
|
+
v.playback._setTime(v.currentTime);
|
|
790
|
+
this.completePlayback(k, v);
|
|
791
|
+
return;
|
|
792
|
+
} else if (v.rangeStart !== null && v.speedRatio < 0 && v.currentTime <= v.rangeStart) {
|
|
793
|
+
v.currentTime = v.rangeStart;
|
|
794
|
+
v.playback._setTime(v.currentTime);
|
|
795
|
+
this.completePlayback(k, v);
|
|
796
|
+
return;
|
|
797
|
+
} else if (v.currentTime > k.timeDuration) {
|
|
798
|
+
const loops = Math.max(1, Math.floor(v.currentTime / k.timeDuration));
|
|
799
|
+
if (v.repeat !== 0 && v.repeatCounter + loops >= v.repeat) {
|
|
800
|
+
v.repeatCounter += loops;
|
|
801
|
+
v.currentTime = k.timeDuration;
|
|
802
|
+
v.playback._setTime(v.currentTime);
|
|
803
|
+
this.emitCrossedMarkers(v, k, previousTime, v.currentTime, direction);
|
|
804
|
+
this.emitCrossedFrames(v, k, previousTime, v.currentTime, direction);
|
|
805
|
+
this.completePlayback(k, v);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
v.repeatCounter += loops;
|
|
549
809
|
v.currentTime %= k.timeDuration;
|
|
810
|
+
const event = this.createPlaybackEvent(v, k);
|
|
811
|
+
v.playback._emitLoop(event);
|
|
812
|
+
this.dispatchEvent('playbackloop', event);
|
|
550
813
|
} else if (v.currentTime < 0) {
|
|
551
|
-
|
|
814
|
+
const loops = Math.max(1, Math.ceil(-v.currentTime / k.timeDuration));
|
|
815
|
+
if (v.repeat !== 0 && v.repeatCounter + loops >= v.repeat) {
|
|
816
|
+
v.repeatCounter += loops;
|
|
817
|
+
v.currentTime = 0;
|
|
818
|
+
v.playback._setTime(v.currentTime);
|
|
819
|
+
this.emitCrossedMarkers(v, k, previousTime, v.currentTime, direction);
|
|
820
|
+
this.emitCrossedFrames(v, k, previousTime, v.currentTime, direction);
|
|
821
|
+
this.completePlayback(k, v);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
v.repeatCounter += loops;
|
|
552
825
|
v.currentTime = (v.currentTime % k.timeDuration + k.timeDuration) % k.timeDuration;
|
|
826
|
+
const event = this.createPlaybackEvent(v, k);
|
|
827
|
+
v.playback._emitLoop(event);
|
|
828
|
+
this.dispatchEvent('playbackloop', event);
|
|
553
829
|
}
|
|
554
830
|
}
|
|
831
|
+
v.playback._setTime(v.currentTime);
|
|
832
|
+
this.emitCrossedMarkers(v, k, previousTime, v.currentTime, direction);
|
|
833
|
+
this.emitCrossedFrames(v, k, previousTime, v.currentTime, direction);
|
|
555
834
|
if (v.repeat !== 0 && v.repeatCounter >= v.repeat) {
|
|
556
|
-
this.
|
|
835
|
+
this.completePlayback(k, v);
|
|
557
836
|
} else if (v.fadeOut > 0) {
|
|
558
837
|
if (v.animateTime - v.fadeOutStart >= v.fadeOut) {
|
|
559
|
-
this.stopAnimation(k.name
|
|
838
|
+
this.stopAnimation(k.name, {
|
|
839
|
+
reason: v.stopReason
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (v.fadeToDuration > 0) {
|
|
844
|
+
const t = Math.min(1, (v.animateTime - v.fadeToStart) / v.fadeToDuration);
|
|
845
|
+
v.weight = v.fadeFromWeight + (v.fadeTargetWeight - v.fadeFromWeight) * t;
|
|
846
|
+
v.playback._setWeight(v.weight);
|
|
847
|
+
if (t >= 1) {
|
|
848
|
+
v.fadeToDuration = 0;
|
|
560
849
|
}
|
|
561
850
|
}
|
|
562
851
|
}
|
|
@@ -596,6 +885,11 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
596
885
|
this._skeletons.forEach((v)=>{
|
|
597
886
|
v.get()?.apply();
|
|
598
887
|
});
|
|
888
|
+
// Advance logical-clock timers (e.g. timeline `wait` steps) with the same delta time,
|
|
889
|
+
// so they stay in sync with playback and pause/scale with the game clock.
|
|
890
|
+
if (this._timelineTickers.size > 0) {
|
|
891
|
+
this._timelineTickers.forEach((ticker)=>ticker(deltaInSeconds));
|
|
892
|
+
}
|
|
599
893
|
}
|
|
600
894
|
/**
|
|
601
895
|
* Check whether an animation is currently playing.
|
|
@@ -635,7 +929,43 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
635
929
|
const info = this._activeAnimations.get(ani);
|
|
636
930
|
if (info) {
|
|
637
931
|
info.weight = weight;
|
|
932
|
+
info.targetWeight = weight;
|
|
933
|
+
info.playback._setWeight(weight);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Create a playback handle without starting it.
|
|
938
|
+
*/ createPlayback(name, options) {
|
|
939
|
+
const ani = this._animations[name];
|
|
940
|
+
if (!ani) {
|
|
941
|
+
console.error(`Animation ${name} not exists`);
|
|
942
|
+
return null;
|
|
638
943
|
}
|
|
944
|
+
return new AnimationPlayback(this, ani, options);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Start an animation and return its playback handle.
|
|
948
|
+
*/ play(name, options) {
|
|
949
|
+
const playback = this.createPlayback(name, options);
|
|
950
|
+
playback?.play();
|
|
951
|
+
return playback;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Get currently active playbacks.
|
|
955
|
+
*/ getPlaybacks(name) {
|
|
956
|
+
const playbacks = [];
|
|
957
|
+
this._activeAnimations.forEach((info, clip)=>{
|
|
958
|
+
if (!name || clip.name === name) {
|
|
959
|
+
playbacks.push(info.playback);
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
return playbacks;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Get the currently active playback for a clip.
|
|
966
|
+
*/ getPlayback(name) {
|
|
967
|
+
const clip = this._animations[name];
|
|
968
|
+
return clip ? this._activeAnimations.get(clip)?.playback ?? null : null;
|
|
639
969
|
}
|
|
640
970
|
/**
|
|
641
971
|
* Start (or update) playback of an animation clip.
|
|
@@ -648,29 +978,55 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
648
978
|
* @param name - Name of the animation to play.
|
|
649
979
|
* @param options - Playback options (repeat, speedRatio, fadeIn).
|
|
650
980
|
*/ playAnimation(name, options) {
|
|
651
|
-
|
|
981
|
+
this.play(name, options);
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Start a previously created playback.
|
|
985
|
+
* @internal
|
|
986
|
+
*/ startPlayback(playback) {
|
|
987
|
+
const ani = playback.clip;
|
|
652
988
|
if (!ani) {
|
|
653
|
-
console.error(`Animation ${name} not exists`);
|
|
654
989
|
return;
|
|
655
990
|
}
|
|
656
|
-
|
|
657
|
-
this.stopAnimation(name);
|
|
658
|
-
}
|
|
991
|
+
const options = playback._getOptions();
|
|
659
992
|
const fadeIn = Math.max(options?.fadeIn ?? 0, 0);
|
|
660
993
|
const repeat = options?.repeat ?? 0;
|
|
661
|
-
const speedRatio =
|
|
662
|
-
const weight =
|
|
994
|
+
const speedRatio = playback.speedRatio;
|
|
995
|
+
const weight = playback.weight;
|
|
996
|
+
const rangeStart = ani.resolveTimeRef(options?.range?.start);
|
|
997
|
+
const rangeEnd = ani.resolveTimeRef(options?.range?.end);
|
|
998
|
+
const syncSource = this.resolvePlaybackSyncSource(options?.sync);
|
|
999
|
+
const initialTime = this.computePlaybackInitialTime(ani, speedRatio, rangeStart, rangeEnd, options?.sync, syncSource);
|
|
1000
|
+
if (this.isPlayingAnimation(ani.name)) {
|
|
1001
|
+
this.stopAnimation(ani.name, {
|
|
1002
|
+
reason: 'replaced'
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
playback._setState('playing');
|
|
1006
|
+
playback._setTime(initialTime);
|
|
663
1007
|
this._activeAnimations.set(ani, {
|
|
1008
|
+
playback,
|
|
664
1009
|
repeat,
|
|
665
1010
|
weight,
|
|
1011
|
+
targetWeight: weight,
|
|
666
1012
|
speedRatio,
|
|
667
1013
|
fadeIn,
|
|
668
1014
|
fadeOut: 0,
|
|
669
1015
|
repeatCounter: 0,
|
|
670
|
-
currentTime:
|
|
1016
|
+
currentTime: initialTime,
|
|
671
1017
|
animateTime: 0,
|
|
672
1018
|
fadeOutStart: 0,
|
|
673
|
-
|
|
1019
|
+
fadeToStart: 0,
|
|
1020
|
+
fadeToDuration: 0,
|
|
1021
|
+
fadeFromWeight: weight,
|
|
1022
|
+
fadeTargetWeight: weight,
|
|
1023
|
+
firstFrame: true,
|
|
1024
|
+
started: false,
|
|
1025
|
+
paused: false,
|
|
1026
|
+
stopReason: 'manual',
|
|
1027
|
+
completed: false,
|
|
1028
|
+
rangeStart,
|
|
1029
|
+
rangeEnd
|
|
674
1030
|
});
|
|
675
1031
|
ani.tracks?.forEach((v, k)=>{
|
|
676
1032
|
let nodeTracks = this._activeTracks.get(k);
|
|
@@ -707,6 +1063,245 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
707
1063
|
}
|
|
708
1064
|
});
|
|
709
1065
|
}
|
|
1066
|
+
resolvePlaybackSyncSource(sync) {
|
|
1067
|
+
if (!sync?.target) {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
const playback = this.resolveActivePlayback(sync.target);
|
|
1071
|
+
if (!playback) {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
const info = this._activeAnimations.get(playback.clip);
|
|
1075
|
+
return info?.playback === playback ? {
|
|
1076
|
+
playback,
|
|
1077
|
+
info,
|
|
1078
|
+
clip: playback.clip
|
|
1079
|
+
} : null;
|
|
1080
|
+
}
|
|
1081
|
+
resolveActivePlayback(target) {
|
|
1082
|
+
const named = this.getPlayback(target);
|
|
1083
|
+
if (named) {
|
|
1084
|
+
return named;
|
|
1085
|
+
}
|
|
1086
|
+
for (const info of this._activeAnimations.values()){
|
|
1087
|
+
if (info.playback.id === target) {
|
|
1088
|
+
return info.playback;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
computePlaybackInitialTime(clip, speedRatio, rangeStart, rangeEnd, sync, syncSource) {
|
|
1094
|
+
const fallback = speedRatio < 0 ? rangeEnd ?? rangeStart ?? clip.timeDuration : rangeStart ?? 0;
|
|
1095
|
+
if (!sync || !syncSource) {
|
|
1096
|
+
return fallback;
|
|
1097
|
+
}
|
|
1098
|
+
const destination = this.getPlaybackEffectiveRange(clip, rangeStart, rangeEnd);
|
|
1099
|
+
if (destination.duration <= 0) {
|
|
1100
|
+
return fallback;
|
|
1101
|
+
}
|
|
1102
|
+
const offset = Number.isFinite(sync.offset) ? sync.offset ?? 0 : 0;
|
|
1103
|
+
const wrap = sync.wrap ?? true;
|
|
1104
|
+
if ((sync.mode ?? 'normalized') === 'time') {
|
|
1105
|
+
return this.constrainPlaybackTime(syncSource.info.currentTime + offset, destination, wrap);
|
|
1106
|
+
}
|
|
1107
|
+
const source = this.getPlaybackEffectiveRange(syncSource.clip, syncSource.info.rangeStart, syncSource.info.rangeEnd);
|
|
1108
|
+
if (source.duration <= 0) {
|
|
1109
|
+
return fallback;
|
|
1110
|
+
}
|
|
1111
|
+
const phase = (syncSource.info.currentTime - source.start) / source.duration + offset;
|
|
1112
|
+
return this.constrainPlaybackTime(destination.start + phase * destination.duration, destination, wrap);
|
|
1113
|
+
}
|
|
1114
|
+
getPlaybackEffectiveRange(clip, rangeStart, rangeEnd) {
|
|
1115
|
+
const start = rangeStart ?? 0;
|
|
1116
|
+
const end = rangeEnd ?? clip.timeDuration;
|
|
1117
|
+
return start <= end ? {
|
|
1118
|
+
start,
|
|
1119
|
+
end,
|
|
1120
|
+
duration: end - start
|
|
1121
|
+
} : {
|
|
1122
|
+
start: end,
|
|
1123
|
+
end: start,
|
|
1124
|
+
duration: start - end
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
constrainPlaybackTime(time, range, wrap) {
|
|
1128
|
+
if (!Number.isFinite(time)) {
|
|
1129
|
+
return range.start;
|
|
1130
|
+
}
|
|
1131
|
+
if (!wrap) {
|
|
1132
|
+
return Math.max(range.start, Math.min(range.end, time));
|
|
1133
|
+
}
|
|
1134
|
+
if (time >= range.start && time < range.end) {
|
|
1135
|
+
return time;
|
|
1136
|
+
}
|
|
1137
|
+
const offset = ((time - range.start) % range.duration + range.duration) % range.duration;
|
|
1138
|
+
return range.start + offset;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Stop a playback handle.
|
|
1142
|
+
* @internal
|
|
1143
|
+
*/ stopPlayback(playback, options) {
|
|
1144
|
+
if (this._activeAnimations.get(playback.clip)?.playback === playback) {
|
|
1145
|
+
this.stopAnimation(playback.clip.name, options);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Pause a playback handle.
|
|
1150
|
+
* @internal
|
|
1151
|
+
*/ pausePlayback(playback) {
|
|
1152
|
+
const info = this._activeAnimations.get(playback.clip);
|
|
1153
|
+
if (info?.playback === playback && !info.paused) {
|
|
1154
|
+
info.paused = true;
|
|
1155
|
+
playback._setState('paused');
|
|
1156
|
+
const event = this.createPlaybackEvent(info, playback.clip);
|
|
1157
|
+
playback._emitPause(event);
|
|
1158
|
+
this.dispatchEvent('playbackpause', event);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Resume a playback handle.
|
|
1163
|
+
* @internal
|
|
1164
|
+
*/ resumePlayback(playback) {
|
|
1165
|
+
const info = this._activeAnimations.get(playback.clip);
|
|
1166
|
+
if (info?.playback === playback && info.paused) {
|
|
1167
|
+
info.paused = false;
|
|
1168
|
+
playback._setState('playing');
|
|
1169
|
+
const event = this.createPlaybackEvent(info, playback.clip);
|
|
1170
|
+
playback._emitResume(event);
|
|
1171
|
+
this.dispatchEvent('playbackresume', event);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Seek a playback handle.
|
|
1176
|
+
* @internal
|
|
1177
|
+
*/ seekPlayback(playback, time, options) {
|
|
1178
|
+
const clip = playback.clip;
|
|
1179
|
+
const info = this._activeAnimations.get(clip);
|
|
1180
|
+
const duration = clip.timeDuration;
|
|
1181
|
+
const clampedTime = duration > 0 ? Math.max(0, Math.min(duration, time)) : Math.max(0, time);
|
|
1182
|
+
const previousTime = info?.currentTime ?? playback.time;
|
|
1183
|
+
playback._setTime(clampedTime);
|
|
1184
|
+
if (info?.playback === playback) {
|
|
1185
|
+
info.currentTime = clampedTime;
|
|
1186
|
+
if (options?.emitEvents) {
|
|
1187
|
+
const direction = clampedTime >= previousTime ? 1 : -1;
|
|
1188
|
+
this.emitCrossedMarkers(info, clip, previousTime, clampedTime, direction);
|
|
1189
|
+
this.emitCrossedFrames(info, clip, previousTime, clampedTime, direction);
|
|
1190
|
+
}
|
|
1191
|
+
if (options?.apply) {
|
|
1192
|
+
this.update(0);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Set playback weight.
|
|
1198
|
+
* @internal
|
|
1199
|
+
*/ setPlaybackWeight(playback, weight) {
|
|
1200
|
+
playback._setWeight(weight);
|
|
1201
|
+
const info = this._activeAnimations.get(playback.clip);
|
|
1202
|
+
if (info?.playback === playback) {
|
|
1203
|
+
info.weight = weight;
|
|
1204
|
+
info.targetWeight = weight;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Set playback speed ratio.
|
|
1209
|
+
* @internal
|
|
1210
|
+
*/ setPlaybackSpeedRatio(playback, speedRatio) {
|
|
1211
|
+
playback._setSpeedRatio(speedRatio);
|
|
1212
|
+
const info = this._activeAnimations.get(playback.clip);
|
|
1213
|
+
if (info?.playback === playback) {
|
|
1214
|
+
info.speedRatio = speedRatio;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Fade playback weight to a target value.
|
|
1219
|
+
* @internal
|
|
1220
|
+
*/ fadePlaybackTo(playback, weight, duration) {
|
|
1221
|
+
const info = this._activeAnimations.get(playback.clip);
|
|
1222
|
+
if (info?.playback === playback) {
|
|
1223
|
+
info.fadeFromWeight = info.weight;
|
|
1224
|
+
info.fadeTargetWeight = weight;
|
|
1225
|
+
info.targetWeight = weight;
|
|
1226
|
+
info.fadeToStart = info.animateTime;
|
|
1227
|
+
info.fadeToDuration = Math.max(duration, 0);
|
|
1228
|
+
if (info.fadeToDuration === 0) {
|
|
1229
|
+
info.weight = weight;
|
|
1230
|
+
playback._setWeight(weight);
|
|
1231
|
+
}
|
|
1232
|
+
} else {
|
|
1233
|
+
playback._setWeight(weight);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
createPlaybackEvent(info, clip) {
|
|
1237
|
+
return {
|
|
1238
|
+
playback: info.playback,
|
|
1239
|
+
animationSet: this,
|
|
1240
|
+
clip,
|
|
1241
|
+
time: info.currentTime,
|
|
1242
|
+
normalizedTime: clip.timeDuration > 0 ? info.currentTime / clip.timeDuration : 0
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
completePlayback(clip, info) {
|
|
1246
|
+
if (info.completed) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const event = this.createPlaybackEvent(info, clip);
|
|
1250
|
+
const completionFadeOut = Math.max(info.playback._getOptions().completionFadeOut ?? 0, 0);
|
|
1251
|
+
info.playback._setState('completed');
|
|
1252
|
+
info.playback._emitComplete(event);
|
|
1253
|
+
this.dispatchEvent('playbackcomplete', event);
|
|
1254
|
+
if (completionFadeOut > 0) {
|
|
1255
|
+
info.completed = true;
|
|
1256
|
+
info.fadeOut = completionFadeOut;
|
|
1257
|
+
info.fadeOutStart = info.animateTime;
|
|
1258
|
+
info.stopReason = 'completed';
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
this.stopAnimation(clip.name, {
|
|
1262
|
+
reason: 'completed'
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
emitCrossedMarkers(info, clip, fromTime, toTime, direction) {
|
|
1266
|
+
for (const marker of this.getCrossedMarkers(clip, fromTime, toTime, direction)){
|
|
1267
|
+
const event = {
|
|
1268
|
+
...this.createPlaybackEvent(info, clip),
|
|
1269
|
+
marker,
|
|
1270
|
+
direction
|
|
1271
|
+
};
|
|
1272
|
+
info.playback._emitMarker(event);
|
|
1273
|
+
this.dispatchEvent('marker', event);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
emitCrossedFrames(info, clip, fromTime, toTime, direction) {
|
|
1277
|
+
for (const frame of info.playback._getWatchedFrames()){
|
|
1278
|
+
const frameTime = frame / clip.frameRate;
|
|
1279
|
+
const crossed = direction >= 0 ? this.crossedForward(fromTime, toTime, frameTime, clip.timeDuration) : this.crossedBackward(fromTime, toTime, frameTime, clip.timeDuration);
|
|
1280
|
+
if (crossed) {
|
|
1281
|
+
const event = {
|
|
1282
|
+
...this.createPlaybackEvent(info, clip),
|
|
1283
|
+
frame
|
|
1284
|
+
};
|
|
1285
|
+
info.playback._emitFrame(event);
|
|
1286
|
+
this.dispatchEvent('frame', event);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
getCrossedMarkers(clip, fromTime, toTime, direction) {
|
|
1291
|
+
return clip.markers.filter((marker)=>{
|
|
1292
|
+
const markerTime = clip.resolveMarkerTime(marker);
|
|
1293
|
+
if (markerTime === null) {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
return direction >= 0 ? this.crossedForward(fromTime, toTime, markerTime, clip.timeDuration) : this.crossedBackward(fromTime, toTime, markerTime, clip.timeDuration);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
crossedForward(fromTime, toTime, targetTime, duration) {
|
|
1300
|
+
return toTime >= fromTime ? targetTime > fromTime && targetTime <= toTime : duration > 0 && targetTime > fromTime && targetTime <= duration || targetTime <= toTime;
|
|
1301
|
+
}
|
|
1302
|
+
crossedBackward(fromTime, toTime, targetTime, duration) {
|
|
1303
|
+
return toTime <= fromTime ? targetTime < fromTime && targetTime >= toTime : duration > 0 && targetTime < fromTime && targetTime >= 0 || targetTime >= toTime;
|
|
1304
|
+
}
|
|
710
1305
|
/**
|
|
711
1306
|
* Stop playback of an animation clip.
|
|
712
1307
|
*
|
|
@@ -728,9 +1323,12 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
728
1323
|
const info = this._activeAnimations.get(ani);
|
|
729
1324
|
if (info) {
|
|
730
1325
|
const fadeOut = Math.max(options?.fadeOut ?? 0, 0);
|
|
1326
|
+
const reason = options?.reason ?? 'manual';
|
|
731
1327
|
if (fadeOut !== 0) {
|
|
732
1328
|
info.fadeOut = fadeOut;
|
|
733
1329
|
info.fadeOutStart = -1;
|
|
1330
|
+
info.stopReason = reason;
|
|
1331
|
+
info.playback._setState('stopping');
|
|
734
1332
|
} else {
|
|
735
1333
|
this._activeAnimations.delete(ani);
|
|
736
1334
|
this._activeTracks.forEach((v)=>{
|
|
@@ -768,6 +1366,15 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
768
1366
|
}
|
|
769
1367
|
}
|
|
770
1368
|
});
|
|
1369
|
+
const event = {
|
|
1370
|
+
...this.createPlaybackEvent(info, ani),
|
|
1371
|
+
reason
|
|
1372
|
+
};
|
|
1373
|
+
if (reason !== 'completed') {
|
|
1374
|
+
info.playback._setState('stopped');
|
|
1375
|
+
}
|
|
1376
|
+
info.playback._emitStop(event);
|
|
1377
|
+
this.dispatchEvent('playbackstop', event);
|
|
771
1378
|
}
|
|
772
1379
|
}
|
|
773
1380
|
}
|
|
@@ -1176,5 +1783,5 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
|
|
|
1176
1783
|
}
|
|
1177
1784
|
}
|
|
1178
1785
|
|
|
1179
|
-
export { AnimationSet };
|
|
1786
|
+
export { AnimationPlayback, AnimationSet };
|
|
1180
1787
|
//# sourceMappingURL=animationset.js.map
|