From 40d926faa225943538e2c8d806d7eaa4e10dc1a5 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Tue, 13 Aug 2024 14:58:40 +0800 Subject: [PATCH] Fix animator play backwards error and onStateExit not triggered when crossFade finished and actualDeltaTime error (#2325) * fix: animator play backwards error * fix: onStateExit not called when crossFade finished but state not finished * fix: actualDelta time calculate error when speed is 0 --- e2e/case/animator-play-backwards.ts | 38 +++++++++ e2e/config.ts | 5 ++ .../Animator_animator-play-backwards.jpg | 3 + packages/core/src/animation/Animator.ts | 80 +++++++++---------- tests/src/core/Animator.test.ts | 67 ++++++++++++++++ 5 files changed, 151 insertions(+), 42 deletions(-) create mode 100644 e2e/case/animator-play-backwards.ts create mode 100644 e2e/fixtures/originImage/Animator_animator-play-backwards.jpg diff --git a/e2e/case/animator-play-backwards.ts b/e2e/case/animator-play-backwards.ts new file mode 100644 index 0000000000..70dd006a17 --- /dev/null +++ b/e2e/case/animator-play-backwards.ts @@ -0,0 +1,38 @@ +/** + * @title Animation Play Backwards + * @category Animation + */ +import { Animator, Camera, DirectLight, GLTFResource, Logger, Vector3, WebGLEngine } from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(2); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(0, 1, 5); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightNode = rootEntity.createChild("light_node"); + lightNode.addComponent(DirectLight).intensity = 0.6; + lightNode.transform.lookAt(new Vector3(0, 0, 1)); + lightNode.transform.rotate(new Vector3(0, 90, 0)); + + engine.resourceManager + .load("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb") + .then((gltfResource) => { + const { defaultSceneRoot } = gltfResource; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + animator.findAnimatorState("walk").speed = -1; + animator.play("walk"); + updateForE2E(engine); + + initScreenshot(engine, camera); + }); +}); diff --git a/e2e/config.ts b/e2e/config.ts index a7c538b76e..8aae126bfb 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -45,6 +45,11 @@ export const E2E_CONFIG = { caseFileName: "animator-play", threshold: 0.1 }, + playBackWards: { + category: "Animator", + caseFileName: "animator-play-backwards", + threshold: 0.1 + }, playBeforeActive: { category: "Animator", caseFileName: "animator-play-beforeActive", diff --git a/e2e/fixtures/originImage/Animator_animator-play-backwards.jpg b/e2e/fixtures/originImage/Animator_animator-play-backwards.jpg new file mode 100644 index 0000000000..5340f4ab4e --- /dev/null +++ b/e2e/fixtures/originImage/Animator_animator-play-backwards.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36fa8a4c41fd548074285a07586e41f1f44493120ab00e7f6206a5cf33975ed2 +size 44841 diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index e22b15811f..ecf0da396d 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -548,27 +548,25 @@ export class Animator extends Component { srcPlayData.update(playDeltaTime); const { clipTime, isForwards } = srcPlayData; + const { transitions } = state; + const { anyStateTransitions } = layer.stateMachine; + const transition = - this._applyTransitionsByCondition( - layerIndex, - layerData, - layer, - state, - layer.stateMachine.anyStateTransitions, - aniUpdate - ) || - this._applyStateTransitions( - layerIndex, - layerData, - layer, - isForwards, - srcPlayData, - state.transitions, - lastClipTime, - clipTime, - playDeltaTime, - aniUpdate - ); + (anyStateTransitions.length && + this._applyTransitionsByCondition(layerIndex, layerData, layer, state, anyStateTransitions, aniUpdate)) || + (transitions.length && + this._applyStateTransitions( + layerIndex, + layerData, + layer, + isForwards, + srcPlayData, + transitions, + lastClipTime, + clipTime, + playDeltaTime, + aniUpdate + )); let playCostTime: number; if (transition) { @@ -686,7 +684,7 @@ export class Animator extends Component { : dstPlayDeltaTime; } - const actualCostTime = dstPlaySpeed === 0 ? 0 : dstPlayCostTime / dstPlaySpeed; + const actualCostTime = dstPlaySpeed === 0 ? deltaTime : dstPlayCostTime / dstPlaySpeed; const srcPlayCostTime = actualCostTime * srcPlaySpeed; srcPlayData.update(srcPlayCostTime); @@ -698,6 +696,7 @@ export class Animator extends Component { const crossFadeFinished = crossWeight === 1.0; if (crossFadeFinished) { + srcPlayData.playState = AnimatorStatePlayState.Finished; this._preparePlayOwner(layerData, destState); this._evaluatePlayingState(destPlayData, weight, additive, aniUpdate); } else { @@ -810,7 +809,7 @@ export class Animator extends Component { : playDeltaTime; } - const actualCostTime = playSpeed === 0 ? 0 : dstPlayCostTime / playSpeed; + const actualCostTime = playSpeed === 0 ? deltaTime : dstPlayCostTime / playSpeed; destPlayData.update(dstPlayCostTime); @@ -899,28 +898,25 @@ export class Animator extends Component { playData.updateOrientation(actualDeltaTime); const { clipTime, isForwards } = playData; + const { transitions } = state; + const { anyStateTransitions } = layer.stateMachine; const transition = - this._applyTransitionsByCondition( - layerIndex, - layerData, - layer, - state, - stateMachine.anyStateTransitions, - aniUpdate - ) || - this._applyStateTransitions( - layerIndex, - layerData, - layer, - isForwards, - playData, - state.transitions, - clipTime, - clipTime, - actualDeltaTime, - aniUpdate - ); + (anyStateTransitions.length && + this._applyTransitionsByCondition(layerIndex, layerData, layer, state, anyStateTransitions, aniUpdate)) || + (transitions.length && + this._applyStateTransitions( + layerIndex, + layerData, + layer, + isForwards, + playData, + transitions, + clipTime, + clipTime, + actualDeltaTime, + aniUpdate + )); if (transition) { this._updateState(layerIndex, layerData, layer, deltaTime, aniUpdate); diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index 8582cc36a4..eb17c66e7c 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -642,4 +642,71 @@ describe("Animator test", function () { const newParam2 = animator.animatorController.addParameter("oldName", 2); expect(newParam2).to.eq(null); }); + + it("stateMachineScript", () => { + const animatorController = new AnimatorController(engine); + const layer = new AnimatorControllerLayer("layer"); + animatorController.addLayer(layer); + const state1 = layer.stateMachine.addState("state1"); + const state2 = layer.stateMachine.addState("state2"); + state1.wrapMode = WrapMode.Once; + state2.wrapMode = WrapMode.Once; + const clip1 = new AnimationClip("clip1"); + const rotationCurve = new AnimationFloatCurve(); + const key1 = new Keyframe(); + const key2 = new Keyframe(); + key1.time = 0; + key1.value = 0; + key2.time = 1; + key2.value = 90; + rotationCurve.addKey(key1); + rotationCurve.addKey(key2); + clip1.addCurveBinding("", Transform, "rotation.x", rotationCurve); + + const clip2 = new AnimationClip("clip2"); + const positionCurve = new AnimationFloatCurve(); + const key3 = new Keyframe(); + const key4 = new Keyframe(); + key3.time = 0; + key3.value = 0; + key4.time = 1; + key4.value = 5; + positionCurve.addKey(key3); + positionCurve.addKey(key4); + clip2.addCurveBinding("", Transform, "position.x", positionCurve); + state1.clip = clip1; + state2.clip = clip2; + + const transition = new AnimatorStateTransition(); + transition.destinationState = state2; + transition.exitTime = 1; + transition.duration = 1; + state1.addTransition(transition); + + animator.animatorController = animatorController; + + class TestScript extends StateMachineScript { + onStateEnter(animator) {} + onStateExit(animator) {} + } + + const testScript = state1.addStateMachineScript(TestScript); + const testScript2 = state2.addStateMachineScript(TestScript); + + const onStateEnterSpy = chai.spy.on(testScript, "onStateEnter"); + const onStateExitSpy = chai.spy.on(testScript, "onStateExit"); + const onStateEnter2Spy = chai.spy.on(testScript2, "onStateEnter"); + const onStateExit2Spy = chai.spy.on(testScript2, "onStateExit"); + + animator.play("state1"); + + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(3); + + expect(onStateEnterSpy).to.have.been.called.exactly(1); + expect(onStateExitSpy).to.have.been.called.exactly(1); + expect(onStateEnter2Spy).to.have.been.called.exactly(1); + expect(onStateExit2Spy).to.have.been.called.exactly(1); + }); });