From ebd6cbfe211935aef25be37df83ff0d4f3714b3e Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Wed, 30 Oct 2024 19:14:37 +0800 Subject: [PATCH] Fix crossFade error caused by transitionOffsetTime and clipStartTime (#2411) * fix: crossFade error caused by transitionOffsetTime and clipStartTime --- packages/core/src/animation/Animator.ts | 33 ++++++------ packages/core/src/animation/AnimatorState.ts | 14 +++++ .../internal/AnimatorStatePlayData.ts | 10 ++-- tests/src/core/Animator.test.ts | 53 ++++++++++++++++--- 4 files changed, 83 insertions(+), 27 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index b3c92ed4e..3be6664ec 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -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; } diff --git a/packages/core/src/animation/AnimatorState.ts b/packages/core/src/animation/AnimatorState.ts index b9cdb987d..2617f58a4 100644 --- a/packages/core/src/animation/AnimatorState.ts +++ b/packages/core/src/animation/AnimatorState.ts @@ -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; + } } diff --git a/packages/core/src/animation/internal/AnimatorStatePlayData.ts b/packages/core/src/animation/internal/AnimatorStatePlayData.ts index e3a7b2a83..a9a88e322 100644 --- a/packages/core/src/animation/internal/AnimatorStatePlayData.ts +++ b/packages/core/src/animation/internal/AnimatorStatePlayData.ts @@ -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) { diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index eb17c66e7..1b73bd6f2 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -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");