Browse Source

Animation: Interpolants & extensibility overhaul.

tschw 9 years ago
parent
commit
2e5d9ef6b3

+ 5 - 7
examples/js/BlendCharacterGui.js

@@ -41,14 +41,12 @@ function BlendCharacterGui( blendMesh ) {
 	this.update = function( time ) {
 
 		var getWeight = function( actionName ) {
-			for( var i = 0; i < blendMesh.mixer.actions.length; i ++ ) {
-				var action = blendMesh.mixer.actions[i];
-				if( action.clip.name === actionName ) {
-					return action.getWeightAt( time );	
-				}
-			}
-			return 0;
+
+			var action = blendMesh.mixer.findActionByName( actionName );
+			return ( action !== null) ? action.getWeightAt( time ) : 0;
+
 		}
+
 		controls[ 'idle' ] = getWeight( 'idle' );
 		controls[ 'walk' ] = getWeight( 'walk' );
 		controls[ 'run' ] = getWeight( 'run' );

+ 9 - 6
examples/webgl_animation_skinning_blending.html

@@ -156,13 +156,16 @@
 				var data = event.detail;
 				for ( var i = 0; i < data.anims.length; ++i ) {
 
-					for( var j = 0; j < blendMesh.mixer.actions.length; j ++ ) {
-						var action = blendMesh.mixer.actions[j];
-						if( action.clip.name === data.anims[i] ) {
-							if( action.getWeightAt( blendMesh.mixer.time ) !== data.weights[i] ) {
-								action.weight = data.weights[i];
-							}
+					var action = blendMesh.mixer.findActionByName( data.anims[i] );
+
+					if ( action !== null ) {
+
+						if( action.getWeightAt( blendMesh.mixer.time ) !== data.weights[i] ) {
+
+							action.weight = data.weights[i];
+
 						}
+
 					}
 
 				}

+ 71 - 9
examples/webgl_animation_skinning_morph.html

@@ -61,7 +61,7 @@
 
 			var mesh, helper;
 
-			var mixer;
+			var mixer, facesAction, bonesAction;
 
 			var mouseX = 0, mouseY = 0;
 
@@ -152,11 +152,11 @@
 
 					createScene( geometry, materials, 0, FLOOR, -300, 60 )
 
-				} );
+					// GUI
 
-				// GUI
+					initGUI();
 
-				initGUI();
+				} );
 
 				//
 
@@ -224,20 +224,26 @@
 				helper.visible = false;
 				scene.add( helper );
 
+				mixer = new THREE.AnimationMixer( mesh );
 
-				var clipMorpher = THREE.AnimationClip.CreateFromMorphTargetSequence( 'facialExpressions', mesh.geometry.morphTargets, 3 );
 				var clipBones = geometry.animations[0];
+				bonesAction = new THREE.AnimationAction( clipBones );
 
-				mixer = new THREE.AnimationMixer( mesh );
-				mixer.addAction( new THREE.AnimationAction( clipMorpher ) );
-				mixer.addAction( new THREE.AnimationAction( clipBones ) );
+				var clipMorpher = THREE.AnimationClip.CreateFromMorphTargetSequence( 'facialExpressions', mesh.geometry.morphTargets, 3 );
+				facesAction = new THREE.AnimationAction( clipMorpher );
 			}
 
 			function initGUI() {
 
 				var API = {
 					'show model'    : true,
-					'show skeleton' : false
+					'show skeleton' : false,
+					'bones action' : true, // use false to see initial allocation
+					'bones enable' : true,
+					'faces action' : true,
+					'faces enable' : true,
+					'release props' : function() { mixer.releaseCachedBindings( true ); },
+					'purge cache' : function() { mixer.releaseCachedBindings(); }
 				};
 
 				var gui = new dat.GUI();
@@ -246,6 +252,62 @@
 
 				gui.add( API, 'show skeleton' ).onChange( function() { helper.visible = API[ 'show skeleton' ]; } );
 
+
+				// Note: .add/removeAction and .enabled = true / false have
+				// different performance characteristics:
+				//
+				// The former changes dynamic data structures in the mixer,
+				// therefore the switch is more expensive but removes the
+				// per-action  base cost caused by the unique property
+				// bindings it uses.
+				//
+				// The latter is a zero-cost switch, but the per-frame base
+				// cost for having the action added to the mixer remains.
+
+				function actionControls( key, action ) {
+
+					var guiNameAddRemove = key + ' action';
+					var guiNameEnabled = key + ' enable';
+
+					// set initial state
+
+					if ( API[ guiNameAddRemove ] ) {
+
+						action.enabled = API[ guiNameEnabled ];
+						mixer.addAction( action );
+
+					}
+
+					// attach controls
+
+					gui.add( API, guiNameAddRemove ).onChange( function() {
+
+						if ( API[ guiNameAddRemove ] ) {
+
+							mixer.addAction( action );
+
+						} else {
+
+							mixer.removeAction( action );
+
+						}
+
+					} );
+
+					gui.add( API, guiNameEnabled ).onChange( function() {
+
+						action.enabled = API[ guiNameEnabled ];
+
+					} );
+
+				}
+
+				actionControls( 'bones', bonesAction );
+				actionControls( 'faces', facesAction );
+
+				gui.add( API, 'release props' );
+				gui.add( API, 'purge cache' );
+
 			}
 
 			function onDocumentMouseMove( event ) {

+ 6 - 0
src/Three.js

@@ -357,6 +357,12 @@ THREE.LoopOnce = 2200;
 THREE.LoopRepeat = 2201;
 THREE.LoopPingPong = 2202;
 
+// Interpolation
+
+THREE.InterpolateDiscrete = 2300;
+THREE.InterpolateLinear = 2301;
+THREE.InterpolateSmooth = 2302;
+
 // DEPRECATED
 
 THREE.Projector = function () {

+ 53 - 29
src/animation/AnimationAction.js

@@ -1,7 +1,11 @@
 /**
  *
- * A clip that has been explicitly scheduled.
- * 
+ * Runnable instance of an AnimationClip.
+ *
+ * Multiple Actions are required to add the same clip with the (same or
+ * different) mixer(s) simultaneously.
+ *
+ *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  */
@@ -9,6 +13,7 @@
 THREE.AnimationAction = function ( clip, startTime, timeScale, weight, loop ) {
 
 	if( clip === undefined ) throw new Error( 'clip is null' );
+	this.name = '';
 	this.clip = clip;
 	this.localRoot = null;
 	this.startTime = startTime || 0;
@@ -21,7 +26,24 @@ THREE.AnimationAction = function ( clip, startTime, timeScale, weight, loop ) {
 	this.actionTime = - this.startTime;
 	this.clipTime = 0;
 
-	this.propertyBindings = [];
+	this.mixer = null;
+
+	var tracks = clip.tracks,
+		nTracks = tracks.length,
+		interpolants = new Array( nTracks );
+
+	for ( var i = 0; i !== nTracks; ++ i ) {
+
+		interpolants[ i ] = tracks[ i ].createInterpolant( null );
+
+	}
+
+	this._interpolants = interpolants;
+	this._propertyBindings = new Array( nTracks );
+
+	this._prevRootUuid = '';
+	this._prevMixerUuid = '';
+
 };
 
 /*
@@ -34,12 +56,18 @@ THREE.AnimationAction.prototype = {
 
 	constructor: THREE.AnimationAction,
 
+	getName: function() {
+
+		return this.name || this.clip.name;
+
+	},
+
 	setLocalRoot: function( localRoot ) {
 
 		this.localRoot = localRoot;
 
 		return this;
-		
+
 	},
 
 	updateTime: function( clipDeltaTime ) {
@@ -49,14 +77,14 @@ THREE.AnimationAction.prototype = {
    		var previousActionTime = this.actionTime;
 
 		var duration = this.clip.duration;
-	
+
 		this.actionTime = this.actionTime + clipDeltaTime;
-	
+
 		if( this.loop === THREE.LoopOnce ) {
 
 			this.loopCount = 0;
 			this.clipTime = Math.min( Math.max( this.actionTime, 0 ), duration );
-	
+
 			// if time is changed since last time, see if we have hit a start/end limit
 			if( this.clipTime !== previousClipTime ) {
 
@@ -73,16 +101,16 @@ THREE.AnimationAction.prototype = {
 
 			}
 
-		
+
 			return this.clipTime;
 
 		}
-		
+
 		this.loopCount = Math.floor( this.actionTime / duration );
-	
+
 		var newClipTime = this.actionTime - this.loopCount * duration;
 		newClipTime = newClipTime % duration;
-	
+
 		// if we are ping pong looping, ensure that we go backwards when appropriate
 		if( this.loop == THREE.LoopPingPong ) {
 
@@ -101,7 +129,7 @@ THREE.AnimationAction.prototype = {
    			this.mixer.dispatchEvent( { type: 'loop', action: this, loopDelta: ( this.loopCount - this.loopCount ) } );
 
    		}
-	
+
 	   	return this.clipTime;
 
 	},
@@ -129,37 +157,33 @@ THREE.AnimationAction.prototype = {
 
 	},
 
-	update: function( clipDeltaTime ) {
-
-		this.updateTime( clipDeltaTime );
+	// pass in time, not clip time, allows for fadein/fadeout across multiple loops of the clip
+	getTimeScaleAt: function( time ) {
 
-		var clipResults = this.clip.getAt( this.clipTime );
+		var timeScale = this.timeScale;
 
-		return clipResults;
-		
-	},
+		if( timeScale.getAt !== undefined ) {
 
-	getTimeScaleAt: function( time ) {
-
-		if( this.timeScale.getAt ) {
-			// pass in time, not clip time, allows for fadein/fadeout across multiple loops of the clip
-			return this.timeScale.getAt( time );
+			return timeScale.getAt( time )[ 0 ];
 
 		}
 
-		return this.timeScale;
+		return timeScale;
 
 	},
 
+	// pass in time, not clip time, allows for fadein/fadeout across multiple loops of the clip
 	getWeightAt: function( time ) {
 
-		if( this.weight.getAt ) {
-			// pass in time, not clip time, allows for fadein/fadeout across multiple loops of the clip
-			return this.weight.getAt( time );
+		var weight = this.weight;
+
+		if( weight.getAt !== undefined ) {
+
+			return weight.getAt( time )[ 0 ];
 
 		}
 
-		return this.weight;
+		return weight;
 
 	}
 

+ 75 - 90
src/animation/AnimationClip.js

@@ -16,7 +16,7 @@ THREE.AnimationClip = function ( name, duration, tracks ) {
 	if( this.duration < 0 ) {
 		for( var i = 0; i < this.tracks.length; i ++ ) {
 			var track = this.tracks[i];
-			this.duration = Math.max( track.keys[ track.keys.length - 1 ].time );
+			this.duration = Math.max( track.times[ track.times.length - 1 ] );
 		}
 	}
 
@@ -25,29 +25,12 @@ THREE.AnimationClip = function ( name, duration, tracks ) {
 	this.trim();
 	this.optimize();
 
-	this.results = [];
-
 };
 
 THREE.AnimationClip.prototype = {
 
 	constructor: THREE.AnimationClip,
 
-	getAt: function( clipTime ) {
-
-		clipTime = Math.max( 0, Math.min( clipTime, this.duration ) );
-
-		for( var i = 0; i < this.tracks.length; i ++ ) {
-
-			var track = this.tracks[ i ];
-
-			this.results[ i ] = track.getAt( clipTime );
-
-		}
-
-		return this.results;
-	},
-
 	trim: function() {
 
 		for( var i = 0; i < this.tracks.length; i ++ ) {
@@ -74,32 +57,78 @@ THREE.AnimationClip.prototype = {
 
 };
 
+// parse the standard JSON format for clips
+THREE.AnimationClip.parse = function( json ) {
+
+	var tracks = [],
+		jsonTracks = json[ 'tracks' ],
+		frameTime = 1.0 / ( json[ 'fps' ] || 1.0 );
 
-THREE.AnimationClip.CreateFromMorphTargetSequence = function( name, morphTargetSequence, fps ) {
+	for( var i = 0, n = jsonTracks.length; i !== n; ++ i ) {
+
+		tracks.push( THREE.KeyframeTrack.parse( jsonTracks[ i ] ).scale( frameTime ) );
+
+	}
+
+	return new THREE.AnimationClip( json[ 'name' ], json[ 'duration' ], tracks );
+
+};
+
+
+THREE.AnimationClip.toJSON = function( clip ) {
+
+	var tracks = [],
+		clipTracks = clip.tracks;
+
+	var json = {
+
+		'name': clip.name,
+		'duration': clip.duration,
+		'tracks': tracks
+
+	};
+
+	for ( var i = 0, n = clipTracks.length; i !== n; ++ i ) {
+
+		tracks.push( THREE.KeyframeTrack.toJSON( clipTracks[ i ] ) );
+
+	}
+
+	return json;
+
+}
 
 
+THREE.AnimationClip.CreateFromMorphTargetSequence = function( name, morphTargetSequence, fps ) {
+
 	var numMorphTargets = morphTargetSequence.length;
 	var tracks = [];
 
 	for( var i = 0; i < numMorphTargets; i ++ ) {
 
-		var keys = [];
+		var times = [];
+		var values = [];
+
+		times.push(
+				( i + numMorphTargets - 1 ) % numMorphTargets,
+				i,
+				( i + 1 ) % numMorphTargets );
 
-		keys.push( { time: ( i + numMorphTargets - 1 ) % numMorphTargets, value: 0 } );
-		keys.push( { time: i, value: 1 } );
-		keys.push( { time: ( i + 1 ) % numMorphTargets, value: 0 } );
+		values.push( 0, 1, 0 );
 
-		keys.sort( THREE.KeyframeTrack.keyComparer );
+		var order = THREE.AnimationUtils.getKeyframeOrder( times );
+		times = THREE.AnimationUtils.sortedArray( times, 1, order );
+		values = THREE.AnimationUtils.sortedArray( values, 1, order );
 
 		// if there is a key at the first frame, duplicate it as the last frame as well for perfect loop.
-		if( keys[0].time === 0 ) {
-			keys.push( {
-				time: numMorphTargets,
-				value: keys[0].value
-			});
+		if( times[ 0 ] === 0 ) {
+
+			times.push( numMorphTargets );
+			values.push( values[ 0 ] );
+
 		}
 
-		tracks.push( new THREE.NumberKeyframeTrack( '.morphTargetInfluences[' + morphTargetSequence[i].name + ']', keys ).scale( 1.0 / fps ) );
+		tracks.push( new THREE.NumberKeyframeTrack( '.morphTargetInfluences[' + morphTargetSequence[ i ].name + ']', times, values ).scale( 1.0 / fps ) );
 	}
 
 	return new THREE.AnimationClip( name, -1, tracks );
@@ -110,9 +139,9 @@ THREE.AnimationClip.findByName = function( clipArray, name ) {
 
 	for( var i = 0; i < clipArray.length; i ++ ) {
 
-		if( clipArray[i].name === name ) {
+		if( clipArray[ i ].name === name ) {
 
-			return clipArray[i];
+			return clipArray[ i ];
 
 		}
 	}
@@ -160,22 +189,6 @@ THREE.AnimationClip.CreateClipsFromMorphTargetSequences = function( morphTargets
 
 };
 
-// parse the standard JSON format for clips
-THREE.AnimationClip.parse = function( json ) {
-
-	var tracks = [];
-
-	for( var i = 0; i < json.tracks.length; i ++ ) {
-
-		tracks.push( THREE.KeyframeTrack.parse( json.tracks[i] ).scale( 1.0 / json.fps ) );
-
-	}
-
-	return new THREE.AnimationClip( json.name, json.duration, tracks );
-
-};
-
-
 // parse the animation.hierarchy format
 THREE.AnimationClip.parseAnimation = function( animation, bones ) {
 
@@ -184,29 +197,17 @@ THREE.AnimationClip.parseAnimation = function( animation, bones ) {
 		return null;
 	}
 
-	var convertTrack = function( trackName, animationKeys, propertyName, trackType, animationKeyToValueFunc ) {
-
-		var keys = [];
-
-		for( var k = 0; k < animationKeys.length; k ++ ) {
-
-			var animationKey = animationKeys[k];
-
-			if( animationKey[propertyName] !== undefined ) {
-
-				keys.push( { time: animationKey.time, value: animationKeyToValueFunc( animationKey ) } );
-			}
-
-		}
+	var convertTrack = function( trackName, animationKeys, propertyName, trackType ) {
 
 		// only return track if there are actually keys.
-		if( keys.length > 0 ) {
+		if ( animationKeys.length === 0 ) return null;
 
-			return new trackType( trackName, keys );
+		var times = [];
+		var values = [];
 
-		}
+		THREE.AnimationUtils.flattenJSON( animationKeys, times, values, propertyName );
 
-		return null;
+		return new trackType( trackName, times, values );
 
 	};
 
@@ -246,16 +247,15 @@ THREE.AnimationClip.parseAnimation = function( animation, bones ) {
 			// create a track for each morph target with all zero morphTargetInfluences except for the keys in which the morphTarget is named.
 			for( var morphTargetName in morphTargetNames ) {
 
-				var keys = [];
+				var times = [];
+				var values = [];
 
 				for( var m = 0; m < animationKeys[k].morphTargets.length; m ++ ) {
 
 					var animationKey = animationKeys[k];
 
-					keys.push( {
-							time: animationKey.time,
-							value: (( animationKey.morphTarget === morphTargetName ) ? 1 : 0 )
-						});
+					times.push( animationKey.time );
+					values.push( ( animationKey.morphTarget === morphTargetName ) ? 1 : 0 )
 
 				}
 
@@ -265,35 +265,20 @@ THREE.AnimationClip.parseAnimation = function( animation, bones ) {
 
 			duration = morphTargetNames.length * ( fps || 1.0 );
 
-		}
-		else {
+		} else {
 
 			var boneName = '.bones[' + bones[ h ].name + ']';
 
 			// track contains positions...
-			var positionTrack = convertTrack( boneName + '.position', animationKeys, 'pos', THREE.VectorKeyframeTrack, function( animationKey ) {
-					return new THREE.Vector3().fromArray( animationKey.pos )
-				} );
-
+			var positionTrack = convertTrack( boneName + '.position', animationKeys, 'pos', THREE.VectorKeyframeTrack );
 			if( positionTrack ) tracks.push( positionTrack );
 
 			// track contains quaternions...
-			var quaternionTrack = convertTrack( boneName + '.quaternion', animationKeys, 'rot', THREE.QuaternionKeyframeTrack, function( animationKey ) {
-					if( animationKey.rot.slerp ) {
-						return animationKey.rot.clone();
-					}
-					else {
-						return new THREE.Quaternion().fromArray( animationKey.rot );
-					}
-				} );
-
+			var quaternionTrack = convertTrack( boneName + '.quaternion', animationKeys, 'rot', THREE.QuaternionKeyframeTrack );
 			if( quaternionTrack ) tracks.push( quaternionTrack );
 
 			// track contains quaternions...
-			var scaleTrack = convertTrack( boneName + '.scale', animationKeys, 'scl', THREE.VectorKeyframeTrack, function( animationKey ) {
-					return new THREE.Vector3().fromArray( animationKey.scl )
-				} );
-
+			var scaleTrack = convertTrack( boneName + '.scale', animationKeys, 'scl', THREE.VectorKeyframeTrack );
 			if( scaleTrack ) tracks.push( scaleTrack );
 
 		}

+ 305 - 92
src/animation/AnimationMixer.js

@@ -1,19 +1,30 @@
 /**
  *
- * Mixes together the AnimationClips scheduled by AnimationActions and applies them to the root and subtree
+ * Sequencer that performs AnimationActions, mixes their results and updates
+ * the scene graph.
  *
  *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
 
 THREE.AnimationMixer = function( root ) {
 
 	this.root = root;
+	this.uuid = THREE.Math.generateUUID();
+
 	this.time = 0;
 	this.timeScale = 1.0;
-	this.actions = [];
-	this.propertyBindingMap = {};
+
+	this._actions = [];
+
+	this._bindingsMaps = {}; // contains a path -> prop_mixer map per root.uuid
+
+	this._bindings = []; // array of all bindings with refCnt != 0
+	this._bindingsDirty = false; // whether we need to rebuild the array
+
+	this._accuIndex = 0;
 
 };
 
@@ -23,57 +34,114 @@ THREE.AnimationMixer.prototype = {
 
 	addAction: function( action ) {
 
-		// TODO: check for duplicate action names?  Or provide each action with a UUID?
+		if ( this._actions.indexOf( action ) !== -1 ) {
 
-		this.actions.push( action );
-		action.init( this.time );
-		action.mixer = this;
+			return; // action is already added - do nothing
+
+		}
+
+		var root = action.localRoot || this.root,
+			rootUuid = root.uuid,
+
+			bindingsMap = this._bindingsMaps[ rootUuid ];
+
+		if ( bindingsMap === undefined ) {
+
+			bindingsMap = {};
+			this._bindingsMaps[ rootUuid ] = bindingsMap;
+
+		}
+
+		var interpolants = action._interpolants,
+			actionBindings = action._propertyBindings,
+
+			tracks = action.clip.tracks,
+			bindingsChanged = false,
+
+			myUuid = this.uuid,
+			prevRootUuid = action._prevRootUuid,
 
-		var tracks = action.clip.tracks;
+			rootSwitchValid =
+				prevRootUuid !== rootUuid &&
+				action._prevMixerUuid === myUuid,
 
-		var root = action.localRoot || this.root;
+			prevRootBindingsMap;
 
-		for( var i = 0; i < tracks.length; i ++ ) {
+		if ( rootSwitchValid ) {
+
+			// in this case we try to transfer currently unused
+			// context infrastructure from the previous root
+
+			prevRootBindingsMap = this._bindingsMaps[ prevRootUuid ];
+
+		}
+
+		for ( var i = 0, n = tracks.length; i !== n; ++ i ) {
 
 			var track = tracks[ i ];
 
-			var propertyBindingKey = root.uuid + '-' + track.name;			
-			var propertyBinding = this.propertyBindingMap[ propertyBindingKey ];
+			var trackName = track.name;
+			var propertyMixer = bindingsMap[ trackName ];
+
+			if ( rootSwitchValid && propertyMixer === undefined ) {
+
+				var candidate = prevRootBindingsMap[ trackName ];
+
+				if ( candidate !== undefined &&
+						candidate.referenceCount === 0 ) {
+
+					propertyMixer = candidate;
+
+					// no longer use with the old root!
+					delete prevRootBindingsMap[ trackName ];
+
+					propertyMixer.binding.setRootNode( root );
+
+					bindingsMap[ trackName ] = propertyMixer;
+
+				}
 
-			if( propertyBinding === undefined ) {
-			
-				propertyBinding = new THREE.PropertyBinding( root, track.name );
-				this.propertyBindingMap[ propertyBindingKey ] = propertyBinding;
-			
 			}
 
-			// push in the same order as the tracks.
-			action.propertyBindings.push( propertyBinding );
-			
-			// track usages of shared property bindings, because if we leave too many around, the mixer can get slow
-			propertyBinding.referenceCount += 1;
+			if ( propertyMixer === undefined ) {
 
-		}
+				propertyMixer = new THREE.PropertyMixer(
+						root, trackName,
+						track.ValueTypeName, track.getValueSize() );
 
-	},
+				bindingsMap[ trackName ] = propertyMixer;
 
-	removeAllActions: function() {
+			}
 
-		for( var i = 0; i < this.actions.length; i ++ ) {
+			if ( propertyMixer.referenceCount === 0 ) {
+
+				propertyMixer.saveOriginalState();
+				bindingsChanged = true;
+
+			}
+
+			++ propertyMixer.referenceCount;
+
+			interpolants[ i ].result = propertyMixer.buffer;
+			actionBindings[ i ] = propertyMixer;
 
-			this.actions[i].mixer = null;
-			
 		}
 
-		// unbind all property bindings
-		for( var properyBindingKey in this.propertyBindingMap ) {
+		if ( bindingsChanged ) {
 
-			this.propertyBindingMap[ properyBindingKey ].unbind();
+			this._bindingsDirty = true; // invalidates this._bindings
 
 		}
 
-		this.actions = [];
-		this.propertyBindingMap = {};
+		action.mixer = this;
+		action._prevRootUuid = rootUuid;
+		action._prevMixerUuid = myUuid;
+
+		// TODO: check for duplicate action names?
+		// Or provide each action with a UUID?
+		this._actions.push( action );
+
+		action.init( this.time );
 
 		return this;
 
@@ -81,48 +149,101 @@ THREE.AnimationMixer.prototype = {
 
 	removeAction: function( action ) {
 
-		var index = this.actions.indexOf( action );
+		var actions = this._actions,
+			index = actions.indexOf( action );
 
-		if ( index !== - 1 ) {
+		if ( index === - 1 ) {
 
-			this.actions.splice( index, 1 );
-			action.mixer = null;
+			return this; // we don't know this action - do nothing
 
 		}
 
+		// unreference all property mixers
+		var uuid = ( action.localRoot || this.root ).uuid,
+			actionBindings = action._propertyBindings,
+			bindings = this._bindingsMaps[ uuid ],
 
-		// remove unused property bindings because if we leave them around the mixer can get slow
-		var root = action.localRoot || this.root;
-		var tracks = action.clip.tracks;
+			bindingsChanged = false;
 
-		for( var i = 0; i < tracks.length; i ++ ) {
-		
-			var track = tracks[ i ];
-
-			var propertyBindingKey = root.uuid + '-' + track.name;			
-			var propertyBinding = this.propertyBindingMap[ propertyBindingKey ];
-	
-			propertyBinding.referenceCount -= 1;
+		for( var i = 0, n = actionBindings.length; i !== n; ++ i ) {
 
-			if( propertyBinding.referenceCount <= 0 ) {
+			var propertyMixer = actionBindings[ i ];
+			actionBindings[ i ] = null;
 
-				propertyBinding.unbind();
+			// eventually remove the binding from the array
+			if( -- propertyMixer.referenceCount === 0 ) {
 
-				delete this.propertyBindingMap[ propertyBindingKey ];
+				propertyMixer.restoreOriginalState();
+				bindingsChanged = true;
 
 			}
+
+		}
+
+		if ( bindingsChanged ) {
+
+			this._bindingsDirty = true; // invalidates this._bindings
+
 		}
 
+		// remove from array-based unordered set
+		actions[ index ] = actions[ actions.length - 1 ];
+		actions.pop();
+
+		action.mixer = null;
+
 		return this;
 
 	},
 
+	removeAllActions: function() {
+
+		if ( this._bindingsDirty ) {
+
+			this._updateBindings();
+
+		}
+
+		var bindings = this._bindings; // all bindings currently in use
+
+		for ( var i = 0, n = bindings.length; i !== n; ++ i ) {
+
+			var binding = bindings[ i ];
+			binding.referenceCount = 0;
+			binding.restoreOriginalState();
+
+		}
+
+		bindings.length = 0;
+
+		this._bindingsDirty = false;
+
+		var actions = this._actions;
+
+		for ( var i = 0, n = actions.length; i !== n; ++ i ) {
+
+			actions[ i ].mixer = null;
+
+		}
+
+		actions.length = 0;
+
+		return this;
+
+	},
+
+
 	// can be optimized if needed
 	findActionByName: function( name ) {
 
-		for( var i = 0; i < this.actions.length; i ++ ) {
+		var actions = this._actions;
+
+		for ( var i = 0, n = actions.length; i !== n; ++ i ) {
 
-			if( this.actions[i].name === name ) return this.actions[i];
+			var action = actions[ i ];
+			var actionName = action.getName();
+
+			if( name === actionName ) return action;
 
 		}
 
@@ -141,25 +262,21 @@ THREE.AnimationMixer.prototype = {
 
 	fadeOut: function( action, duration ) {
 
-		var keys = [];
+		var time = this.time,
+			times = Float64Array.of( time, time + duration );
 
-		keys.push( { time: this.time, value: 1 } );
-		keys.push( { time: this.time + duration, value: 0 } );
-		
-		action.weight = new THREE.NumberKeyframeTrack( "weight", keys );
+		action.weight = new THREE.LinearInterpolant( times, this._Down, 1, this._tmp );
 
 		return this;
 
 	},
 
 	fadeIn: function( action, duration ) {
-		
-		var keys = [];
-		
-		keys.push( { time: this.time, value: 0 } );
-		keys.push( { time: this.time + duration, value: 1 } );
-		
-		action.weight = new THREE.NumberKeyframeTrack( "weight", keys );
+
+		var time = this.time,
+			times = Float64Array.of( time, time + duration );
+
+		action.weight = new THREE.LinearInterpolant( times, this._Up, 1, this._tmp );
 
 		return this;
 
@@ -167,12 +284,11 @@ THREE.AnimationMixer.prototype = {
 
 	warp: function( action, startTimeScale, endTimeScale, duration ) {
 
-		var keys = [];
-		
-		keys.push( { time: this.time, value: startTimeScale } );
-		keys.push( { time: this.time + duration, value: endTimeScale } );
-		
-		action.timeScale = new THREE.NumberKeyframeTrack( "timeScale", keys );
+		var time = this.time,
+			times = Float64Array.of( time, time + duration ),
+			values = Float64Array.of( startTimeScale, endTimeScale );
+
+		action.timeScale = new THREE.LinearInterpolant( times, values, 1, this._tmp );
 
 		return this;
 
@@ -184,7 +300,7 @@ THREE.AnimationMixer.prototype = {
 		this.fadeIn( fadeInAction, duration );
 
 		if( warp ) {
-	
+
 			var startEndRatio = fadeOutAction.clip.duration / fadeInAction.clip.duration;
 			var endStartRatio = 1.0 / startEndRatio;
 
@@ -194,47 +310,144 @@ THREE.AnimationMixer.prototype = {
 		}
 
 		return this;
-		
+
 	},
 
 	update: function( deltaTime ) {
 
-		var mixerDeltaTime = deltaTime * this.timeScale;
-		this.time += mixerDeltaTime;
+		var actions = this._actions,
+			mixerDeltaTime = deltaTime * this.timeScale;
 
-		for( var i = 0; i < this.actions.length; i ++ ) {
+		var time = this.time += mixerDeltaTime;
+		var accuIndex = this.accuIndex ^= 1;
 
-			var action = this.actions[i];
+		// perform all actions
 
-			var weight = action.getWeightAt( this.time );
+		for ( var i = 0, n = actions.length; i !== n; ++ i ) {
 
-			var actionTimeScale = action.getTimeScaleAt( this.time );
-			var actionDeltaTime = mixerDeltaTime * actionTimeScale;
-		
-			var actionResults = action.update( actionDeltaTime );
+			var action = actions[ i ];
+			if ( ! action.enabled ) continue;
 
-			if( action.weight <= 0 || ! action.enabled ) continue;
+			var weight = action.getWeightAt( time );
+			if ( weight <= 0 ) continue;
 
-			for( var j = 0; j < actionResults.length; j ++ ) {
+			var actionTimeScale = action.getTimeScaleAt( time );
+			var actionTime = action.updateTime( mixerDeltaTime * actionTimeScale );
 
-				var name = action.clip.tracks[j].name;
+			var interpolants = action._interpolants;
+			var propertyMixers = action._propertyBindings;
 
-				action.propertyBindings[ j ].accumulate( actionResults[j], weight );
+			for ( var j = 0, m = interpolants.length; j !== m; ++ j ) {
+
+				interpolants[ j ].getAt( actionTime );
+				propertyMixers[ j ].accumulate( accuIndex, weight );
 
 			}
 
 		}
-	
-		// apply to nodes
-		for( var propertyBindingKey in this.propertyBindingMap ) {
 
-			this.propertyBindingMap[ propertyBindingKey ].apply();
+		if ( this._bindingsDirty ) {
+
+			this._updateBindings();
+			this._bindingsDirty = false;
+
+		}
+
+		var bindings = this._bindings;
+		for ( var i = 0, n = bindings.length; i !== n; ++ i ) {
+
+			bindings[ i ].apply( accuIndex );
 
 		}
 
 		return this;
-		
-	}
+
+	},
+
+	// releases cached references to scene graph nodes
+	// pass 'true' for 'unbindOnly' to allow a quick rebind at
+	// the expense of higher cost add / removeAction operations
+	releaseCachedBindings: function( unbindOnly ) {
+
+		var bindingsMaps = this._bindingsMaps;
+
+		for ( var rootUuid in bindingsMaps ) {
+
+			var bindingsMap = bindingsMaps[ rootUuid ];
+
+			var mapChanged = false;
+
+			for ( var trackName in bindingsMap ) {
+
+				var propertyMixer = bindingsMap[ trackName ];
+
+				if ( propertyMixer.referenceCount === 0 ) {
+
+					if ( unbindOnly ) {
+
+						propertyMixer.binding.unbind();
+
+					} else {
+
+						delete bindingsMap[ trackName ];
+						mapChanged = true;
+
+					}
+
+				}
+
+			}
+
+			if ( mapChanged ) {
+
+				remove_empty_map: for (;;) {
+
+					// unless not empty...
+					for ( var k in bindingsMap ) break remove_empty_map;
+
+					delete bindingsMaps[ rootUuid ];
+					break;
+
+				}
+
+			}
+
+		}
+
+	},
+
+	_updateBindings: function() {
+
+		var bindingsMaps = this._bindingsMaps,
+			bindings = this._bindings,
+			writeIndex = 0;
+
+		for ( var rootUuid in bindingsMaps ) {
+
+			var bindingsMap = bindingsMaps[ rootUuid ];
+
+			for ( var trackName in bindingsMap ) {
+
+				var propertyMixer = bindingsMap[ trackName ];
+
+				if ( propertyMixer.referenceCount !== 0 ) {
+
+					bindings[ writeIndex ++ ] = propertyMixer;
+
+				}
+
+			}
+
+		}
+
+		bindings.length = writeIndex;
+
+	},
+
+	_Up: Float64Array.of( 0, 1 ),
+	_Down: Float64Array.of( 1, 0 ),
+
+	_tmp: new Float64Array( 1 )
 
 };
 

+ 132 - 77
src/animation/AnimationUtils.js

@@ -1,116 +1,171 @@
 /**
+ * @author tschw
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  */
 
- THREE.AnimationUtils = {
+THREE.AnimationUtils = {
 
- 	getEqualsFunc: function( exemplarValue ) {
+	// same as Array.prototype.slice, but also works on typed arrays
+	arraySlice: function( array, from, to ) {
+
+		if ( array.slice === undefined ) {
+
+			return new array.constructor( array.subarray( from, to ) );
 
-		if( exemplarValue.equals ) {
-			return function equals_object( a, b ) {
-				return a.equals( b );
-			}
 		}
 
-		return function equals_primitive( a, b ) {
-			return ( a === b );	
-		};
+		return array.slice( from, to );
 
 	},
 
- 	clone: function( exemplarValue ) {
+	// converts an array to a specific type
+	convertArray: function( array, type, forceClone ) {
+
+		if ( ! forceClone && array.constructor === type ) return array;
+
+		if ( typeof type.BYTES_PER_ELEMENT === 'number' ) {
+
+			return new type( array );
 
- 		var typeName = typeof exemplarValue;
-		if( typeName === "object" ) {
-			if( exemplarValue.clone ) {
-				return exemplarValue.clone();
-			}
-			console.error( "can not figure out how to copy exemplarValue", exemplarValue );
 		}
 
-		return exemplarValue;
+		var result = [];
+		result.push.apply( result, array );
+		return result;
 
 	},
 
- 	lerp: function( a, b, alpha, interTrack ) {
+	// returns an array by which times and values can be sorted
+	getKeyframeOrder: function( times ) {
 
-		var lerpFunc = THREE.AnimationUtils.getLerpFunc( a, interTrack );
+		function compareTime( i, j ) {
 
-		return lerpFunc( a, b, alpha );
+			return times[ i ] - times[ j ];
 
-	},
+		}
 
-	lerp_object: function( a, b, alpha ) {
-		return a.lerp( b, alpha );
-	},
-	
-	slerp_object: function( a, b, alpha ) {
-		return a.slerp( b, alpha );
-	},
+		var n = times.length;
+		var result = new Array( n );
+		for ( var i = 0; i !== n; ++ i ) result[ i ] = i;
 
-	lerp_number: function( a, b, alpha ) {
-		return a * ( 1 - alpha ) + b * alpha;
-	},
+		result.sort( compareTime );
+
+		return result;
 
-	lerp_boolean: function( a, b, alpha ) {
-		return ( alpha < 0.5 ) ? a : b;
 	},
 
-	lerp_boolean_immediate: function( a, b, alpha ) {
-		return a;
+	// uses the array previously returned by 'getKeyframeOrder' to sort data
+	sortedArray: function( values, stride, order ) {
+
+		var nValues = values.length;
+		var result = new values.constructor( nValues );
+
+		for ( var i = 0, dstOffset = 0; dstOffset !== nValues; ++ i ) {
+
+			var srcOffset = order[ i ] * stride;
+
+			for ( var j = 0; j !== stride; ++ j ) {
+
+				result[ dstOffset ++ ] = values[ srcOffset + j ];
+
+			}
+
+		}
+
+		return result;
+
 	},
-	
-	lerp_string: function( a, b, alpha ) {
-		return ( alpha < 0.5 ) ? a : b;
+
+	// function for parsing AOS keyframe formats
+	flattenJSON: function( jsonKeys, times, values, valuePropertyName ) {
+
+		for ( var i = 0, n = jsonKeys.length; i !== n; ++ i ) {
+
+			var key = jsonKeys[ i ];
+			var value = key[ valuePropertyName ];
+
+			if ( value === undefined ) continue;
+
+			times.push( key[ 'time' ] );
+
+			if ( value[ 'splice' ] !== undefined ) {
+
+				values.push.apply( values, value );
+
+			} else if ( value[ 'toArray' ] !== undefined ) {
+
+				value.toArray( values, values.length );
+
+			} else {
+
+				values.push( value )
+
+			}
+
+		}
+
 	},
-	
-	lerp_string_immediate: function( a, b, alpha ) {
- 		return a;		 		
- 	},
 
-	// NOTE: this is an accumulator function that modifies the first argument (e.g. a).  This is to minimize memory alocations.
-	getLerpFunc: function( exemplarValue, interTrack ) {
+	// fuzz-free, array-based Quaternion SLERP operation
+	slerp: function( dst, dstOffset, src, srcOffset0, srcOffset1, t ) {
+
+		var x0 = src[   srcOffset0   ],
+			y0 = src[ srcOffset0 + 1 ],
+			z0 = src[ srcOffset0 + 2 ],
+			w0 = src[ srcOffset0 + 3 ],
 
-		if( exemplarValue === undefined || exemplarValue === null ) throw new Error( "examplarValue is null" );
+			x1 = src[   srcOffset1   ],
+			y1 = src[ srcOffset1 + 1 ],
+			z1 = src[ srcOffset1 + 2 ],
+			w1 = src[ srcOffset1 + 3 ];
 
-		var typeName = typeof exemplarValue;
-		switch( typeName ) {
-		 	case "object": {
+		if ( w0 !== w1 || x0 !== x1 || y0 !== y1 || z0 !== z1 ) {
 
-				if( exemplarValue.lerp ) {
+			var s = 1 - t,
 
-					return THREE.AnimationUtils.lerp_object;
+				cos = x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1,
 
-				}
-				if( exemplarValue.slerp ) {
+				dir = ( cos >= 0 ? 1 : -1 ),
+				sqrSin = 1 - cos * cos;
 
-					return THREE.AnimationUtils.slerp_object;
+			// Skip the Slerp for tiny steps to avoid numeric problems:
+			if ( sqrSin > Number.EPSILON ) {
+
+				var sin = Math.sqrt( sqrSin ),
+					len = Math.atan2( sin, cos * dir );
+
+				s = Math.sin( s * len ) / sin;
+				t = Math.sin( t * len ) / sin;
 
-				}
-				break;
 			}
-		 	case "number": {
-				return THREE.AnimationUtils.lerp_number;
-		 	}	
-		 	case "boolean": {
-		 		if( interTrack ) {
-					return THREE.AnimationUtils.lerp_boolean;
-		 		}
-		 		else {
-					return THREE.AnimationUtils.lerp_boolean_immediate;
-		 		}
-		 	}
-		 	case "string": {
-		 		if( interTrack ) {
-					return THREE.AnimationUtils.lerp_string;
-		 		}
-		 		else {
-					return THREE.AnimationUtils.lerp_string_immediate;
-			 	}
-		 	}
-		};
+
+			var tDir = t * dir;
+
+			x0 = x0 * s + x1 * tDir;
+			y0 = y0 * s + y1 * tDir;
+			z0 = z0 * s + z1 * tDir;
+			w0 = w0 * s + w1 * tDir;
+
+			// Normalize in case we just did a lerp:
+			if ( s === 1 - t ) {
+
+				var f = 1 / Math.sqrt( x0 * x0 + y0 * y0 + z0 * z0 + w0 * w0 );
+
+				x0 *= f;
+				y0 *= f;
+				z0 *= f;
+				w0 *= f;
+
+			}
+
+		}
+
+		dst[   dstOffset   ] = x0;
+		dst[ dstOffset + 1 ] = y0;
+		dst[ dstOffset + 2 ] = z0;
+		dst[ dstOffset + 3 ] = w0;
 
 	}
-	
-};
+
+};

+ 220 - 0
src/animation/Interpolant.js

@@ -0,0 +1,220 @@
+/**
+ *
+ * Abstract base class for interpolants over timed keyframe values.
+ * It handles seeking of the interval and boundary cases. Concrete
+ * subclasses then implement the actual intepolation by filling in
+ * the Template Methods.
+ *
+ *
+ * @author tschw
+ */
+
+THREE.Interpolant = function( times, values, stride, result ) {
+
+	this.times = times;
+	this.values = values;
+	this.stride = stride;
+
+	this.result = result;
+
+	this.cachedIndex = 0;
+
+};
+
+THREE.Interpolant.prototype = {
+
+	constructor: THREE.Intepolant,
+
+	getAt: function( time ) {
+
+		var times = this.times;
+		var index = this.cachedIndex;
+
+		var keyTime = times[ index ];
+		var prevKeyTime = times[ index - 1 ];
+
+		change_interval: for (;;) {
+
+			seek: for (;;) {
+
+				var right;
+
+				if ( ! ( time < keyTime ) ) {
+
+					// linear scan forward
+
+					for ( var giveUpAt = index + 2; ;) {
+
+						if ( keyTime === undefined ) {
+
+							// after end
+
+							index = times.length - 1;
+							this.cachedIndex = index;
+							return this._afterEnd( index, time, prevKeyTime );
+
+						}
+
+						if ( index === giveUpAt ) break;
+
+						prevKeyTime = keyTime;
+						keyTime = times[ ++ index ];
+
+						if ( time < keyTime ) {
+
+							// we have arrived at the sought interval
+							break seek;
+
+						}
+
+					}
+
+					// prepare binary search on the right side of the index
+					right = times.length;
+
+				} else if ( ! ( time >= prevKeyTime ) ) {
+
+					// looping?
+
+					var secondKeyTime = times[ 1 ];
+
+					if ( time < secondKeyTime ) {
+
+						index = 2; // + 1, using the scan for the details
+						prevKeyTime = secondKeyTime;
+
+					}
+
+					// linear reverse scan
+
+					for ( var giveUpAt = index - 2; ;) {
+
+						if ( prevKeyTime === undefined ) {
+
+							// before start
+
+							this.cachedIndex = 0;
+							return this._beforeStart( 0, time, keyTime );
+
+						}
+
+						if ( index === giveUpAt ) break;
+
+						keyTime = prevKeyTime;
+						prevKeyTime = times[ -- index - 1 ];
+
+						if ( time >= prevKeyTime ) {
+
+							// we have arrived at the sought interval
+							break seek;
+
+						}
+
+					}
+
+					// prepare binary search on the left side of the index
+					right = index;
+					index = 0;
+
+				} else {
+
+					// the current interval is still valid
+
+					break change_interval;
+
+				}
+
+				// binary search
+
+				while ( index < right ) {
+
+					var mid = ( index + right ) >>> 1;
+
+					if ( time >= times[ mid ] ) {
+
+						index = mid + 1;
+
+					} else {
+
+						right = mid;
+
+					}
+
+				}
+
+				keyTime = times[ index ];
+				prevKeyTime = times[ index - 1 ];
+
+				continue; // check boundary cases, again
+
+			} // seek
+
+			this.cachedIndex = index;
+
+			this._intervalChanged( index, prevKeyTime, keyTime );
+			break;
+
+		}
+
+		return this._interpolate( index, prevKeyTime, time, keyTime );
+
+	},
+
+	parameters: null, // optional, subclass-specific parameter structure
+	// Note: The indirection allows central control of many interpolants.
+
+	DefaultParameters: {},
+
+	// --- Protected interface
+
+	_getParameters: function() {
+
+		return this.parameters || this.DefaultParameters;
+
+	},
+
+	_copyKeyframe: function( index ) {
+
+		// copies the state at a keyframe to the result buffer
+
+		var result = this.result,
+
+			values = this.values,
+			stride = this.stride,
+			offset = index * stride;
+
+		for ( var i = 0; i !== stride; ++ i ) {
+
+			result[ i ] = values[ offset + i ];
+
+		}
+
+		return result;
+
+	},
+
+	// Template methods for derived classes:
+
+	_interpolate: function( i1, t0, t, t1 ) {
+
+		throw new Error( "call to abstract method" );
+
+	},
+
+	_intervalChanged: function( i1, t0, t1 ) {
+
+		// empty
+
+	}
+
+};
+
+Object.assign( THREE.Interpolant.prototype, {
+
+	_beforeStart: //( 0, t, t0 )
+		THREE.Interpolant.prototype._copyKeyframe,
+
+	_afterEnd: //( N-1, tN, t )
+		THREE.Interpolant.prototype._copyKeyframe
+
+} );

+ 339 - 115
src/animation/KeyframeTrack.js

@@ -1,75 +1,144 @@
 /**
  *
- * A Track that returns a keyframe interpolated value, currently linearly interpolated
+ * A timed sequence of keyframes for a specific property.
+ *
  *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
 
-THREE.KeyframeTrack = function ( name, keys ) {
+THREE.KeyframeTrack = function ( name, times, values, interpolation ) {
 
 	if( name === undefined ) throw new Error( "track name is undefined" );
-	if( keys === undefined || keys.length === 0 ) throw new Error( "no keys in track named " + name );
+
+	if( times === undefined || times.length === 0 ||
+			values === undefined || values.length === 0 ) {
+
+		throw new Error( "no keyframes in track named " + name );
+
+	}
 
 	this.name = name;
-	this.keys = keys;	// time in seconds, value as value
 
-	// the index of the last result, used as a starting point for local search.
-	this.lastIndex = 0;
+	this.times = THREE.AnimationUtils.convertArray( times, this.TimeBufferType );
+	this.values = THREE.AnimationUtils.convertArray( values, this.ValueBufferType );
+
+	this.setInterpolation( interpolation || this.DefaultInterpolation );
 
 	this.validate();
 	this.optimize();
+
 };
 
 THREE.KeyframeTrack.prototype = {
 
 	constructor: THREE.KeyframeTrack,
 
-	getAt: function( time ) {
+	TimeBufferType: Float64Array,
+	ValueBufferType: Float64Array,
 
+	DefaultInterpolation: THREE.InterpolateLinear,
 
-		// this can not go higher than this.keys.length.
-		while( ( this.lastIndex < this.keys.length ) && ( time >= this.keys[this.lastIndex].time ) ) {
-			this.lastIndex ++;
-		};
+	InterpolantFactoryMethodDiscrete: function( result ) {
 
-		// this can not go lower than 0.
-		while( ( this.lastIndex > 0 ) && ( time < this.keys[this.lastIndex - 1].time ) ) {
-			this.lastIndex --;
-		}
+		return new THREE.DiscreteInterpolant(
+				this.times, this.values, this.getValueSize(), result );
+
+	},
+
+	InterpolantFactoryMethodLinear: function( result ) {
+
+		return new THREE.LinearInterpolant(
+				this.times, this.values, this.getValueSize(), result );
+
+	},
+
+	InterpolantFactoryMethodSmooth: function( result ) {
+
+		return new THREE.SmoothInterpolant(
+				this.times, this.values, this.getValueSize(), result );
+
+	},
+
+	setInterpolation: function( interpolation ) {
+
+		var factoryMethod = undefined;
 
-		if( this.lastIndex >= this.keys.length ) {
+		switch ( interpolation ) {
 
-			this.setResult( this.keys[ this.keys.length - 1 ].value );
+			case THREE.InterpolateDiscrete:
 
-			return this.result;
+				factoryMethod = this.InterpolantFactoryMethodDiscrete;
+
+				break;
+
+			case THREE.InterpolateLinear:
+
+				factoryMethod = this.InterpolantFactoryMethodLinear;
+
+				break;
+
+			case THREE.InterpolateSmooth:
+
+				factoryMethod = this.InterpolantFactoryMethodSmooth;
+
+				break;
 
 		}
 
-		if( this.lastIndex === 0 ) {
+		if ( factoryMethod === undefined ) {
 
-			this.setResult( this.keys[ 0 ].value );
+			var message = "unsupported interpolation for " +
+					this.ValueTypeName + " keyframe track named " + this.name;
 
-			return this.result;
+			if ( this.createInterpolant === undefined ) {
+
+				// fall back to default, unless the default itself is messed up
+				if ( interpolation !== this.DefaultInterpolation ) {
+
+					this.setInterpolation( this.DefaultInterpolation );
+
+				} else {
+
+					throw new Error( message ); // fatal, in this case
+
+				}
+
+			}
+
+			console.warn( message );
+			return;
 
 		}
 
-		var prevKey = this.keys[ this.lastIndex - 1 ];
-		this.setResult( prevKey.value );
+		this.createInterpolant = factoryMethod;
 
-		// if true, means that prev/current keys are identical, thus no interpolation required.
-		if( prevKey.constantToNext ) {
+	},
 
-			return this.result;
+	getInterpolation: function() {
+
+		switch ( this.createInterpolant ) {
+
+			case this.InterpolantFactoryMethodDiscrete:
+
+				return THREE.InterpolateDiscrete;
+
+			case this.InterpolantFactoryMethodLinear:
+
+				return THREE.InterpolateLinear;
+
+			case this.InterpolantFactoryMethodSmooth:
+
+				return THREE.InterpolateSmooth;
 
 		}
 
-		// linear interpolation to start with
-		var currentKey = this.keys[ this.lastIndex ];
-		var alpha = ( time - prevKey.time ) / ( currentKey.time - prevKey.time );
-		this.result = this.lerpValues( this.result, currentKey.value, alpha );
+	},
 
-		return this.result;
+	getValueSize: function() {
+
+		return this.values.length / this.times.length;
 
 	},
 
@@ -78,8 +147,12 @@ THREE.KeyframeTrack.prototype = {
 
 		if( timeOffset !== 0.0 ) {
 
-			for( var i = 0; i < this.keys.length; i ++ ) {
-				this.keys[i].time += timeOffset;
+			var times = this.times;
+
+			for( var i = 0, n = times.length; i !== n; ++ i ) {
+
+				times[ i ] += timeOffset;
+
 			}
 
 		}
@@ -93,8 +166,12 @@ THREE.KeyframeTrack.prototype = {
 
 		if( timeScale !== 1.0 ) {
 
-			for( var i = 0; i < this.keys.length; i ++ ) {
-				this.keys[i].time *= timeScale;
+			var times = this.times;
+
+			for( var i = 0, n = times.length; i !== n; ++ i ) {
+
+				times[ i ] *= timeScale;
+
 			}
 
 		}
@@ -105,80 +182,109 @@ THREE.KeyframeTrack.prototype = {
 
 	// removes keyframes before and after animation without changing any values within the range [startTime, endTime].
 	// IMPORTANT: We do not shift around keys to the start of the track time, because for interpolated keys this will change their values
- 	trim: function( startTime, endTime ) {
+	trim: function( startTime, endTime ) {
+
+		var times = this.times;
+		var nKeys = times.length;
 
 		var firstKeysToRemove = 0;
-		for( var i = 1; i < this.keys.length; i ++ ) {
-			if( this.keys[i] <= startTime ) {
-				firstKeysToRemove ++;
-			}
+		for ( var i = 1; i !== nKeys; ++ i ) {
+
+			if ( times[i] <= startTime ) ++ firstKeysToRemove;
+
 		}
 
 		var lastKeysToRemove = 0;
-		for( var i = this.keys.length - 2; i > 0; i ++ ) {
-			if( this.keys[i] >= endTime ) {
-				lastKeysToRemove ++;
-			}
-			else {
-				break;
-			}
+		for ( var i = nKeys - 2; i !== 0; -- i ) {
+
+			if ( times[i] >= endTime ) ++ lastKeysToRemove;
+			else break;
+
 		}
 
 		// remove last keys first because it doesn't affect the position of the first keys (the otherway around doesn't work as easily)
-		if( ( firstKeysToRemove + lastKeysToRemove ) > 0 ) {
-			this.keys = this.keys.splice( firstKeysToRemove, this.keys.length - lastKeysToRemove - firstKeysToRemove );;
-		}
+		if( ( firstKeysToRemove + lastKeysToRemove ) !== 0 ) {
 
-		return this;
+			var from = firstKeysToRemove;
+			var to = nKeys - lastKeysToRemove - firstKeysToRemove;
 
-	},
+			this.times = THREE.AnimationUtils.arraySlice( times, from, to );
 
-	/* NOTE: This is commented out because we really shouldn't have to handle unsorted key lists
-	         Tracks with out of order keys should be considered to be invalid.  - bhouston
-	sort: function() {
+			var values = this.values;
+			var stride = this.getValueSize();
+			this.values = THREE.AnimationUtils.arraySlice( values, from * stride, to * stride );
 
-		this.keys.sort( THREE.KeyframeTrack.keyComparer );
+		}
 
 		return this;
 
-	},*/
+	},
 
 	// ensure we do not get a GarbageInGarbageOut situation, make sure tracks are at least minimally viable
 	// One could eventually ensure that all key.values in a track are all of the same type (otherwise interpolation makes no sense.)
 	validate: function() {
 
-		var prevKey = null;
+		if ( this.getValueSize() % 1 !== 0 ) {
+
+			throw new Error( "invalid value size for track named " + this.name );
+
+		}
+
+		var times = this.times;
+		var values = this.values;
+		var stride = this.getValueSize();
+
+		var nKeys = times.length;
+
+		if( nKeys === 0 ) {
 
-		if( this.keys.length === 0 ) {
 			console.error( "  track is empty, no keys", this );
 			return;
+
 		}
 
-		for( var i = 0; i < this.keys.length; i ++ ) {
+		var prevTime = null;
+
+		for( var i = 0; i !== nKeys; i ++ ) {
+
+			var currTime = times[ i ];
 
-			var currKey = this.keys[i];
+			if( currTime === undefined || currTime === null ) {
 
-			if( ! currKey ) {
-				console.error( "  key is null in track", this, i );
+				console.error( "  time is null in track", this, i );
 				return;
+
 			}
 
-			if ( ( typeof currKey.time ) !== 'number' || isNaN( currKey.time ) ) {
-				console.error( "  key.time is not a valid number", this, i, currKey );
+			if( ( typeof currTime ) !== 'number' || Number.isNaN( currTime ) ) {
+
+				console.error( "  time is not a valid number", this, i, currTime );
 				return;
+
 			}
 
-			if( currKey.value === undefined || currKey.value === null) {
-				console.error( "  key.value is null in track", this, i, currKey );
-				return;
+			var offset = i * stride;
+			for ( var j = offset, e = offset + stride; j !== e; ++ j ) {
+
+				var value = values[ j ];
+
+				if( value === undefined || value === null) {
+
+					console.error( "  value is null in track", this, j, value );
+					return;
+
+				}
+
 			}
 
-			if( prevKey && prevKey.time > currKey.time ) {
-				console.error( "  key.time is less than previous key time, out of order keys", this, i, currKey, prevKey );
+			if( prevTime !== null && prevTime > currTime ) {
+
+				console.error( "  time is less than previous key time, out of order keys", this, i, currTime, prevTime );
 				return;
+
 			}
 
-			prevKey = currKey;
+			prevTime = currTime;
 
 		}
 
@@ -186,43 +292,80 @@ THREE.KeyframeTrack.prototype = {
 
 	},
 
-	// currently only removes equivalent sequential keys (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0), which are common in morph target animations
+	// removes equivalent sequential keys as common in morph target sequences
+	// (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)
 	optimize: function() {
 
-		var newKeys = [];
-		var prevKey = this.keys[0];
-		newKeys.push( prevKey );
+		var times = this.times;
+		var values = this.values;
+		var stride = this.getValueSize();
 
-		var equalsFunc = THREE.AnimationUtils.getEqualsFunc( prevKey.value );
+		var writeIndex = 1;
 
-		for( var i = 1; i < this.keys.length - 1; i ++ ) {
-			var currKey = this.keys[i];
-			var nextKey = this.keys[i+1];
+		for( var i = 1, n = times.length - 1; i <= n; ++ i ) {
 
-			// if prevKey & currKey are the same time, remove currKey.  If you want immediate adjacent keys, use an epsilon offset
-			// it is not possible to have two keys at the same time as we sort them.  The sort is not stable on keys with the same time.
-			if( ( prevKey.time === currKey.time ) ) {
+			var keep = false;
 
-				continue;
+			var time = times[ i ];
+			var timeNext = times[ i + 1 ];
 
-			}
+			// remove adjacent keyframes scheduled at the same time
+
+			if ( time !== timeNext && ( i !== 1 || time !== time[ 0 ] ) ) {
 
-			// remove completely unnecessary keyframes that are the same as their prev and next keys
-			if( this.compareValues( prevKey.value, currKey.value ) && this.compareValues( currKey.value, nextKey.value ) ) {
+				// remove unnecessary keyframes same as their neighbors
+				var offset = i * stride;
+				var offsetP = offset - stride;
+				var offsetN = offset + stride;
 
-				continue;
+				for ( var j = 0; j !== stride; ++ j ) {
+
+					var value = values[ offset + j ];
+
+					if ( value !== values[ offsetP + j ] ||
+							value !== values[ offsetN + j ] ) {
+
+						keep = true;
+						break;
+
+					}
+
+				}
 
 			}
 
-			// determine if interpolation is required
-			prevKey.constantToNext = this.compareValues( prevKey.value, currKey.value );
+			// in-place compaction
+
+			if ( keep ) {
+
+				if ( i !== writeIndex ) {
+
+					times[ writeIndex ] = times[ i ];
+
+					var readOffset = i * stride;
+					var writeOffset = writeIndex * stride;
+
+					for ( var j = 0; j !== stride; ++ j ) {
+
+						values[ writeOffset + j ] = values[ readOffset + j ];
+
+					}
+
+
+				}
+
+				++ writeIndex;
+
+			}
 
-			newKeys.push( currKey );
-			prevKey = currKey;
 		}
-		newKeys.push( this.keys[ this.keys.length - 1 ] );
 
-		this.keys = newKeys;
+		if ( writeIndex !== times.length ) {
+
+			this.times = THREE.AnimationUtils.arraySlice( times, 0, writeIndex );
+			this.values = THREE.AnimationUtils.arraySlice( values, 0, writeIndex * stride );
+
+		}
 
 		return this;
 
@@ -230,45 +373,126 @@ THREE.KeyframeTrack.prototype = {
 
 };
 
-THREE.KeyframeTrack.keyComparer = function keyComparator(key0, key1) {
-	return key0.time - key1.time;
-};
+// Serialization (in static context, because of constructor invocation
+// and automatic invocation of .toJSON):
 
 THREE.KeyframeTrack.parse = function( json ) {
 
-	if( json.type === undefined ) throw new Error( "track type undefined, can not parse" );
+	if( json[ 'type' ] === undefined ) {
+
+		throw new Error( "track type undefined, can not parse" );
+
+	}
+
+	var trackType = THREE.KeyframeTrack.GetTrackTypeForValueTypeName( json.type );
 
-	var trackType = THREE.KeyframeTrack.GetTrackTypeForTypeName( json.type );
+	if ( json[ 'times' ] === undefined ) {
 
-	return trackType.parse( json );
+		console.warn( "legacy JSON format detected, converting" );
+
+		var times = [], values = [];
+
+		THREE.AnimationUtils.flattenJSON( json, times, values, 'value' );
+
+		json[ 'times' ] = times;
+		json[ 'values' ] = values;
+
+	}
+
+	// derived classes can define a static parse method
+	if ( trackType.parse !== undefined ) {
+
+		return trackType.parse( json );
+
+	} else {
+
+		// by default, we asssume a constructor compatible with the base
+		return new trackType(
+				json[ 'name' ], json[ 'times' ], json[ 'values' ], json[ 'interpolation' ] );
+
+	}
+
+};
+
+THREE.KeyframeTrack.toJSON = function( track ) {
+
+	var trackType = track.constructor;
+
+	var json;
+
+	// derived classes can define a static toJSON method
+	if ( trackType.toJSON !== undefined ) {
+
+		json = trackType.toJSON( track );
+
+	} else {
+
+		// by default, we assume the data can be serialized as-is
+		json = {
+
+			'name': track.name,
+			'times': THREE.AnimationUtils.convertArray( track.times, Array ),
+			'values': THREE.AnimationUtils.convertArray( track.values, Array )
+
+		};
+
+		var interpolation = track.getInterpolation();
+
+		if ( interpolation !== track.DefaultInterpolation ) {
+
+			json[ 'interpolation' ] = interpolation;
+
+		}
+
+	}
+
+	json[ 'type' ] = track.ValueTypeName; // mandatory
+
+	return json;
 
 };
 
-THREE.KeyframeTrack.GetTrackTypeForTypeName = function( typeName ) {
+THREE.KeyframeTrack.GetTrackTypeForValueTypeName = function( typeName ) {
+
 	switch( typeName.toLowerCase() ) {
-	 	case "vector":
-	 	case "vector2":
-	 	case "vector3":
-	 	case "vector4":
+
+		case "scalar":
+		case "double":
+		case "float":
+		case "number":
+		case "integer":
+
+			return THREE.NumberKeyframeTrack;
+
+		case "vector":
+		case "vector2":
+		case "vector3":
+		case "vector4":
+
 			return THREE.VectorKeyframeTrack;
 
-	 	case "quaternion":
+		case "color":
+
+			return THREE.ColorKeyframeTrack;
+
+		case "quaternion":
+
 			return THREE.QuaternionKeyframeTrack;
 
-	 	case "integer":
-	 	case "scalar":
-	 	case "double":
-	 	case "float":
-	 	case "number":
-			return THREE.NumberKeyframeTrack;
+		case "bool":
+		case "boolean":
 
-	 	case "bool":
-	 	case "boolean":
 			return THREE.BooleanKeyframeTrack;
 
-	 	case "string":
-	 		return THREE.StringKeyframeTrack;
+		case "string":
+
+			return THREE.StringKeyframeTrack;
+
 	};
 
 	throw new Error( "Unsupported typeName: " + typeName );
+<<<<<<< HEAD
+=======
+
+>>>>>>> Animation: Interpolants & extensibility overhaul.
 };

+ 192 - 126
src/animation/PropertyBinding.js

@@ -1,19 +1,19 @@
 /**
  *
- * A track bound to a real value in the scene graph.
- * 
+ * A reference to a real property in the scene graph.
+ *
+ *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
 
-THREE.PropertyBinding = function ( rootNode, trackName ) {
+THREE.PropertyBinding = function ( rootNode, path ) {
 
 	this.rootNode = rootNode;
-	this.trackName = trackName;
-	this.referenceCount = 0;
-	this.originalValue = null; // the value of the property before it was controlled by this binding
+	this.path = path;
 
-	var parseResults = THREE.PropertyBinding.parseTrackName( trackName );
+	var parseResults = THREE.PropertyBinding.parseTrackName( path );
 
 	this.directoryName = parseResults.directoryName;
 	this.nodeName = parseResults.nodeName;
@@ -22,74 +22,71 @@ THREE.PropertyBinding = function ( rootNode, trackName ) {
 	this.propertyName = parseResults.propertyName;
 	this.propertyIndex = parseResults.propertyIndex;
 
-	this.node = THREE.PropertyBinding.findNode( rootNode, this.nodeName ) || rootNode;
-	
-	this.cumulativeValue = null;
-	this.cumulativeWeight = 0;
+	this.setRootNode( rootNode );
+
 };
 
 THREE.PropertyBinding.prototype = {
 
 	constructor: THREE.PropertyBinding,
 
-	reset: function() {
+	getValue: function getValue_unbound( targetArray, offset ) {
+
+		this.bind();
+		this.getValue( targetArray, offset );
 
-		this.cumulativeValue = null;
-		this.cumulativeWeight = 0;
+		// Note: This class uses a State pattern on a per-method basis:
+		// 'bind' sets 'this.getValue' / 'setValue' and shadows the
+		// prototype version of these methods with one that represents
+		// the bound state. When the property is not found, the methods
+		// become no-ops.
 
 	},
 
-	accumulate: function( value, weight ) {
-		
-		if( ! this.isBound ) this.bind();
+	setValue: function getValue_unbound( sourceArray, offset ) {
 
-		if( this.cumulativeWeight === 0 ) {
+		this.bind();
+		this.setValue( sourceArray, offset );
 
-			if( weight > 0 ) {
+	},
 
-				if( this.cumulativeValue === null ) {
-					this.cumulativeValue = THREE.AnimationUtils.clone( value );
-				}
-				this.cumulativeWeight = weight;
+	// change the root used for binding
+	setRootNode: function( rootNode ) {
 
-			}
+		var oldNode = this.node,
+			newNode = THREE.PropertyBinding.findNode( rootNode, this.nodeName ) || rootNode;
 
-		}
-		else {
+		if ( oldNode && oldNode !== newNode ) {
 
-			var lerpAlpha = weight / ( this.cumulativeWeight + weight );
-			this.cumulativeValue = this.lerpValue( this.cumulativeValue, value, lerpAlpha );
-			this.cumulativeWeight += weight;
+			this.unbind(); // for the change to take effect on the next call
 
 		}
 
-	},
+		this.rootNode = rootNode;
+		this.node = newNode;
 
-	unbind: function() {
+	},
 
-		if( ! this.isBound ) return;
+	// create getter / setter pair for a property in the scene graph
+	bind: function() {
 
-		this.setValue( this.originalValue );
+		var targetObject = this.node;
 
-		this.setValue = null;
-		this.getValue = null;
-		this.lerpValue = null;
-		this.equalsValue = null;
-		this.triggerDirty = null;	
-		this.isBound = false;
+		if ( ! targetObject ) {
 
-	},
+			targetObject = THREE.PropertyBinding.findNode( this.rootNode, this.nodeName ) || this.rootNode;
 
-	// bind to the real property in the scene graph, remember original value, memorize various accessors for speed/inefficiency
-	bind: function() {
+			this.node = targetObject;
 
-		if( this.isBound ) return;
+		}
 
-		var targetObject = this.node;
+		// set fail state so we can just 'return' on error
+		this.getValue = this._getValue_unavailable;
+		this.setValue = this._setValue_unavailable;
 
  		// ensure there is a value node
 		if( ! targetObject ) {
-			console.error( "  trying to update node for track: " + this.trackName + " but it wasn't found." );
+			console.error( "  trying to update node for track: " + this.path + " but it wasn't found." );
 			return;
 		}
 
@@ -98,11 +95,11 @@ THREE.PropertyBinding.prototype = {
 			if( this.objectName === "materials" ) {
 				if( ! targetObject.material ) {
 					console.error( '  can not bind to material as node does not have a material', this );
-					return;				
+					return;
 				}
 				if( ! targetObject.material.materials ) {
 					console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
-					return;				
+					return;
 				}
 				targetObject = targetObject.material.materials;
 			}
@@ -112,7 +109,7 @@ THREE.PropertyBinding.prototype = {
 					return;
 				}
 				// potential future optimization: skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
-				
+
 				targetObject = targetObject.skeleton.bones;
 
 				// support resolving morphTarget names into indices.
@@ -126,16 +123,16 @@ THREE.PropertyBinding.prototype = {
 			else {
 
 				if( targetObject[ this.objectName ] === undefined ) {
-					console.error( '  can not bind to objectName of node, undefined', this );			
+					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;				
+					return;
 				}
 
 				targetObject = targetObject[ this.objectIndex ];
@@ -146,24 +143,39 @@ THREE.PropertyBinding.prototype = {
  		// 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 );				
+			console.error( "  trying to update property for track: " + this.nodeName + '.' + this.propertyName + " but it wasn't found.", targetObject );
 			return;
 		}
 
+		// determine versioning scheme
+		var versioning = 0;
+		var NeedsUpdate = 1;
+		var MatrixWorldNeedsUpdate = 2;
+
+		if( targetObject.needsUpdate !== undefined ) { // material
+
+			versioning = NeedsUpdate;
+
+		} else if( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
+
+			versioning = MatrixWorldNeedsUpdate;
+
+		}
+
 		// access a sub element of the property array (only primitives are supported right now)
 		if( this.propertyIndex !== undefined ) {
 
 			if( this.propertyName === "morphTargetInfluences" ) {
 				// potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
-				
+
 				// support resolving morphTarget names into indices.
 				if( ! targetObject.geometry ) {
-					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry', this );				
+					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry', this );
 				}
 				if( ! targetObject.geometry.morphTargets ) {
-					console.error( '  can not bind to morphTargetInfluences becasuse node does not have a geometry.morphTargets', this );				
+					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( targetObject.geometry.morphTargets[i].name === this.propertyIndex ) {
 						this.propertyIndex = i;
@@ -172,109 +184,163 @@ THREE.PropertyBinding.prototype = {
 				}
 			}
 
-			this.setValue = function setValue_propertyIndexed( value ) {
-				if( ! this.equalsValue( nodeProperty[ this.propertyIndex ], value ) ) {
-					nodeProperty[ this.propertyIndex ] = value;
-					return true;
-				}
-				return false;
-			};
+			var propertyIndex = this.propertyIndex;
+
+			this.getValue = function getValue_propertyIndexed( buffer, offset ) {
+
+				buffer[ offset ] = nodeProperty[ this.propertyIndex ];
 
-			this.getValue = function getValue_propertyIndexed() {
-				return nodeProperty[ this.propertyIndex ];
 			};
 
-		}
-		// must use copy for Object3D.Euler/Quaternion		
-		else if( nodeProperty.copy ) {
-			
-			this.setValue = function setValue_propertyObject( value ) {
-				if( ! this.equalsValue( nodeProperty, value ) ) {
-					nodeProperty.copy( value );
-					return true;
-				}
-				return false;
+			switch ( versioning ) {
+
+				case NeedsUpdate:
+
+					this.setValue = function setValue_propertyIndexed( buffer, offset ) {
+
+						nodeProperty[ propertyIndex ] = buffer[ offset ];
+						targetObject.needsUpdate = true;
+
+					};
+
+					break;
+
+				case MatrixWorldNeedsUpdate:
+
+					this.setValue = function setValue_propertyIndexed( buffer, offset ) {
+
+						nodeProperty[ propertyIndex ] = buffer[ offset ];
+						targetObject.matrixWorldNeedsUpdate = true;
+
+					};
+
+					break;
+
+				default:
+
+					this.setValue = function setValue_propertyIndexed( buffer, offset ) {
+
+						nodeProperty[ propertyIndex ] = buffer[ offset ];
+
+					};
+
 			}
 
-			this.getValue = function getValue_propertyObject() {
-				return nodeProperty;
+		}
+		// must use copy for Object3D.Euler/Quaternion
+		else if( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {
+
+			this.getValue = function getValue_propertyObject( buffer, offset ) {
+
+				nodeProperty.toArray( buffer, offset );
+
 			};
 
+
+			switch ( versioning ) {
+
+				case NeedsUpdate:
+
+					this.setValue = function setValue_propertyObject( buffer, offset ) {
+
+						nodeProperty.fromArray( buffer, offset );
+						targetObject.needsUpdate = true;
+
+					}
+
+				case MatrixWorldNeedsUpdate:
+
+					this.setValue = function setValue_propertyObject( buffer, offset ) {
+
+						nodeProperty.fromArray( buffer, offset );
+						targetObject.matrixWorldNeedsUpdate = true;
+
+					}
+
+				default:
+
+					this.setValue = function setValue_propertyObject( buffer, offset ) {
+
+						nodeProperty.fromArray( buffer, offset );
+
+					}
+
+			}
+
 		}
 		// otherwise just set the property directly on the node (do not use nodeProperty as it may not be a reference object)
 		else {
 
-			this.setValue = function setValue_property( value ) {
-				if( ! this.equalsValue( targetObject[ this.propertyName ], value ) ) {
-					targetObject[ this.propertyName ] = value;	
-					return true;
-				}
-				return false;
-			}
+			var propertyName = this.propertyName;
+
+			this.getValue = function getValue_property( buffer, offset ) {
+
+				buffer[ offset ] = nodeProperty[ propertyName ];
 
-			this.getValue = function getValue_property() {
-				return targetObject[ this.propertyName ];
 			};
 
-		}
+			switch ( versioning ) {
 
-		// trigger node dirty			
-		if( targetObject.needsUpdate !== undefined ) { // material
-			
-			this.triggerDirty = function triggerDirty_needsUpdate() {
-				this.node.needsUpdate = true;
-			}
+				case NeedsUpdate:
 
-		}			
-		else if( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
-			
-			this.triggerDirty = function triggerDirty_matrixWorldNeedsUpdate() {
-				targetObject.matrixWorldNeedsUpdate = true;
-			}
+					this.setValue = function setValue_property( buffer, offset ) {
 
-		}
+						nodeProperty[ propertyName ] = buffer[ offset ];
+						targetObject.needsUpdate = true;
 
-		this.originalValue = this.getValue();
+					}
 
-		this.equalsValue = THREE.AnimationUtils.getEqualsFunc( this.originalValue );
-		this.lerpValue = THREE.AnimationUtils.getLerpFunc( this.originalValue, true );
+					break;
 
-		this.isBound = true;
+				case MatrixWorldNeedsUpdate:
 
-	},
+					this.setValue = function setValue_property( buffer, offset ) {
 
-	apply: function() {
+						nodeProperty[ propertyName ] = buffer[ offset ];
+						targetObject.matrixWorldNeedsUpdate = true;
 
-		// for speed capture the setter pattern as a closure (sort of a memoization pattern: https://en.wikipedia.org/wiki/Memoization)
-		if( ! this.isBound ) this.bind();
+					}
 
-		// early exit if there is nothing to apply.
-		if( this.cumulativeWeight > 0 ) {
-		
-			// blend with original value
-			if( this.cumulativeWeight < 1 ) {
+					break;
 
-				var remainingWeight = 1 - this.cumulativeWeight;
-				var lerpAlpha = remainingWeight / ( this.cumulativeWeight + remainingWeight );
-				this.cumulativeValue = this.lerpValue( this.cumulativeValue, this.originalValue, lerpAlpha );
+				default:
 
-			}
+					this.setValue = function setValue_property( buffer, offset ) {
 
-			var valueChanged = this.setValue( this.cumulativeValue );
+						nodeProperty[ propertyName ] = buffer[ offset ];
 
-			if( valueChanged && this.triggerDirty ) {
-				this.triggerDirty();
-			}
+					}
 
-			// reset accumulator
-			this.cumulativeValue = null;
-			this.cumulativeWeight = 0;
+			}
 
 		}
+
+	},
+
+	unbind: function() {
+
+		this.node = null;
+
+		// back to the prototype version of getValue / setValue
+		// note: avoiding to mutate the shape of 'this' via 'delete'
+		this.getValue = this._getValue_unbound;
+		this.setValue = this._setValue_unbound;
+
 	}
 
 };
 
+Object.assign( THREE.PropertyBinding.prototype, {
+
+	// these are used to "bind" a nonexistent property
+	_getValue_unavailable: function() {},
+	_setValue_unavailable: function() {},
+
+	// initial state of these methods that calls 'bind'
+	_getValue_unbound: THREE.PropertyBinding.prototype.getValue,
+	_setValue_unbound: THREE.PropertyBinding.prototype.setValue
+
+} );
 
 THREE.PropertyBinding.parseTrackName = function( trackName ) {
 
@@ -289,7 +355,7 @@ THREE.PropertyBinding.parseTrackName = function( trackName ) {
 	//	  .bone[Armature.DEF_cog].position
 	// created and tested via https://regex101.com/#javascript
 
-	var re = /^(([\w]+\/)*)([\w-\d]+)?(\.([\w]+)(\[([\w\d\[\]\_. ]+)\])?)?(\.([\w.]+)(\[([\w\d\[\]\_. ]+)\])?)$/; 
+	var re = /^(([\w]+\/)*)([\w-\d]+)?(\.([\w]+)(\[([\w\d\[\]\_. ]+)\])?)?(\.([\w.]+)(\[([\w\d\[\]\_. ]+)\])?)$/;
 	var matches = re.exec(trackName);
 
 	if( ! matches ) {
@@ -375,7 +441,7 @@ THREE.PropertyBinding.findNode = function( root, nodeName ) {
 
 			}
 
-			return null;	
+			return null;
 
 		};
 

+ 202 - 0
src/animation/PropertyMixer.js

@@ -0,0 +1,202 @@
+/**
+ *
+ * Buffered scene graph property that allows weighted accumulation.
+ *
+ *
+ * @author Ben Houston / http://clara.io/
+ * @author David Sarno / http://lighthaus.us/
+ * @author tschw
+ */
+
+THREE.PropertyMixer = function ( rootNode, path, typeName, valueSize ) {
+
+	this.binding = new THREE.PropertyBinding( rootNode, path );
+	this.valueSize = valueSize;
+
+	var bufferType = Float64Array,
+		mixFunction;
+
+	switch ( typeName ) {
+
+		case 'quaternion':			mixFunction = this._slerp;		break;
+
+		case 'string':
+		case 'bool':
+
+			bufferType = Array,		mixFunction = this._select;		break;
+
+		default:					mixFunction = this._lerp;
+
+	}
+
+	this.buffer = new bufferType( valueSize * 4 );
+	// layout: [ incoming | accu0 | accu1 | orig ]
+	//
+	// interpolators can use .buffer as their .result
+	// the data then goes to 'incoming'
+	//
+	// 'accu0' and 'accu1' are used frame-interleaved for
+	// the cumulative result and are compared to detect
+	// changes
+	//
+	// 'orig' stores the original state of the property
+
+	this._mixBufferRegion = mixFunction;
+
+	this.cumulativeWeight = 0;
+
+	this.referenceCount = 0;
+
+};
+
+THREE.PropertyMixer.prototype = {
+
+	constructor: THREE.PropertyMixer,
+
+	// accumulate data in the 'incoming' region into 'accu<i>'
+	accumulate: function( accuIndex, weight ) {
+
+		// note: happily accumulating nothing when weight = 0, the caller knows
+		// the weight and shouldn't have made the call in the first place
+
+		var buffer = this.buffer,
+			stride = this.valueSize,
+			offset = accuIndex * stride + stride,
+
+			currentWeight = this.cumulativeWeight;
+
+		if ( currentWeight === 0 ) {
+
+			// accuN := incoming * weight
+
+			for ( var i = 0; i !== stride; ++ i ) {
+
+				buffer[ offset + i ] = buffer[ i ];
+
+			}
+
+			currentWeight = weight;
+
+		} else {
+
+			// accuN := accuN + incoming * weight
+
+			currentWeight += weight;
+			var mix = weight / currentWeight;
+			this._mixBufferRegion( buffer, offset, 0, mix, stride );
+
+		}
+
+		this.cumulativeWeight = currentWeight;
+
+	},
+
+	// apply the state of 'accu<i>' to the binding when accus differ
+	apply: function( accuIndex ) {
+
+		var stride = this.valueSize,
+			buffer = this.buffer,
+			offset = accuIndex * stride + stride,
+
+			weight = this.cumulativeWeight,
+
+			binding = this.binding;
+
+		this.cumulativeWeight = 0;
+
+		if ( weight < 1 ) {
+
+			// accuN := accuN + original * ( 1 - cumulativeWeight )
+
+			var originalValueOffset = stride * 3;
+
+			this._mixBufferRegion(
+					buffer, offset, originalValueOffset, 1 - weight, stride );
+
+		}
+
+		for ( var i = stride, e = stride + stride; i !== e; ++ i ) {
+
+			if ( buffer[ i ] !== buffer[ i + stride ] ) {
+
+				// value has changed -> update scene graph
+
+				binding.setValue( buffer, offset );
+				break;
+
+			}
+
+		}
+
+	},
+
+	// remember the state of the bound property and copy it to both accus
+	saveOriginalState: function() {
+
+		var binding = this.binding;
+
+		var buffer = this.buffer,
+			stride = this.valueSize,
+
+			originalValueOffset = stride * 3;
+
+		binding.getValue( buffer, originalValueOffset );
+
+		// accu[0..1] := orig -- initially detect changes against the original
+		for ( var i = stride, e = originalValueOffset; i !== e; ++ i ) {
+
+			buffer[ i ] = buffer[ originalValueOffset + ( i % stride ) ];
+
+		}
+
+		this.cumulativeWeight = 0;
+
+	},
+
+	// apply the state previously taken via 'saveOriginalState' to the binding
+	restoreOriginalState: function() {
+
+		var originalValueOffset = this.valueSize * 3;
+		this.binding.setValue( this.buffer, originalValueOffset );
+
+	},
+
+
+	// mix functions
+
+	_select: function( buffer, dstOffset, srcOffset, t, stride ) {
+
+		if ( t >= 0.5 ) {
+
+			for ( var i = 0; i !== stride; ++ i ) {
+
+				buffer[ dstOffset + i ] = buffer[ srcOffset + i ];
+
+			}
+
+		}
+
+	},
+
+	_slerp: function( buffer, dstOffset, srcOffset, t, stride ) {
+
+		THREE.AnimationUtils.slerp(
+				buffer, dstOffset, buffer, dstOffset, srcOffset, t );
+
+	},
+
+	_lerp: function( buffer, dstOffset, srcOffset, t, stride ) {
+
+		var s = 1 - t;
+
+		for ( var i = 0; i !== stride; ++ i ) {
+
+			var j = dstOffset + i;
+
+			buffer[ j ] = buffer[ j ] * s + buffer[ srcOffset + i ] * t;
+
+		}
+
+	}
+
+};

+ 25 - 0
src/animation/interpolants/DiscreteInterpolant.js

@@ -0,0 +1,25 @@
+/**
+ *
+ * Interpolant that yields the keyframe value at the start of its interval.
+ *
+ *
+ * @author tschw
+ */
+
+THREE.DiscreteInterpolant = function( times, values, stride, result ) {
+
+	THREE.Interpolant.call( this, times, values, stride, result );
+
+};
+
+Object.assign( THREE.DiscreteInterpolant.prototype, THREE.Interpolant.prototype, {
+
+	constructor: THREE.DiscreteInterpolant,
+
+	_interpolate: function( i1, t0, t, t1 ) {
+
+		return this._copyKeyframe( i1 - 1 );
+
+	}
+
+} );

+ 44 - 0
src/animation/interpolants/LinearInterpolant.js

@@ -0,0 +1,44 @@
+/**
+ *
+ * Interpolant the returns a time-proportional mix of the keyframe values of
+ * the surrounding interval.
+ *
+ *
+ * @author tschw
+ */
+
+THREE.LinearInterpolant = function( times, values, stride, result ) {
+
+	THREE.Interpolant.call( this, times, values, stride, result );
+
+};
+
+Object.assign( THREE.LinearInterpolant.prototype, THREE.Interpolant.prototype, {
+
+	constructor: THREE.LinearInterpolant,
+
+	_interpolate: function( i1, t0, t, t1 ) {
+
+		var values = this.values,
+			stride = this.stride,
+			result = this.result,
+
+			offset1 = i1 * stride,
+			offset0 = offset1 - stride,
+
+			weight1 = ( t - t0 ) / ( t1 - t0 ),
+			weight0 = 1 - weight1;
+
+		for ( var i = 0; i !== stride; ++ i ) {
+
+			result[ i ] =
+					values[ offset0 + i ] * weight0 +
+					values[ offset1 + i ] * weight1;
+
+		}
+
+		return result;
+
+	}
+
+} );

+ 40 - 0
src/animation/interpolants/SlerpInterpolant.js

@@ -0,0 +1,40 @@
+/**
+ *
+ * Spherical linear quaternion interpolant.
+ *
+ *
+ * @author tschw
+ */
+
+THREE.SlerpInterpolant = function( times, values, stride, result ) {
+
+	THREE.Interpolant.call( this, times, values, stride, result );
+
+};
+
+Object.assign( THREE.SlerpInterpolant.prototype, THREE.Interpolant.prototype, {
+
+	constructor: THREE.SlerpInterpolant,
+
+	_interpolate: function( i1, t0, t, t1 ) {
+
+		var values = this.values,
+			stride = this.stride,
+			result = this.result,
+
+			offset = i1 * stride,
+
+			alpha = ( t - t0 ) / ( t1 - t0 );
+
+		for ( var end = offset + stride; offset !== end; offset += 4 ) {
+
+			THREE.AnimationUtils.slerp(
+					result, 0, values, offset - stride, offset, alpha );
+
+		}
+
+		return result;
+
+	}
+
+} );

+ 103 - 0
src/animation/interpolants/SmoothInterpolant.js

@@ -0,0 +1,103 @@
+/**
+ *
+ * Cubic hermite spline interpolant.
+ *
+ *
+ * @author tschw
+ */
+
+THREE.SmoothInterpolant = function( times, values, stride, result ) {
+
+	THREE.Interpolant.call( this, times, values, stride, result );
+
+};
+
+Object.assign( THREE.SmoothInterpolant.prototype, THREE.Interpolant.prototype, {
+
+	constructor: THREE.SmoothInterpolant,
+
+	DefaultParameters: {
+
+		zeroVelocityAtStart: false,
+		zeroVelocityAtEnd: false
+
+	},
+
+	_intervalChanged: function( i1, t0, t1 ) {
+
+		var times = this.times,
+			iPrev = i1 - 2,
+			iNext = i1 + 1,
+
+			tPrev = times[ iPrev ],
+			tNext = times[ iNext ];
+
+		if ( tPrev === undefined ) {
+
+			iPrev = i1;
+
+			tPrev = this._getParameters().zeroVelocityAtStart ?
+					2 * t0 - t1 : // yields f'(t0) = 0, IOW accelerates
+					t1; // yields f''(t0) = 0, IOW turns into a straight line
+
+		}
+
+		if ( tNext === undefined ) {
+
+			iNext = i1 - 1;
+			tNext = this._getParameters().zeroVelocityAtEnd ?
+					2 * t1 - t0 : // yields f'(tN) = 0, IOW decelerates
+					t0; // yields f''(tN) = 0, IOW turns into a straight line
+
+		}
+
+		var halfDt = ( t1 - t0 ) * 0.5,
+			stride = this.stride;
+
+		this.weightPrev = halfDt / ( t0 - tPrev );
+		this.weightNext = halfDt / ( tNext - t1 );
+		this.offsetPrev = iPrev * stride;
+		this.offsetNext = iNext * stride;
+
+	},
+
+	_interpolate: function( i1, t0, t, t1 ) {
+
+		var times = this.times,
+			values = this.values,
+			stride = this.stride,
+			result = this.result,
+
+			o1 = i1 * stride,		o0 = o1 - stride,
+			oP = this.offsetPrev, 	oN = this.offsetNext,
+
+			wP = this.weightPrev, wN = this.weightNext,
+
+			p = ( t - t0 ) / ( t1 - t0 ),
+			pp = p * p,
+			ppp = pp * p;
+
+		// evaluate polynomials
+
+		var sP = - wP * ppp + 2 * wP * pp  - wP * p;
+		var s0 = ( 1 + wP ) * ppp + ( -1.5 - 2 * wP ) * pp + ( -0.5 + wP ) * p + 1;
+		var s1 = ( -1 - wN ) * ppp + ( 1.5 + wN ) * pp + 0.5 * p;
+		var sN = wN * ppp - wN * pp;
+
+		// mix down
+
+		for ( var i = 0; i !== stride; ++ i ) {
+
+			result[ i ] =
+					sP * values[ oP + i ] +
+					s0 * values[ o0 + i ] +
+					s1 * values[ o1 + i ] +
+					sN * values[ oN + i ];
+
+		}
+
+		return result;
+
+	}
+
+} );

+ 17 - 48
src/animation/tracks/BooleanKeyframeTrack.js

@@ -1,64 +1,33 @@
 /**
  *
- * A Track that interpolates Boolean
- * 
+ * A Track of Boolean keyframe values.
+ *
+ *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
 
-THREE.BooleanKeyframeTrack = function ( name, keys ) {
-
-	THREE.KeyframeTrack.call( this, name, keys );
-
-	// local cache of value type to avoid allocations during runtime.
-	this.result = this.keys[0].value;
-
-};
-
-THREE.BooleanKeyframeTrack.prototype = Object.create( THREE.KeyframeTrack.prototype );
-
-THREE.BooleanKeyframeTrack.prototype.constructor = THREE.BooleanKeyframeTrack;
-
-THREE.BooleanKeyframeTrack.prototype.setResult = function( value ) {
-
-	this.result = value;
+THREE.BooleanKeyframeTrack = function ( name, times, values ) {
 
-};
-
-// memoization of the lerp function for speed.
-// NOTE: Do not optimize as a prototype initialization closure, as value0 will be different on a per class basis.
-THREE.BooleanKeyframeTrack.prototype.lerpValues = function( value0, value1, alpha ) {
-
-	return ( alpha < 1.0 ) ? value0 : value1;
-
-};
-
-THREE.BooleanKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
-
-	return ( value0 === value1 );
+	THREE.KeyframeTrack.call( this, name, times, values );
 
 };
 
-THREE.BooleanKeyframeTrack.prototype.clone = function() {
+Object.assign( THREE.BooleanKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
 
-	var clonedKeys = [];
+	constructor: THREE.BooleanKeyframeTrack,
 
-	for( var i = 0; i < this.keys.length; i ++ ) {
-		
-		var key = this.keys[i];
-		clonedKeys.push( {
-			time: key.time,
-			value: key.value
-		} );
-	}
+	ValueTypeName: 'bool',
+	ValueBufferType: Array,
 
-	return new THREE.BooleanKeyframeTrack( this.name, clonedKeys );
+	DefaultInterpolation: THREE.IntepolateDiscrete,
 
-};
+	InterpolantFactoryMethodLinear: undefined,
+	InterpolantFactoryMethodSmooth: undefined
 
-THREE.BooleanKeyframeTrack.parse = function( json ) {
+	// Note: Actually this track could have a optimized / compressed
+	// representation of a single value and a custom interpolant that
+	// computes "firstValue ^ isOdd( index )".
 
-	return new THREE.BooleanKeyframeTrack( json.name, json.keys );
-
-};
- 
+} );

+ 14 - 57
src/animation/tracks/ColorKeyframeTrack.js

@@ -1,74 +1,31 @@
 /**
  *
- * A Track that interpolates Color
- * 
+ * A Track of keyframe values that represent color.
+ *
+ *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
 
-THREE.ColorKeyframeTrack = function ( name, keys ) {
-
-	THREE.KeyframeTrack.call( this, name, keys );
+THREE.ColorKeyframeTrack = function ( name, times, values, interpolation ) {
 
-	// local cache of value type to avoid allocations during runtime.
-	this.result = this.keys[0].value.clone();
+	THREE.KeyframeTrack.call( this, name, keys, interpolation );
 
 };
 
-THREE.ColorKeyframeTrack.prototype = Object.create( THREE.KeyframeTrack.prototype );
+Object.assign( THREE.ColorKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
 
-THREE.ColorKeyframeTrack.prototype.constructor = THREE.ColorKeyframeTrack;
+	constructor: THREE.ColorKeyframeTrack,
 
-THREE.ColorKeyframeTrack.prototype.setResult = function( value ) {
-
-	this.result.copy( value );
-
-};
+	ValueTypeName: 'color'
 
-// memoization of the lerp function for speed.
-// NOTE: Do not optimize as a prototype initialization closure, as value0 will be different on a per class basis.
-THREE.ColorKeyframeTrack.prototype.lerpValues = function( value0, value1, alpha ) {
+	// ValueBufferType is inherited
 
-	return value0.lerp( value1, alpha );
+	// DefaultInterpolation is inherited
 
-};
-
-THREE.ColorKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
-
-	return value0.equals( value1 );
-
-};
 
-THREE.ColorKeyframeTrack.prototype.clone = function() {
+	// Note: Very basic implementation and nothing special yet.
+	// However, this is the place for color space parameterization.
 
-	var clonedKeys = [];
-
-	for( var i = 0; i < this.keys.length; i ++ ) {
-		
-		var key = this.keys[i];
-		clonedKeys.push( {
-			time: key.time,
-			value: key.value.clone()
-		} );
-	}
-
-	return new THREE.ColorKeyframeTrack( this.name, clonedKeys );
-
-};
-
-THREE.ColorKeyframeTrack.parse = function( json ) {
-
-	var keys = [];
-
-	for( var i = 0; i < json.keys.length; i ++ ) {
-		var jsonKey = json.keys[i];
-		keys.push( {
-			value: new THREE.Color().fromArray( jsonKey.value ),
-			time: jsonKey.time
-		} );
-	}
-
-	return new THREE.ColorKeyframeTrack( json.name, keys );
-
-};
- 
+} );

+ 11 - 49
src/animation/tracks/NumberKeyframeTrack.js

@@ -1,64 +1,26 @@
 /**
  *
- * A Track that interpolates Numbers
- * 
+ * A Track of numeric keyframe values.
+ *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
 
-THREE.NumberKeyframeTrack = function ( name, keys ) {
-
-	THREE.KeyframeTrack.call( this, name, keys );
-
-	// local cache of value type to avoid allocations during runtime.
-	this.result = this.keys[0].value;
-
-};
-
-THREE.NumberKeyframeTrack.prototype = Object.create( THREE.KeyframeTrack.prototype );
-
-THREE.NumberKeyframeTrack.prototype.constructor = THREE.NumberKeyframeTrack;
-
-THREE.NumberKeyframeTrack.prototype.setResult = function( value ) {
-
-	this.result = value;
-
-};
-
-// memoization of the lerp function for speed.
-// NOTE: Do not optimize as a prototype initialization closure, as value0 will be different on a per class basis.
-THREE.NumberKeyframeTrack.prototype.lerpValues = function( value0, value1, alpha ) {
-
-	return value0 * ( 1 - alpha ) + value1 * alpha;
-
-};
+THREE.NumberKeyframeTrack = function ( name, times, values, interpolation ) {
 
-THREE.NumberKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
-
-	return ( value0 === value1 );
+	THREE.KeyframeTrack.call( this, name, times, values, interpolation );
 
 };
 
-THREE.NumberKeyframeTrack.prototype.clone = function() {
-
-	var clonedKeys = [];
+Object.assign( THREE.NumberKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
 
-	for( var i = 0; i < this.keys.length; i ++ ) {
-		
-		var key = this.keys[i];
-		clonedKeys.push( {
-			time: key.time,
-			value: key.value
-		} );
-	}
+	constructor: THREE.NumberKeyframeTrack,
 
-	return new THREE.NumberKeyframeTrack( this.name, clonedKeys );
+	ValueTypeName: 'number',
 
-};
+	// ValueBufferType is inherited
 
-THREE.NumberKeyframeTrack.parse = function( json ) {
+	// DefaultInterpolation is inherited
 
-	return new THREE.NumberKeyframeTrack( json.name, json.keys );
-
-};
- 
+} );

+ 16 - 67
src/animation/tracks/QuaternionKeyframeTrack.js

@@ -1,86 +1,35 @@
 /**
  *
- * A Track that interpolates Quaternion
- * 
+ * A Track of quaternion keyframe values.
+ *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
 
-THREE.QuaternionKeyframeTrack = function ( name, keys ) {
-
-	THREE.KeyframeTrack.call( this, name, keys );
-
-	// local cache of value type to avoid allocations during runtime.
-	this.result = this.keys[0].value.clone();
-
-};
- 
-THREE.QuaternionKeyframeTrack.prototype = Object.create( THREE.KeyframeTrack.prototype );
-
-THREE.QuaternionKeyframeTrack.prototype.constructor = THREE.QuaternionKeyframeTrack;
+THREE.QuaternionKeyframeTrack = function ( name, times, values, interpolation ) {
 
-THREE.QuaternionKeyframeTrack.prototype.setResult = function( value ) {
-
-	this.result.copy( value );
+	THREE.KeyframeTrack.call( this, name, times, values, interpolation );
 
 };
 
-// memoization of the lerp function for speed.
-// NOTE: Do not optimize as a prototype initialization closure, as value0 will be different on a per class basis.
-THREE.QuaternionKeyframeTrack.prototype.lerpValues = function( value0, value1, alpha ) {
-
-	return value0.slerp( value1, alpha );
-
-};
+Object.assign( THREE.QuaternionKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
 
-THREE.QuaternionKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
+	constructor: THREE.QuaternionKeyframeTrack,
 
-	return value0.equals( value1 );
+	ValueTypeName: 'quaternion',
 
-};
+	// ValueBufferType is inherited
 
-THREE.QuaternionKeyframeTrack.prototype.multiply = function( quat ) {
+	DefaultInterpolation: THREE.InterpolateLinear,
 
-	for( var i = 0; i < this.keys.length; i ++ ) {
+	InterpolantFactoryMethodLinear: function( result ) {
 
-		this.keys[i].value.multiply( quat );
-		
-	}
+		return new THREE.SlerpInterpolant(
+				this.times, this.values, this.getValueSize(), result );
 
-	return this;
+	},
 
-};
+	InterpolantFactoryMethodSmooth: undefined // not yet implemented
 
-THREE.QuaternionKeyframeTrack.prototype.clone = function() {
-
-	var clonedKeys = [];
-
-	for( var i = 0; i < this.keys.length; i ++ ) {
-		
-		var key = this.keys[i];
-		clonedKeys.push( {
-			time: key.time,
-			value: key.value.clone()
-		} );
-	}
-
-	return new THREE.QuaternionKeyframeTrack( this.name, clonedKeys );
-
-};
-
-THREE.QuaternionKeyframeTrack.parse = function( json ) {
-
-	var keys = [];
-
-	for( var i = 0; i < json.keys.length; i ++ ) {
-		var jsonKey = json.keys[i];
-		keys.push( {
-			value: new THREE.Quaternion().fromArray( jsonKey.value ),
-			time: jsonKey.time
-		} );
-	}
-
-	return new THREE.QuaternionKeyframeTrack( json.name, keys );
-
-};
- 
+} );

+ 13 - 47
src/animation/tracks/StringKeyframeTrack.js

@@ -1,64 +1,30 @@
 /**
  *
  * A Track that interpolates Strings
- * 
+ *
+ *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
 
-THREE.StringKeyframeTrack = function ( name, keys ) {
-
-	THREE.KeyframeTrack.call( this, name, keys );
-
-	// local cache of value type to avoid allocations during runtime.
-	this.result = this.keys[0].value;
-
-};
-
-THREE.StringKeyframeTrack.prototype = Object.create( THREE.KeyframeTrack.prototype );
-
-THREE.StringKeyframeTrack.prototype.constructor = THREE.StringKeyframeTrack;
-
-THREE.StringKeyframeTrack.prototype.setResult = function( value ) {
-
-	this.result = value;
+THREE.StringKeyframeTrack = function ( name, times, values, interpolation ) {
 
-};
-
-// memoization of the lerp function for speed.
-// NOTE: Do not optimize as a prototype initialization closure, as value0 will be different on a per class basis.
-THREE.StringKeyframeTrack.prototype.lerpValues = function( value0, value1, alpha ) {
-
-	return ( alpha < 1.0 ) ? value0 : value1;
-
-};
-
-THREE.StringKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
-
-	return ( value0 === value1 );
+	THREE.KeyframeTrack.call( this, name, times, values, interpolation );
 
 };
 
-THREE.StringKeyframeTrack.prototype.clone = function() {
+Object.assign( THREE.StringKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
 
-	var clonedKeys = [];
+	constructor: THREE.StringKeyframeTrack,
 
-	for( var i = 0; i < this.keys.length; i ++ ) {
-		
-		var key = this.keys[i];
-		clonedKeys.push( {
-			time: key.time,
-			value: key.value
-		} );
-	}
+	ValueTypeName: 'string',
+	ValueBufferType: Array,
 
-	return new THREE.StringKeyframeTrack( this.name, clonedKeys );
+	DefaultInterpolation: THREE.IntepolateDiscrete,
 
-};
+	InterpolantFactoryMethodLinear: undefined,
 
-THREE.StringKeyframeTrack.parse = function( json ) {
+	InterpolantFactoryMethodSmooth: undefined
 
-	return new THREE.StringKeyframeTrack( json.name, json.keys );
-
-};
- 
+} );

+ 12 - 62
src/animation/tracks/VectorKeyframeTrack.js

@@ -1,77 +1,27 @@
 /**
  *
- * A Track that interpolates Vectors
- * 
+ * A Track of vectored keyframe values.
+ *
+ *
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
 
-THREE.VectorKeyframeTrack = function ( name, keys ) {
-
-	THREE.KeyframeTrack.call( this, name, keys );
-
-	// local cache of value type to avoid allocations during runtime.
-	this.result = this.keys[0].value.clone();
-
-};
-
-THREE.VectorKeyframeTrack.prototype = Object.create( THREE.KeyframeTrack.prototype );
-
-THREE.VectorKeyframeTrack.prototype.constructor = THREE.VectorKeyframeTrack;
-
-THREE.VectorKeyframeTrack.prototype.setResult = function( value ) {
-
-	this.result.copy( value );
-
-};
-
-// memoization of the lerp function for speed.
-// NOTE: Do not optimize as a prototype initialization closure, as value0 will be different on a per class basis.
-THREE.VectorKeyframeTrack.prototype.lerpValues = function( value0, value1, alpha ) {
-
-	return value0.lerp( value1, alpha );
-
-};
-
-THREE.VectorKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
-
-	return value0.equals( value1 );
+THREE.VectorKeyframeTrack = function ( name, times, values, interpolation ) {
 
-};
-
-THREE.VectorKeyframeTrack.prototype.clone = function() {
-
-	var clonedKeys = [];
-
-	for( var i = 0; i < this.keys.length; i ++ ) {
-		
-		var key = this.keys[i];
-		clonedKeys.push( {
-			time: key.time,
-			value: key.value.clone()
-		} );
-	}
-
-	return new THREE.VectorKeyframeTrack( this.name, clonedKeys );
+	THREE.KeyframeTrack.call( this, name, times, values, interpolation );
 
 };
 
-THREE.VectorKeyframeTrack.parse = function( json ) {
+Object.assign( THREE.VectorKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
 
-	var elementCount = json.keys[0].value.length;
-	var valueType = THREE[ 'Vector' + elementCount ];
+	constructor: THREE.VectorKeyframeTrack,
 
-	var keys = [];
+	ValueTypeName: 'vector'
 
-	for( var i = 0; i < json.keys.length; i ++ ) {
-		var jsonKey = json.keys[i];
-		keys.push( {
-			value: new valueType().fromArray( jsonKey.value ),
-			time: jsonKey.time
-		} );
-	}
+	// ValueBufferType is inherited
 
-	return new THREE.VectorKeyframeTrack( json.name, keys );
+	// DefaultInterpolation is inherited
 
-};
- 
+} );

+ 9 - 3
utils/build/includes/common.json

@@ -38,13 +38,19 @@
 	"src/animation/AnimationClip.js",
 	"src/animation/AnimationMixer.js",
 	"src/animation/AnimationUtils.js",
+	"src/animation/Interpolant.js",
 	"src/animation/KeyframeTrack.js",
 	"src/animation/PropertyBinding.js",
-	"src/animation/tracks/VectorKeyframeTrack.js",
-	"src/animation/tracks/QuaternionKeyframeTrack.js",
-	"src/animation/tracks/StringKeyframeTrack.js",
+	"src/animation/PropertyMixer.js",
 	"src/animation/tracks/BooleanKeyframeTrack.js",
 	"src/animation/tracks/NumberKeyframeTrack.js",
+	"src/animation/tracks/QuaternionKeyframeTrack.js",
+	"src/animation/tracks/StringKeyframeTrack.js",
+	"src/animation/tracks/VectorKeyframeTrack.js",
+	"src/animation/interpolants/DiscreteInterpolant.js",
+	"src/animation/interpolants/LinearInterpolant.js",
+	"src/animation/interpolants/SlerpInterpolant.js",
+	"src/animation/interpolants/SmoothInterpolant.js",
 	"src/cameras/Camera.js",
 	"src/cameras/CubeCamera.js",
 	"src/cameras/OrthographicCamera.js",