@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
338 lines (316 loc) • 9.32 kB
text/typescript
import {Object3D, AnimationMixer, AnimationAction, AnimationClip, Event, EventListener} from 'three';
import {arraySortBy} from '../../core/ArrayUtils';
import {isBooleanTrue} from '../../core/Type';
import {
NamedFunction1,
NamedFunction2,
// NamedFunction1,
// NamedFunction2,
NamedFunction3,
// NamedFunction4,
NamedFunction5,
ObjectNamedFunction0,
} from './_Base';
//
//
// MAPS
//
//
const _mixerByObject: Map<Object3D, AnimationMixer> = new Map();
const _actionByNameByMixer: Map<AnimationMixer, Map<string, AnimationAction>> = new Map();
export function findOrCreateAnimationMixer(object3D: Object3D) {
let animationMixer = _mixerByObject.get(object3D);
if (!animationMixer) {
animationMixer = new AnimationMixer(object3D);
_mixerByObject.set(object3D, animationMixer);
// const animations = object3D.animations;
// if (animations) {
// for (let animation of animations) {
// findOrCreateAnimationAction(animationMixer, animation.name, false);
// }
// }
}
return animationMixer;
}
function findOrCreateAnimationAction(mixer: AnimationMixer, clipName: string, autoPlay: boolean) {
let mixerMap = _actionByNameByMixer.get(mixer);
if (!mixerMap) {
mixerMap = new Map();
_actionByNameByMixer.set(mixer, mixerMap);
}
let action = mixerMap.get(clipName);
if (!action) {
const root = mixer.getRoot();
const animations = (root as Object3D).animations;
if (!animations) {
console.warn('no animations');
return;
}
const animation = animations.find((animation) => animation.name == clipName);
if (!animation) {
return;
}
action = mixer.existingAction(animation) || mixer.clipAction(animation);
if (isBooleanTrue(autoPlay)) {
action.play();
}
mixerMap.set(clipName, action);
}
return action;
}
//
//
// FADE UTILS
//
//
type AnimationActionLoopEvent = Event & {
type: 'loop';
} & {
target: AnimationMixer;
} & {
action: AnimationAction;
};
type AnimationActionEventListenerExtended = EventListener<Event, 'loop', AnimationMixer> &
((e: AnimationActionLoopEvent) => void);
export function animationClipsFromAnimationMixer(animationMixer: AnimationMixer): AnimationClip[] | undefined {
const root = animationMixer.getRoot();
return (root as Object3D).animations;
}
export function existingAnimationActionsFromAnimationMixer(animationMixer: AnimationMixer): AnimationAction[] {
const root = animationMixer.getRoot();
const animations = (root as Object3D).animations;
if (!animations) {
console.warn('no animations found', root);
return [];
}
const animationActions: AnimationAction[] = [];
for (let animation of animations) {
const existingAnimationAction = animationMixer.existingAction(animation);
if (existingAnimationAction) {
animationActions.push(existingAnimationAction);
}
}
return animationActions;
}
export function getMostActiveAnimationActionFromMixer(animationMixer: AnimationMixer, except?: AnimationAction) {
const existing = existingAnimationActionsFromAnimationMixer(animationMixer);
const otherActions = existing.filter((action) => action !== except);
const actionsSortedByWeight = arraySortBy(otherActions, (action) => -action.getEffectiveWeight());
const mostActiveAnimationAction = actionsSortedByWeight[0];
return {
otherActions,
mostActiveAnimationAction,
};
}
function _setAnimationActionWeight(action: AnimationAction, weight: number) {
action.enabled = true;
action.setEffectiveTimeScale(1);
action.setEffectiveWeight(weight);
}
function _crossFade(from: AnimationAction, to: AnimationAction, duration: number, warp: boolean) {
_setAnimationActionWeight(to, 1);
// animationActionTo.time = 0;
to.syncWith(from);
to.play();
from.crossFadeTo(to, duration, warp);
}
function _fadeOutOtherActions(
animationActionTo: AnimationAction,
duration: number,
warp: boolean,
startOnFromActionEnd: boolean
) {
const mixer = animationActionTo.getMixer();
const {otherActions, mostActiveAnimationAction} = getMostActiveAnimationActionFromMixer(mixer, animationActionTo);
const animationActionFrom = mostActiveAnimationAction;
const _fadeInCurrentAndFadeOutOtherActions = () => {
_crossFade(animationActionFrom, animationActionTo, duration, warp);
for (let action of otherActions) {
action.fadeOut(duration);
}
};
if (otherActions.length == 0) {
_fadeInSimple(animationActionTo, duration);
} else {
if (startOnFromActionEnd) {
animationActionTo.stop();
_fadeInWhenPreviousLoopCompleted(mixer, animationActionFrom, _fadeInCurrentAndFadeOutOtherActions);
} else {
_fadeInCurrentAndFadeOutOtherActions();
}
}
}
function _fadeInSimple(animationActionTo: AnimationAction, duration: number) {
_setAnimationActionWeight(animationActionTo, 1);
animationActionTo.fadeIn(duration);
}
function _fadeInWhenPreviousLoopCompleted(
mixer: AnimationMixer,
animationActionFrom: AnimationAction,
callback: () => void
) {
const onLoop: AnimationActionEventListenerExtended = ((event: AnimationActionLoopEvent) => {
if (event.action === animationActionFrom) {
mixer.removeEventListener('loop', onLoop);
callback();
}
}) as EventListener<Event, 'loop', AnimationMixer>;
mixer.addEventListener('loop', onLoop);
}
type GetAnimationAction = () => AnimationAction;
function startCrossFade(actionFrom: AnimationAction, actionToGet: GetAnimationAction, duration: number, warp: boolean) {
// only request animationActionTo at the last moment,
// in case it is set to autoPlay,
// as it would otherwise start playing before as soon as it is created,
// which could be way before this function is called
// const animationActionTo = this._inputValue<ActorConnectionPointType.ANIMATION_ACTION>(
// AnimationActionCrossFadeActorNodeInputName.TO,
// context
// );
const actionTo = actionToGet();
if (!actionTo) {
return;
}
_crossFade(actionFrom, actionTo, duration, warp);
}
//
//
// FUNCTIONS
//
//
export class getAnimationMixer extends ObjectNamedFunction0 {
static override type() {
return 'getAnimationMixer';
}
func(object3D: Object3D): AnimationMixer {
// TODO: we need to handle cases where a result is undefined
const action = findOrCreateAnimationMixer(object3D)!;
return action;
}
}
export class animationMixerUpdate extends ObjectNamedFunction0 {
static override type() {
return 'animationMixerUpdate';
}
func(object3D: Object3D): void {
const mixer = _mixerByObject.get(object3D);
if (!mixer) {
return;
}
const delta = this.scene.timeController.delta();
const root = mixer.getRoot();
if ((root as Object3D).traverse) {
(root as Object3D).traverse((child) => {
if (!child.matrixAutoUpdate) {
child.updateMatrix();
}
});
}
mixer.update(delta);
}
}
export class getAnimationAction extends NamedFunction3<[AnimationMixer, string, boolean]> {
static override type() {
return 'getAnimationAction';
}
func(mixer: AnimationMixer, clipName: string, autoPlay: boolean): AnimationAction {
// TODO: we need to handle cases where a result is undefined
const action = findOrCreateAnimationAction(mixer, clipName, autoPlay)!;
return action;
}
}
//
//
// Play / Stop
//
//
export class animationActionPlay extends NamedFunction1<[AnimationAction]> {
static override type() {
return 'animationActionPlay';
}
func(action: AnimationAction): void {
if (!action) {
return;
}
action.play();
}
}
export class animationActionStop extends NamedFunction1<[AnimationAction]> {
static override type() {
return 'animationActionStop';
}
func(action: AnimationAction): void {
if (!action) {
return;
}
action.stop();
}
}
//
//
// FadeX
//
//
export class animationActionFadeIn extends NamedFunction5<[AnimationAction, number, boolean, boolean, boolean]> {
static override type() {
return 'animationActionFadeIn';
}
func(
action: AnimationAction,
duration: number,
fadeOutOtherActions: boolean,
warp: boolean,
startOnFromActionEnd: boolean
): void {
if (!action) {
console.warn(`action '${action}' not found`);
return;
}
if (fadeOutOtherActions) {
_fadeOutOtherActions(action, duration, warp, startOnFromActionEnd);
} else {
_fadeInSimple(action, duration);
}
}
}
export class animationActionFadeOut extends NamedFunction2<[AnimationAction, number]> {
static override type() {
return 'animationActionFadeOut';
}
func(action: AnimationAction, duration: number): void {
if (!action) {
return;
}
action.fadeOut(duration);
}
}
export class animationActionCrossFade extends NamedFunction5<
[AnimationAction, GetAnimationAction, number, boolean, boolean]
> {
static override type() {
return 'animationActionCrossFade';
}
func(
actionFrom: AnimationAction,
actionToGet: GetAnimationAction,
duration: number,
warp: boolean,
startOnFromActionEnd: boolean
): void {
if (!actionFrom) {
return;
}
if (startOnFromActionEnd) {
const mixer = actionFrom.getMixer();
const onLoop: AnimationActionEventListenerExtended = ((event: AnimationActionLoopEvent) => {
if (event.action === actionFrom) {
mixer.removeEventListener('loop', onLoop);
startCrossFade(actionFrom, actionToGet, duration, warp);
}
}) as EventListener<Event, 'loop', AnimationMixer>;
mixer.addEventListener('loop', onLoop);
} else {
startCrossFade(actionFrom, actionToGet, duration, warp);
}
}
}