Fix crossFade error caused by transitionOffsetTime and clipStartTime (#2411)

* fix: crossFade error caused by transitionOffsetTime and clipStartTime
This commit is contained in:
luzhuang
2024-10-30 19:14:37 +08:00
committed by GitHub
parent 722661898c
commit ebd6cbfe21
4 changed files with 83 additions and 27 deletions

View File

@@ -570,9 +570,8 @@ export class Animator extends Component {
let playCostTime: number;
if (transition) {
const clipDuration = state.clip.length;
const clipEndTime = state.clipEndTime * clipDuration;
const exitTime = transition.exitTime * state._getDuration();
const clipEndTime = state._getClipActualEndTime();
const exitTime = transition.exitTime * state._getDuration() + state._getClipActualStartTime();
if (isForwards) {
if (exitTime < lastClipTime) {
@@ -581,7 +580,7 @@ export class Animator extends Component {
playCostTime = exitTime - lastClipTime;
}
} else {
const startTime = state.clipStartTime * clipDuration;
const startTime = state._getClipActualStartTime();
if (lastClipTime < exitTime) {
playCostTime = clipEndTime - exitTime + lastClipTime - startTime;
} else {
@@ -667,15 +666,15 @@ export class Animator extends Component {
let dstPlayCostTime: number;
if (destPlayData.isForwards) {
// The time that has been played
const playedTime = destPlayData.playedTime;
dstPlayCostTime =
lastDestClipTime + dstPlayDeltaTime > transitionDuration
? transitionDuration - lastDestClipTime
: dstPlayDeltaTime;
playedTime + dstPlayDeltaTime > transitionDuration ? transitionDuration - playedTime : dstPlayDeltaTime;
} else {
// The time that has been played
const playedTime = destStateDuration - lastDestClipTime;
const playedTime = destPlayData.playedTime;
dstPlayCostTime =
// -actualDestDeltaTime: The time that will be played, negative are meant to make ite be a periods
// -dstPlayDeltaTime: The time that will be played, negative are meant to make it be a periods
// > transition: The time that will be played is enough to finish the transition
playedTime - dstPlayDeltaTime > transitionDuration
? // Negative number is used to convert a time period into a reverse deltaTime.
@@ -690,7 +689,7 @@ export class Animator extends Component {
srcPlayData.update(srcPlayCostTime);
destPlayData.update(dstPlayCostTime);
let crossWeight = Math.abs(destPlayData.frameTime) / transitionDuration;
let crossWeight = Math.abs(destPlayData.playedTime) / transitionDuration;
(crossWeight >= 1.0 - MathUtil.zeroTolerance || transitionDuration === 0) && (crossWeight = 1.0);
const crossFadeFinished = crossWeight === 1.0;
@@ -794,11 +793,13 @@ export class Animator extends Component {
let dstPlayCostTime: number;
if (destPlayData.isForwards) {
// The time that has been played
const playedTime = destPlayData.playedTime;
dstPlayCostTime =
lastDestClipTime + playDeltaTime > transitionDuration ? transitionDuration - lastDestClipTime : playDeltaTime;
playedTime + playDeltaTime > transitionDuration ? transitionDuration - playedTime : playDeltaTime;
} else {
// The time that has been played
const playedTime = stateDuration - lastDestClipTime;
const playedTime = destPlayData.playedTime;
dstPlayCostTime =
// -actualDestDeltaTime: The time that will be played, negative are meant to make ite be a periods
// > transition: The time that will be played is enough to finish the transition
@@ -813,7 +814,7 @@ export class Animator extends Component {
destPlayData.update(dstPlayCostTime);
let crossWeight = Math.abs(destPlayData.frameTime) / transitionDuration;
let crossWeight = Math.abs(destPlayData.playedTime) / transitionDuration;
(crossWeight >= 1.0 - MathUtil.zeroTolerance || transitionDuration === 0) && (crossWeight = 1.0);
const crossFadeFinished = crossWeight === 1.0;
@@ -1086,10 +1087,9 @@ export class Animator extends Component {
): AnimatorStateTransition {
const { state } = playState;
let transitionIndex = playState.currentTransitionIndex;
const duration = state._getDuration();
for (let n = transitions.length; transitionIndex < n; transitionIndex++) {
const transition = transitions[transitionIndex];
const exitTime = transition.exitTime * duration;
const exitTime = transition.exitTime * state._getDuration() + state._getClipActualStartTime();
if (exitTime > curClipTime) {
break;
}
@@ -1120,10 +1120,9 @@ export class Animator extends Component {
): AnimatorStateTransition {
const { state } = playState;
let transitionIndex = playState.currentTransitionIndex;
const duration = playState.state._getDuration();
for (; transitionIndex >= 0; transitionIndex--) {
const transition = transitions[transitionIndex];
const exitTime = transition.exitTime * duration;
const exitTime = transition.exitTime * state._getDuration() + state._getClipActualStartTime();
if (exitTime < curClipTime) {
break;
}

View File

@@ -232,4 +232,18 @@ export class AnimatorState {
}
}
}
/**
* @internal
*/
_getClipActualStartTime(): number {
return this._clipStartTime * this.clip.length;
}
/**
* @internal
*/
_getClipActualEndTime(): number {
return this._clipEndTime * this.clip.length;
}
}

View File

@@ -9,18 +9,20 @@ import { AnimatorStateData } from "./AnimatorStateData";
export class AnimatorStatePlayData {
state: AnimatorState;
stateData: AnimatorStateData;
frameTime: number;
playedTime: number;
playState: AnimatorStatePlayState;
clipTime: number;
currentEventIndex: number;
currentTransitionIndex: number;
isForwards = true;
offsetFrameTime: number;
private _changedOrientation = false;
reset(state: AnimatorState, stateData: AnimatorStateData, offsetFrameTime: number): void {
this.state = state;
this.frameTime = offsetFrameTime;
this.playedTime = 0;
this.offsetFrameTime = offsetFrameTime;
this.stateData = stateData;
this.playState = AnimatorStatePlayState.UnStarted;
this.clipTime = state.clipStartTime * state.clip.length;
@@ -41,9 +43,9 @@ export class AnimatorStatePlayData {
}
update(deltaTime: number): void {
this.frameTime += deltaTime;
this.playedTime += deltaTime;
const state = this.state;
let time = this.frameTime;
let time = this.playedTime + this.offsetFrameTime;
const duration = state._getDuration();
this.playState = AnimatorStatePlayState.Playing;
if (state.wrapMode === WrapMode.Loop) {

View File

@@ -81,24 +81,24 @@ describe("Animator test", function () {
const speed = 1;
let expectedSpeed = speed * 0.5;
animator.speed = expectedSpeed;
let lastFrameTime = srcPlayData.frameTime;
let lastFrameTime = srcPlayData.playedTime;
// @ts-ignore
animator.engine.time._frameCount++;
animator.update(5);
expect(animator.speed).to.eq(expectedSpeed);
expect(srcPlayData.frameTime).to.eq(lastFrameTime + 5 * expectedSpeed);
expect(srcPlayData.playedTime).to.eq(lastFrameTime + 5 * expectedSpeed);
expectedSpeed = speed * 2;
animator.speed = expectedSpeed;
lastFrameTime = srcPlayData.frameTime;
lastFrameTime = srcPlayData.playedTime;
animator.update(10);
expect(animator.speed).to.eq(expectedSpeed);
expect(srcPlayData.frameTime).to.eq(lastFrameTime + 10 * expectedSpeed);
expect(srcPlayData.playedTime).to.eq(lastFrameTime + 10 * expectedSpeed);
expectedSpeed = speed * 0;
animator.speed = expectedSpeed;
lastFrameTime = srcPlayData.frameTime;
lastFrameTime = srcPlayData.playedTime;
animator.update(15);
expect(animator.speed).to.eq(expectedSpeed);
expect(srcPlayData.frameTime).to.eq(lastFrameTime + 15 * expectedSpeed);
expect(srcPlayData.playedTime).to.eq(lastFrameTime + 15 * expectedSpeed);
});
it("play animation", () => {
@@ -567,6 +567,47 @@ describe("Animator test", function () {
expect(animator.getCurrentAnimatorState(0).name).to.eq("Survey");
});
it("transitionOffset", () => {
const walkState = animator.findAnimatorState("Walk");
walkState.clearTransitions();
const runState = animator.findAnimatorState("Run");
runState.clearTransitions();
const toRunTransition = walkState.addTransition(runState);
toRunTransition.exitTime = 0;
toRunTransition.duration = 1;
toRunTransition.offset = 0.5;
animator.play("Walk");
// @ts-ignore
animator.engine.time._frameCount++;
animator.update(0.01);
const destPlayData = animator["_animatorLayersData"][0].destPlayData;
const destState = destPlayData.state;
const transitionDuration = toRunTransition.duration * destState._getDuration();
const crossWeight = animator["_animatorLayersData"][0].destPlayData.playedTime / transitionDuration;
expect(crossWeight).to.lessThan(0.01);
});
it("clipStartTime crossFade", () => {
const walkState = animator.findAnimatorState("Walk");
walkState.wrapMode = WrapMode.Once;
walkState.clipStartTime = 0.8;
walkState.clearTransitions();
const runState = animator.findAnimatorState("Run");
runState.clearTransitions();
const toRunTransition = walkState.addTransition(runState);
toRunTransition.exitTime = 0.5;
toRunTransition.duration = 1;
runState.clipStartTime = 0.5;
animator.play("Walk");
// @ts-ignore
animator.engine.time._frameCount++;
animator.update(0.1);
const destPlayData = animator["_animatorLayersData"][0].destPlayData;
expect(destPlayData.state?.name).to.eq("Run");
});
it("change state in one update", () => {
const animatorController = new AnimatorController(engine);
const layer = new AnimatorControllerLayer("layer");