Skip to content

Commit

Permalink
Animation clip embedded players (#11320)
Browse files Browse the repository at this point in the history
* Animation clip subregions

* Log

* Test

* Eval

* Update

* Update

* No longer use animation state to implement clip motion

* Clear subregions

* Fix

* Mark subregion as editor extendable

* Animation clip subregion player path

* fix isnan(0.0) with webgl1.0 (#11262)

* fix isinf(0.0)==true with webgl1.0

* code style

* fix specular params with surface toon (#11265)

* code format and add sliders for parameters (#11266)

* Broadcasting of animation state\'s speed

* Fix

* Rename

* Update

* Fix

* Revert "No longer use animation state to implement clip motion"

This reverts commit 20755f9.

* Revert some exotic changes

* Update docs

Co-authored-by: jk20012001 <jkregister@163.com>
Co-authored-by: 徐兵 <49358166+xubing0906@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 6, 2022
1 parent e969f3c commit 1b47c6c
Show file tree
Hide file tree
Showing 12 changed files with 1,040 additions and 2 deletions.
4 changes: 4 additions & 0 deletions EngineErrorMap.md
Original file line number Diff line number Diff line change
Expand Up @@ -1837,6 +1837,10 @@ We don't currently support conversion of \`CubicSplineQuatValue\`.

Instancing/Batching enabled for non-baked skinning model '%s', this may result in unexpected rendering artifacts. Consider turning it off in the material if you do not intend to do this.

### 3938

'%s' is not found from '%s'. It's specified as the root node to play animation clip '%s'.

### 4000

<!-- DEPRECATED -->
Expand Down
243 changes: 242 additions & 1 deletion cocos/core/animation/animation-clip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { murmurhash2_32_gc } from '../utils/murmurhash2_gc';
import { SkelAnimDataHub } from '../../3d/skeletal-animation/skeletal-animation-data-hub';
import { WrapMode as AnimationWrapMode, WrapMode, WrapModeMask } from './types';
import { legacyCC } from '../global-exports';
import { Mat4, Quat, Vec3 } from '../math';
import { approx, clamp, Mat4, Quat, Vec3 } from '../math';
import { Node } from '../scene-graph/node';
import { assertIsTrue } from '../data/utils/asserts';
import type { PoseOutput } from './pose-output';
Expand All @@ -49,6 +49,7 @@ import './exotic-animation/exotic-animation';
import { array } from '../utils/js';
import type { AnimationMask } from './marionette/animation-mask';
import { getGlobalAnimationManager } from './global-animation-manager';
import { EmbeddedPlayableState, EmbeddedPlayer } from './embedded-player/embedded-player';

export declare namespace AnimationClip {
export interface IEvent {
Expand Down Expand Up @@ -82,6 +83,12 @@ interface SkeletonAnimationBakeInfo {

export const exoticAnimationTag = Symbol('ExoticAnimation');

export const embeddedPlayerCountTag = Symbol('[[EmbeddedPlayerCount]]');
export const getEmbeddedPlayersTag = Symbol('[[GetEmbeddedPlayers]]');
export const addEmbeddedPlayerTag = Symbol('[[AddEmbeddedPlayer]]');
export const removeEmbeddedPlayerTag = Symbol('[[RemoveEmbeddedPlayer]]');
export const clearEmbeddedPlayersTag = Symbol('[[ClearEmbeddedPlayers]]');

/**
* @zh 动画剪辑表示一段使用动画编辑器编辑的关键帧动画或是外部美术工具生产的骨骼动画。
* 它的数据主要被分为几层:轨道、关键帧和曲线。
Expand Down Expand Up @@ -569,6 +576,44 @@ export class AnimationClip extends Asset {

// #endregion

/**
* @internal
*/
get [embeddedPlayerCountTag] () {
return this._embeddedPlayers.length;
}

/**
* @internal
*/
public [getEmbeddedPlayersTag] (): Iterable<EmbeddedPlayer> {
return this._embeddedPlayers;
}

/**
* @internal
*/
public [addEmbeddedPlayerTag] (embeddedPlayer: EmbeddedPlayer) {
this._embeddedPlayers.push(embeddedPlayer);
}

/**
* @internal
*/
public [removeEmbeddedPlayerTag] (embeddedPlayer: EmbeddedPlayer) {
const iEmbeddedPlayer = this._embeddedPlayers.indexOf(embeddedPlayer);
if (iEmbeddedPlayer >= 0) {
this._embeddedPlayers.splice(iEmbeddedPlayer, 1);
}
}

/**
* @internal
*/
public [clearEmbeddedPlayersTag] () {
this._embeddedPlayers.length = 0;
}

@serializable
private _duration = 0;

Expand All @@ -590,6 +635,9 @@ export class AnimationClip extends Asset {
@serializable
private _events: AnimationClip.IEvent[] = [];

@serializable
private _embeddedPlayers: EmbeddedPlayer[] = [];

private _runtimeEvents: {
ratios: number[];
eventGroups: IAnimationEventGroup[];
Expand Down Expand Up @@ -642,10 +690,19 @@ export class AnimationClip extends Asset {
exoticAnimationEvaluator = this._exoticAnimation.createEvaluator(binder);
}

let embeddedPlayerEvaluation: EmbeddedPlayerEvaluation | undefined;
if (target instanceof Node) { // Note: when this method is called from bake(), target is undefined.
const { _embeddedPlayers: embeddedPlayers } = this;
if (this._embeddedPlayers.length !== 0) {
embeddedPlayerEvaluation = new EmbeddedPlayerEvaluation(embeddedPlayers, target);
}
}

const evaluation = new AnimationClipEvaluation(
trackEvalStatues,
exoticAnimationEvaluator,
rootMotionEvaluation,
embeddedPlayerEvaluation,
);

return evaluation;
Expand Down Expand Up @@ -846,15 +903,167 @@ interface RootMotionOptions {

type ExoticAnimationEvaluator = ReturnType<ExoticAnimation['createEvaluator']>;

class EmbeddedPlayerEvaluation {
constructor (embeddedPlayers: ReadonlyArray<EmbeddedPlayer>, rootNode: Node) {
this._embeddedPlayers = embeddedPlayers;
this._embeddedPlayerEvaluationInfos = embeddedPlayers.map(
(embeddedPlayer): EmbeddedPlayerEvaluation['_embeddedPlayerEvaluationInfos'][0] => {
const { playable: player } = embeddedPlayer;
if (!player) {
return null;
}
const instantiatedPlayer = player.instantiate(rootNode);
if (!instantiatedPlayer) {
return null;
}
return {
instantiatedPlayer,
entered: false,
hostPauseTime: 0.0,
};
},
);
}

public evaluate (time: number) {
const {
_embeddedPlayers: embeddedPlayers,
_embeddedPlayerEvaluationInfos: embeddedPlayerEvaluationInfos,
} = this;
const nEmbeddedPlayers = embeddedPlayers.length;
for (let iEmbeddedPlayer = 0; iEmbeddedPlayer < nEmbeddedPlayers; ++iEmbeddedPlayer) {
const embeddedPlayerEvaluationInfo = embeddedPlayerEvaluationInfos[iEmbeddedPlayer];
if (!embeddedPlayerEvaluationInfo) {
continue;
}
const { entered, instantiatedPlayer } = embeddedPlayerEvaluationInfo;
const { begin, end } = embeddedPlayers[iEmbeddedPlayer];
const withinEmbeddedPlayer = time >= begin && time <= end;
if (withinEmbeddedPlayer) {
if (!entered) {
instantiatedPlayer.play(time - begin);
embeddedPlayerEvaluationInfo.entered = true;
}
} else if (entered) {
instantiatedPlayer.stop();
embeddedPlayerEvaluationInfo.entered = false;
}
}
}

public notifyHostSpeedChanged (speed: number) {
// Transmit the speed to embedded players that want a reconciled speed.
const {
_embeddedPlayers: embeddedPlayers,
_embeddedPlayerEvaluationInfos: embeddedPlayerEvaluationInfos,
} = this;
const nEmbeddedPlayers = embeddedPlayers.length;
for (let iEmbeddedPlayer = 0; iEmbeddedPlayer < nEmbeddedPlayers; ++iEmbeddedPlayer) {
const embeddedPlayerEvaluationInfo = embeddedPlayerEvaluationInfos[iEmbeddedPlayer];
if (!embeddedPlayerEvaluationInfo) {
continue;
}
const { instantiatedPlayer } = embeddedPlayerEvaluationInfo;
const { reconciledSpeed } = embeddedPlayers[iEmbeddedPlayer];
if (reconciledSpeed) {
instantiatedPlayer.setSpeed(speed);
}
}
}

public notifyHostPlay (time: number) {
// Host has switched to "playing", this can be happened when:
// - Previous state is "stopped": we must have stopped all embedded players.
// - Is pausing: we need to resume all embedded players.
const {
_embeddedPlayers: embeddedPlayers,
_embeddedPlayerEvaluationInfos: embeddedPlayerEvaluationInfos,
} = this;
const nEmbeddedPlayers = embeddedPlayers.length;
for (let iEmbeddedPlayer = 0; iEmbeddedPlayer < nEmbeddedPlayers; ++iEmbeddedPlayer) {
const embeddedPlayerEvaluationInfo = embeddedPlayerEvaluationInfos[iEmbeddedPlayer];
if (!embeddedPlayerEvaluationInfo) {
continue;
}
const { begin, end } = embeddedPlayers[iEmbeddedPlayer];
const { instantiatedPlayer, entered } = embeddedPlayerEvaluationInfo;
if (entered) {
const { hostPauseTime } = embeddedPlayerEvaluationInfo;
// We can resume the embedded player
// only if the pause/play happened at the same time
// or the embedded player supports random access.
// Otherwise we have to say goodbye to that embedded player.
if (instantiatedPlayer.randomAccess || approx(hostPauseTime, time, 1e-5)) {
const startTime = clamp(time, begin, end);
instantiatedPlayer.play(startTime - begin);
} else {
instantiatedPlayer.stop();
}
}
}
}

public notifyHostPause (time: number) {
// Host is paused, simply transmit this to embedded players.
const {
_embeddedPlayers: embeddedPlayers,
_embeddedPlayerEvaluationInfos: embeddedPlayerEvaluationInfos,
} = this;
const nEmbeddedPlayers = embeddedPlayers.length;
for (let iEmbeddedPlayer = 0; iEmbeddedPlayer < nEmbeddedPlayers; ++iEmbeddedPlayer) {
const embeddedPlayerEvaluationInfo = embeddedPlayerEvaluationInfos[iEmbeddedPlayer];
if (!embeddedPlayerEvaluationInfo) {
continue;
}
const { instantiatedPlayer, entered } = embeddedPlayerEvaluationInfo;
if (entered) {
instantiatedPlayer.pause();
embeddedPlayerEvaluationInfo.hostPauseTime = time;
}
}
}

public notifyHostStop () {
// Now that host is stopped, we stop all embedded players' playing
// regardless of their progresses.
const {
_embeddedPlayers: embeddedPlayers,
_embeddedPlayerEvaluationInfos: embeddedPlayerEvaluationInfos,
} = this;
const nEmbeddedPlayers = embeddedPlayers.length;
for (let iEmbeddedPlayer = 0; iEmbeddedPlayer < nEmbeddedPlayers; ++iEmbeddedPlayer) {
const embeddedPlayerEvaluationInfo = embeddedPlayerEvaluationInfos[iEmbeddedPlayer];
if (!embeddedPlayerEvaluationInfo) {
continue;
}
const { instantiatedPlayer, entered } = embeddedPlayerEvaluationInfo;
if (entered) {
embeddedPlayerEvaluationInfo.entered = false;
instantiatedPlayer.stop();
}
}
}

private declare _embeddedPlayers: ReadonlyArray<EmbeddedPlayer>;

private declare _embeddedPlayerEvaluationInfos: Array<null | {
instantiatedPlayer: EmbeddedPlayableState;
entered: boolean;
hostPauseTime: number;
}>;
}

class AnimationClipEvaluation {
constructor (
trackEvalStatuses: TrackEvalStatus[],
exoticAnimationEvaluator: ExoticAnimationEvaluator | undefined,
rootMotionEvaluation: RootMotionEvaluation | undefined,
embeddedPlayerEvaluation: EmbeddedPlayerEvaluation | undefined,
) {
this._trackEvalStatues = trackEvalStatuses;
this._exoticAnimationEvaluator = exoticAnimationEvaluator;
this._rootMotionEvaluation = rootMotionEvaluation;
this._embeddedPlayerEvaluation = embeddedPlayerEvaluation;
}

/**
Expand All @@ -865,6 +1074,7 @@ class AnimationClipEvaluation {
const {
_trackEvalStatues: trackEvalStatuses,
_exoticAnimationEvaluator: exoticAnimationEvaluator,
_embeddedPlayerEvaluation: embeddedPlayerEvaluation,
} = this;

const nTrackEvalStatuses = trackEvalStatuses.length;
Expand All @@ -877,6 +1087,36 @@ class AnimationClipEvaluation {
if (exoticAnimationEvaluator) {
exoticAnimationEvaluator.evaluate(time);
}

if (embeddedPlayerEvaluation) {
embeddedPlayerEvaluation.evaluate(time);
}
}

public notifyHostSpeedChanged (value: number) {
this._embeddedPlayerEvaluation?.notifyHostSpeedChanged(value);
}

/**
* Notifies that the host has ran into **playing** state.
* @param time The time where host ran into playing state.
*/
public notifyHostPlay (time: number) {
this._embeddedPlayerEvaluation?.notifyHostPlay(time);
}

/**
* Notifies that the host has ran into **pause** state.
*/
public notifyHostPause (time: number) {
this._embeddedPlayerEvaluation?.notifyHostPause(time);
}

/**
* Notifies that the host has ran into **stopped** state.
*/
public notifyHostStop () {
this._embeddedPlayerEvaluation?.notifyHostStop();
}

/**
Expand All @@ -894,6 +1134,7 @@ class AnimationClipEvaluation {
private _exoticAnimationEvaluator: ExoticAnimationEvaluator | undefined;
private _trackEvalStatues:TrackEvalStatus[] = [];
private _rootMotionEvaluation: RootMotionEvaluation | undefined = undefined;
private _embeddedPlayerEvaluation: EmbeddedPlayerEvaluation | undefined = undefined;
}

class BoneTransform {
Expand Down
16 changes: 15 additions & 1 deletion cocos/core/animation/animation-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,14 @@ export class AnimationState extends Playable {
* @zh 播放速率。
* @default: 1.0
*/
public speed = 1.0;
get speed () {
return this._speed;
}

set speed (value) {
this._speed = value;
this._clipEval?.notifyHostSpeedChanged(value);
}

/**
* @en The current accumulated time of this animation in seconds.
Expand Down Expand Up @@ -271,6 +278,7 @@ export class AnimationState extends Playable {
protected _curveLoaded = false;

private _clip: AnimationClip;
private _speed = 1.0;
private _useSimpleProcess = false;
private _target: Node | null = null;
private _wrapMode = WrapMode.Normal;
Expand Down Expand Up @@ -367,6 +375,8 @@ export class AnimationState extends Playable {
if (!(EDITOR && !legacyCC.GAME_VIEW)) {
this._clipEventEval = clip.createEventEvaluator(this._targetNode);
}

this._clipEval?.notifyHostSpeedChanged(this._speed);
}

public destroy () {
Expand Down Expand Up @@ -494,23 +504,27 @@ export class AnimationState extends Playable {
this._delayTime = this._delay;
this._onReplayOrResume();
this.emit(EventType.PLAY, this);
this._clipEval?.notifyHostPlay(this.current);
}

protected onStop () {
if (!this.isPaused) {
this._onPauseOrStop();
}
this.emit(EventType.STOP, this);
this._clipEval?.notifyHostStop();
}

protected onResume () {
this._onReplayOrResume();
this.emit(EventType.RESUME, this);
this._clipEval?.notifyHostPlay(this.current);
}

protected onPause () {
this._onPauseOrStop();
this.emit(EventType.PAUSE, this);
this._clipEval?.notifyHostPause(this.current);
}

/**
Expand Down
Loading

0 comments on commit 1b47c6c

Please sign in to comment.