Spaces:
Running
Running
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'; | |
/** | |
* | |
* Player for AnimationClips. | |
* | |
* | |
* @author Ben Houston / http://clara.io/ | |
* @author David Sarno / http://lighthaus.us/ | |
* @author tschw | |
*/ | |
function AnimationMixer( root ) { | |
this._root = root; | |
this._initMemoryManager(); | |
this._accuIndex = 0; | |
this.time = 0; | |
this.timeScale = 1.0; | |
} | |
AnimationMixer.prototype = Object.assign( Object.create( EventDispatcher.prototype ), { | |
constructor: AnimationMixer, | |
_bindAction: function ( action, prototypeAction ) { | |
var root = action._localRoot || this._root, | |
tracks = action._clip.tracks, | |
nTracks = tracks.length, | |
bindings = action._propertyBindings, | |
interpolants = action._interpolants, | |
rootUuid = root.uuid, | |
bindingsByRoot = this._bindingsByRootAndName, | |
bindingsByName = bindingsByRoot[ rootUuid ]; | |
if ( bindingsByName === undefined ) { | |
bindingsByName = {}; | |
bindingsByRoot[ rootUuid ] = bindingsByName; | |
} | |
for ( var i = 0; i !== nTracks; ++ i ) { | |
var track = tracks[ i ], | |
trackName = track.name, | |
binding = bindingsByName[ trackName ]; | |
if ( binding !== undefined ) { | |
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; | |
} | |
var 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: function ( 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 | |
var 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 ); | |
} | |
var bindings = action._propertyBindings; | |
// increment reference counts / sort out state | |
for ( var i = 0, n = bindings.length; i !== n; ++ i ) { | |
var binding = bindings[ i ]; | |
if ( binding.useCount ++ === 0 ) { | |
this._lendBinding( binding ); | |
binding.saveOriginalState(); | |
} | |
} | |
this._lendAction( action ); | |
} | |
}, | |
_deactivateAction: function ( action ) { | |
if ( this._isActiveAction( action ) ) { | |
var bindings = action._propertyBindings; | |
// decrement reference counts / sort out state | |
for ( var i = 0, n = bindings.length; i !== n; ++ i ) { | |
var binding = bindings[ i ]; | |
if ( -- binding.useCount === 0 ) { | |
binding.restoreOriginalState(); | |
this._takeBackBinding( binding ); | |
} | |
} | |
this._takeBackAction( action ); | |
} | |
}, | |
// Memory manager | |
_initMemoryManager: function () { | |
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; | |
var 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: function ( action ) { | |
var index = action._cacheIndex; | |
return index !== null && index < this._nActiveActions; | |
}, | |
_addInactiveAction: function ( action, clipUuid, rootUuid ) { | |
var actions = this._actions, | |
actionsByClip = this._actionsByClip, | |
actionsForClip = actionsByClip[ clipUuid ]; | |
if ( actionsForClip === undefined ) { | |
actionsForClip = { | |
knownActions: [ action ], | |
actionByRoot: {} | |
}; | |
action._byClipCacheIndex = 0; | |
actionsByClip[ clipUuid ] = actionsForClip; | |
} else { | |
var knownActions = actionsForClip.knownActions; | |
action._byClipCacheIndex = knownActions.length; | |
knownActions.push( action ); | |
} | |
action._cacheIndex = actions.length; | |
actions.push( action ); | |
actionsForClip.actionByRoot[ rootUuid ] = action; | |
}, | |
_removeInactiveAction: function ( action ) { | |
var actions = this._actions, | |
lastInactiveAction = actions[ actions.length - 1 ], | |
cacheIndex = action._cacheIndex; | |
lastInactiveAction._cacheIndex = cacheIndex; | |
actions[ cacheIndex ] = lastInactiveAction; | |
actions.pop(); | |
action._cacheIndex = null; | |
var 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; | |
var actionByRoot = actionsForClip.actionByRoot, | |
rootUuid = ( action._localRoot || this._root ).uuid; | |
delete actionByRoot[ rootUuid ]; | |
if ( knownActionsForClip.length === 0 ) { | |
delete actionsByClip[ clipUuid ]; | |
} | |
this._removeInactiveBindingsForAction( action ); | |
}, | |
_removeInactiveBindingsForAction: function ( action ) { | |
var bindings = action._propertyBindings; | |
for ( var i = 0, n = bindings.length; i !== n; ++ i ) { | |
var binding = bindings[ i ]; | |
if ( -- binding.referenceCount === 0 ) { | |
this._removeInactiveBinding( binding ); | |
} | |
} | |
}, | |
_lendAction: function ( action ) { | |
// [ active actions | inactive actions ] | |
// [ active actions >| inactive actions ] | |
// s a | |
// <-swap-> | |
// a s | |
var 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: function ( action ) { | |
// [ active actions | inactive actions ] | |
// [ active actions |< inactive actions ] | |
// a s | |
// <-swap-> | |
// s a | |
var 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: function ( binding, rootUuid, trackName ) { | |
var bindingsByRoot = this._bindingsByRootAndName, | |
bindingByName = bindingsByRoot[ rootUuid ], | |
bindings = this._bindings; | |
if ( bindingByName === undefined ) { | |
bindingByName = {}; | |
bindingsByRoot[ rootUuid ] = bindingByName; | |
} | |
bindingByName[ trackName ] = binding; | |
binding._cacheIndex = bindings.length; | |
bindings.push( binding ); | |
}, | |
_removeInactiveBinding: function ( binding ) { | |
var 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 ]; | |
remove_empty_map: { | |
for ( var _ in bindingByName ) break remove_empty_map; // eslint-disable-line no-unused-vars | |
delete bindingsByRoot[ rootUuid ]; | |
} | |
}, | |
_lendBinding: function ( binding ) { | |
var 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: function ( binding ) { | |
var 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: function () { | |
var interpolants = this._controlInterpolants, | |
lastActiveIndex = this._nActiveControlInterpolants ++, | |
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: function ( interpolant ) { | |
var interpolants = this._controlInterpolants, | |
prevIndex = interpolant.__cacheIndex, | |
firstInactiveIndex = -- this._nActiveControlInterpolants, | |
lastActiveInterpolant = interpolants[ firstInactiveIndex ]; | |
interpolant.__cacheIndex = firstInactiveIndex; | |
interpolants[ firstInactiveIndex ] = interpolant; | |
lastActiveInterpolant.__cacheIndex = prevIndex; | |
interpolants[ prevIndex ] = lastActiveInterpolant; | |
}, | |
_controlInterpolantsResultBuffer: new Float32Array( 1 ), | |
// 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: function ( clip, optionalRoot ) { | |
var root = optionalRoot || this._root, | |
rootUuid = root.uuid, | |
clipObject = typeof clip === 'string' ? | |
AnimationClip.findByName( root, clip ) : clip, | |
clipUuid = clipObject !== null ? clipObject.uuid : clip, | |
actionsForClip = this._actionsByClip[ clipUuid ], | |
prototypeAction = null; | |
if ( actionsForClip !== undefined ) { | |
var existingAction = | |
actionsForClip.actionByRoot[ rootUuid ]; | |
if ( existingAction !== undefined ) { | |
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 | |
var newAction = new AnimationAction( this, clipObject, optionalRoot ); | |
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: function ( clip, optionalRoot ) { | |
var 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: function () { | |
var actions = this._actions, | |
nActions = this._nActiveActions, | |
bindings = this._bindings, | |
nBindings = this._nActiveBindings; | |
this._nActiveActions = 0; | |
this._nActiveBindings = 0; | |
for ( var i = 0; i !== nActions; ++ i ) { | |
actions[ i ].reset(); | |
} | |
for ( var i = 0; i !== nBindings; ++ i ) { | |
bindings[ i ].useCount = 0; | |
} | |
return this; | |
}, | |
// advance the time and update apply the animation | |
update: function ( deltaTime ) { | |
deltaTime *= this.timeScale; | |
var actions = this._actions, | |
nActions = this._nActiveActions, | |
time = this.time += deltaTime, | |
timeDirection = Math.sign( deltaTime ), | |
accuIndex = this._accuIndex ^= 1; | |
// run active actions | |
for ( var i = 0; i !== nActions; ++ i ) { | |
var action = actions[ i ]; | |
action._update( time, deltaTime, timeDirection, accuIndex ); | |
} | |
// update scene graph | |
var bindings = this._bindings, | |
nBindings = this._nActiveBindings; | |
for ( var i = 0; i !== nBindings; ++ i ) { | |
bindings[ i ].apply( accuIndex ); | |
} | |
return this; | |
}, | |
// return this mixer's root target object | |
getRoot: function () { | |
return this._root; | |
}, | |
// free all resources specific to a particular clip | |
uncacheClip: function ( clip ) { | |
var 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 | |
var actionsToRemove = actionsForClip.knownActions; | |
for ( var i = 0, n = actionsToRemove.length; i !== n; ++ i ) { | |
var action = actionsToRemove[ i ]; | |
this._deactivateAction( action ); | |
var 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: function ( root ) { | |
var rootUuid = root.uuid, | |
actionsByClip = this._actionsByClip; | |
for ( var clipUuid in actionsByClip ) { | |
var actionByRoot = actionsByClip[ clipUuid ].actionByRoot, | |
action = actionByRoot[ rootUuid ]; | |
if ( action !== undefined ) { | |
this._deactivateAction( action ); | |
this._removeInactiveAction( action ); | |
} | |
} | |
var bindingsByRoot = this._bindingsByRootAndName, | |
bindingByName = bindingsByRoot[ rootUuid ]; | |
if ( bindingByName !== undefined ) { | |
for ( var trackName in bindingByName ) { | |
var binding = bindingByName[ trackName ]; | |
binding.restoreOriginalState(); | |
this._removeInactiveBinding( binding ); | |
} | |
} | |
}, | |
// remove a targeted clip from the cache | |
uncacheAction: function ( clip, optionalRoot ) { | |
var action = this.existingAction( clip, optionalRoot ); | |
if ( action !== null ) { | |
this._deactivateAction( action ); | |
this._removeInactiveAction( action ); | |
} | |
} | |
} ); | |
export { AnimationMixer }; | |