Skip to content

Commit

Permalink
No longer use animation state to implement clip motion
Browse files Browse the repository at this point in the history
  • Loading branch information
shrinktofit committed May 5, 2022
1 parent 0943cec commit 20755f9
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 84 deletions.
81 changes: 11 additions & 70 deletions cocos/core/animation/animation-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { AnimationMask } from './marionette/animation-mask';
import { PoseOutput } from './pose-output';
import { BlendStateBuffer } from '../../3d/skeletal-animation/skeletal-animation-blending';
import { getGlobalAnimationManager } from './global-animation-manager';
import { wrap } from './wrap';

/**
* @en The event type supported by Animation
Expand Down Expand Up @@ -598,81 +599,21 @@ export class AnimationState extends Playable {
}
}

private _needReverse (currentIterations: number) {
const wrapMode = this.wrapMode;
let needReverse = false;

if ((wrapMode & WrapModeMask.PingPong) === WrapModeMask.PingPong) {
const isEnd = currentIterations - (currentIterations | 0) === 0;
if (isEnd && (currentIterations > 0)) {
currentIterations -= 1;
}

const isOddIteration = currentIterations & 1;
if (isOddIteration) {
needReverse = !needReverse;
}
}
if ((wrapMode & WrapModeMask.Reverse) === WrapModeMask.Reverse) {
needReverse = !needReverse;
}
return needReverse;
}

private getWrappedInfo (time: number, info?: WrappedInfo) {
info = info || new WrappedInfo();

info = info ?? new WrappedInfo();
const playbackStart = this._getPlaybackStart();
const playbackEnd = this._getPlaybackEnd();
const playbackDuration = playbackEnd - playbackStart;

let stopped = false;
const repeatCount = this.repeatCount;

time -= playbackStart;

let currentIterations = time > 0 ? (time / playbackDuration) : -(time / playbackDuration);
if (currentIterations >= repeatCount) {
currentIterations = repeatCount;

stopped = true;
let tempRatio = repeatCount - (repeatCount | 0);
if (tempRatio === 0) {
tempRatio = 1; // 如果播放过,动画不复位
}
time = tempRatio * playbackDuration * (time > 0 ? 1 : -1);
}

if (time > playbackDuration) {
const tempTime = time % playbackDuration;
time = tempTime === 0 ? playbackDuration : tempTime;
} else if (time < 0) {
time %= playbackDuration;
if (time !== 0) { time += playbackDuration; }
}

let needReverse = false;
const shouldWrap = this._wrapMode & WrapModeMask.ShouldWrap;
if (shouldWrap) {
needReverse = this._needReverse(currentIterations);
}

let direction = needReverse ? -1 : 1;
if (this.speed < 0) {
direction *= -1;
}

// calculate wrapped time
if (shouldWrap && needReverse) {
time = playbackDuration - time;
}

info.time = playbackStart + time;
info.ratio = info.time / this.duration;
info.direction = direction;
info.stopped = stopped;
info.iterations = currentIterations;

wrap(
time,
playbackDuration,
this._wrapMode,
this._repeatCount,
this._speed < 0,
info,
);
info.time += playbackStart;
return info;
}

Expand Down
61 changes: 48 additions & 13 deletions cocos/core/animation/marionette/clip-motion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { editorExtrasTag } from '../../data';
import { ccclass, type } from '../../data/class-decorator';
import { EditorExtendable } from '../../data/editor-extendable';
import { AnimationClip } from '../animation-clip';
import { AnimationState } from '../animation-state';
import { PoseOutput } from '../pose-output';
import { WrapModeMask, WrappedInfo } from '../types';
import { wrap } from '../wrap';
import { createEval } from './create-eval';
import { getMotionRuntimeID, graphDebug, GRAPH_DEBUG_ENABLED, pushWeight, RUNTIME_ID_ENABLED } from './graph-debug';
import { ClipStatus } from './graph-eval';
Expand Down Expand Up @@ -39,14 +41,19 @@ class ClipMotionEval implements MotionEval {

public declare runtimeId?: number;

private declare _state: AnimationState;

public declare readonly duration: number;

constructor (context: MotionEvalContext, clip: AnimationClip) {
this.duration = clip.duration / clip.speed;
this._state = new AnimationState(clip);
this._state.initialize(context.node, context.blendBuffer, context.mask);
this._clip = clip;
const poseOutput = new PoseOutput(context.blendBuffer);
this._poseOutput = poseOutput;
this._clipEval = clip.createEvaluator({
target: context.node,
pose: poseOutput,
mask: context.mask,
});
this._clipEventEval = clip.createEventEvaluator(context.node);
}

public getClipStatuses (baseWeight: number): Iterator<ClipStatus, any, undefined> {
Expand All @@ -64,7 +71,7 @@ class ClipMotionEval implements MotionEval {
done: false,
value: {
__DEBUG_ID__: this.__DEBUG__ID__,
clip: this._state.clip,
clip: this._clip,
weight: baseWeight,
},
};
Expand All @@ -74,18 +81,46 @@ class ClipMotionEval implements MotionEval {
}

get progress () {
return this._state.time / this.duration;
return this._elapsedTime / this.duration;
}

public sample (progress: number, weight: number) {
if (weight === 0.0) {
return;
}
pushWeight(this._state.name, weight);
const time = this._state.duration * progress;
this._state.time = time;
this._state.weight = weight;
this._state.sample();
this._state.weight = 0.0;
pushWeight(this._clip.name, weight);
const { duration } = this;
const elapsedTime = this.duration * progress;
const { wrapMode } = this._clip;
const repeatCount = (wrapMode & WrapModeMask.Loop) === WrapModeMask.Loop
? Infinity : 1;
const wrapInfo = wrap(
elapsedTime,
duration,
wrapMode,
repeatCount,
false,
this._wrapInfo,
);
this._poseOutput.weight = weight;
this._clipEval.evaluate(wrapInfo.time);
this._poseOutput.weight = 0.0;
this._clipEventEval.sample(
wrapInfo.ratio,
wrapInfo.direction,
wrapInfo.iterations,
);
}

private _clip: AnimationClip;

private _poseOutput: PoseOutput;

private _clipEval: ReturnType<AnimationClip['createEvaluator']>;

private _clipEventEval: ReturnType<AnimationClip['createEventEvaluator']>;

private _elapsedTime = 0.0;

private _wrapInfo = new WrappedInfo();
}
74 changes: 74 additions & 0 deletions cocos/core/animation/wrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { WrapMode, WrapModeMask, WrappedInfo } from './types';

export function wrap (
elapsedTime: number,
duration: number,
wrapMode: WrapMode,
repeatCount: number,
negativeSpeed: boolean,
info: WrappedInfo,
): WrappedInfo {
let stopped = false;

let currentIterations = elapsedTime > 0 ? (elapsedTime / duration) : -(elapsedTime / duration);
if (currentIterations >= repeatCount) {
currentIterations = repeatCount;

stopped = true;
let tempRatio = repeatCount - (repeatCount | 0);
if (tempRatio === 0) {
tempRatio = 1; // 如果播放过,动画不复位
}
elapsedTime = tempRatio * duration * (elapsedTime > 0 ? 1 : -1);
}

if (elapsedTime > duration) {
const tempTime = elapsedTime % duration;
elapsedTime = tempTime === 0 ? duration : tempTime;
} else if (elapsedTime < 0) {
elapsedTime %= duration;
if (elapsedTime !== 0) { elapsedTime += duration; }
}

let needReverse = false;
const shouldWrap = wrapMode & WrapModeMask.ShouldWrap;
if (shouldWrap) {
needReverse = isReverseIteration(wrapMode, currentIterations);
}

let direction = needReverse ? -1 : 1;
if (negativeSpeed) {
direction *= -1;
}

// calculate wrapped time
if (shouldWrap && needReverse) {
elapsedTime = duration - elapsedTime;
}

info.time = elapsedTime;
info.ratio = info.time / duration;
info.direction = direction;
info.stopped = stopped;
info.iterations = currentIterations;

return info;
}

function isReverseIteration (wrapMode: WrapMode, currentIterations: number) {
let needReverse = false;
if ((wrapMode & WrapModeMask.PingPong) === WrapModeMask.PingPong) {
const isEnd = currentIterations - (currentIterations | 0) === 0;
if (isEnd && (currentIterations > 0)) {
currentIterations -= 1;
}
const isOddIteration = currentIterations & 1;
if (isOddIteration) {
needReverse = !needReverse;
}
}
if ((wrapMode & WrapModeMask.Reverse) === WrapModeMask.Reverse) {
needReverse = !needReverse;
}
return needReverse;
}
71 changes: 70 additions & 1 deletion tests/animation/newgenanim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import 'jest-extended';
import { assertIsTrue } from '../../cocos/core/data/utils/asserts';
import { AnimationClip } from '../../cocos/core/animation/animation-clip';
import { TriggerResetMode } from '../../cocos/core/animation/marionette/variable';
import { getGlobalAnimationManager } from '../../cocos/core/animation/global-animation-manager';

describe('NewGen Anim', () => {
describe('Marionette', () => {
test('Defaults', () => {
const graph = new AnimationGraph();
expect(graph.layers).toHaveLength(0);
Expand Down Expand Up @@ -2267,6 +2268,74 @@ describe('NewGen Anim', () => {
['t1', VariableType.TRIGGER, true],
]);
});

class FrameEventHelper {
constructor() {
const mock = jest.spyOn(getGlobalAnimationManager(), 'pushDelayEvent');
mock.mockImplementationOnce((...args: [fn: (...args: any[]) => void, thisArg: any, args: any[]]) => {
this._delayEvents.push(args);
});
this._mock = mock;
}

public unload() {
this._mock.mockRestore();
}

public apply() {
const { _delayEvents: delayEvents } = this;
for (const [fn, thisArg, args] of delayEvents) {
fn.call(thisArg, ...args);
}
delayEvents.length = 0;
}

private _mock: jest.SpyInstance;
private _delayEvents: Array<[fn: (...args: any[]) => void, thisArg: any, args: any[]]> = [];
}

test('Animation clip frame events', () => {
class TestComponent extends Component {
public handleEvent = jest.fn();
}

const node = new Node();
const component = node.addComponent(TestComponent) as TestComponent;

const animationGraph = new AnimationGraph();
const mainLayer = animationGraph.addLayer();
const motionState = mainLayer.stateMachine.addMotion();
const clipMotion = motionState.motion = new ClipMotion();
const animationClip = clipMotion.clip = new AnimationClip();
animationClip.duration = 1.8;
animationClip.events = [{
frame: 0.3,
func: 'handleEvent',
params: ['2'],
}];
mainLayer.stateMachine.connect(mainLayer.stateMachine.entryState, motionState);

const { graphEval } = createAnimationGraphEval2(animationGraph, node);
const graphUpdater = new GraphUpdater(graphEval);

const frameEventHelper = new FrameEventHelper();

graphUpdater.goto(0.1);
frameEventHelper.apply();
expect(component.handleEvent).not.toBeCalled();

graphUpdater.goto(0.32);
frameEventHelper.apply();
expect(component.handleEvent).toBeCalledTimes(1);
expect(component.handleEvent.mock.calls[0][0]).toBe('2');
component.handleEvent.mockClear();

graphUpdater.goto(0.37);
frameEventHelper.apply();
expect(component.handleEvent).not.toBeCalled();

frameEventHelper.unload();
});
});

function createEmptyClipMotion (duration: number, name = '') {
Expand Down

0 comments on commit 20755f9

Please sign in to comment.