Browse Source

allow for PropertyBindings to bind/unbind. PropertyBindings are reference counted by Mixer for efficiency.

Ben Houston 10 years ago
parent
commit
8590ad568f
2 changed files with 226 additions and 144 deletions
  1. 44 2
      src/animation/AnimationMixer.js
  2. 182 142
      src/animation/PropertyBinding.js

+ 44 - 2
src/animation/AnimationMixer.js

@@ -34,21 +34,35 @@ THREE.AnimationMixer.prototype = {
 
 			var track = tracks[ i ];
 
+			var propertyBinding = this.propertyBindings[ track.name ];
+
 			if( ! this.propertyBindings[ track.name ] ) {
 			
-				var propertyBinding = new THREE.PropertyBinding( this.root, track.name );
+				propertyBinding = new THREE.PropertyBinding( this.root, track.name );
 				this.propertyBindings[ track.name ] = propertyBinding;
-
 				this.propertyBindingsArray.push( propertyBinding );
 			
 			}
+
+			// track usages of shared property bindings, because if we leave too many around, the mixer can get slow
+			propertyBinding.referenceCount += 1;
+
 		}
 
 	},
 
 	removeAllActions: function() {
 
+		// unbind all property bindings
+		for( var i = 0; i < this.propertyBindingsArray.length; i ++ ) {
+
+			this.propertyBindingsArray[i].unbind();
+
+		}
+
 		this.actions = [];
+		this.propertyBindings = {};
+		this.propertyBindingsArray = [];
 
 	},
 
@@ -62,6 +76,27 @@ THREE.AnimationMixer.prototype = {
 			this.actions.splice( index, 1 );
 
 		}
+
+		// remove unused property bindings because if we leave them around the mixer can get slow
+		var tracks = action.clip.tracks;
+
+		for( var i = 0; i < tracks.length; i ++ ) {
+		
+			var track = tracks[ i ];
+			var propertyBinding = this.propertyBindings[ track.name ];
+
+			propertyBinding.referenceCount -= 1;
+
+			if( propertyBinding.referenceCount <= 0 ) {
+
+				propertyBinding.unbind();
+
+				delete this.propertyBindings[ track.name ];
+				this.propertyBindingArray.splice( this.propertyBindingArray.indexOf( propertyBinding ), 1 );
+
+			}
+		}
+
 	},
 
 	fadeOut: function( action, duration ) {
@@ -85,6 +120,13 @@ THREE.AnimationMixer.prototype = {
 
 	},
 
+	crossFade: function( fadeOutAction, faceInAction, duration ) {
+
+		this.fadeOut( fadeOutAction, duration );
+		this.fadeIn( fadeInAction, duration );
+		
+	},
+
 	update: function( deltaTime ) {
 
 		this.time += deltaTime * this.timeScale;

+ 182 - 142
src/animation/PropertyBinding.js

@@ -10,6 +10,8 @@ THREE.PropertyBinding = function ( rootNode, trackName ) {
 
 	this.rootNode = rootNode;
 	this.trackName = trackName;
+	this.referenceCount = 0;
+	this.originalValue = null; // the value of the property before it was controlled by this binding
 
 	var parseResults = THREE.PropertyBinding.parseTrackName( trackName );
 
@@ -40,185 +42,229 @@ THREE.PropertyBinding.prototype = {
 
 	accumulate: function( value, weight ) {
 		
-		var lerp = THREE.AnimationUtils.getLerpFunc( value, true );
+		if( this.cumulativeWeight === 0 ) {
 
-		this.accumulate = function( value, weight ) {
-
-			if( this.cumulativeWeight === 0 ) {
-
-				if( this.cumulativeValue === null ) {
-					this.cumulativeValue = THREE.AnimationUtils.clone( value );
-				}
-				this.cumulativeWeight = weight;
-				//console.log( this );
-	
+			if( this.cumulativeValue === null ) {
+				this.cumulativeValue = THREE.AnimationUtils.clone( value );
 			}
-			else {
+			this.cumulativeWeight = weight;
+			//console.log( this );
 
-				var lerpAlpha = weight / ( this.cumulativeWeight + weight );
-				this.cumulativeValue = lerp( this.cumulativeValue, value, lerpAlpha );
-				this.cumulativeWeight += weight;
-				//console.log( this );
+		}
+		else {
+
+			var lerpAlpha = weight / ( this.cumulativeWeight + weight );
+			this.cumulativeValue = this.lerp( this.cumulativeValue, value, lerpAlpha );
+			this.cumulativeWeight += weight;
+			//console.log( this );
 
-			}
 		}
 
-		this.accumulate( value, weight );
+	},
+
+	unbind: function() {
 
+		if( ! this.setValue ) throw new Error( "can not unbind if not bound in the first place." );
+
+		this.setValue( this.originalValue );
 	},
 
-	apply: function() {
+	// creates the member functions:
+	//	- setValue( value )
+	//  - getValue()
+	//  - triggerDirty()
 
-		// for speed capture the setter pattern as a closure (sort of a memoization pattern: https://en.wikipedia.org/wiki/Memoization)
-		if( ! this.internalApply ) {
+	bind: function() {
 
-			 //console.log( "PropertyBinding", this );
+		if( this.setValue ) throw new Error( "can not bind if already bound." );
+		
+		//console.log( "PropertyBinding", this );
 
-			 var equalsFunc = THREE.AnimationUtils.getEqualsFunc( this.cumulativeValue );
+		var equalsFunc = THREE.AnimationUtils.getEqualsFunc( this.cumulativeValue );
 
-			 var targetObject = this.node;
+		var targetObject = this.node;
 
-	 		// ensure there is a value node
-			if( ! targetObject ) {
-				console.error( "  trying to update node for track: " + this.trackName + " but it wasn't found." );
-				return;
-			}
+ 		// ensure there is a value node
+		if( ! targetObject ) {
+			console.error( "  trying to update node for track: " + this.trackName + " but it wasn't found." );
+			return;
+		}
 
-			if( this.objectName ) {
-				// special case were we need to reach deeper into the hierarchy to get the face materials....
-				if( this.objectName === "materials" ) {
-					if( ! targetObject.material ) {
-						console.error( '  can not bind to material as node does not have a material', this );
-						return;				
-					}
-					if( ! targetObject.material.materials ) {
-						console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
-						return;				
-					}
-					targetObject = targetObject.material.materials;
+		if( this.objectName ) {
+			// special case were we need to reach deeper into the hierarchy to get the face materials....
+			if( this.objectName === "materials" ) {
+				if( ! targetObject.material ) {
+					console.error( '  can not bind to material as node does not have a material', this );
+					return;				
 				}
-				else if( this.objectName === "bones" ) {
-					if( ! targetObject.skeleton ) {
-						console.error( '  can not bind to bones as node does not have a skeleton', this );
-					}
-					targetObject = targetObject.skeleton.bones;
-
-					// TODO/OPTIMIZE, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
-					
-					// support resolving morphTarget names into indices.
-					//console.log( "  resolving bone name: ", this.objectIndex );
-					for( var i = 0; i < this.node.skeleton.bones.length; i ++ ) {
-						if( this.node.skeleton.bones[i].name === this.objectIndex ) {
-							//console.log( "  resolved to index: ", i );
-							this.objectIndex = i;
-							break;
-						}
-					}
+				if( ! targetObject.material.materials ) {
+					console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
+					return;				
 				}
-				else {
-
-					if( targetObject[ this.objectName ] === undefined ) {
-						console.error( '  can not bind to objectName of node, undefined', this );			
-						return;
-					}
-					targetObject = targetObject[ this.objectName ];
+				targetObject = targetObject.material.materials;
+			}
+			else if( this.objectName === "bones" ) {
+				if( ! targetObject.skeleton ) {
+					console.error( '  can not bind to bones as node does not have a skeleton', this );
 				}
+				targetObject = targetObject.skeleton.bones;
+
+				// TODO/OPTIMIZE, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
 				
-				if( this.objectIndex !== undefined ) {
-					if( targetObject[ this.objectIndex ] === undefined ) {
-						console.error( "  trying to bind to objectIndex of objectName, but is undefined:", this, targetObject );
-						return;				
+				// support resolving morphTarget names into indices.
+				//console.log( "  resolving bone name: ", this.objectIndex );
+				for( var i = 0; i < this.node.skeleton.bones.length; i ++ ) {
+					if( this.node.skeleton.bones[i].name === this.objectIndex ) {
+						//console.log( "  resolved to index: ", i );
+						this.objectIndex = i;
+						break;
 					}
-
-					targetObject = targetObject[ this.objectIndex ];
 				}
+			}
+			else {
 
+				if( targetObject[ this.objectName ] === undefined ) {
+					console.error( '  can not bind to objectName of node, undefined', this );			
+					return;
+				}
+				targetObject = targetObject[ this.objectName ];
 			}
+			
+			if( this.objectIndex !== undefined ) {
+				if( targetObject[ this.objectIndex ] === undefined ) {
+					console.error( "  trying to bind to objectIndex of objectName, but is undefined:", this, targetObject );
+					return;				
+				}
 
-	 		// special case mappings
-	 		var nodeProperty = targetObject[ this.propertyName ];
-			if( ! nodeProperty ) {
-				console.error( "  trying to update property for track: " + this.nodeName + '.' + this.propertyName + " but it wasn't found.", targetObject );				
-				return;
+				targetObject = targetObject[ this.objectIndex ];
 			}
 
-			// access a sub element of the property array (only primitives are supported right now)
-			if( this.propertyIndex !== undefined ) {
+		}
 
-				if( this.propertyName === "morphTargetInfluences" ) {
-					// TODO/OPTIMIZE, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
-					
-					// support resolving morphTarget names into indices.
-					//console.log( "  resolving morphTargetInfluence name: ", this.propertyIndex );
-					if( ! this.node.geometry ) {
-						console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry', this );				
-					}
-					if( ! this.node.geometry.morphTargets ) {
-						console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry.morphTargets', this );				
-					}
-					
-					for( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
-						if( this.node.geometry.morphTargets[i].name === this.propertyIndex ) {
-							//console.log( "  resolved to index: ", i );
-							this.propertyIndex = i;
-							break;
-						}
-					}
-				}
+ 		// special case mappings
+ 		var nodeProperty = targetObject[ this.propertyName ];
+		if( ! nodeProperty ) {
+			console.error( "  trying to update property for track: " + this.nodeName + '.' + this.propertyName + " but it wasn't found.", targetObject );				
+			return;
+		}
 
-				//console.log( '  update property array ' + this.propertyName + '[' + this.propertyIndex + '] via assignment.' );				
-				this.internalApply = function() {
-					if( ! equalsFunc( nodeProperty[ this.propertyIndex ], this.cumulativeValue ) ) {
-						nodeProperty[ this.propertyIndex ] = this.cumulativeValue;
-						return true;
-					}
-					return false;
-				};
-			}
-			// must use copy for Object3D.Euler/Quaternion		
-			else if( nodeProperty.copy ) {
-				//console.log( '  update property ' + this.name + '.' + this.propertyName + ' via a set() function.' );				
-				this.internalApply = function() {
-					if( ! equalsFunc( nodeProperty, this.cumulativeValue ) ) {
-						nodeProperty.copy( this.cumulativeValue );
-						return true;
+		// access a sub element of the property array (only primitives are supported right now)
+		if( this.propertyIndex !== undefined ) {
+
+			if( this.propertyName === "morphTargetInfluences" ) {
+				// TODO/OPTIMIZE, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
+				
+				// support resolving morphTarget names into indices.
+				//console.log( "  resolving morphTargetInfluence name: ", this.propertyIndex );
+				if( ! this.node.geometry ) {
+					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry', this );				
+				}
+				if( ! this.node.geometry.morphTargets ) {
+					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry.morphTargets', this );				
+				}
+				
+				for( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
+					if( this.node.geometry.morphTargets[i].name === this.propertyIndex ) {
+						//console.log( "  resolved to index: ", i );
+						this.propertyIndex = i;
+						break;
 					}
-					return false;
 				}
 			}
-			// otherwise just set the property directly on the node (do not use nodeProperty as it may not be a reference object)
-			else {
 
-				//console.log( '  update property ' + this.name + '.' + this.propertyName + ' via assignment.' );				
-				this.internalApply = function() {
-					if( ! equalsFunc( targetObject[ this.propertyName ], this.cumulativeValue ) ) {
-						targetObject[ this.propertyName ] = this.cumulativeValue;	
-						return true;
-					}
-					return false;
+			//console.log( '  update property array ' + this.propertyName + '[' + this.propertyIndex + '] via assignment.' );				
+			this.setValue = function( value ) {
+				if( ! equalsFunc( nodeProperty[ this.propertyIndex ], value ) ) {
+					nodeProperty[ this.propertyIndex ] = value;
+					return true;
 				}
-			}
+				return false;
+			};
+
+			this.getValue = function() {
+				return nodeProperty[ this.propertyIndex ];
+			};
 
-			// trigger node dirty			
-			if( targetObject.needsUpdate !== undefined ) { // material
-				//console.log( '  triggering material as dirty' );
-				this.triggerDirty = function() {
-					this.node.needsUpdate = true;
+		}
+		// must use copy for Object3D.Euler/Quaternion		
+		else if( nodeProperty.copy ) {
+			
+			//console.log( '  update property ' + this.name + '.' + this.propertyName + ' via a set() function.' );				
+			this.setValue = function( value ) {
+				if( ! equalsFunc( nodeProperty, value ) ) {
+					nodeProperty.copy( value );
+					return true;
 				}
-			}			
-			else if( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
-				//console.log( '  triggering node as dirty' );
-				this.triggerDirty = function() {
-					targetObject.matrixWorldNeedsUpdate = true;
+				return false;
+			}
+
+			this.getValue = function() {
+				return nodeProperty;
+			};
+
+		}
+		// otherwise just set the property directly on the node (do not use nodeProperty as it may not be a reference object)
+		else {
+
+			//console.log( '  update property ' + this.name + '.' + this.propertyName + ' via assignment.' );				
+			this.setValue = function( value ) {
+				if( ! equalsFunc( targetObject[ this.propertyName ], value ) ) {
+					targetObject[ this.propertyName ] = value;	
+					return true;
 				}
+				return false;
 			}
 
+			this.getValue = function() {
+				return targetObject[ this.propertyName ];
+			};
+
+		}
+
+		// trigger node dirty			
+		if( targetObject.needsUpdate !== undefined ) { // material
+			
+			//console.log( '  triggering material as dirty' );
+			this.triggerDirty = function() {
+				this.node.needsUpdate = true;
+			}
+
+		}			
+		else if( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
+			
+			//console.log( '  triggering node as dirty' );
+			this.triggerDirty = function() {
+				targetObject.matrixWorldNeedsUpdate = true;
+			}
+
+		}
+
+		this.originalValue = this.getValue();
+
+		this.lerp = THREE.AnimationUtils.getLerpFunc( value, true );
+
+	},
+
+	apply: function() {
+
+		// for speed capture the setter pattern as a closure (sort of a memoization pattern: https://en.wikipedia.org/wiki/Memoization)
+		if( ! this.setValue ) {
+			this.bind();
 		}
 
 		// early exit if there is nothing to apply.
 		if( this.cumulativeWeight > 0 ) {
 		
-			var valueChanged = this.internalApply();
+			// blend with original value
+			if( this.cumulativeWeight < 1 ) {
+
+				var remainingWeight = 1 - this.cumulativeWeight;
+				var lerpAlpha = remainingWeight / ( this.cumulativeWeight + remainingWeight );
+				this.cumulativeValue = this.lerp( this.cumulativeValue, this.originalValue, lerpAlpha );
+
+			}
+
+			var valueChanged = this.setValue( this.cumulativeValue );
 
 			if( valueChanged && this.triggerDirty ) {
 				this.triggerDirty();
@@ -229,12 +275,6 @@ THREE.PropertyBinding.prototype = {
 			this.cumulativeWeight = 0;
 
 		}
-	},
-
-	get: function() {
-
-		throw new Error( "TODO" );
-
 	}
 
 };