starry / backend /libs /three /animation /AnimationMixer.js
k-l-lambda's picture
feat: add Python ML services (CPU mode) with model download
2b7aae2
import { AnimationAction } from './AnimationAction.js';
import { EventDispatcher } from '../core/EventDispatcher.js';
import { LinearInterpolant } from '../math/interpolants/LinearInterpolant.js';
import { PropertyBinding } from './PropertyBinding.js';
import { PropertyMixer } from './PropertyMixer.js';
import { AnimationClip } from './AnimationClip.js';
import { NormalAnimationBlendMode } from '../constants.js';
class AnimationMixer extends EventDispatcher {
constructor(root) {
super();
this._root = root;
this._initMemoryManager();
this._accuIndex = 0;
this.time = 0;
this.timeScale = 1.0;
}
_bindAction(action, prototypeAction) {
const root = action._localRoot || this._root,
tracks = action._clip.tracks,
nTracks = tracks.length,
bindings = action._propertyBindings,
interpolants = action._interpolants,
rootUuid = root.uuid,
bindingsByRoot = this._bindingsByRootAndName;
let bindingsByName = bindingsByRoot[rootUuid];
if (bindingsByName === undefined) {
bindingsByName = {};
bindingsByRoot[rootUuid] = bindingsByName;
}
for (let i = 0; i !== nTracks; ++i) {
const track = tracks[i],
trackName = track.name;
let binding = bindingsByName[trackName];
if (binding !== undefined) {
++binding.referenceCount;
bindings[i] = binding;
} else {
binding = bindings[i];
if (binding !== undefined) {
// existing binding, make sure the cache knows
if (binding._cacheIndex === null) {
++binding.referenceCount;
this._addInactiveBinding(binding, rootUuid, trackName);
}
continue;
}
const path = prototypeAction && prototypeAction._propertyBindings[i].binding.parsedPath;
binding = new PropertyMixer(PropertyBinding.create(root, trackName, path), track.ValueTypeName, track.getValueSize());
++binding.referenceCount;
this._addInactiveBinding(binding, rootUuid, trackName);
bindings[i] = binding;
}
interpolants[i].resultBuffer = binding.buffer;
}
}
_activateAction(action) {
if (!this._isActiveAction(action)) {
if (action._cacheIndex === null) {
// this action has been forgotten by the cache, but the user
// appears to be still using it -> rebind
const rootUuid = (action._localRoot || this._root).uuid,
clipUuid = action._clip.uuid,
actionsForClip = this._actionsByClip[clipUuid];
this._bindAction(action, actionsForClip && actionsForClip.knownActions[0]);
this._addInactiveAction(action, clipUuid, rootUuid);
}
const bindings = action._propertyBindings;
// increment reference counts / sort out state
for (let i = 0, n = bindings.length; i !== n; ++i) {
const binding = bindings[i];
if (binding.useCount++ === 0) {
this._lendBinding(binding);
binding.saveOriginalState();
}
}
this._lendAction(action);
}
}
_deactivateAction(action) {
if (this._isActiveAction(action)) {
const bindings = action._propertyBindings;
// decrement reference counts / sort out state
for (let i = 0, n = bindings.length; i !== n; ++i) {
const binding = bindings[i];
if (--binding.useCount === 0) {
binding.restoreOriginalState();
this._takeBackBinding(binding);
}
}
this._takeBackAction(action);
}
}
// Memory manager
_initMemoryManager() {
this._actions = []; // 'nActiveActions' followed by inactive ones
this._nActiveActions = 0;
this._actionsByClip = {};
// inside:
// {
// knownActions: Array< AnimationAction > - used as prototypes
// actionByRoot: AnimationAction - lookup
// }
this._bindings = []; // 'nActiveBindings' followed by inactive ones
this._nActiveBindings = 0;
this._bindingsByRootAndName = {}; // inside: Map< name, PropertyMixer >
this._controlInterpolants = []; // same game as above
this._nActiveControlInterpolants = 0;
const scope = this;
this.stats = {
actions: {
get total() {
return scope._actions.length;
},
get inUse() {
return scope._nActiveActions;
},
},
bindings: {
get total() {
return scope._bindings.length;
},
get inUse() {
return scope._nActiveBindings;
},
},
controlInterpolants: {
get total() {
return scope._controlInterpolants.length;
},
get inUse() {
return scope._nActiveControlInterpolants;
},
},
};
}
// Memory management for AnimationAction objects
_isActiveAction(action) {
const index = action._cacheIndex;
return index !== null && index < this._nActiveActions;
}
_addInactiveAction(action, clipUuid, rootUuid) {
const actions = this._actions,
actionsByClip = this._actionsByClip;
let actionsForClip = actionsByClip[clipUuid];
if (actionsForClip === undefined) {
actionsForClip = {
knownActions: [action],
actionByRoot: {},
};
action._byClipCacheIndex = 0;
actionsByClip[clipUuid] = actionsForClip;
} else {
const knownActions = actionsForClip.knownActions;
action._byClipCacheIndex = knownActions.length;
knownActions.push(action);
}
action._cacheIndex = actions.length;
actions.push(action);
actionsForClip.actionByRoot[rootUuid] = action;
}
_removeInactiveAction(action) {
const actions = this._actions,
lastInactiveAction = actions[actions.length - 1],
cacheIndex = action._cacheIndex;
lastInactiveAction._cacheIndex = cacheIndex;
actions[cacheIndex] = lastInactiveAction;
actions.pop();
action._cacheIndex = null;
const clipUuid = action._clip.uuid,
actionsByClip = this._actionsByClip,
actionsForClip = actionsByClip[clipUuid],
knownActionsForClip = actionsForClip.knownActions,
lastKnownAction = knownActionsForClip[knownActionsForClip.length - 1],
byClipCacheIndex = action._byClipCacheIndex;
lastKnownAction._byClipCacheIndex = byClipCacheIndex;
knownActionsForClip[byClipCacheIndex] = lastKnownAction;
knownActionsForClip.pop();
action._byClipCacheIndex = null;
const actionByRoot = actionsForClip.actionByRoot,
rootUuid = (action._localRoot || this._root).uuid;
delete actionByRoot[rootUuid];
if (knownActionsForClip.length === 0) {
delete actionsByClip[clipUuid];
}
this._removeInactiveBindingsForAction(action);
}
_removeInactiveBindingsForAction(action) {
const bindings = action._propertyBindings;
for (let i = 0, n = bindings.length; i !== n; ++i) {
const binding = bindings[i];
if (--binding.referenceCount === 0) {
this._removeInactiveBinding(binding);
}
}
}
_lendAction(action) {
// [ active actions | inactive actions ]
// [ active actions >| inactive actions ]
// s a
// <-swap->
// a s
const actions = this._actions,
prevIndex = action._cacheIndex,
lastActiveIndex = this._nActiveActions++,
firstInactiveAction = actions[lastActiveIndex];
action._cacheIndex = lastActiveIndex;
actions[lastActiveIndex] = action;
firstInactiveAction._cacheIndex = prevIndex;
actions[prevIndex] = firstInactiveAction;
}
_takeBackAction(action) {
// [ active actions | inactive actions ]
// [ active actions |< inactive actions ]
// a s
// <-swap->
// s a
const actions = this._actions,
prevIndex = action._cacheIndex,
firstInactiveIndex = --this._nActiveActions,
lastActiveAction = actions[firstInactiveIndex];
action._cacheIndex = firstInactiveIndex;
actions[firstInactiveIndex] = action;
lastActiveAction._cacheIndex = prevIndex;
actions[prevIndex] = lastActiveAction;
}
// Memory management for PropertyMixer objects
_addInactiveBinding(binding, rootUuid, trackName) {
const bindingsByRoot = this._bindingsByRootAndName,
bindings = this._bindings;
let bindingByName = bindingsByRoot[rootUuid];
if (bindingByName === undefined) {
bindingByName = {};
bindingsByRoot[rootUuid] = bindingByName;
}
bindingByName[trackName] = binding;
binding._cacheIndex = bindings.length;
bindings.push(binding);
}
_removeInactiveBinding(binding) {
const bindings = this._bindings,
propBinding = binding.binding,
rootUuid = propBinding.rootNode.uuid,
trackName = propBinding.path,
bindingsByRoot = this._bindingsByRootAndName,
bindingByName = bindingsByRoot[rootUuid],
lastInactiveBinding = bindings[bindings.length - 1],
cacheIndex = binding._cacheIndex;
lastInactiveBinding._cacheIndex = cacheIndex;
bindings[cacheIndex] = lastInactiveBinding;
bindings.pop();
delete bindingByName[trackName];
if (Object.keys(bindingByName).length === 0) {
delete bindingsByRoot[rootUuid];
}
}
_lendBinding(binding) {
const bindings = this._bindings,
prevIndex = binding._cacheIndex,
lastActiveIndex = this._nActiveBindings++,
firstInactiveBinding = bindings[lastActiveIndex];
binding._cacheIndex = lastActiveIndex;
bindings[lastActiveIndex] = binding;
firstInactiveBinding._cacheIndex = prevIndex;
bindings[prevIndex] = firstInactiveBinding;
}
_takeBackBinding(binding) {
const bindings = this._bindings,
prevIndex = binding._cacheIndex,
firstInactiveIndex = --this._nActiveBindings,
lastActiveBinding = bindings[firstInactiveIndex];
binding._cacheIndex = firstInactiveIndex;
bindings[firstInactiveIndex] = binding;
lastActiveBinding._cacheIndex = prevIndex;
bindings[prevIndex] = lastActiveBinding;
}
// Memory management of Interpolants for weight and time scale
_lendControlInterpolant() {
const interpolants = this._controlInterpolants,
lastActiveIndex = this._nActiveControlInterpolants++;
let interpolant = interpolants[lastActiveIndex];
if (interpolant === undefined) {
interpolant = new LinearInterpolant(new Float32Array(2), new Float32Array(2), 1, this._controlInterpolantsResultBuffer);
interpolant.__cacheIndex = lastActiveIndex;
interpolants[lastActiveIndex] = interpolant;
}
return interpolant;
}
_takeBackControlInterpolant(interpolant) {
const interpolants = this._controlInterpolants,
prevIndex = interpolant.__cacheIndex,
firstInactiveIndex = --this._nActiveControlInterpolants,
lastActiveInterpolant = interpolants[firstInactiveIndex];
interpolant.__cacheIndex = firstInactiveIndex;
interpolants[firstInactiveIndex] = interpolant;
lastActiveInterpolant.__cacheIndex = prevIndex;
interpolants[prevIndex] = lastActiveInterpolant;
}
// return an action for a clip optionally using a custom root target
// object (this method allocates a lot of dynamic memory in case a
// previously unknown clip/root combination is specified)
clipAction(clip, optionalRoot, blendMode) {
const root = optionalRoot || this._root,
rootUuid = root.uuid;
let clipObject = typeof clip === 'string' ? AnimationClip.findByName(root, clip) : clip;
const clipUuid = clipObject !== null ? clipObject.uuid : clip;
const actionsForClip = this._actionsByClip[clipUuid];
let prototypeAction = null;
if (blendMode === undefined) {
if (clipObject !== null) {
blendMode = clipObject.blendMode;
} else {
blendMode = NormalAnimationBlendMode;
}
}
if (actionsForClip !== undefined) {
const existingAction = actionsForClip.actionByRoot[rootUuid];
if (existingAction !== undefined && existingAction.blendMode === blendMode) {
return existingAction;
}
// we know the clip, so we don't have to parse all
// the bindings again but can just copy
prototypeAction = actionsForClip.knownActions[0];
// also, take the clip from the prototype action
if (clipObject === null) clipObject = prototypeAction._clip;
}
// clip must be known when specified via string
if (clipObject === null) return null;
// allocate all resources required to run it
const newAction = new AnimationAction(this, clipObject, optionalRoot, blendMode);
this._bindAction(newAction, prototypeAction);
// and make the action known to the memory manager
this._addInactiveAction(newAction, clipUuid, rootUuid);
return newAction;
}
// get an existing action
existingAction(clip, optionalRoot) {
const root = optionalRoot || this._root,
rootUuid = root.uuid,
clipObject = typeof clip === 'string' ? AnimationClip.findByName(root, clip) : clip,
clipUuid = clipObject ? clipObject.uuid : clip,
actionsForClip = this._actionsByClip[clipUuid];
if (actionsForClip !== undefined) {
return actionsForClip.actionByRoot[rootUuid] || null;
}
return null;
}
// deactivates all previously scheduled actions
stopAllAction() {
const actions = this._actions,
nActions = this._nActiveActions;
for (let i = nActions - 1; i >= 0; --i) {
actions[i].stop();
}
return this;
}
// advance the time and update apply the animation
update(deltaTime) {
deltaTime *= this.timeScale;
const actions = this._actions,
nActions = this._nActiveActions,
time = (this.time += deltaTime),
timeDirection = Math.sign(deltaTime),
accuIndex = (this._accuIndex ^= 1);
// run active actions
for (let i = 0; i !== nActions; ++i) {
const action = actions[i];
action._update(time, deltaTime, timeDirection, accuIndex);
}
// update scene graph
const bindings = this._bindings,
nBindings = this._nActiveBindings;
for (let i = 0; i !== nBindings; ++i) {
bindings[i].apply(accuIndex);
}
return this;
}
// Allows you to seek to a specific time in an animation.
setTime(timeInSeconds) {
this.time = 0; // Zero out time attribute for AnimationMixer object;
for (let i = 0; i < this._actions.length; i++) {
this._actions[i].time = 0; // Zero out time attribute for all associated AnimationAction objects.
}
return this.update(timeInSeconds); // Update used to set exact time. Returns "this" AnimationMixer object.
}
// return this mixer's root target object
getRoot() {
return this._root;
}
// free all resources specific to a particular clip
uncacheClip(clip) {
const actions = this._actions,
clipUuid = clip.uuid,
actionsByClip = this._actionsByClip,
actionsForClip = actionsByClip[clipUuid];
if (actionsForClip !== undefined) {
// note: just calling _removeInactiveAction would mess up the
// iteration state and also require updating the state we can
// just throw away
const actionsToRemove = actionsForClip.knownActions;
for (let i = 0, n = actionsToRemove.length; i !== n; ++i) {
const action = actionsToRemove[i];
this._deactivateAction(action);
const cacheIndex = action._cacheIndex,
lastInactiveAction = actions[actions.length - 1];
action._cacheIndex = null;
action._byClipCacheIndex = null;
lastInactiveAction._cacheIndex = cacheIndex;
actions[cacheIndex] = lastInactiveAction;
actions.pop();
this._removeInactiveBindingsForAction(action);
}
delete actionsByClip[clipUuid];
}
}
// free all resources specific to a particular root target object
uncacheRoot(root) {
const rootUuid = root.uuid,
actionsByClip = this._actionsByClip;
for (const clipUuid in actionsByClip) {
const actionByRoot = actionsByClip[clipUuid].actionByRoot,
action = actionByRoot[rootUuid];
if (action !== undefined) {
this._deactivateAction(action);
this._removeInactiveAction(action);
}
}
const bindingsByRoot = this._bindingsByRootAndName,
bindingByName = bindingsByRoot[rootUuid];
if (bindingByName !== undefined) {
for (const trackName in bindingByName) {
const binding = bindingByName[trackName];
binding.restoreOriginalState();
this._removeInactiveBinding(binding);
}
}
}
// remove a targeted clip from the cache
uncacheAction(clip, optionalRoot) {
const action = this.existingAction(clip, optionalRoot);
if (action !== null) {
this._deactivateAction(action);
this._removeInactiveAction(action);
}
}
}
AnimationMixer.prototype._controlInterpolantsResultBuffer = new Float32Array(1);
export { AnimationMixer };