Browse Source

Merge pull request #7312 from tschw/Animation

Animation: Interpolants & extensibility overhaul.
Mr.doob 9 years ago
parent
commit
2142b8978d
50 changed files with 4821 additions and 1270 deletions
  1. 1 1
      examples/canvas_morphtargets_horse.html
  2. 23 28
      examples/js/BlendCharacter.js
  3. 3 12
      examples/js/BlendCharacterGui.js
  4. 26 12
      examples/js/MD2Character.js
  5. 3 4
      examples/js/MorphAnimMesh.js
  6. 1 1
      examples/js/UCSCharacter.js
  7. 1 1
      examples/webgl_animation_blend.html
  8. 1 1
      examples/webgl_animation_scene.html
  9. 1 8
      examples/webgl_animation_skinning_blending.html
  10. 401 15
      examples/webgl_animation_skinning_morph.html
  11. 1 1
      examples/webgl_lights_hemisphere.html
  12. 8 13
      examples/webgl_loader_json_blender.html
  13. 1 1
      examples/webgl_loader_scene.html
  14. 2 2
      examples/webgl_morphnormals.html
  15. 1 1
      examples/webgl_morphtargets_horse.html
  16. 1 1
      examples/webgl_shading_physical.html
  17. 12 7
      examples/webgl_shadowmap.html
  18. 42 5
      examples/webgl_shadowmap_performance.html
  19. 1 1
      examples/webgl_skinning_simple.html
  20. 1 1
      examples/webgl_terrain_dynamic.html
  21. 65 0
      src/Three.js
  22. 0 165
      src/animation/AnimationAction.js
  23. 181 155
      src/animation/AnimationClip.js
  24. 1258 99
      src/animation/AnimationMixer.js
  25. 370 0
      src/animation/AnimationObjectGroup.js
  26. 110 63
      src/animation/AnimationUtils.js
  27. 371 140
      src/animation/KeyframeTrack.js
  28. 390 196
      src/animation/PropertyBinding.js
  29. 203 0
      src/animation/PropertyMixer.js
  30. 17 46
      src/animation/tracks/BooleanKeyframeTrack.js
  31. 14 55
      src/animation/tracks/ColorKeyframeTrack.js
  32. 11 47
      src/animation/tracks/NumberKeyframeTrack.js
  33. 16 65
      src/animation/tracks/QuaternionKeyframeTrack.js
  34. 13 45
      src/animation/tracks/StringKeyframeTrack.js
  35. 12 60
      src/animation/tracks/VectorKeyframeTrack.js
  36. 257 0
      src/math/Interpolant.js
  37. 71 3
      src/math/Quaternion.js
  38. 152 0
      src/math/interpolants/CubicInterpolant.js
  39. 28 0
      src/math/interpolants/DiscreteInterpolant.js
  40. 42 0
      src/math/interpolants/LinearInterpolant.js
  41. 41 0
      src/math/interpolants/QuaternionLinearInterpolant.js
  42. 107 0
      test/unit/animation/AnimationObjectGroup.js
  43. 387 0
      test/unit/math/Interpolant.js
  44. 143 8
      test/unit/math/Quaternion.js
  45. 4 0
      test/unit/unittests_sources.html
  46. 3 0
      test/unit/unittests_three-math.html
  47. 6 2
      test/unit/unittests_three.html
  48. 2 0
      test/unit/unittests_three.min.html
  49. 10 4
      utils/build/includes/common.json
  50. 6 1
      utils/build/includes/math.json

+ 1 - 1
examples/canvas_morphtargets_horse.html

@@ -78,7 +78,7 @@
 					mixer = new THREE.AnimationMixer( mesh );
 					mixer = new THREE.AnimationMixer( mesh );
 
 
 					var clip = THREE.AnimationClip.CreateFromMorphTargetSequence( 'gallop', geometry.morphTargets, 30 );
 					var clip = THREE.AnimationClip.CreateFromMorphTargetSequence( 'gallop', geometry.morphTargets, 30 );
-					mixer.addAction( new THREE.AnimationAction( clip ).warpToDuration( 1 ) );
+					mixer.clipAction( clip ).setDuration( 1 ).play();
 
 
 				} );
 				} );
 
 

+ 23 - 28
examples/js/BlendCharacter.js

@@ -4,7 +4,6 @@
 
 
 THREE.BlendCharacter = function () {
 THREE.BlendCharacter = function () {
 
 
-	this.animations = {};
 	this.weightSchedule = [];
 	this.weightSchedule = [];
 	this.warpSchedule = [];
 	this.warpSchedule = [];
 
 
@@ -20,13 +19,13 @@ THREE.BlendCharacter = function () {
 
 
 			THREE.SkinnedMesh.call( scope, geometry, originalMaterial );
 			THREE.SkinnedMesh.call( scope, geometry, originalMaterial );
 
 
-			scope.mixer = new THREE.AnimationMixer( scope );
+			var mixer = new THREE.AnimationMixer( scope );
+			scope.mixer = mixer;
 
 
-			// Create the animations		
+			// Create the animations
 			for ( var i = 0; i < geometry.animations.length; ++ i ) {
 			for ( var i = 0; i < geometry.animations.length; ++ i ) {
 
 
-				var animName = geometry.animations[ i ].name;
-				scope.animations[ animName ] = geometry.animations[ i ];
+				mixer.clipAction( geometry.animations[ i ] );
 
 
 			}
 			}
 
 
@@ -45,49 +44,45 @@ THREE.BlendCharacter = function () {
 
 
 	this.play = function( animName, weight ) {
 	this.play = function( animName, weight ) {
 
 
-		this.mixer.removeAllActions();
-		
-		this.mixer.play( new THREE.AnimationAction( this.animations[ animName ] ) );
-
+		//console.log("play('%s', %f)", animName, weight);
+		return this.mixer.clipAction( animName ).
+				setEffectiveWeight( weight ).play();
 	};
 	};
 
 
 	this.crossfade = function( fromAnimName, toAnimName, duration ) {
 	this.crossfade = function( fromAnimName, toAnimName, duration ) {
 
 
-		this.mixer.removeAllActions();
- 
-		var fromAction = new THREE.AnimationAction( this.animations[ fromAnimName ] );
-		var toAction = new THREE.AnimationAction( this.animations[ toAnimName ] );
+		this.mixer.stopAllAction();
 
 
-		this.mixer.play( fromAction );
-		this.mixer.play( toAction );
+		var fromAction = this.play( fromAnimName, 1 );
+		var toAction = this.play( toAnimName, 1 );
 
 
-		this.mixer.crossFade( fromAction, toAction, duration, false );
+		fromAction.crossFadeTo( toAction, duration, false );
 
 
 	};
 	};
 
 
 	this.warp = function( fromAnimName, toAnimName, duration ) {
 	this.warp = function( fromAnimName, toAnimName, duration ) {
 
 
-		this.mixer.removeAllActions();
-
-		var fromAction = new THREE.AnimationAction( this.animations[ fromAnimName ] );
-		var toAction = new THREE.AnimationAction( this.animations[ toAnimName ] );
+		this.mixer.stopAllAction();
 
 
-		this.mixer.play( fromAction );
-		this.mixer.play( toAction );
+		var fromAction = this.play( fromAnimName, 1 );
+		var toAction = this.play( toAnimName, 1 );
 
 
-		this.mixer.crossFade( fromAction, toAction, duration, true );
+		fromAction.crossFadeTo( toAction, duration, true );
 
 
 	};
 	};
 
 
 	this.applyWeight = function( animName, weight ) {
 	this.applyWeight = function( animName, weight ) {
 
 
-		var action = this.mixer.findActionByName( animName );
-		if( action ) {
-			action.weight = weight;
-		}
+		this.mixer.clipAction( animName ).setEffectiveWeight( weight );
 
 
 	};
 	};
 
 
+	this.getWeight = function( animName ) {
+
+		return this.mixer.clipAction( animName ).getEffectiveWeight();
+
+	}
+
 	this.pauseAll = function() {
 	this.pauseAll = function() {
 
 
 		this.mixer.timeScale = 0;
 		this.mixer.timeScale = 0;
@@ -103,7 +98,7 @@ THREE.BlendCharacter = function () {
 
 
 	this.stopAll = function() {
 	this.stopAll = function() {
 
 
-		this.mixer.removeAllActions();
+		this.mixer.stopAllAction();
 
 
 	};
 	};
 
 

+ 3 - 12
examples/js/BlendCharacterGui.js

@@ -40,18 +40,9 @@ function BlendCharacterGui( blendMesh ) {
 
 
 	this.update = function( time ) {
 	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;
-		}
-		controls[ 'idle' ] = getWeight( 'idle' );
-		controls[ 'walk' ] = getWeight( 'walk' );
-		controls[ 'run' ] = getWeight( 'run' );
+		controls[ 'idle' ] = blendMesh.getWeight( 'idle' );
+		controls[ 'walk' ] = blendMesh.getWeight( 'walk' );
+		controls[ 'run' ] = blendMesh.getWeight( 'run' );
 
 
 	};
 	};
 
 

+ 26 - 12
examples/js/MD2Character.js

@@ -33,7 +33,6 @@ THREE.MD2Character = function () {
 
 
 		var weaponsTextures = [];
 		var weaponsTextures = [];
 		for ( var i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
 		for ( var i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
-
 		// SKINS
 		// SKINS
 
 
 		this.skinsBody = loadTextures( config.baseUrl + "skins/", config.skins );
 		this.skinsBody = loadTextures( config.baseUrl + "skins/", config.skins );
@@ -81,6 +80,21 @@ THREE.MD2Character = function () {
 				scope.weapons[ index ] = mesh;
 				scope.weapons[ index ] = mesh;
 				scope.meshWeapon = mesh;
 				scope.meshWeapon = mesh;
 
 
+
+				// the animation system requires unique names, so append the
+				// uuid of the source geometry:
+
+				var geometry = mesh.geometry,
+					animations = geometry.animations;
+
+				for ( var i = 0, n = animations.length; i !== n; ++ i ) {
+
+					var animation = animations[ i ];
+					animation.name += geometry.uuid;
+
+				}
+
+
 				checkLoadingComplete();
 				checkLoadingComplete();
 
 
 			}
 			}
@@ -154,17 +168,15 @@ THREE.MD2Character = function () {
 		if ( this.meshBody ) {
 		if ( this.meshBody ) {
 
 
 			if( this.meshBody.activeAction ) {
 			if( this.meshBody.activeAction ) {
-				scope.mixer.removeAction( this.meshBody.activeAction );
+				this.meshBody.activeAction.stop();
 				this.meshBody.activeAction = null;
 				this.meshBody.activeAction = null;
 			}
 			}
 
 
 			var clip = THREE.AnimationClip.findByName( this.meshBody.geometry.animations, clipName );
 			var clip = THREE.AnimationClip.findByName( this.meshBody.geometry.animations, clipName );
 			if( clip ) {
 			if( clip ) {
 
 
-				var action = new THREE.AnimationAction( clip, this.mixer.time ).setLocalRoot( this.meshBody );
-				scope.mixer.addAction( action );
-
-				this.meshBody.activeAction = action;
+				this.meshBody.activeAction =
+						this.mixer.clipAction( clip, this.meshBody ).play();
 
 
 			}
 			}
 
 
@@ -183,17 +195,19 @@ THREE.MD2Character = function () {
 		if ( scope.meshWeapon ) {
 		if ( scope.meshWeapon ) {
 
 
 			if( this.meshWeapon.activeAction ) {
 			if( this.meshWeapon.activeAction ) {
-				scope.mixer.removeAction( this.meshWeapon.activeAction );
+				this.meshWeapon.activeAction.stop();
 				this.meshWeapon.activeAction = null;
 				this.meshWeapon.activeAction = null;
 			}
 			}
 
 
-			var clip = THREE.AnimationClip.findByName( this.meshWeapon.geometry.animations, clipName );
-			if( clip ) {
+			var geometry = this.meshWeapon.geometry,
+				animations = geometry.animations;
 
 
-				var action = new THREE.AnimationAction( clip ).syncWith( this.meshBody.activeAction ).setLocalRoot( this.meshWeapon );
-				scope.mixer.addAction( action );
+			var clip = THREE.AnimationClip.findByName( animations, clipName + geometry.uuid );
+			if( clip ) {
 
 
-				this.meshWeapon.activeAction = action;
+				this.meshWeapon.activeAction =
+						this.mixer.clipAction( clip, this.meshWeapon ).
+							syncWith( this.meshBody.activeAction ).play();
 
 
 			}
 			}
 
 

+ 3 - 4
examples/js/MorphAnimMesh.js

@@ -31,7 +31,7 @@ THREE.MorphAnimMesh.prototype.playAnimation = function ( label, fps ) {
 
 
 	if( this.activeAction ) {
 	if( this.activeAction ) {
 
 
-		this.mixer.removeAction( this.activeAction );
+		this.activeAction.stop();
 		this.activeAction = null;
 		this.activeAction = null;
 		
 		
 	}
 	}
@@ -40,10 +40,9 @@ THREE.MorphAnimMesh.prototype.playAnimation = function ( label, fps ) {
 
 
 	if ( clip ) {
 	if ( clip ) {
 
 
-		var action = new THREE.AnimationAction( clip );
+		var action = this.mixer.clipAction( clip );
 		action.timeScale = ( clip.tracks.length * fps ) / clip.duration;
 		action.timeScale = ( clip.tracks.length * fps ) / clip.duration;
-		this.mixer.addAction( action );
-		this.activeAction = action;
+		this.activeAction = action.play();
 
 
 	} else {
 	} else {
 
 

+ 1 - 1
examples/js/UCSCharacter.js

@@ -55,7 +55,7 @@ THREE.UCSCharacter = function() {
 			mesh.castShadow = true;
 			mesh.castShadow = true;
 			mesh.receiveShadow = true;
 			mesh.receiveShadow = true;
 
 
-			scope.mixer.addAction( new THREE.AnimationAction( geometry.animations[0] ).setLocalRoot( mesh ) );
+			scope.mixer.clipAction( geometry.animations[0], mesh ).play();
 			
 			
 			scope.setSkin( 0 );
 			scope.setSkin( 0 );
 			
 			

+ 1 - 1
examples/webgl_animation_blend.html

@@ -133,7 +133,7 @@
 					var blendObject = scene.getObjectByName( 'tree-morph' );
 					var blendObject = scene.getObjectByName( 'tree-morph' );
 					var clip = blendObject.geometry.animations[0];
 					var clip = blendObject.geometry.animations[0];
 					mixer = new THREE.AnimationMixer( blendObject );
 					mixer = new THREE.AnimationMixer( blendObject );
-					mixer.addAction( new THREE.AnimationAction( clip ) );
+					mixer.clipAction( clip ).play();
 
 
 				} );
 				} );
 
 

+ 1 - 1
examples/webgl_animation_scene.html

@@ -131,7 +131,7 @@
 		
 		
 					mixer = new THREE.AnimationMixer( scene );
 					mixer = new THREE.AnimationMixer( scene );
 			
 			
-					mixer.addAction( new THREE.AnimationAction( sceneAnimationClip ) );
+					mixer.clipAction( sceneAnimationClip ).play();
 
 
 				} );
 				} );
 
 

+ 1 - 8
examples/webgl_animation_skinning_blending.html

@@ -156,14 +156,7 @@
 				var data = event.detail;
 				var data = event.detail;
 				for ( var i = 0; i < data.anims.length; ++i ) {
 				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];
-							}
-						}
-					}
+					blendMesh.applyWeight( data.anims[ i ], data.weights[ i ] );
 
 
 				}
 				}
 
 

+ 401 - 15
examples/webgl_animation_skinning_morph.html

@@ -22,6 +22,12 @@
 				padding: 5px;
 				padding: 5px;
 			}
 			}
 
 
+			#meminfo {
+				margin-top: 8px;
+				font-size: 10px;
+				display: none;
+			}
+
 			a {
 			a {
 				color: #0af;
 				color: #0af;
 			}
 			}
@@ -40,6 +46,7 @@
 		<div id="info">
 		<div id="info">
 		<a href="http://threejs.org" target="_blank">three.js</a> webgl - clip system
 		<a href="http://threejs.org" target="_blank">three.js</a> webgl - clip system
 		- knight by <a href="http://vimeo.com/36113323">apendua</a>
 		- knight by <a href="http://vimeo.com/36113323">apendua</a>
+			<div id="meminfo"></div>
 		</div>
 		</div>
 
 
 		<script src="../build/three.min.js"></script>
 		<script src="../build/three.min.js"></script>
@@ -59,9 +66,9 @@
 			var camera, scene;
 			var camera, scene;
 			var renderer;
 			var renderer;
 
 
-			var mesh, helper;
+			var mesh, mesh2, helper;
 
 
-			var mixer;
+			var mixer, facesClip, bonesClip;
 
 
 			var mouseX = 0, mouseY = 0;
 			var mouseX = 0, mouseY = 0;
 
 
@@ -70,6 +77,9 @@
 
 
 			var clock = new THREE.Clock();
 			var clock = new THREE.Clock();
 
 
+			var domMemInfo = document.getElementById( 'meminfo' ),
+				showMemInfo = false;
+
 			document.addEventListener( 'mousemove', onDocumentMouseMove, false );
 			document.addEventListener( 'mousemove', onDocumentMouseMove, false );
 
 
 			init();
 			init();
@@ -152,11 +162,11 @@
 
 
 					createScene( geometry, materials, 0, FLOOR, -300, 60 )
 					createScene( geometry, materials, 0, FLOOR, -300, 60 )
 
 
-				} );
+					// GUI
 
 
-				// GUI
+					initGUI();
 
 
-				initGUI();
+				} );
 
 
 				//
 				//
 
 
@@ -212,6 +222,7 @@
 				}
 				}
 
 
 				mesh = new THREE.SkinnedMesh( geometry, new THREE.MeshFaceMaterial( materials ) );
 				mesh = new THREE.SkinnedMesh( geometry, new THREE.MeshFaceMaterial( materials ) );
+				mesh.name = "Knight Mesh";
 				mesh.position.set( x, y - bb.min.y * s, z );
 				mesh.position.set( x, y - bb.min.y * s, z );
 				mesh.scale.set( s, s, s );
 				mesh.scale.set( s, s, s );
 				scene.add( mesh );
 				scene.add( mesh );
@@ -219,32 +230,395 @@
 				mesh.castShadow = true;
 				mesh.castShadow = true;
 				mesh.receiveShadow = true;
 				mesh.receiveShadow = true;
 
 
+
+				mesh2 = new THREE.SkinnedMesh( geometry, new THREE.MeshFaceMaterial( materials ) );
+				mesh2.name = "Lil' Bro Mesh";
+				mesh2.position.set( x - 240, y - bb.min.y * s, z + 500 );
+				mesh2.scale.set( s / 2, s / 2, s / 2 );
+				mesh2.rotation.y = THREE.Math.degToRad( 60 );
+
+				mesh2.visible = false;
+
+				mesh2.castShadow = true;
+				mesh2.receiveShadow = true;
+				scene.add( mesh2 );
+
 				helper = new THREE.SkeletonHelper( mesh );
 				helper = new THREE.SkeletonHelper( mesh );
 				helper.material.linewidth = 3;
 				helper.material.linewidth = 3;
 				helper.visible = false;
 				helper.visible = false;
 				scene.add( helper );
 				scene.add( helper );
 
 
-
-				var clipMorpher = THREE.AnimationClip.CreateFromMorphTargetSequence( 'facialExpressions', mesh.geometry.morphTargets, 3 );
-				var clipBones = geometry.animations[0];
-
 				mixer = new THREE.AnimationMixer( mesh );
 				mixer = new THREE.AnimationMixer( mesh );
-				mixer.addAction( new THREE.AnimationAction( clipMorpher ) );
-				mixer.addAction( new THREE.AnimationAction( clipBones ) );
+
+				bonesClip = geometry.animations[0];
+				facesClip = THREE.AnimationClip.CreateFromMorphTargetSequence( 'facialExpressions', mesh.geometry.morphTargets, 3 );
 			}
 			}
 
 
 			function initGUI() {
 			function initGUI() {
 
 
 				var API = {
 				var API = {
-					'show model'    : true,
-					'show skeleton' : false
+					'show model'    	: true,
+					'show skeleton'		: false,
+					'show 2nd model'	: false,
+					'show mem. info'	: false
 				};
 				};
 
 
 				var gui = new dat.GUI();
 				var gui = new dat.GUI();
 
 
-				gui.add( API, 'show model' ).onChange( function() { mesh.visible = API[ 'show model' ]; } );
+				gui.add( API, 'show model' ).onChange( function() {
+						mesh.visible = API[ 'show model' ];
+				} );
+
+				gui.add( API, 'show skeleton' ).onChange( function() {
+						helper.visible = API[ 'show skeleton' ];
+				} );
+
+				gui.add( API, 'show 2nd model' ).onChange( function() {
+						mesh2.visible = API[ 'show 2nd model' ];
+				} );
+
+
+				gui.add( API, 'show mem. info' ).onChange( function() {
+
+					showMemInfo = API[ 'show mem. info' ];
+					domMemInfo.style.display = showMemInfo ? 'block' : 'none';
+
+				} );
+
+				// utility function used for drop-down options lists in the GUI
+				var objectNames = function( objects ) {
+
+					var result = [];
+
+					for ( var i = 0, n = objects.length; i !== n; ++ i ) {
+
+						var obj = objects[ i ];
+						result.push( obj && obj.name || '&lt;null&gt;' );
+
+					}
+
+					return result;
+
+				};
+
+
+				// creates gui folder with tests / examples for the action API
+				var clipControl = function clipControl( gui, mixer, clip, rootObjects ) {
+
+					var folder = gui.addFolder( "Clip '" + clip.name + "'" ),
+
+						rootNames = objectNames( rootObjects ),
+						rootName = rootNames[ 0 ],
+						root = rootObjects[ 0 ],
+
+						action = null,
+
+						API = {
+
+							'play()': function play() {
+
+								action = mixer.clipAction( clip, root );
+								action.play();
+
+							},
+
+							'stop()': function() {
+
+								action = mixer.clipAction( clip, root );
+								action.stop();
+
+							},
+
+							'reset()': function() {
+
+								action = mixer.clipAction( clip, root );
+								action.reset();
+
+							},
+
+							get 'time ='() {
+
+								return action !== null ? action.time : 0;
+
+							},
+
+							set 'time ='( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.time = value;
+
+							},
+
+							get 'paused ='() {
+
+								return action !== null && action.paused;
+
+							},
+
+							set 'paused ='( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.paused = value;
+
+							},
+
+							get 'enabled ='() {
+
+								return action !== null && action.enabled;
+
+							},
+
+							set 'enabled ='( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.enabled = value;
+
+							},
+
+							get 'clamp ='() {
+
+								return action !== null ? action.clampWhenFinished : false;
+
+							},
+
+							set 'clamp ='( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.clampWhenFinished = value;
+
+							},
+
+							get 'isRunning() ='() {
+
+								return action !== null && action.isRunning();
+
+							},
+
+							set 'isRunning() ='( value ) {
+
+								alert( "Read only - this is the result of a method." );
+
+							},
+
+							'play delayed': function() {
+
+								action = mixer.clipAction( clip, root );
+								action.startAt( mixer.time + 0.5 ).play();
+
+							},
+
+							get 'weight ='() {
+
+								return action !== null ? action.weight : 1;
+
+							},
+
+							set 'weight ='( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.weight = value;
+
+							},
+
+							get 'eff. weight'() {
+
+								return action !== null ? action.getEffectiveWeight() : 1;
+
+							},
+
+							set 'eff. weight'( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.setEffectiveWeight( value );
+
+							},
+
+							'fade in': function() {
+
+								action = mixer.clipAction( clip, root );
+								action.reset().fadeIn( 0.25 ).play();
+
+							},
+
+							'fade out': function() {
+
+								action = mixer.clipAction( clip, root );
+								action.fadeOut( 0.25 ).play();
+
+							},
+
+							get 'timeScale ='() {
+
+								return ( action !== null ) ? action.timeScale : 1;
+
+							},
+
+							set 'timeScale ='( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.timeScale = value;
+
+							},
+
+							get 'eff.T.Scale'() {
+
+								return ( action !== null ) ? action.getEffectiveTimeScale() : 1;
+
+							},
+
+							set 'eff.T.Scale'( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.setEffectiveTimeScale( value );
+
+							},
+
+							'time warp': function() {
+
+								action = mixer.clipAction( clip, root );
+								var timeScaleNow = action.getEffectiveTimeScale();
+								var destTimeScale = timeScaleNow > 0 ? -1 : 1;
+								action.warp( timeScaleNow, destTimeScale, 4 ).play();
+
+							},
+
+							get 'loop mode'() {
+
+								return action !== null ? action.loop : THREE.LoopRepeat;
+
+							},
+
+							set 'loop mode'( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.loop = + value;
+
+							},
+
+							get 'repetitions'() {
+
+								return action !== null ? action.repetitions : Infinity;
+
+							},
+
+							set 'repetitions'( value ) {
+
+								action = mixer.clipAction( clip, root );
+								action.repetitions = + value;
+
+							},
+
+							get 'local root'() { return rootName; },
+
+							set 'local root'( value ) {
+
+								rootName = value;
+								root = rootObjects[ rootNames.indexOf( rootName ) ];
+
+								if ( action !== null ) {
+
+								// TODO
+
+								}
+
+							}
+
+						};
+
+					folder.add( API, 'play()' );
+					folder.add( API, 'stop()' );
+					folder.add( API, 'reset()' );
+					folder.add( API, 'time =', 0, clip.duration ).listen();
+					folder.add( API, 'paused =' ).listen();
+					folder.add( API, 'enabled =' ).listen();
+					folder.add( API, 'clamp =' );
+					folder.add( API, 'isRunning() =').listen();
+					folder.add( API, 'play delayed' );
+					folder.add( API, 'weight =', 0, 1 ).listen();
+					folder.add( API, 'eff. weight', 0, 1 ).listen();
+					folder.add( API, 'fade in' );
+					folder.add( API, 'fade out' );
+					folder.add( API, 'timeScale =', -2, 2).listen();
+					folder.add( API, 'eff.T.Scale', -2, 2).listen();
+					folder.add( API, 'time warp' );
+					folder.add( API, 'loop mode', {
+						"LoopOnce": THREE.LoopOnce,
+						"LoopRepeat": THREE.LoopRepeat,
+						"LoopPingPong": THREE.LoopPingPong
+					} );
+					folder.add( API, 'repetitions', 0, Infinity );
+					folder.add( API, 'local root', rootNames );
+
+				}; // function clipControl
+
+				// one folder per clip
+				clipControl( gui, mixer, bonesClip, [ null, mesh, mesh2 ] );
+				clipControl( gui, mixer, facesClip, [ null, mesh, mesh2 ] );
+
+				var memoryControl = function( gui, mixer, clips, rootObjects ) {
+
+					var clipNames = objectNames( clips ),
+						rootNames = objectNames( rootObjects );
+
+					var folder = gui.addFolder( "Memory Management" ),
+
+						clipName 	= clipNames[ 0 ],
+						clip 		= clips[ 0 ],
+
+						rootName 	= rootNames[ 0 ],
+						root		= rootObjects[ 0 ],
+
+						API = {
+
+							get 'clip'() { return clipName; },
+
+							set 'clip'( value ) {
+
+								clipName = value;
+								clip = clips[ clipNames.indexOf( clipName ) ];
+
+							},
+
+							get 'root'() { return rootName; },
+
+							set 'root'( value ) {
+
+								rootName = value;
+								root = rootObjects[ rootNames.indexOf( rootName ) ];
+
+							},
+
+							'uncache clip': function() {
+
+								mixer.uncacheClip( clip );
+
+							},
+
+							'uncache root': function() {
+
+								mixer.uncacheRoot( root );
+
+							},
+
+							'uncache action': function() {
+
+								mixer.uncacheAction( clip, root );
+
+							}
+
+						};
+
+					folder.add( API, 'clip', clipNames );
+					folder.add( API, 'root', rootNames );
+					folder.add( API, 'uncache root' );
+					folder.add( API, 'uncache clip' );
+					folder.add( API, 'uncache action' );
+
+				}
+
+				memoryControl( gui, mixer,
+						[ bonesClip, facesClip ], [ mesh, mesh2 ] );
 
 
-				gui.add( API, 'show skeleton' ).onChange( function() { helper.visible = API[ 'show skeleton' ]; } );
 
 
 			}
 			}
 
 
@@ -264,6 +638,18 @@
 				render();
 				render();
 				stats.update();
 				stats.update();
 
 
+				if ( showMemInfo ) {
+
+					var s = mixer.stats,
+						ciS = s.controlInterpolants;
+
+					domMemInfo.innerHTML =
+							s.actions.inUse + " / " + s.actions.total + " actions " +
+							s.bindings.inUse + " / " + s.bindings.total + " bindings " +
+							ciS.inUse + " / " + ciS.total + " control interpolants";
+
+				}
+
 			}
 			}
 
 
 			function render() {
 			function render() {

+ 1 - 1
examples/webgl_lights_hemisphere.html

@@ -214,7 +214,7 @@
 					scene.add( mesh );
 					scene.add( mesh );
 
 
 					var mixer = new THREE.AnimationMixer( mesh );
 					var mixer = new THREE.AnimationMixer( mesh );
-					mixer.addAction( new THREE.AnimationAction( geometry.animations[ 0 ] ).warpToDuration( 1 ) );
+					mixer.clipAction( geometry.animations[ 0 ] ).setDuration( 1 ).play();
 					mixers.push( mixer );
 					mixers.push( mixer );
 
 
 				} );
 				} );

+ 8 - 13
examples/webgl_loader_json_blender.html

@@ -59,7 +59,7 @@
 			var dae;
 			var dae;
 
 
 			var clock = new THREE.Clock();
 			var clock = new THREE.Clock();
-			var mixers = [];
+			var mixer;
 
 
 			// Collada model
 			// Collada model
 
 
@@ -102,6 +102,8 @@
 
 
 				// Add Blender exported Collada model
 				// Add Blender exported Collada model
 
 
+				mixer = new THREE.AnimationMixer( scene );
+
 				var loader = new THREE.JSONLoader();
 				var loader = new THREE.JSONLoader();
 				loader.load( 'models/animated/monster/monster.js', function ( geometry, materials ) {
 				loader.load( 'models/animated/monster/monster.js', function ( geometry, materials ) {
 
 
@@ -137,13 +139,10 @@
 
 
 						scene.add( mesh );
 						scene.add( mesh );
 						
 						
-						var mixer = new THREE.AnimationMixer( mesh );
-						mixer.addAction( new THREE.AnimationAction( geometry.animations[0] ).warpToDuration( 1 ) );
-
-						// random animation offset
-						mixer.update( 1000 * Math.random() );
-
-						mixers.push( mixer );
+						mixer.clipAction( geometry.animations[0], mesh )
+								.setDuration( 1 )			// one second
+								.startAt( - Math.random() )	// random phase (already running)
+								.play();					// let's go
 
 
 					}
 					}
 
 
@@ -203,11 +202,7 @@
 
 
 				THREE.AnimationHandler.update( delta );
 				THREE.AnimationHandler.update( delta );
 
 
-				for ( var i = 0; i < mixers.length; i ++ ) {
-
-					mixers[ i ].update( delta );
-
-				}
+				mixer.update( delta );
 
 
 
 
 				render();
 				render();

+ 1 - 1
examples/webgl_loader_scene.html

@@ -214,7 +214,7 @@
 							if( object.geometry && object.geometry.animations && object.geometry.animations.length > 0 ) {
 							if( object.geometry && object.geometry.animations && object.geometry.animations.length > 0 ) {
 
 
 								var mixer = new THREE.AnimationMixer( object );
 								var mixer = new THREE.AnimationMixer( object );
-								mixer.addAction( new THREE.AnimationAction( object.geometry.animations[0] ) );
+								mixer.clipAction( object.geometry.animations[0] ).play();
 								mixers.push( mixer );
 								mixers.push( mixer );
 
 
 							}
 							}

+ 2 - 2
examples/webgl_morphnormals.html

@@ -93,7 +93,7 @@
 					scene.add( mesh );
 					scene.add( mesh );
 
 
 					var mixer = new THREE.AnimationMixer( mesh );
 					var mixer = new THREE.AnimationMixer( mesh );
-					mixer.addAction( new THREE.AnimationAction( geometry.animations[ 0 ] ).warpToDuration( 1 ) );
+					mixer.clipAction( geometry.animations[ 0 ] ).setDuration( 1 ).play();
 
 
 					mixers.push( mixer );
 					mixers.push( mixer );
 
 
@@ -120,7 +120,7 @@
 					scene.add( mesh );
 					scene.add( mesh );
 
 
 					var mixer = new THREE.AnimationMixer( mesh );
 					var mixer = new THREE.AnimationMixer( mesh );
-					mixer.addAction( new THREE.AnimationAction( geometry.animations[ 0 ] ).warpToDuration( 1 ) );
+					mixer.clipAction( geometry.animations[ 0 ] ).setDuration( 1 ).play();
 
 
 					mixers.push( mixer );
 					mixers.push( mixer );
 
 

+ 1 - 1
examples/webgl_morphtargets_horse.html

@@ -72,7 +72,7 @@
 					mixer = new THREE.AnimationMixer( mesh );
 					mixer = new THREE.AnimationMixer( mesh );
 
 
 					var clip = THREE.AnimationClip.CreateFromMorphTargetSequence( 'gallop', geometry.morphTargets, 30 );
 					var clip = THREE.AnimationClip.CreateFromMorphTargetSequence( 'gallop', geometry.morphTargets, 30 );
-					mixer.addAction( new THREE.AnimationAction( clip ).warpToDuration( 1 ) );
+					mixer.clipAction( clip ).setDuration( 1 ).play();
 
 
 				} );
 				} );
 
 

+ 1 - 1
examples/webgl_shading_physical.html

@@ -275,7 +275,7 @@
 
 
 					mixer = new THREE.AnimationMixer( mesh );
 					mixer = new THREE.AnimationMixer( mesh );
 
 
-					mixer.addAction( new THREE.AnimationAction( geometry.animations[0] ).warpToDuration( 10 ) );
+					mixer.clipAction( geometry.animations[0] ).setDuration( 10 ).play();
 
 
 					var s = 200;
 					var s = 200;
 					mesh.scale.set( s, s, s );
 					mesh.scale.set( s, s, s );

+ 12 - 7
examples/webgl_shadowmap.html

@@ -64,7 +64,7 @@
 
 
 			var sceneHUD, cameraOrtho, hudMesh;
 			var sceneHUD, cameraOrtho, hudMesh;
 
 
-			var morphs = [];
+			var mixer, morphs = [];
 
 
 			var light;
 			var light;
 
 
@@ -306,6 +306,8 @@
 
 
 				// MORPHS
 				// MORPHS
 
 
+				mixer = new THREE.AnimationMixer( scene );
+
 				function addMorph( geometry, speed, duration, x, y, z, fudgeColor ) {
 				function addMorph( geometry, speed, duration, x, y, z, fudgeColor ) {
 
 
 					var material = new THREE.MeshLambertMaterial( { color: 0xffaa55, morphTargets: true, vertexColors: THREE.FaceColors } );
 					var material = new THREE.MeshLambertMaterial( { color: 0xffaa55, morphTargets: true, vertexColors: THREE.FaceColors } );
@@ -319,10 +321,13 @@
 					var mesh = new THREE.Mesh( geometry, material );
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.speed = speed;
 					mesh.speed = speed;
 
 
-					var mixer = new THREE.AnimationMixer( mesh );
-					mixer.addAction( new THREE.AnimationAction( geometry.animations[0] ).warpToDuration( duration ) );
-					mixer.update( 600 * Math.random() );
-					mesh.mixer = mixer;
+					var clip = geometry.animations[ 0 ];
+
+					mixer.clipAction( clip, mesh ).
+							setDuration( duration ).
+							// to shift the playback out of phase:
+							startAt( - duration * Math.random() ).
+							play();
 
 
 					mesh.position.set( x, y, z );
 					mesh.position.set( x, y, z );
 					mesh.rotation.y = Math.PI/2;
 					mesh.rotation.y = Math.PI/2;
@@ -383,12 +388,12 @@
 
 
 				var delta = clock.getDelta();
 				var delta = clock.getDelta();
 
 
+				mixer.update( delta );
+
 				for ( var i = 0; i < morphs.length; i ++ ) {
 				for ( var i = 0; i < morphs.length; i ++ ) {
 
 
 					morph = morphs[ i ];
 					morph = morphs[ i ];
 
 
-					morph.mixer.update( delta );
-
 					morph.position.x += morph.speed * delta;
 					morph.position.x += morph.speed * delta;
 
 
 					if ( morph.position.x  > 2000 )  {
 					if ( morph.position.x  > 2000 )  {

+ 42 - 5
examples/webgl_shadowmap_performance.html

@@ -54,12 +54,14 @@
 			var SCREEN_HEIGHT = window.innerHeight;
 			var SCREEN_HEIGHT = window.innerHeight;
 			var FLOOR = -250;
 			var FLOOR = -250;
 
 
+			var ANIMATION_GROUPS = 25;
+
 			var camera, controls, scene, renderer;
 			var camera, controls, scene, renderer;
 			var container, stats;
 			var container, stats;
 
 
 			var NEAR = 5, FAR = 3000;
 			var NEAR = 5, FAR = 3000;
 
 
-			var morph, morphs = [], mixer;
+			var morph, morphs = [], mixer, animGroups = [];
 
 
 			var light;
 			var light;
 
 
@@ -240,9 +242,16 @@
 
 
 				mixer = new THREE.AnimationMixer( scene );
 				mixer = new THREE.AnimationMixer( scene );
 
 
+				for ( var i = 0; i !== ANIMATION_GROUPS; ++ i ) {
+
+					var group = new THREE.AnimationObjectGroup();
+					animGroups.push( new THREE.AnimationObjectGroup() );
+
+				}
+
 				// MORPHS
 				// MORPHS
 
 
-				function addMorph( geometry, speed, duration, x, y, z, fudgeColor ) {
+				function addMorph( geometry, speed, duration, x, y, z, fudgeColor, massOptimization ) {
 
 
 					var material = new THREE.MeshLambertMaterial( { color: 0xffaa55, morphTargets: true, vertexColors: THREE.FaceColors } );
 					var material = new THREE.MeshLambertMaterial( { color: 0xffaa55, morphTargets: true, vertexColors: THREE.FaceColors } );
 
 
@@ -255,7 +264,35 @@
 					var mesh = new THREE.Mesh( geometry, material );
 					var mesh = new THREE.Mesh( geometry, material );
 					mesh.speed = speed;
 					mesh.speed = speed;
 
 
-					mixer.addAction( new THREE.AnimationAction( geometry.animations[0], Math.random() ).warpToDuration( duration ).setLocalRoot( mesh ) );
+					var clip = geometry.animations[ 0 ];
+
+					if ( massOptimization ) {
+
+						var index = Math.floor( Math.random() * ANIMATION_GROUPS ),
+							animGroup = animGroups[ index ];
+
+						animGroup.add( mesh );
+
+						if ( ! mixer.existingAction( clip, animGroup ) ) {
+
+							var randomness = 0.6 * Math.random() - 0.3;
+							var phase = ( index + randomness ) / ANIMATION_GROUPS;
+
+							mixer.clipAction( clip, animGroup ).
+									setDuration( duration ).
+									startAt( - duration * phase ).
+									play();
+
+						}
+
+					} else {
+
+						mixer.clipAction( clip, mesh ).
+								setDuration( duration ).
+								startAt( - duration * Math.random() ).
+								play();
+
+					}
 
 
 					mesh.position.set( x, y, z );
 					mesh.position.set( x, y, z );
 					mesh.rotation.y = Math.PI/2;
 					mesh.rotation.y = Math.PI/2;
@@ -275,7 +312,7 @@
 
 
 					for ( var i = - 600; i < 601; i += 2 ) {
 					for ( var i = - 600; i < 601; i += 2 ) {
 
 
-						addMorph( geometry, 550, 1, 100 - Math.random() * 3000, FLOOR, i, true );
+						addMorph( geometry, 550, 1, 100 - Math.random() * 3000, FLOOR, i, true, true );
 
 
 					}
 					}
 
 
@@ -342,7 +379,7 @@
 
 
 				var delta = clock.getDelta();
 				var delta = clock.getDelta();
 
 
-				if( mixer ) mixer.update( delta );
+				if ( mixer ) mixer.update( delta );
 
 
 				for ( var i = 0; i < morphs.length; i ++ ) {
 				for ( var i = 0; i < morphs.length; i ++ ) {
 
 

+ 1 - 1
examples/webgl_skinning_simple.html

@@ -76,7 +76,7 @@
 					scene.add( skinnedMesh );
 					scene.add( skinnedMesh );
 
 
 					mixer = new THREE.AnimationMixer( skinnedMesh );
 					mixer = new THREE.AnimationMixer( skinnedMesh );
-					mixer.addAction( new THREE.AnimationAction( skinnedMesh.geometry.animations[0] ) );					
+					mixer.clipAction( skinnedMesh.geometry.animations[ 0 ] ).play();
 
 
 				});
 				});
 
 

+ 1 - 1
examples/webgl_terrain_dynamic.html

@@ -502,7 +502,7 @@
 					mesh.speed = speed;
 					mesh.speed = speed;
 
 
 					var mixer = new THREE.AnimationMixer( mesh );
 					var mixer = new THREE.AnimationMixer( mesh );
-					mixer.addAction( new THREE.AnimationAction( geometry.animations[0] ).warpToDuration( duration ) );
+					mixer.clipAction( geometry.animations[ 0 ] ).setDuration( duration ).play();
 					mixer.update( 600 * Math.random() );
 					mixer.update( 600 * Math.random() );
 					mesh.mixer = mixer;
 					mesh.mixer = mixer;
 
 

+ 65 - 0
src/Three.js

@@ -129,6 +129,59 @@ if ( Function.prototype.name === undefined && Object.defineProperty !== undefine
 
 
 }
 }
 
 
+if ( Object.assign === undefined ) {
+
+	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
+
+	Object.defineProperty( Object, 'assign', {
+
+		writable: true,
+		configurable: true,
+
+		value: function(target) {
+
+			'use strict';
+
+			if ( target === undefined || target === null ) {
+
+				throw new TypeError( "Cannot convert first argument to object" );
+
+			}
+
+			var to = Object( target );
+
+			for ( var i = 1, n = arguments.length; i !== n; ++ i ) {
+
+				var nextSource = arguments[ i ];
+
+				if ( nextSource === undefined || nextSource === null ) continue;
+
+				nextSource = Object( nextSource );
+
+				var keysArray = Object.keys( nextSource );
+
+				for ( var nextIndex = 0, len = keysArray.length; nextIndex !== len; ++ nextIndex ) {
+
+					var nextKey = keysArray[ nextIndex ];
+					var desc = Object.getOwnPropertyDescriptor( nextSource, nextKey );
+
+					if ( desc !== undefined && desc.enumerable ) {
+
+						to[ nextKey ] = nextSource[ nextKey ];
+
+					}
+
+				}
+
+			}
+
+			return to;
+
+		}
+
+	} );
+}
+
 // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.button
 // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.button
 
 
 THREE.MOUSE = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
 THREE.MOUSE = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
@@ -304,6 +357,18 @@ THREE.LoopOnce = 2200;
 THREE.LoopRepeat = 2201;
 THREE.LoopRepeat = 2201;
 THREE.LoopPingPong = 2202;
 THREE.LoopPingPong = 2202;
 
 
+// Interpolation
+
+THREE.InterpolateDiscrete = 2300;
+THREE.InterpolateLinear = 2301;
+THREE.InterpolateSmooth = 2302;
+
+// Interpolant ending modes
+
+THREE.ZeroCurvatureEnding = 2400;
+THREE.ZeroSlopeEnding = 2401;
+THREE.WrapAroundEnding = 2402;
+
 // DEPRECATED
 // DEPRECATED
 
 
 THREE.Projector = function () {
 THREE.Projector = function () {

+ 0 - 165
src/animation/AnimationAction.js

@@ -1,165 +0,0 @@
-/**
- *
- * A clip that has been explicitly scheduled.
- *
- * @author Ben Houston / http://clara.io/
- * @author David Sarno / http://lighthaus.us/
- */
-
-THREE.AnimationAction = function ( clip, startTime, timeScale, weight, loop ) {
-
-	if ( clip === undefined ) throw new Error( 'clip is null' );
-	this.clip = clip;
-	this.localRoot = null;
-	this.startTime = startTime || 0;
-	this.timeScale = timeScale || 1;
-	this.weight = weight || 1;
-	this.loop = loop || THREE.LoopRepeat;
-	this.loopCount = 0;
-	this.enabled = true;	// allow for easy disabling of the action.
-
-	this.actionTime = - this.startTime;
-	this.clipTime = 0;
-
-	this.propertyBindings = [];
-};
-
-/*
-THREE.LoopOnce = 2200;
-THREE.LoopRepeat = 2201;
-THREE.LoopPingPing = 2202;
-*/
-
-THREE.AnimationAction.prototype = {
-
-	constructor: THREE.AnimationAction,
-
-	setLocalRoot: function( localRoot ) {
-
-		this.localRoot = localRoot;
-
-		return this;
-
-	},
-
-	updateTime: function( clipDeltaTime ) {
-
-		var previousClipTime = this.clipTime;
-   		var previousLoopCount = this.loopCount;
-   		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 ) {
-
-				if ( this.clipTime === duration ) {
-
-					this.mixer.dispatchEvent( { type: 'finished', action: this, direction: 1 } );
-
-				} else if ( this.clipTime === 0 ) {
-
-					this.mixer.dispatchEvent( { type: 'finished', action: this, direction: -1 } );
-
-				}
-
-			}
-
-
-			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 ) {
-
-			if ( Math.abs( this.loopCount % 2 ) === 1 ) {
-
-				newClipTime = duration - newClipTime;
-
-			}
-
-		}
-
-		this.clipTime = newClipTime;
-
-		if ( this.loopCount !== previousLoopCount ) {
-
-   			this.mixer.dispatchEvent( { type: 'loop', action: this, loopDelta: ( this.loopCount - this.loopCount ) } );
-
-   		}
-
-	   	return this.clipTime;
-
-	},
-
-	syncWith: function( action ) {
-
-		this.actionTime = action.actionTime;
-		this.timeScale = action.timeScale;
-
-		return this;
-	},
-
-	warpToDuration: function( duration ) {
-
-		this.timeScale = this.clip.duration / duration;
-
-		return this;
-	},
-
-	init: function( time ) {
-
-		this.clipTime = time - this.startTime;
-
-		return this;
-
-	},
-
-	update: function( clipDeltaTime ) {
-
-		this.updateTime( clipDeltaTime );
-
-		var clipResults = this.clip.getAt( this.clipTime );
-
-		return clipResults;
-
-	},
-
-	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 this.timeScale;
-
-	},
-
-	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 );
-
-		}
-
-		return this.weight;
-
-	}
-
-};

+ 181 - 155
src/animation/AnimationClip.js

@@ -8,16 +8,20 @@
 
 
 THREE.AnimationClip = function ( name, duration, tracks ) {
 THREE.AnimationClip = function ( name, duration, tracks ) {
 
 
-	this.name = name;
+	this.name = name || THREE.Math.generateUUID();
 	this.tracks = tracks;
 	this.tracks = tracks;
 	this.duration = ( duration !== undefined ) ? duration : -1;
 	this.duration = ( duration !== undefined ) ? duration : -1;
 
 
 	// this means it should figure out its duration by scanning the tracks
 	// this means it should figure out its duration by scanning the tracks
 	if ( this.duration < 0 ) {
 	if ( this.duration < 0 ) {
+
 		for ( var i = 0; i < this.tracks.length; i ++ ) {
 		for ( var i = 0; i < this.tracks.length; i ++ ) {
+
 			var track = this.tracks[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 ] );
+
 		}
 		}
+
 	}
 	}
 
 
 	// maybe only do these on demand, as doing them here could potentially slow down loading
 	// maybe only do these on demand, as doing them here could potentially slow down loading
@@ -25,29 +29,12 @@ THREE.AnimationClip = function ( name, duration, tracks ) {
 	this.trim();
 	this.trim();
 	this.optimize();
 	this.optimize();
 
 
-	this.results = [];
-
 };
 };
 
 
 THREE.AnimationClip.prototype = {
 THREE.AnimationClip.prototype = {
 
 
 	constructor: THREE.AnimationClip,
 	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() {
 	trim: function() {
 
 
 		for ( var i = 0; i < this.tracks.length; i ++ ) {
 		for ( var i = 0; i < this.tracks.length; i ++ ) {
@@ -74,237 +61,276 @@ THREE.AnimationClip.prototype = {
 
 
 };
 };
 
 
+// Static methods:
 
 
-THREE.AnimationClip.CreateFromMorphTargetSequence = function( name, morphTargetSequence, fps ) {
+Object.assign( THREE.AnimationClip, {
 
 
+	parse: function( json ) {
 
 
-	var numMorphTargets = morphTargetSequence.length;
-	var tracks = [];
+		var tracks = [],
+			jsonTracks = json.tracks,
+			frameTime = 1.0 / ( json.fps || 1.0 );
 
 
-	for ( var i = 0; i < numMorphTargets; i ++ ) {
+		for ( var i = 0, n = jsonTracks.length; i !== n; ++ i ) {
 
 
-		var keys = [];
+			tracks.push( THREE.KeyframeTrack.parse( jsonTracks[ i ] ).scale( frameTime ) );
 
 
-		keys.push( { time: ( i + numMorphTargets - 1 ) % numMorphTargets, value: 0 } );
-		keys.push( { time: i, value: 1 } );
-		keys.push( { time: ( i + 1 ) % numMorphTargets, value: 0 } );
+		}
 
 
-		keys.sort( THREE.KeyframeTrack.keyComparer );
+		return new THREE.AnimationClip( json.name, json.duration, tracks );
 
 
-		// 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
-			});
-		}
+	},
 
 
-		tracks.push( new THREE.NumberKeyframeTrack( '.morphTargetInfluences[' + morphTargetSequence[i].name + ']', keys ).scale( 1.0 / fps ) );
-	}
 
 
-	return new THREE.AnimationClip( name, -1, tracks );
+	toJSON: function( clip ) {
 
 
-};
+		var tracks = [],
+			clipTracks = clip.tracks;
+
+		var json = {
 
 
-THREE.AnimationClip.findByName = function( clipArray, name ) {
+			'name': clip.name,
+			'duration': clip.duration,
+			'tracks': tracks
 
 
-	for ( var i = 0; i < clipArray.length; i ++ ) {
+		};
 
 
-		if ( clipArray[i].name === name ) {
+		for ( var i = 0, n = clipTracks.length; i !== n; ++ i ) {
 
 
-			return clipArray[i];
+			tracks.push( THREE.KeyframeTrack.toJSON( clipTracks[ i ] ) );
 
 
 		}
 		}
-	}
 
 
-	return null;
+		return json;
 
 
-};
+	},
 
 
-THREE.AnimationClip.CreateClipsFromMorphTargetSequences = function( morphTargets, fps ) {
 
 
-	var animationToMorphTargets = {};
+	CreateFromMorphTargetSequence: function( name, morphTargetSequence, fps ) {
 
 
-	// tested with https://regex101.com/ on trick sequences such flamingo_flyA_003, flamingo_run1_003, crdeath0059
-	var pattern = /^([\w-]*?)([\d]+)$/;
+		var numMorphTargets = morphTargetSequence.length;
+		var tracks = [];
 
 
-	// sort morph target names into animation groups based patterns like Walk_001, Walk_002, Run_001, Run_002
-	for ( var i = 0, il = morphTargets.length; i < il; i ++ ) {
+		for ( var i = 0; i < numMorphTargets; i ++ ) {
 
 
-		var morphTarget = morphTargets[ i ];
-		var parts = morphTarget.name.match( pattern );
+			var times = [];
+			var values = [];
 
 
-		if ( parts && parts.length > 1 ) {
+			times.push(
+					( i + numMorphTargets - 1 ) % numMorphTargets,
+					i,
+					( i + 1 ) % numMorphTargets );
 
 
-			var name = parts[ 1 ];
+			values.push( 0, 1, 0 );
 
 
-			var animationMorphTargets = animationToMorphTargets[ name ];
-			if ( ! animationMorphTargets ) {
-				animationToMorphTargets[ name ] = animationMorphTargets = [];
-			}
+			var order = THREE.AnimationUtils.getKeyframeOrder( times );
+			times = THREE.AnimationUtils.sortedArray( times, 1, order );
+			values = THREE.AnimationUtils.sortedArray( values, 1, order );
 
 
-			animationMorphTargets.push( morphTarget );
+			// if there is a key at the first frame, duplicate it as the
+			// last frame as well for perfect loop.
+			if ( times[ 0 ] === 0 ) {
 
 
-		}
+				times.push( numMorphTargets );
+				values.push( values[ 0 ] );
 
 
-	}
+			}
 
 
-	var clips = [];
+			tracks.push(
+					new THREE.NumberKeyframeTrack(
+						'.morphTargetInfluences[' + morphTargetSequence[ i ].name + ']',
+						times, values
+					).scale( 1.0 / fps ) );
+		}
 
 
-	for ( var name in animationToMorphTargets ) {
+		return new THREE.AnimationClip( name, -1, tracks );
 
 
-		clips.push( THREE.AnimationClip.CreateFromMorphTargetSequence( name, animationToMorphTargets[ name ], fps ) );
-	}
+	},
 
 
-	return clips;
+	findByName: function( clipArray, name ) {
 
 
-};
+		for ( var i = 0; i < clipArray.length; i ++ ) {
 
 
-// parse the standard JSON format for clips
-THREE.AnimationClip.parse = function( json ) {
+			if ( clipArray[ i ].name === name ) {
 
 
-	var tracks = [];
+				return clipArray[ i ];
 
 
-	for ( var i = 0; i < json.tracks.length; i ++ ) {
+			}
+		}
 
 
-		tracks.push( THREE.KeyframeTrack.parse( json.tracks[i] ).scale( 1.0 / json.fps ) );
+		return null;
 
 
-	}
+	},
 
 
-	return new THREE.AnimationClip( json.name, json.duration, tracks );
+	CreateClipsFromMorphTargetSequences: function( morphTargets, fps ) {
 
 
-};
+		var animationToMorphTargets = {};
 
 
+		// tested with https://regex101.com/ on trick sequences
+		// such flamingo_flyA_003, flamingo_run1_003, crdeath0059
+		var pattern = /^([\w-]*?)([\d]+)$/;
 
 
-// parse the animation.hierarchy format
-THREE.AnimationClip.parseAnimation = function( animation, bones ) {
+		// sort morph target names into animation groups based
+		// patterns like Walk_001, Walk_002, Run_001, Run_002
+		for ( var i = 0, il = morphTargets.length; i < il; i ++ ) {
 
 
-	if ( ! animation ) {
-		console.error( "  no animation in JSONLoader data" );
-		return null;
-	}
+			var morphTarget = morphTargets[ i ];
+			var parts = morphTarget.name.match( pattern );
 
 
-	var convertTrack = function( trackName, animationKeys, propertyName, trackType, animationKeyToValueFunc ) {
+			if ( parts && parts.length > 1 ) {
 
 
-		var keys = [];
+				var name = parts[ 1 ];
 
 
-		for ( var k = 0; k < animationKeys.length; k ++ ) {
+				var animationMorphTargets = animationToMorphTargets[ name ];
+				if ( ! animationMorphTargets ) {
 
 
-			var animationKey = animationKeys[k];
+					animationToMorphTargets[ name ] = animationMorphTargets = [];
 
 
-			if ( animationKey[propertyName] !== undefined ) {
+				}
+
+				animationMorphTargets.push( morphTarget );
 
 
-				keys.push( { time: animationKey.time, value: animationKeyToValueFunc( animationKey ) } );
 			}
 			}
 
 
 		}
 		}
 
 
-		// only return track if there are actually keys.
-		if ( keys.length > 0 ) {
+		var clips = [];
 
 
-			return new trackType( trackName, keys );
+		for ( var name in animationToMorphTargets ) {
 
 
-		}
+			clips.push( THREE.AnimationClip.CreateFromMorphTargetSequence( name, animationToMorphTargets[ name ], fps ) );
 
 
-		return null;
+		}
 
 
-	};
+		return clips;
 
 
-	var tracks = [];
-	
-	var clipName = animation.name || 'default';
-	var duration = animation.length || -1; // automatic length determination in AnimationClip.
-	var fps = animation.fps || 30;
+	},
 
 
-	var hierarchyTracks = animation.hierarchy || [];
+	// parse the animation.hierarchy format
+	parseAnimation: function( animation, bones, nodeName ) {
 
 
-	for ( var h = 0; h < hierarchyTracks.length; h ++ ) {
+		if ( ! animation ) {
 
 
-		var animationKeys = hierarchyTracks[ h ].keys;
+			console.error( "  no animation in JSONLoader data" );
+			return null;
 
 
-		// skip empty tracks
-		if ( ! animationKeys || animationKeys.length == 0 ) {
-			continue;
 		}
 		}
 
 
-		// process morph targets in a way exactly compatible with AnimationHandler.init( animation )
-		if ( animationKeys[0].morphTargets ) {
+		var addNonemptyTrack = function(
+				trackType, trackName, animationKeys, propertyName, destTracks ) {
 
 
-			// figure out all morph targets used in this track
-			var morphTargetNames = {};
-			for ( var k = 0; k < animationKeys.length; k ++ ) {
+			// only return track if there are actually keys.
+			if ( animationKeys.length !== 0 ) {
 
 
-				if ( animationKeys[k].morphTargets ) {
-					for ( var m = 0; m < animationKeys[k].morphTargets.length; m ++ ) {
+				var times = [];
+				var values = [];
+
+				THREE.AnimationUtils.flattenJSON(
+						animationKeys, times, values, propertyName );
+
+				// empty keys are filtered out, so check again
+				if ( times.length !== 0 ) {
+
+					destTracks.push( new trackType( trackName, times, values ) );
 
 
-						morphTargetNames[ animationKeys[k].morphTargets[m] ] = -1;
-					}
 				}
 				}
 
 
 			}
 			}
 
 
-			// 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 tracks = [];
 
 
-				for ( var m = 0; m < animationKeys[k].morphTargets.length; m ++ ) {
+		var clipName = animation.name || 'default';
+		// automatic length determination in AnimationClip.
+		var duration = animation.length || -1;
+		var fps = animation.fps || 30;
 
 
-					var animationKey = animationKeys[k];
+		var hierarchyTracks = animation.hierarchy || [];
 
 
-					keys.push( {
-							time: animationKey.time,
-							value: (( animationKey.morphTarget === morphTargetName ) ? 1 : 0 )
-						});
+		for ( var h = 0; h < hierarchyTracks.length; h ++ ) {
 
 
-				}
+			var animationKeys = hierarchyTracks[ h ].keys;
 
 
-				tracks.push( new THREE.NumberKeyframeTrack( '.morphTargetInfluence[' + morphTargetName + ']', keys ) );
+			// skip empty tracks
+			if ( ! animationKeys || animationKeys.length == 0 ) continue;
 
 
-			}
+			// process morph targets in a way exactly compatible
+			// with AnimationHandler.init( animation )
+			if ( animationKeys[0].morphTargets ) {
+
+				// figure out all morph targets used in this track
+				var morphTargetNames = {};
+				for ( var k = 0; k < animationKeys.length; k ++ ) {
+
+					if ( animationKeys[k].morphTargets ) {
+
+						for ( var m = 0; m < animationKeys[k].morphTargets.length; m ++ ) {
+
+							morphTargetNames[ animationKeys[k].morphTargets[m] ] = -1;
+						}
+
+					}
+
+				}
 
 
-			duration = morphTargetNames.length * ( fps || 1.0 );
+				// 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 ) {
 
 
-		} else {
+					var times = [];
+					var values = [];
 
 
-			var boneName = '.bones[' + bones[ h ].name + ']';
+					for ( var m = 0;
+							m !== animationKeys[k].morphTargets.length; ++ m ) {
 
 
-			// track contains positions...
-			var positionTrack = convertTrack( boneName + '.position', animationKeys, 'pos', THREE.VectorKeyframeTrack, function( animationKey ) {
-					return new THREE.Vector3().fromArray( animationKey.pos )
-				} );
+						var animationKey = animationKeys[k];
 
 
-			if ( positionTrack ) tracks.push( positionTrack );
+						times.push( animationKey.time );
+						values.push( ( animationKey.morphTarget === morphTargetName ) ? 1 : 0 )
 
 
-			// 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 );
 					}
 					}
-				} );
 
 
-			if ( quaternionTrack ) tracks.push( quaternionTrack );
+					tracks.push( new THREE.NumberKeyframeTrack(
+							'.morphTargetInfluence[' + morphTargetName + ']', times, values ) );
+
+				}
+
+				duration = morphTargetNames.length * ( fps || 1.0 );
+
+			} else {
+				// ...assume skeletal animation
+
+				var boneName = '.bones[' + bones[ h ].name + ']';
 
 
-			// track contains quaternions...
-			var scaleTrack = convertTrack( boneName + '.scale', animationKeys, 'scl', THREE.VectorKeyframeTrack, function( animationKey ) {
-					return new THREE.Vector3().fromArray( animationKey.scl )
-				} );
+				addNonemptyTrack(
+						THREE.VectorKeyframeTrack, boneName + '.position',
+						animationKeys, 'pos', tracks );
 
 
-			if ( scaleTrack ) tracks.push( scaleTrack );
+				addNonemptyTrack(
+						THREE.QuaternionKeyframeTrack, boneName + '.quaternion',
+						animationKeys, 'rot', tracks );
+
+				addNonemptyTrack(
+						THREE.VectorKeyframeTrack, boneName + '.scale',
+						animationKeys, 'scl', tracks );
+
+			}
 
 
 		}
 		}
-	}
 
 
-	if ( tracks.length === 0 ) {
+		if ( tracks.length === 0 ) {
 
 
-		return null;
+			return null;
 
 
-	}
+		}
 
 
-	var clip = new THREE.AnimationClip( clipName, duration, tracks );
+		var clip = new THREE.AnimationClip( clipName, duration, tracks );
 
 
-	return clip;
+		return clip;
+
+	}
+
+} );
 
 
-};

+ 1258 - 99
src/animation/AnimationMixer.js

@@ -1,19 +1,22 @@
 /**
 /**
  *
  *
- * Mixes together the AnimationClips scheduled by AnimationActions and applies them to the root and subtree
+ * Player for AnimationClips.
  *
  *
  *
  *
  * @author Ben Houston / http://clara.io/
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
  */
 
 
 THREE.AnimationMixer = function( root ) {
 THREE.AnimationMixer = function( root ) {
 
 
-	this.root = root;
+	this._root = root;
+	this._initMemoryManager();
+	this._accuIndex = 0;
+
 	this.time = 0;
 	this.time = 0;
+
 	this.timeScale = 1.0;
 	this.timeScale = 1.0;
-	this.actions = [];
-	this.propertyBindingMap = {};
 
 
 };
 };
 
 
@@ -21,221 +24,1377 @@ THREE.AnimationMixer.prototype = {
 
 
 	constructor: THREE.AnimationMixer,
 	constructor: THREE.AnimationMixer,
 
 
-	addAction: function( action ) {
+	// return an action for a clip optionally using a custom root target
+	// object (this method allocates a lot of dynamic memory in case a
+	// previously unknown clip/root combination is specified)
+	clipAction: function( clip, optionalRoot ) {
+
+		var root = optionalRoot || this._root,
+			rootUuid = root.uuid,
+			clipName = ( typeof clip === 'string' ) ? clip : clip.name,
+			clipObject = ( clip !== clipName ) ? clip : null,
+
+			actionsForClip = this._actionsByClip[ clipName ],
+			prototypeAction;
 
 
-		// TODO: check for duplicate action names?  Or provide each action with a UUID?
+		if ( actionsForClip !== undefined ) {
 
 
-		this.actions.push( action );
-		action.init( this.time );
-		action.mixer = this;
+			var existingAction =
+					actionsForClip.actionByRoot[ rootUuid ];
 
 
-		var tracks = action.clip.tracks;
+			if ( existingAction !== undefined ) {
 
 
-		var root = action.localRoot || this.root;
+				return existingAction;
 
 
-		for ( var i = 0; i < tracks.length; i ++ ) {
+			}
 
 
-			var track = tracks[ i ];
+			// we know the clip, so we don't have to parse all
+			// the bindings again but can just copy
+			prototypeAction = actionsForClip.knownActions[ 0 ];
 
 
-			var propertyBindingKey = root.uuid + '-' + track.name;
-			var propertyBinding = this.propertyBindingMap[ propertyBindingKey ];
+			// also, take the clip from the prototype action
+			clipObject = prototypeAction._clip;
 
 
-			if ( propertyBinding === undefined ) {
+			if ( clip !== clipName && clip !== clipObject ) {
 
 
-				propertyBinding = new THREE.PropertyBinding( root, track.name );
-				this.propertyBindingMap[ propertyBindingKey ] = propertyBinding;
+				throw new Error(
+						"Different clips with the same name detected!" );
 
 
 			}
 			}
 
 
-			// push in the same order as the tracks.
-			action.propertyBindings.push( propertyBinding );
+		}
+
+		// clip must be known when specified via string
+		if ( clipObject === null ) return null;
+
+		// allocate all resources required to run it
+		var newAction = new THREE.
+				AnimationMixer._Action( this, clipObject, optionalRoot );
+
+		this._bindAction( newAction, prototypeAction );
+
+		// and make the action known to the memory manager
+		this._addInactiveAction( newAction, clipName, rootUuid );
+
+		return newAction;
+
+	},
+
+	// get an existing action
+	existingAction: function( clip, optionalRoot ) {
+
+		var root = optionalRoot || this._root,
+			rootUuid = root.uuid,
+			clipName = ( typeof clip === 'string' ) ? clip : clip.name,
+			actionsForClip = this._actionsByClip[ clipName ];
+
+		if ( actionsForClip !== undefined ) {
 
 
-			// track usages of shared property bindings, because if we leave too many around, the mixer can get slow
-			propertyBinding.referenceCount += 1;
+			return actionsForClip.actionByRoot[ rootUuid ] || null;
 
 
 		}
 		}
 
 
+		return null;
+
 	},
 	},
 
 
-	removeAllActions: function() {
+	// deactivates all previously scheduled actions
+	stopAllAction: function() {
+
+		var actions = this._actions,
+			nActions = this._nActiveActions,
+			bindings = this._bindings,
+			nBindings = this._nActiveBindings;
+
+		this._nActiveActions = 0;
+		this._nActiveBindings = 0;
+
+		for ( var i = 0; i !== nActions; ++ i ) {
+
+			actions[ i ].reset();
+
+		}
 
 
-		for ( var i = 0; i < this.actions.length; i ++ ) {
+		for ( var i = 0; i !== nBindings; ++ i ) {
 
 
-			this.actions[i].mixer = null;
+			bindings[ i ].useCount = 0;
 
 
 		}
 		}
 
 
-		// unbind all property bindings
-		for ( var properyBindingKey in this.propertyBindingMap ) {
+		return this;
+
+	},
+
+	// advance the time and update apply the animation
+	update: function( deltaTime ) {
+
+		deltaTime *= this.timeScale;
+
+		var actions = this._actions,
+			nActions = this._nActiveActions,
+
+			time = this.time += deltaTime,
+			timeDirection = Math.sign( deltaTime ),
 
 
-			this.propertyBindingMap[ properyBindingKey ].unbind();
+			accuIndex = this._accuIndex ^= 1;
+
+		// run active actions
+
+		for ( var i = 0; i !== nActions; ++ i ) {
+
+			var action = actions[ i ];
+
+			if ( action.enabled ) {
+
+				action._update( time, deltaTime, timeDirection, accuIndex );
+
+			}
 
 
 		}
 		}
 
 
-		this.actions = [];
-		this.propertyBindingMap = {};
+		// update scene graph
+
+		var bindings = this._bindings,
+			nBindings = this._nActiveBindings;
+
+		for ( var i = 0; i !== nBindings; ++ i ) {
+
+			bindings[ i ].apply( accuIndex );
+
+		}
 
 
 		return this;
 		return this;
 
 
 	},
 	},
 
 
-	removeAction: function( action ) {
+	// return this mixer's root target object
+	getRoot: function() {
+
+		return this._root;
+
+	},
+
+	// free all resources specific to a particular clip
+	uncacheClip: function( clip ) {
 
 
-		var index = this.actions.indexOf( action );
+		var actions = this._actions,
+			clipName = clip.name,
+			actionsByClip = this._actionsByClip,
+			actionsForClip = actionsByClip[ clipName ];
 
 
-		if ( index !== - 1 ) {
+		if ( actionsForClip !== undefined ) {
 
 
-			this.actions.splice( index, 1 );
-			action.mixer = null;
+			// note: just calling _removeInactiveAction would mess up the
+			// iteration state and also require updating the state we can
+			// just throw away
+
+			var actionsToRemove = actionsForClip.knownActions;
+
+			for ( var i = 0, n = actionsToRemove.length; i !== n; ++ i ) {
+
+				var action = actionsToRemove[ i ];
+
+				this._deactivateAction( action );
+
+				var cacheIndex = action._cacheIndex,
+					lastInactiveAction = actions[ actions.length - 1 ];
+
+				action._cacheIndex = null;
+				action._byClipCacheIndex = null;
+
+				lastInactiveAction._cacheIndex = cacheIndex;
+				actions[ cacheIndex ] = lastInactiveAction;
+				actions.pop();
+
+				this._removeInactiveBindingsForAction( action );
+
+			}
+
+			delete actionsByClip[ clipName ];
 
 
 		}
 		}
 
 
+	},
+
+	// free all resources specific to a particular root target object
+	uncacheRoot: function( root ) {
+
+		var rootUuid = root.uuid,
+			actionsByClip = this._actionsByClip;
+
+		for ( var clipName in actionsByClip ) {
+
+			var actionByRoot = actionsByClip[ clipName ].actionByRoot,
+				action = actionByRoot[ rootUuid ];
 
 
-		// 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;
+			if ( action !== undefined ) {
 
 
-		for ( var i = 0; i < tracks.length; i ++ ) {
+				this._deactivateAction( action );
+				this._removeInactiveAction( action );
 
 
-			var track = tracks[ i ];
+			}
 
 
-			var propertyBindingKey = root.uuid + '-' + track.name;
-			var propertyBinding = this.propertyBindingMap[ propertyBindingKey ];
+		}
 
 
-			propertyBinding.referenceCount -= 1;
+		var bindingsByRoot = this._bindingsByRootAndName,
+			bindingByName = bindingsByRoot[ rootUuid ];
 
 
-			if ( propertyBinding.referenceCount <= 0 ) {
+		if ( bindingByName !== undefined ) {
 
 
-				propertyBinding.unbind();
+			for ( var trackName in bindingByName ) {
 
 
-				delete this.propertyBindingMap[ propertyBindingKey ];
+				var binding = bindingByName[ trackName ];
+				binding.restoreOriginalState();
+				this._removeInactiveBinding( binding );
 
 
 			}
 			}
+
+		}
+
+	},
+
+	// remove a targeted clip from the cache
+	uncacheAction: function( clip, optionalRoot ) {
+
+		var action = this.existingAction( clip, optionalRoot );
+
+		if ( action !== null ) {
+
+			this._deactivateAction( action );
+			this._removeInactiveAction( action );
+
 		}
 		}
 
 
+	}
+
+};
+
+THREE.EventDispatcher.prototype.apply( THREE.AnimationMixer.prototype );
+
+THREE.AnimationMixer._Action =
+		function( mixer, clip, localRoot ) {
+
+	this._mixer = mixer;
+	this._clip = clip;
+	this._localRoot = localRoot || null;
+
+	var tracks = clip.tracks,
+		nTracks = tracks.length,
+		interpolants = new Array( nTracks );
+
+	var interpolantSettings = {
+			endingStart: 	THREE.ZeroCurvatureEnding,
+			endingEnd:		THREE.ZeroCurvatureEnding
+	};
+
+	for ( var i = 0; i !== nTracks; ++ i ) {
+
+		var interpolant = tracks[ i ].createInterpolant( null );
+		interpolants[ i ] = interpolant;
+		interpolant.settings = interpolantSettings
+
+	}
+
+	this._interpolantSettings = interpolantSettings;
+
+	this._interpolants = interpolants;	// bound by the mixer
+
+	// inside: PropertyMixer (managed by the mixer)
+	this._propertyBindings = new Array( nTracks );
+
+	this._cacheIndex = null;			// for the memory manager
+	this._byClipCacheIndex = null;		// for the memory manager
+
+	this._timeScaleInterpolant = null;
+	this._weightInterpolant = null;
+
+	this.loop = THREE.LoopRepeat;
+	this._loopCount = -1;
+
+	// global mixer time when the action is to be started
+	// it's set back to 'null' upon start of the action
+	this._startTime = null;
+
+	// scaled local time of the action
+	// gets clamped or wrapped to 0..clip.duration according to loop
+	this.time = 0;
+
+	this.timeScale = 1;
+	this._effectiveTimeScale = 1;
+
+	this.weight = 1;
+	this._effectiveWeight = 1;
+
+	this.repetitions = Infinity; 		// no. of repetitions when looping
+
+	this.paused = false;				// false -> zero effective time scale
+	this.enabled = true;				// true -> zero effective weight
+
+	this.clampWhenFinished 	= false;	// keep feeding the last frame?
+
+	this.zeroSlopeAtStart 	= true;		// for smooth interpolation w/o separate
+	this.zeroSlopeAtEnd		= true;		// clips for start, loop and end
+
+};
+
+THREE.AnimationMixer._Action.prototype = {
+
+	constructor: THREE.AnimationMixer._Action,
+
+	// State & Scheduling
+
+	play: function() {
+
+		this._mixer._activateAction( this );
+
 		return this;
 		return this;
 
 
 	},
 	},
 
 
-	// can be optimized if needed
-	findActionByName: function( name ) {
+	stop: function() {
 
 
-		for ( var i = 0; i < this.actions.length; i ++ ) {
+		this._mixer._deactivateAction( this );
 
 
-			if ( this.actions[i].name === name ) return this.actions[i];
+		return this.reset();
 
 
-		}
+	},
 
 
-		return null;
+	reset: function() {
+
+		this.paused = false;
+		this.enabled = true;
+
+		this.time = 0;			// restart clip
+		this._loopCount = -1;	// forget previous loops
+		this._startTime = null;	// forget scheduling
+
+		return this.stopFading().stopWarping();
 
 
 	},
 	},
 
 
-	play: function( action, optionalFadeInDuration ) {
+	isRunning: function() {
 
 
-		action.startTime = this.time;
-		this.addAction( action );
+		var start = this._startTime;
 
 
-		return this;
+		return this.enabled && ! this.paused && this.timeScale !== 0 &&
+				this._startTime === null && this._mixer._isActiveAction( this )
+
+	},
+
+	// return true when play has been called
+	isScheduled: function() {
+
+		return this._mixer._isActiveAction( this );
 
 
 	},
 	},
 
 
-	fadeOut: function( action, duration ) {
+	startAt: function( time ) {
+
+		this._startTime = time;
+
+		return this;
 
 
-		var keys = [];
+	},
 
 
-		keys.push( { time: this.time, value: 1 } );
-		keys.push( { time: this.time + duration, value: 0 } );
+	setLoop: function( mode, repetitions ) {
 
 
-		action.weight = new THREE.NumberKeyframeTrack( "weight", keys );
+		this.loop = mode;
+		this.repetitions = repetitions;
 
 
 		return this;
 		return this;
 
 
 	},
 	},
 
 
-	fadeIn: function( action, duration ) {
+	// Weight
+
+	// set the weight stopping any scheduled fading
+	// although .enabled = false yields an effective weight of zero, this
+	// method does *not* change .enabled, because it would be confusing
+	setEffectiveWeight: function( weight ) {
+
+		this.weight = weight;
+
+		// note: same logic as when updated at runtime
+		this._effectiveWeight = this.enabled ? weight : 0;
+
+		return this.stopFading();
+
+	},
+
+	// return the weight considering fading and .enabled
+	getEffectiveWeight: function() {
+
+		return this._effectiveWeight;
+
+	},
+
+	fadeIn: function( duration ) {
+
+		return this._scheduleFading( duration, 0, 1 );
+
+	},
+
+	fadeOut: function( duration ) {
+
+		return this._scheduleFading( duration, 1, 0 );
+
+	},
+
+	crossFadeFrom: function( fadeOutAction, duration, warp ) {
+
+		var mixer = this._mixer;
+
+		fadeOutAction.fadeOut( duration );
+		this.fadeIn( duration );
+
+		if( warp ) {
 
 
-		var keys = [];
+			var fadeInDuration = this._clip.duration,
+				fadeOutDuration = fadeOutAction._clip.duration,
 
 
-		keys.push( { time: this.time, value: 0 } );
-		keys.push( { time: this.time + duration, value: 1 } );
+				startEndRatio = fadeOutDuration / fadeInDuration,
+				endStartRatio = fadeInDuration / fadeOutDuration;
 
 
-		action.weight = new THREE.NumberKeyframeTrack( "weight", keys );
+			fadeOutAction.warp( 1.0, startEndRatio, duration );
+			this.warp( endStartRatio, 1.0, duration );
+
+		}
 
 
 		return this;
 		return this;
 
 
 	},
 	},
 
 
-	warp: function( action, startTimeScale, endTimeScale, duration ) {
+	crossFadeTo: function( fadeInAction, duration, warp ) {
+
+		return fadeInAction.crossFadeFrom( this, duration, warp );
+
+	},
+
+	stopFading: function() {
 
 
-		var keys = [];
+		var weightInterpolant = this._weightInterpolant;
 
 
-		keys.push( { time: this.time, value: startTimeScale } );
-		keys.push( { time: this.time + duration, value: endTimeScale } );
+		if ( weightInterpolant !== null ) {
 
 
-		action.timeScale = new THREE.NumberKeyframeTrack( "timeScale", keys );
+			this._weightInterpolant = null;
+			this._mixer._takeBackControlInterpolant( weightInterpolant );
+
+		}
 
 
 		return this;
 		return this;
 
 
 	},
 	},
 
 
-	crossFade: function( fadeOutAction, fadeInAction, duration, warp ) {
+	// Time Scale Control
+
+	// set the weight stopping any scheduled warping
+	// although .paused = true yields an effective time scale of zero, this
+	// method does *not* change .paused, because it would be confusing
+	setEffectiveTimeScale: function( timeScale ) {
+
+		this.timeScale = timeScale;
+		this._effectiveTimeScale = this.paused ? 0 :timeScale;
+
+		return this.stopWarping();
+
+	},
+
+	// return the time scale considering warping and .paused
+	getEffectiveTimeScale: function() {
+
+		return this._effectiveTimeScale;
+
+	},
+
+	setDuration: function( duration ) {
+
+		this.timeScale = this._clip.duration / duration;
+
+		return this.stopWarping();
+
+	},
+
+	syncWith: function( action ) {
+
+		this.time = action.time;
+		this.timeScale = action.timeScale;
+
+		return this.stopWarping();
+
+	},
+
+	halt: function( duration ) {
 
 
-		this.fadeOut( fadeOutAction, duration );
-		this.fadeIn( fadeInAction, duration );
+		return this.warp( this._currentTimeScale, 0, duration );
 
 
-		if ( warp ) {
+	},
+
+	warp: function( startTimeScale, endTimeScale, duration ) {
+
+		var mixer = this._mixer, now = mixer.time,
+			interpolant = this._timeScaleInterpolant,
+
+			timeScale = this.timeScale;
 
 
-			var startEndRatio = fadeOutAction.clip.duration / fadeInAction.clip.duration;
-			var endStartRatio = 1.0 / startEndRatio;
+		if ( interpolant === null ) {
 
 
-			this.warp( fadeOutAction, 1.0, startEndRatio, duration );
-			this.warp( fadeInAction, endStartRatio, 1.0, duration );
+			interpolant = mixer._lendControlInterpolant(),
+			this._timeScaleInterpolant = interpolant;
 
 
 		}
 		}
 
 
+		var times = interpolant.parameterPositions,
+			values = interpolant.sampleValues;
+
+		times[ 0 ] = now;
+		times[ 1 ] = now + duration;
+
+		values[ 0 ] = startTimeScale / timeScale;
+		values[ 1 ] = endTimeScale / timeScale;
+
 		return this;
 		return this;
 
 
 	},
 	},
 
 
-	update: function( deltaTime ) {
+	stopWarping: function() {
+
+		var timeScaleInterpolant = this._timeScaleInterpolant;
+
+		if ( timeScaleInterpolant !== null ) {
+
+			this._timeScaleInterpolant = null;
+			this._mixer._takeBackControlInterpolant( timeScaleInterpolant );
+
+		}
+
+		return this;
+
+	},
+
+	// Object Accessors
+
+	getMixer: function() {
+
+		return this._mixer;
+
+	},
+
+	getClip: function() {
+
+		return this._clip;
 
 
-		var mixerDeltaTime = deltaTime * this.timeScale;
-		this.time += mixerDeltaTime;
+	},
+
+	getRoot: function() {
 
 
-		for ( var i = 0; i < this.actions.length; i ++ ) {
+		return this._localRoot || this._mixer._root;
 
 
-			var action = this.actions[i];
+	},
 
 
-			var weight = action.getWeightAt( this.time );
+	// Interna
 
 
-			var actionTimeScale = action.getTimeScaleAt( this.time );
-			var actionDeltaTime = mixerDeltaTime * actionTimeScale;
+	_update: function( time, deltaTime, timeDirection, accuIndex ) {
+		// called by the mixer
 
 
-			var actionResults = action.update( actionDeltaTime );
+		var startTime = this._startTime;
 
 
-			if ( action.weight <= 0 || ! action.enabled ) continue;
+		if ( startTime !== null ) {
 
 
-			for ( var j = 0; j < actionResults.length; j ++ ) {
+			// check for scheduled start of action
 
 
-				var name = action.clip.tracks[j].name;
+			var timeRunning = ( time - startTime ) * timeDirection;
+			if ( timeRunning < 0 || timeDirection === 0 ) {
 
 
-				action.propertyBindings[ j ].accumulate( actionResults[j], weight );
+				return; // yet to come / don't decide when delta = 0
 
 
 			}
 			}
 
 
+			// start
+
+			this._startTime = null; // unschedule
+			deltaTime = timeDirection * timeRunning;
+
 		}
 		}
 
 
-		// apply to nodes
-		for ( var propertyBindingKey in this.propertyBindingMap ) {
+		// apply time scale and advance time
+
+		deltaTime *= this._updateTimeScale( time );
+		var clipTime = this._updateTime( deltaTime );
+
+		// note: _updateTime may disable the action resulting in
+		// an effective weight of 0
+
+		var weight = this._updateWeight( time );
 
 
-			this.propertyBindingMap[ propertyBindingKey ].apply();
+		if ( weight > 0 ) {
+
+			var interpolants = this._interpolants;
+			var propertyMixers = this._propertyBindings;
+
+			for ( var j = 0, m = interpolants.length; j !== m; ++ j ) {
+
+				interpolants[ j ].evaluate( clipTime );
+				propertyMixers[ j ].accumulate( accuIndex, weight );
+
+			}
 
 
 		}
 		}
 
 
-		return this;
+	},
 
 
-	}
+	_updateWeight: function( time ) {
 
 
-};
+		var weight = 0;
+
+		if ( this.enabled ) {
+
+			weight = this.weight;
+			var interpolant = this._weightInterpolant;
+
+			if ( interpolant !== null ) {
+
+				var interpolantValue = interpolant.evaluate( time )[ 0 ];
+
+				weight *= interpolantValue;
+
+				if ( time > interpolant.parameterPositions[ 1 ] ) {
+
+					this.stopFading();
+
+					if ( interpolantValue === 0 ) {
+
+						// faded out, disable
+						this.enabled = false;
+
+					}
+
+				}
+
+			}
+
+		}
+
+		this._effectiveWeight = weight;
+		return weight;
+
+	},
+
+	_updateTimeScale: function( time ) {
+
+		var timeScale = 0;
+
+		if ( ! this.paused ) {
+
+			timeScale = this.timeScale;
+
+			var interpolant = this._timeScaleInterpolant;
+
+			if ( interpolant !== null ) {
+
+				var interpolantValue = interpolant.evaluate( time )[ 0 ];
+
+				timeScale *= interpolantValue;
+
+				if ( time > interpolant.parameterPositions[ 1 ] ) {
+
+					this.stopWarping();
+
+					if ( timeScale === 0 ) {
+
+						// motion has halted, pause
+						this.pause = true;
+
+					} else {
+
+						// warp done - apply final time scale
+						this.timeScale = timeScale;
+
+					}
+
+				}
+
+			}
+
+		}
+
+		this._effectiveTimeScale = timeScale;
+		return timeScale;
+
+	},
+
+	_updateTime: function( deltaTime ) {
+
+		var time = this.time + deltaTime;
+
+		if ( deltaTime === 0 ) return time;
+
+		var duration = this._clip.duration,
+
+			loop = this.loop,
+			loopCount = this._loopCount,
+
+			pingPong = false;
+
+		switch ( loop ) {
+
+			case THREE.LoopOnce:
+			case THREE.LoopOnceClamp:
+
+				if ( loopCount === -1 ) {
+
+					// just started
+
+					this.loopCount = 0;
+					this._setEndings( true, true, false );
+
+				}
+
+				if ( time >= duration ) {
+
+					time = duration;
+
+				} else if ( time < 0 ) {
+
+					time = 0;
+
+				} else break;
+
+				// reached the end
+
+				if ( this.clampWhenFinished ) this.pause = true;
+				else this.enabled = false;
+
+				this._mixer.dispatchEvent( {
+					type: 'finished', action: this,
+					direction: deltaTime < 0 ? -1 : 1
+				} );
+
+				break;
+
+			case THREE.LoopPingPong:
+
+				pingPong = true;
+
+			case THREE.LoopRepeat:
+
+				if ( loopCount === -1 ) {
+
+					// just started
+
+					if ( deltaTime > 0 ) {
+
+						loopCount = 0;
+
+						this._setEndings(
+								true, this.repetitions === 0, pingPong );
+
+					} else {
+
+						// when looping in reverse direction, the initial
+						// transition through zero counts as a repetition,
+						// so leave loopCount at -1
+
+						this._setEndings(
+								this.repetitions === 0, true, pingPong );
+
+					}
+
+				}
+
+				if ( time >= duration || time < 0 ) {
+
+					// wrap around
+
+					var loopDelta = Math.floor( time / duration ); // signed
+					time -= duration * loopDelta;
+
+					loopCount += Math.abs( loopDelta );
+
+					var pending = this.repetitions - loopCount;
+
+					if ( pending < 0 ) {
+
+						// stop (switch state, clamp time, fire event)
+
+						if ( this.clampWhenFinished ) this.paused = true;
+						else this.enabled = false;
+
+						time = deltaTime > 0 ? duration : 0;
+
+						this._mixer.dispatchEvent( {
+							type: 'finished', action: this,
+							direction: deltaTime > 0 ? 1 : -1
+						} );
+
+						break;
+
+					} else if ( pending === 0 ) {
+
+						// transition to last round
+
+						var atStart = deltaTime < 0;
+						this._setEndings( atStart, ! atStart, pingPong );
+
+					} else {
+
+						this._setEndings( false, false, pingPong );
+
+					}
+
+					this._loopCount = loopCount;
+
+					this._mixer.dispatchEvent( {
+						type: 'loop', action: this, loopDelta: loopDelta
+					} );
+
+				}
+
+				if ( loop === THREE.LoopPingPong && ( loopCount & 1 ) === 1 ) {
+
+					// invert time for the "pong round"
+
+					this.time = time;
+
+					return duration - time;
+
+				}
+
+				break;
+
+		}
+
+		this.time = time;
+
+		return time;
+
+	},
+
+	_setEndings: function( atStart, atEnd, pingPong ) {
+
+		var settings = this._interpolantSettings;
+
+		if ( pingPong ) {
+
+			settings.endingStart 	= THREE.ZeroSlopeEnding;
+			settings.endingEnd		= THREE.ZeroSlopeEnding;
+
+		} else {
+
+			// assuming for LoopOnce atStart == atEnd == true
+
+			if ( atStart ) {
+
+				settings.endingStart = this.zeroSlopeAtStart ?
+						THREE.ZeroSlopeEnding : THREE.ZeroCurvatureEnding;
+
+			} else {
+
+				settings.endingStart = THREE.WrapAroundEnding;
+
+			}
+
+			if ( atEnd ) {
+
+				settings.endingEnd = this.zeroSlopeAtEnd ?
+						THREE.ZeroSlopeEnding : THREE.ZeroCurvatureEnding;
+
+			} else {
+
+				settings.endingEnd 	 = THREE.WrapAroundEnding;
+
+			}
+
+		}
+
+	},
+
+	_scheduleFading: function( duration, weightNow, weightThen ) {
+
+		var mixer = this._mixer, now = mixer.time,
+			interpolant = this._weightInterpolant;
+
+		if ( interpolant === null ) {
+
+			interpolant = mixer._lendControlInterpolant(),
+			this._weightInterpolant = interpolant;
+
+		}
+
+		var times = interpolant.parameterPositions,
+			values = interpolant.sampleValues;
+
+		times[ 0 ] = now; 				values[ 0 ] = weightNow;
+		times[ 1 ] = now + duration;	values[ 1 ] = weightThen;
+
+		return this;
+
+	}
+
+};
+
+// Implementation details:
+
+Object.assign( THREE.AnimationMixer.prototype, {
+
+	_bindAction: function( action, prototypeAction ) {
+
+		var root = action._localRoot || this._root,
+			tracks = action._clip.tracks,
+			nTracks = tracks.length,
+			bindings = action._propertyBindings,
+			interpolants = action._interpolants,
+			rootUuid = root.uuid,
+			bindingsByRoot = this._bindingsByRootAndName,
+			bindingsByName = bindingsByRoot[ rootUuid ];
+
+		if ( bindingsByName === undefined ) {
+
+			bindingsByName = {};
+			bindingsByRoot[ rootUuid ] = bindingsByName;
+
+		}
+
+		for ( var i = 0; i !== nTracks; ++ i ) {
+
+			var track = tracks[ i ],
+				trackName = track.name,
+				binding = bindingsByName[ trackName ];
+
+			if ( binding !== undefined ) {
+
+				bindings[ i ] = binding;
+
+			} else {
+
+				binding = bindings[ i ];
+
+				if ( binding !== undefined ) {
+
+					// existing binding, make sure the cache knows
+
+					if ( binding._cacheIndex === null ) {
+
+						++ binding.referenceCount;
+						this._addInactiveBinding( binding, rootUuid, trackName );
+
+					}
+
+					continue;
+
+				}
+
+				var path = prototypeAction && prototypeAction.
+						_propertyBindings[ i ].binding.parsedPath;
+
+				binding = new THREE.PropertyMixer(
+						THREE.PropertyBinding.create( root, trackName, path ),
+						track.ValueTypeName, track.getValueSize() );
+
+				++ binding.referenceCount;
+				this._addInactiveBinding( binding, rootUuid, trackName );
+
+				bindings[ i ] = binding;
+
+			}
+
+			interpolants[ i ].resultBuffer = binding.buffer;
+
+		}
+
+	},
+
+	_activateAction: function( action ) {
+
+		if ( ! this._isActiveAction( action ) ) {
+
+			if ( action._cacheIndex === null ) {
+
+				// this action has been forgotten by the cache, but the user
+				// appears to be still using it -> rebind
+
+				var rootUuid = ( action._localRoot || this._root ).uuid,
+					clipName = action._clip.name,
+					actionsForClip = this._actionsByClip[ clipName ];
+
+				this._bindAction( action,
+						actionsForClip && actionsForClip.knownActions[ 0 ] );
+
+				this._addInactiveAction( action, clipName, rootUuid );
+
+			}
+
+			var bindings = action._propertyBindings;
+
+			// increment reference counts / sort out state
+			for ( var i = 0, n = bindings.length; i !== n; ++ i ) {
+
+				var binding = bindings[ i ];
+
+				if ( binding.useCount ++ === 0 ) {
+
+					this._lendBinding( binding );
+					binding.saveOriginalState();
+
+				}
+
+			}
+
+			this._lendAction( action );
+
+		}
+
+	},
+
+	_deactivateAction: function( action ) {
+
+		if ( this._isActiveAction( action ) ) {
+
+			var bindings = action._propertyBindings;
+
+			// decrement reference counts / sort out state
+			for ( var i = 0, n = bindings.length; i !== n; ++ i ) {
+
+				var binding = bindings[ i ];
+
+				if ( -- binding.useCount === 0 ) {
+
+					binding.restoreOriginalState();
+					this._takeBackBinding( binding );
+
+				}
+
+			}
+
+			this._takeBackAction( action );
+
+		}
+
+	},
+
+	// Memory manager
+
+	_initMemoryManager: function() {
+
+		this._actions = []; // 'nActiveActions' followed by inactive ones
+		this._nActiveActions = 0;
+
+		this._actionsByClip = {};
+		// inside:
+		// {
+		// 		knownActions: Array< _Action >	- used as prototypes
+		// 		actionByRoot: _Action			- lookup
+		// }
+
+
+		this._bindings = []; // 'nActiveBindings' followed by inactive ones
+		this._nActiveBindings = 0;
+
+		this._bindingsByRootAndName = {}; // inside: Map< name, PropertyMixer >
+
+
+		this._controlInterpolants = []; // same game as above
+		this._nActiveControlInterpolants = 0;
+
+		var scope = this;
+
+		this.stats = {
+
+			actions: {
+				get total() { return scope._actions.length; },
+				get inUse() { return scope._nActiveActions; }
+			},
+			bindings: {
+				get total() { return scope._bindings.length; },
+				get inUse() { return scope._nActiveBindings; }
+			},
+			controlInterpolants: {
+				get total() { return scope._controlInterpolants.length; },
+				get inUse() { return scope._nActiveControlInterpolants; }
+			}
+
+		};
+
+	},
+
+	// Memory management for _Action objects
+
+	_isActiveAction: function( action ) {
+
+		var index = action._cacheIndex;
+		return index !== null && index < this._nActiveActions;
+
+	},
+
+	_addInactiveAction: function( action, clipName, rootUuid ) {
+
+		var actions = this._actions,
+			actionsByClip = this._actionsByClip,
+			actionsForClip = actionsByClip[ clipName ];
+
+		if ( actionsForClip === undefined ) {
+
+			actionsForClip = {
+
+				knownActions: [ action ],
+				actionByRoot: {}
+
+			};
+
+			action._byClipCacheIndex = 0;
+
+			actionsByClip[ clipName ] = actionsForClip;
+
+		} else {
+
+			var knownActions = actionsForClip.knownActions;
+
+			action._byClipCacheIndex = knownActions.length;
+			knownActions.push( action );
+
+		}
+
+		action._cacheIndex = actions.length;
+		actions.push( action );
+
+		actionsForClip.actionByRoot[ rootUuid ] = action;
+
+	},
+
+	_removeInactiveAction: function( action ) {
+
+		var actions = this._actions,
+			lastInactiveAction = actions[ actions.length - 1 ],
+			cacheIndex = action._cacheIndex;
+
+		lastInactiveAction._cacheIndex = cacheIndex;
+		actions[ cacheIndex ] = lastInactiveAction;
+		actions.pop();
+
+		action._cacheIndex = null;
+
+
+		var clipName = action._clip.name,
+			actionsByClip = this._actionsByClip,
+			actionsForClip = actionsByClip[ clipName ],
+			knownActionsForClip = actionsForClip.knownActions,
+
+			lastKnownAction =
+				knownActionsForClip[ knownActionsForClip.length - 1 ],
+
+			byClipCacheIndex = action._byClipCacheIndex;
+
+		lastKnownAction._byClipCacheIndex = byClipCacheIndex;
+		knownActionsForClip[ byClipCacheIndex ] = lastKnownAction;
+		knownActionsForClip.pop();
+
+		action._byClipCacheIndex = null;
+
+
+		var actionByRoot = actionsForClip.actionByRoot,
+			rootUuid = ( actions._localRoot || this._root ).uuid;
+
+		delete actionByRoot[ rootUuid ];
+
+		if ( knownActionsForClip.length === 0 ) {
+
+			delete actionsByClip[ clipName ];
+
+		}
+
+		this._removeInactiveBindingsForAction( action );
+
+	},
+
+	_removeInactiveBindingsForAction: function( action ) {
+
+		var bindings = action._propertyBindings;
+		for ( var i = 0, n = bindings.length; i !== n; ++ i ) {
+
+			var binding = bindings[ i ];
+
+			if ( -- binding.referenceCount === 0 ) {
+
+				this._removeInactiveBinding( binding );
+
+			}
+
+		}
+
+	},
+
+	_lendAction: function( action ) {
+
+		// [ active actions |  inactive actions  ]
+		// [  active actions >| inactive actions ]
+		//                 s        a
+		//                  <-swap->
+		//                 a        s
+
+		var actions = this._actions,
+			prevIndex = action._cacheIndex,
+
+			lastActiveIndex = this._nActiveActions ++,
+
+			firstInactiveAction = actions[ lastActiveIndex ];
+
+		action._cacheIndex = lastActiveIndex;
+		actions[ lastActiveIndex ] = action;
+
+		firstInactiveAction._cacheIndex = prevIndex;
+		actions[ prevIndex ] = firstInactiveAction;
+
+	},
+
+	_takeBackAction: function( action ) {
+
+		// [  active actions  | inactive actions ]
+		// [ active actions |< inactive actions  ]
+		//        a        s
+		//         <-swap->
+		//        s        a
+
+		var actions = this._actions,
+			prevIndex = action._cacheIndex,
+
+			firstInactiveIndex = -- this._nActiveActions,
+
+			lastActiveAction = actions[ firstInactiveIndex ];
+
+		action._cacheIndex = firstInactiveIndex;
+		actions[ firstInactiveIndex ] = action;
+
+		lastActiveAction._cacheIndex = prevIndex;
+		actions[ prevIndex ] = lastActiveAction;
+
+	},
+
+	// Memory management for PropertyMixer objects
+
+	_addInactiveBinding: function( binding, rootUuid, trackName ) {
+
+		var bindingsByRoot = this._bindingsByRootAndName,
+			bindingByName = bindingsByRoot[ rootUuid ],
+
+			bindings = this._bindings;
+
+		if ( bindingByName === undefined ) {
+
+			bindingByName = {};
+			bindingsByRoot[ rootUuid ] = bindingByName;
+
+		}
+
+		bindingByName[ trackName ] = binding;
+
+		binding._cacheIndex = bindings.length;
+		bindings.push( binding );
+
+	},
+
+	_removeInactiveBinding: function( binding ) {
+
+		var bindings = this._bindings,
+			propBinding = binding.binding,
+			rootUuid = propBinding.rootNode.uuid,
+			trackName = propBinding.path,
+			bindingsByRoot = this._bindingsByRootAndName,
+			bindingByName = bindingsByRoot[ rootUuid ],
+
+			lastInactiveBinding = bindings[ bindings.length - 1 ],
+			cacheIndex = binding._cacheIndex;
+
+		lastInactiveBinding._cacheIndex = cacheIndex;
+		bindings[ cacheIndex ] = lastInactiveBinding;
+		bindings.pop();
+
+		delete bindingByName[ trackName ];
+
+		remove_empty_map: {
+
+			for ( var _ in bindingByName ) break remove_empty_map;
+
+			delete bindingsByRoot[ rootUuid ];
+
+		}
+
+	},
+
+	_lendBinding: function( binding ) {
+
+		var bindings = this._bindings,
+			prevIndex = binding._cacheIndex,
+
+			lastActiveIndex = this._nActiveBindings ++,
+
+			firstInactiveBinding = bindings[ lastActiveIndex ];
+
+		binding._cacheIndex = lastActiveIndex;
+		bindings[ lastActiveIndex ] = binding;
+
+		firstInactiveBinding._cacheIndex = prevIndex;
+		bindings[ prevIndex ] = firstInactiveBinding;
+
+	},
+
+	_takeBackBinding: function( binding ) {
+
+		var bindings = this._bindings,
+			prevIndex = binding._cacheIndex,
+
+			firstInactiveIndex = -- this._nActiveBindings,
+
+			lastActiveBinding = bindings[ firstInactiveIndex ];
+
+		binding._cacheIndex = firstInactiveIndex;
+		bindings[ firstInactiveIndex ] = binding;
+
+		lastActiveBinding._cacheIndex = prevIndex;
+		bindings[ prevIndex ] = lastActiveBinding;
+
+	},
+
+
+	// Memory management of Interpolants for weight and time scale
+
+	_lendControlInterpolant: function() {
+
+		var interpolants = this._controlInterpolants,
+			lastActiveIndex = this._nActiveControlInterpolants ++,
+			interpolant = interpolants[ lastActiveIndex ];
+
+		if ( interpolant === undefined ) {
+
+			interpolant = new THREE.LinearInterpolant(
+					new Float32Array( 2 ), new Float32Array( 2 ),
+						1, this._controlInterpolantsResultBuffer );
+
+			interpolant.__cacheIndex = lastActiveIndex;
+			interpolants[ lastActiveIndex ] = interpolant;
+
+		}
+
+		return interpolant;
+
+	},
+
+	_takeBackControlInterpolant: function( interpolant ) {
+
+		var interpolants = this._controlInterpolants,
+			prevIndex = interpolant.__cacheIndex,
+
+			firstInactiveIndex = -- this._nActiveControlInterpolants,
+
+			lastActiveInterpolant = interpolants[ firstInactiveIndex ];
+
+		interpolant.__cacheIndex = firstInactiveIndex;
+		interpolants[ firstInactiveIndex ] = interpolant;
+
+		lastActiveInterpolant.__cacheIndex = prevIndex;
+		interpolants[ prevIndex ] = lastActiveInterpolant;
+
+	},
+
+	_controlInterpolantsResultBuffer: new Float32Array( 1 )
+
+} );
 
 
-THREE.EventDispatcher.prototype.apply( THREE.AnimationMixer.prototype );

+ 370 - 0
src/animation/AnimationObjectGroup.js

@@ -0,0 +1,370 @@
+/**
+ *
+ * A group of objects that receives a shared animation state.
+ *
+ * Usage:
+ *
+ * 	-	Add objects you would otherwise pass as 'root' to the
+ * 		constructor or the .clipAction method of AnimationMixer.
+ *
+ * 	-	Instead pass this object as 'root'.
+ *
+ * 	-	You can also add and remove objects later when the mixer
+ * 		is running.
+ *
+ * Note:
+ *
+ *  	Objects of this class appear as one object to the mixer,
+ *  	so cache control of the individual objects must be done
+ *  	on the group.
+ *
+ * Limitation:
+ *
+ * 	- 	The animated properties must be compatible among the
+ * 		all objects in the group.
+ *
+ *  -	A single property can either be controlled through a
+ *  	target group or directly, but not both.
+ *
+ * @author tschw
+ */
+
+THREE.AnimationObjectGroup = function( var_args ) {
+
+	this.uuid = THREE.Math.generateUUID();
+
+	// cached objects followed by the active ones
+	this._objects = Array.prototype.slice.call( arguments );
+
+	this.nCachedObjects_ = 0;			// threshold
+	// note: read by PropertyBinding.Composite
+
+	var indices = {};
+	this._indicesByUUID = indices;		// for bookkeeping
+
+	for ( var i = 0, n = arguments.length; i !== n; ++ i ) {
+
+		indices[ arguments[ i ].uuid ] = i;
+
+	}
+
+	this._paths = [];					// inside: string
+	this._parsedPaths = [];				// inside: { we don't care, here }
+	this._bindings = []; 				// inside: Array< PropertyBinding >
+	this._bindingsIndicesByPath = {}; 	// inside: indices in these arrays
+
+	var scope = this;
+
+	this.stats = {
+
+		objects: {
+			get total() { return scope._objects.length; },
+			get inUse() { return this.total - scope.nCachedObjects_;  }
+		},
+
+		get bindingsPerObject() { return scope._bindings.length; }
+
+	};
+
+};
+
+THREE.AnimationObjectGroup.prototype = {
+
+	constructor: THREE.AnimationObjectGroup,
+
+	add: function( var_args ) {
+
+		var objects = this._objects,
+			nObjects = objects.length,
+			nCachedObjects = this.nCachedObjects_,
+			indicesByUUID = this._indicesByUUID,
+			paths = this._paths,
+			parsedPaths = this._parsedPaths,
+			bindings = this._bindings,
+			nBindings = bindings.length;
+
+		for ( var i = 0, n = arguments.length; i !== n; ++ i ) {
+
+			var object = arguments[ i ],
+				uuid = object.uuid,
+				index = indicesByUUID[ uuid ];
+
+			if ( index === undefined ) {
+
+				// unknown object -> add it to the ACTIVE region
+
+				index = nObjects ++;
+				indicesByUUID[ uuid ] = index;
+				objects.push( object );
+
+				// accounting is done, now do the same for all bindings
+
+				for ( var j = 0, m = nBindings; j !== m; ++ j ) {
+
+					bindings[ j ].push(
+							new THREE.PropertyBinding(
+								object, paths[ j ], parsedPaths[ j ] ) );
+
+				}
+
+			} else if ( index < nCachedObjects ) {
+
+				var knownObject = objects[ index ];
+
+				// move existing object to the ACTIVE region
+
+				var firstActiveIndex = -- nCachedObjects,
+					lastCachedObject = objects[ firstActiveIndex ];
+
+				indicesByUUID[ lastCachedObject.uuid ] = index;
+				objects[ index ] = lastCachedObject;
+
+				indicesByUUID[ uuid ] = firstActiveIndex;
+				objects[ firstActiveIndex ] = object;
+
+				// accounting is done, now do the same for all bindings
+
+				for ( var j = 0, m = nBindings; j !== m; ++ j ) {
+
+					var bindingsForPath = bindings[ j ],
+						lastCached = bindingsForPath[ firstActiveIndex ],
+						binding = bindingsForPath[ index ];
+
+					bindingsForPath[ index ] = lastCached;
+
+					if ( binding === undefined ) {
+
+						// since we do not bother to create new bindings
+						// for objects that are cached, the binding may
+						// or may not exist
+
+						binding = new THREE.PropertyBinding(
+								object, paths[ j ], parsedPaths[ j ] );
+
+					}
+
+					bindingsForPath[ firstActiveIndex ] = binding;
+
+				}
+
+			} else if ( objects[ index ] !== knownObject) {
+
+				console.error( "Different objects with the same UUID " +
+						"detected. Clean the caches or recreate your " +
+						"infrastructure when reloading scenes..." );
+
+			} // else the object is already where we want it to be
+
+		} // for arguments
+
+		this.nCachedObjects_ = nCachedObjects;
+
+	},
+
+	remove: function( var_args ) {
+
+		var objects = this._objects,
+			nObjects = objects.length,
+			nCachedObjects = this.nCachedObjects_,
+			indicesByUUID = this._indicesByUUID,
+			bindings = this._bindings,
+			nBindings = bindings.length;
+
+		for ( var i = 0, n = arguments.length; i !== n; ++ i ) {
+
+			var object = arguments[ i ],
+				uuid = object.uuid,
+				index = indicesByUUID[ uuid ];
+
+			if ( index !== undefined && index >= nCachedObjects ) {
+
+				// move existing object into the CACHED region
+
+				var lastCachedIndex = nCachedObjects ++,
+					firstActiveObject = objects[ lastCachedIndex ];
+
+				indicesByUUID[ firstActiveObject.uuid ] = index;
+				objects[ index ] = firstActiveObject;
+
+				indicesByUUID[ uuid ] = lastCachedIndex;
+				objects[ lastCachedIndex ] = object;
+
+				// accounting is done, now do the same for all bindings
+
+				for ( var j = 0, m = nBindings; j !== m; ++ j ) {
+
+					var bindingsForPath = bindings[ j ],
+						firstActive = bindingsForPath[ lastCachedIndex ],
+						binding = bindingsForPath[ index ];
+
+					bindingsForPath[ index ] = firstActive;
+					bindingsForPath[ lastCachedIndex ] = binding;
+
+				}
+
+			}
+
+		} // for arguments
+
+		this.nCachedObjects_ = nCachedObjects;
+
+	},
+
+	// remove & forget
+	uncache: function( var_args ) {
+
+		var objects = this._objects,
+			nObjects = objects.length,
+			nCachedObjects = this.nCachedObjects_,
+			indicesByUUID = this._indicesByUUID,
+			bindings = this._bindings,
+			nBindings = bindings.length;
+
+		for ( var i = 0, n = arguments.length; i !== n; ++ i ) {
+
+			var object = arguments[ i ],
+				uuid = object.uuid,
+				index = indicesByUUID[ uuid ];
+
+			if ( index !== undefined ) {
+
+				delete indicesByUUID[ uuid ];
+
+				if ( index < nCachedObjects ) {
+
+					// object is cached, shrink the CACHED region
+
+					var firstActiveIndex = -- nCachedObjects,
+						lastCachedObject = objects[ firstActiveIndex ],
+						lastIndex = -- nObjects,
+						lastObject = objects[ lastIndex ];
+
+					// last cached object takes this object's place
+					indicesByUUID[ lastCachedObject.uuid ] = index;
+					objects[ index ] = lastCachedObject;
+
+					// last object goes to the activated slot and pop
+					indicesByUUID[ lastObject.uuid ] = firstActiveIndex;
+					objects[ firstActiveIndex ] = lastObject;
+					objects.pop();
+
+					// accounting is done, now do the same for all bindings
+
+					for ( var j = 0, m = nBindings; j !== m; ++ j ) {
+
+						var bindingsForPath = bindings[ j ],
+							lastCached = bindingsForPath[ firstActiveIndex ],
+							last = bindingsForPath[ lastIndex ];
+
+						bindingsForPath[ index ] = lastCached;
+						bindingsForPath[ firstActiveIndex ] = last;
+						bindingsForPath.pop();
+
+					}
+
+				} else {
+
+					// object is active, just swap with the last and pop
+
+					var lastIndex = -- nObjects,
+						lastObject = objects[ lastIndex ];
+
+					indicesByUUID[ lastObject.uuid ] = index;
+					objects[ index ] = lastObject;
+					objects.pop();
+
+					// accounting is done, now do the same for all bindings
+
+					for ( var j = 0, m = nBindings; j !== m; ++ j ) {
+
+						var bindingsForPath = bindings[ j ];
+
+						bindingsForPath[ index ] = bindingsForPath[ lastIndex ];
+						bindingsForPath.pop();
+
+					}
+
+				} // cached or active
+
+			} // if object is known
+
+		} // for arguments
+
+		this.nCachedObjects_ = nCachedObjects;
+
+	},
+
+	// Internal interface used by befriended PropertyBinding.Composite:
+
+	subscribe_: function( path, parsedPath ) {
+		// returns an array of bindings for the given path that is changed
+		// according to the contained objects in the group
+
+		var indicesByPath = this._bindingsIndicesByPath,
+			index = indicesByPath[ path ],
+			bindings = this._bindings;
+
+		if ( index !== undefined ) return bindings[ index ];
+
+		var paths = this._paths,
+			parsedPaths = this._parsedPaths,
+			objects = this._objects,
+			nObjects = objects.length,
+			nCachedObjects = this.nCachedObjects_,
+			bindingsForPath = new Array( nObjects );
+
+		index = bindings.length;
+
+		indicesByPath[ path ] = index;
+
+		paths.push( path );
+		parsedPaths.push( parsedPath );
+		bindings.push( bindingsForPath );
+
+		for ( var i = nCachedObjects,
+				n = objects.length; i !== n; ++ i ) {
+
+			var object = objects[ i ];
+
+			bindingsForPath[ i ] =
+					new THREE.PropertyBinding( object, path, parsedPath );
+
+		}
+
+		return bindingsForPath;
+
+	},
+
+	unsubscribe_: function( path ) {
+		// tells the group to forget about a property path and no longer
+		// update the array previously obtained with 'subscribe_'
+
+		var indicesByPath = this._bindingsIndicesByPath,
+			index = indicesByPath[ path ];
+
+		if ( index !== undefined ) {
+
+			var paths = this._paths,
+				parsedPaths = this._parsedPaths,
+				bindings = this._bindings,
+				lastBindingsIndex = bindings.length - 1,
+				lastBindings = bindings[ lastBindingsIndex ],
+				lastBindingsPath = path[ lastBindingsIndex ];
+
+			indicesByPath[ lastBindingsPath ] = index;
+
+			bindings[ index ] = lastBindings;
+			bindings.pop();
+
+			parsedPaths[ index ] = parsedPaths[ lastBindingsIndex ];
+			parsedPaths.pop();
+
+			paths[ index ] = paths[ lastBindingsIndex ];
+			paths.pop();
+
+		}
+
+	}
+
+};
+

+ 110 - 63
src/animation/AnimationUtils.js

@@ -1,110 +1,157 @@
 /**
 /**
+ * @author tschw
  * @author Ben Houston / http://clara.io/
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @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 ( THREE.AnimationUtils.isTypedArray( array ) ) {
+
+			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 ( ! array || // let 'undefined' and 'null' pass
+				! forceClone && array.constructor === type ) return array;
+
+		if ( typeof type.BYTES_PER_ELEMENT === 'number' ) {
+
+			return new type( array ); // create typed 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;
+		return Array.prototype.slice.call( array ); // create Array
 
 
 	},
 	},
 
 
-	lerp: function( a, b, alpha, interTrack ) {
+	isTypedArray: function( object ) {
 
 
-		var lerpFunc = THREE.AnimationUtils.getLerpFunc( a, interTrack );
-
-		return lerpFunc( a, b, alpha );
+		return ArrayBuffer.isView( object ) &&
+				! ( object instanceof DataView );
 
 
 	},
 	},
 
 
-	lerp_object: function( a, b, alpha ) {
-		return a.lerp( b, alpha );
-	},
+	// returns an array by which times and values can be sorted
+	getKeyframeOrder: function( times ) {
 
 
-	slerp_object: function( a, b, alpha ) {
-		return a.slerp( b, alpha );
-	},
+		function compareTime( i, j ) {
 
 
-	lerp_number: function( a, b, alpha ) {
-		return a * ( 1 - alpha ) + b * alpha;
-	},
+			return times[ i ] - times[ j ];
 
 
-	lerp_boolean: function( a, b, alpha ) {
-		return ( alpha < 0.5 ) ? a : b;
-	},
+		}
+
+		var n = times.length;
+		var result = new Array( n );
+		for ( var i = 0; i !== n; ++ i ) result[ i ] = i;
+
+		result.sort( compareTime );
+
+		return result;
 
 
-	lerp_boolean_immediate: function( a, b, alpha ) {
-		return a;
 	},
 	},
 
 
-	lerp_string: function( a, b, alpha ) {
-		return ( alpha < 0.5 ) ? a : b;
+	// 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_immediate: function( a, b, alpha ) {
- 		return a;
- 	},
+	// function for parsing AOS keyframe formats
+	flattenJSON: function( jsonKeys, times, values, valuePropertyName ) {
 
 
-	// NOTE: this is an accumulator function that modifies the first argument (e.g. a).	This is to minimize memory alocations.
-	getLerpFunc: function( exemplarValue, interTrack ) {
+		var i = 1, key = jsonKeys[ 0 ];
 
 
-		if ( exemplarValue === undefined || exemplarValue === null ) throw new Error( "examplarValue is null" );
+		while ( key !== undefined && key[ valuePropertyName ] === undefined ) {
 
 
-		var typeName = typeof exemplarValue;
+			key = jsonKeys[ i ++ ];
 
 
-		switch( typeName ) {
+		}
 
 
-			case "object":
-				if ( exemplarValue.lerp ) {
-					return THREE.AnimationUtils.lerp_object;
-				}
+		if ( key === undefined ) return; // no data
+
+		var value = key[ valuePropertyName ];
+		if ( value === undefined ) return; // no data
+
+		if ( Array.isArray( value ) ) {
+
+			do {
+
+				value = key[ valuePropertyName ];
+
+				if ( value !== undefined ) {
+
+					times.push( key.time );
+					values.push.apply( values, value ); // push all elements
 
 
-				if ( exemplarValue.slerp ) {
-					return THREE.AnimationUtils.slerp_object;
 				}
 				}
-				break;
 
 
-			case "number":
-				return THREE.AnimationUtils.lerp_number;
+				key = jsonKeys[ i ++ ];
+
+			} while ( key !== undefined );
+
+		} else if ( value.toArray !== undefined ) {
+			// ...assume THREE.Math-ish
+
+			do {
+
+				value = key[ valuePropertyName ];
+
+				if ( value !== undefined ) {
+
+					times.push( key.time );
+					value.toArray( values, values.length );
 
 
-			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;
+				key = jsonKeys[ i ++ ];
+
+			} while ( key !== undefined );
+
+		} else {
+			// otherwise push as-is
+
+			do {
+
+				value = key[ valuePropertyName ];
+
+				if ( value !== undefined ) {
+
+					times.push( key.time );
+					values.push( value );
+
 				}
 				}
 
 
+				key = jsonKeys[ i ++ ];
+
+			} while ( key !== undefined );
+
 		}
 		}
 
 
 	}
 	}

+ 371 - 140
src/animation/KeyframeTrack.js

@@ -1,21 +1,29 @@
 /**
 /**
  *
  *
- * 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 Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @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( times === undefined || times.length === 0 ) {
 
 
-	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 );
+		throw new Error( "no keyframes in track named " + name );
+
+	}
 
 
 	this.name = 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.validate();
 	this.optimize();
 	this.optimize();
@@ -26,61 +34,124 @@ THREE.KeyframeTrack.prototype = {
 
 
 	constructor: THREE.KeyframeTrack,
 	constructor: THREE.KeyframeTrack,
 
 
-	getAt: function( time ) {
+	TimeBufferType: Float32Array,
+	ValueBufferType: Float32Array,
 
 
+	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 ) {
 
 
-		if ( this.lastIndex >= this.keys.length ) {
+		return new THREE.LinearInterpolant(
+				this.times, this.values, this.getValueSize(), result );
 
 
-			this.setResult( this.keys[ this.keys.length - 1 ].value );
+	},
+
+	InterpolantFactoryMethodSmooth: function( result ) {
+
+		return new THREE.CubicInterpolant(
+				this.times, this.values, this.getValueSize(), result );
+
+	},
+
+	setInterpolation: function( interpolation ) {
+
+		var factoryMethod = undefined;
+
+		switch ( interpolation ) {
+
+			case THREE.InterpolateDiscrete:
+
+				factoryMethod = this.InterpolantFactoryMethodDiscrete;
+
+				break;
 
 
-			return this.result;
+			case THREE.InterpolateLinear:
+
+				factoryMethod = this.InterpolantFactoryMethodLinear;
+
+				break;
+
+			case THREE.InterpolateSmooth:
+
+				factoryMethod = this.InterpolantFactoryMethodSmooth;
+
+				break;
 
 
 		}
 		}
 
 
-		if ( this.lastIndex === 0 ) {
+		if ( factoryMethod === undefined ) {
+
+			var message = "unsupported interpolation for " +
+					this.ValueTypeName + " keyframe track named " + this.name;
+
+			if ( this.createInterpolant === undefined ) {
+
+				// fall back to default, unless the default itself is messed up
+				if ( interpolation !== this.DefaultInterpolation ) {
+
+					this.setInterpolation( this.DefaultInterpolation );
 
 
-			this.setResult( this.keys[ 0 ].value );
+				} else {
 
 
-			return this.result;
+					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;
+
+	},
+
+	getInterpolation: function() {
+
+		switch ( this.createInterpolant ) {
+
+			case this.InterpolantFactoryMethodDiscrete:
+
+				return THREE.InterpolateDiscrete;
 
 
-		// if true, means that prev/current keys are identical, thus no interpolation required.
-		if ( prevKey.constantToNext ) {
+			case this.InterpolantFactoryMethodLinear:
 
 
-			return this.result;
+				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 );
+	},
+
+	getValueSize: function() {
 
 
-		return this.result;
+		return this.values.length / this.times.length;
 
 
 	},
 	},
 
 
 	// move all keyframes either forwards or backwards in time
 	// move all keyframes either forwards or backwards in time
 	shift: function( timeOffset ) {
 	shift: function( timeOffset ) {
 
 
-		if ( timeOffset !== 0.0 ) {
+		if( timeOffset !== 0.0 ) {
+
+			var times = this.times;
+
+			for( var i = 0, n = times.length; i !== n; ++ i ) {
+
+				times[ i ] += timeOffset;
 
 
-			for ( var i = 0; i < this.keys.length; i ++ ) {
-				this.keys[i].time += timeOffset;
 			}
 			}
 
 
 		}
 		}
@@ -92,10 +163,14 @@ THREE.KeyframeTrack.prototype = {
 	// scale all keyframe times by a factor (useful for frame <-> seconds conversions)
 	// scale all keyframe times by a factor (useful for frame <-> seconds conversions)
 	scale: function( timeScale ) {
 	scale: function( timeScale ) {
 
 
-		if ( timeScale !== 1.0 ) {
+		if( timeScale !== 1.0 ) {
+
+			var times = this.times;
+
+			for( var i = 0, n = times.length; i !== n; ++ i ) {
+
+				times[ i ] *= timeScale;
 
 
-			for ( var i = 0; i < this.keys.length; i ++ ) {
-				this.keys[i].time *= timeScale;
 			}
 			}
 
 
 		}
 		}
@@ -106,123 +181,195 @@ THREE.KeyframeTrack.prototype = {
 
 
 	// removes keyframes before and after animation without changing any values within the range [startTime, endTime].
 	// 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
 	// 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;
 		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;
 		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)
 		// 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;
 		return this;
 
 
-	},*/
+	},
 
 
 	// ensure we do not get a GarbageInGarbageOut situation, make sure tracks are at least minimally viable
 	// 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() {
 	validate: function() {
 
 
-		var prevKey = null;
+		var valid = true;
+
+		var valueSize = this.getValueSize();
+		if ( valueSize - Math.floor( valueSize ) !== 0 ) {
+
+			console.error( "invalid value size in track", this );
+			valid = false;
 
 
-		if ( this.keys.length === 0 ) {
-			console.error( "  track is empty, no keys", this );
-			return;
 		}
 		}
 
 
-		for ( var i = 0; i < this.keys.length; i ++ ) {
+		var times = this.times,
+			values = this.values,
 
 
-			var currKey = this.keys[i];
+			nKeys = times.length;
 
 
-			if ( ! currKey ) {
-				console.error( "  key is null in track", this, i );
-				return;
-			}
+		if( nKeys === 0 ) {
 
 
-			if ( ( typeof currKey.time ) !== 'number' || isNaN( currKey.time ) ) {
-				console.error( "  key.time is not a valid number", this, i, currKey );
-				return;
-			}
+			console.error( "track is empty", this );
+			valid = false;
+
+		}
+
+		var prevTime = null;
+
+		for( var i = 0; i !== nKeys; i ++ ) {
+
+			var currTime = times[ i ];
+
+			if ( typeof currTime === 'number' && isNaN( currTime ) ) {
+
+				console.error( "time is not a valid number", this, i, currTime );
+				valid = false;
+				break;
 
 
-			if ( currKey.value === undefined || currKey.value === null) {
-				console.error( "  key.value is null in track", this, i, currKey );
-				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 );
-				return;
+			if( prevTime !== null && prevTime > currTime ) {
+
+				console.error( "out of order keys", this, i, currTime, prevTime );
+				valid = false;
+				break;
+
 			}
 			}
 
 
-			prevKey = currKey;
+			prevTime = currTime;
 
 
 		}
 		}
 
 
-		return this;
+		if ( values !== undefined ) {
+
+			if ( THREE.AnimationUtils.isTypedArray( values ) ) {
+
+				for ( var i = 0, n = values.length; i !== n; ++ i ) {
+
+					var value = values[ i ];
+
+					if ( isNaN( value ) ) {
+
+						console.error( "value is not a valid number", this, i, value );
+						valid = false;
+						break;
+
+					}
+
+				}
+
+			}
+
+		}
+
+		return valid;
 
 
 	},
 	},
 
 
-	// 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() {
 	optimize: function() {
 
 
-		var newKeys = [];
-		var prevKey = this.keys[0];
-		newKeys.push( prevKey );
+		var times = this.times,
+			values = this.values,
+			stride = this.getValueSize(),
 
 
-		var equalsFunc = THREE.AnimationUtils.getEqualsFunc( prevKey.value );
+			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 unnecessary keyframes same as their neighbors
+				var offset = i * stride,
+					offsetP = offset - stride,
+					offsetN = offset + stride;
+
+				for ( var j = 0; j !== stride; ++ j ) {
+
+					var value = values[ offset + j ];
 
 
-			// 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 ) ) {
+					if ( value !== values[ offsetP + j ] ||
+							value !== values[ offsetN + j ] ) {
 
 
-				continue;
+						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,
+						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;
 		return this;
 
 
@@ -230,45 +377,129 @@ THREE.KeyframeTrack.prototype = {
 
 
 };
 };
 
 
-THREE.KeyframeTrack.keyComparer = function keyComparator(key0, key1) {
-	return key0.time - key1.time;
-};
+// Static methods:
 
 
-THREE.KeyframeTrack.parse = function( json ) {
+Object.assign( THREE.KeyframeTrack, {
 
 
-	if ( json.type === undefined ) throw new Error( "track type undefined, can not parse" );
+	// Serialization (in static context, because of constructor invocation
+	// and automatic invocation of .toJSON):
 
 
-	var trackType = THREE.KeyframeTrack.GetTrackTypeForTypeName( json.type );
+	parse: function( json ) {
 
 
-	return trackType.parse( json );
+		if( json.type === undefined ) {
 
 
-};
+			throw new Error( "track type undefined, can not parse" );
 
 
-THREE.KeyframeTrack.GetTrackTypeForTypeName = function( typeName ) {
-	switch( typeName.toLowerCase() ) {
-	 	case "vector":
-	 	case "vector2":
-	 	case "vector3":
-	 	case "vector4":
-			return THREE.VectorKeyframeTrack;
-
-	 	case "quaternion":
-			return THREE.QuaternionKeyframeTrack;
-
-	 	case "integer":
-	 	case "scalar":
-	 	case "double":
-	 	case "float":
-	 	case "number":
-			return THREE.NumberKeyframeTrack;
-
-	 	case "bool":
-	 	case "boolean":
-			return THREE.BooleanKeyframeTrack;
-
-	 	case "string":
-	 		return THREE.StringKeyframeTrack;
-	};
-
-	throw new Error( "Unsupported typeName: " + typeName );
-};
+		}
+
+		var trackType = THREE.KeyframeTrack._getTrackTypeForValueTypeName( json.type );
+
+		if ( json.times === undefined ) {
+
+			console.warn( "legacy JSON format detected, converting" );
+
+			var times = [], values = [];
+
+			THREE.AnimationUtils.flattenJSON( json.keys, 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 );
+
+		}
+
+	},
+
+	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;
+
+	},
+
+	_getTrackTypeForValueTypeName: function( typeName ) {
+
+		switch( typeName.toLowerCase() ) {
+
+			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 "color":
+
+				return THREE.ColorKeyframeTrack;
+
+			case "quaternion":
+
+				return THREE.QuaternionKeyframeTrack;
+
+			case "bool":
+			case "boolean":
+
+				return THREE.BooleanKeyframeTrack;
+
+			case "string":
+
+				return THREE.StringKeyframeTrack;
+
+		};
+
+		throw new Error( "Unsupported typeName: " + typeName );
+
+	}
+
+} );

+ 390 - 196
src/animation/PropertyBinding.js

@@ -1,276 +1,471 @@
 /**
 /**
  *
  *
- * 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 Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
  */
 
 
-THREE.PropertyBinding = function ( rootNode, trackName ) {
-
-	this.rootNode = rootNode;
-	this.trackName = trackName;
-	this.referenceCount = 0;
-	this.originalValue = null; // the value of the property before it was controlled by this binding
+THREE.PropertyBinding = function ( rootNode, path, parsedPath ) {
 
 
-	var parseResults = THREE.PropertyBinding.parseTrackName( trackName );
+	this.path = path;
+	this.parsedPath = parsedPath ||
+			THREE.PropertyBinding.parseTrackName( path );
 
 
-	this.directoryName = parseResults.directoryName;
-	this.nodeName = parseResults.nodeName;
-	this.objectName = parseResults.objectName;
-	this.objectIndex = parseResults.objectIndex;
-	this.propertyName = parseResults.propertyName;
-	this.propertyIndex = parseResults.propertyIndex;
+	this.node = THREE.PropertyBinding.findNode(
+			rootNode, this.parsedPath.nodeName ) || rootNode;
 
 
-	this.node = THREE.PropertyBinding.findNode( rootNode, this.nodeName ) || rootNode;
+	this.rootNode = rootNode;
 
 
-	this.cumulativeValue = null;
-	this.cumulativeWeight = 0;
 };
 };
 
 
 THREE.PropertyBinding.prototype = {
 THREE.PropertyBinding.prototype = {
 
 
 	constructor: THREE.PropertyBinding,
 	constructor: THREE.PropertyBinding,
 
 
-	reset: function() {
+	getValue: function getValue_unbound( targetArray, offset ) {
 
 
-		this.cumulativeValue = null;
-		this.cumulativeWeight = 0;
+		this.bind();
+		this.getValue( targetArray, offset );
+
+		// 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 ) {
+	setValue: function getValue_unbound( sourceArray, offset ) {
+
+		this.bind();
+		this.setValue( sourceArray, offset );
 
 
-		if ( ! this.isBound ) this.bind();
+	},
 
 
-		if ( this.cumulativeWeight === 0 ) {
+	// create getter / setter pair for a property in the scene graph
+	bind: function() {
 
 
-			if ( weight > 0 ) {
+		var targetObject = this.node,
+			parsedPath = this.parsedPath,
 
 
-				if ( this.cumulativeValue === null ) {
-					this.cumulativeValue = THREE.AnimationUtils.clone( value );
-				}
-				this.cumulativeWeight = weight;
+			objectName = parsedPath.objectName,
+			propertyName = parsedPath.propertyName,
+			propertyIndex = parsedPath.propertyIndex;
 
 
-			}
+		if ( ! targetObject ) {
 
 
-		} else {
+			targetObject = THREE.PropertyBinding.findNode(
+					this.rootNode, parsedPath.nodeName ) || this.rootNode;
 
 
-			var lerpAlpha = weight / ( this.cumulativeWeight + weight );
-			this.cumulativeValue = this.lerpValue( this.cumulativeValue, value, lerpAlpha );
-			this.cumulativeWeight += weight;
+			this.node = targetObject;
 
 
 		}
 		}
 
 
-	},
+		// set fail state so we can just 'return' on error
+		this.getValue = this._getValue_unavailable;
+		this.setValue = this._setValue_unavailable;
 
 
-	unbind: function() {
+ 		// ensure there is a value node
+		if ( ! targetObject ) {
 
 
-		if ( ! this.isBound ) return;
+			console.error( "  trying to update node for track: " + this.path + " but it wasn't found." );
+			return;
 
 
-		this.setValue( this.originalValue );
+		}
 
 
-		this.setValue = null;
-		this.getValue = null;
-		this.lerpValue = null;
-		this.equalsValue = null;
-		this.triggerDirty = null;
-		this.isBound = false;
+		if( objectName ) {
 
 
-	},
+			var objectIndex = parsedPath.objectIndex;
 
 
-	// bind to the real property in the scene graph, remember original value, memorize various accessors for speed/inefficiency
-	bind: function() {
+			// special cases were we need to reach deeper into the hierarchy to get the face materials....
+			switch ( objectName ) {
 
 
-		if ( this.isBound ) return;
+				case 'materials':
 
 
-		var targetObject = this.node;
+					if( ! targetObject.material ) {
 
 
- 		// ensure there is a value node
-		if ( ! targetObject ) {
-			console.error( "  trying to update node for track: " + this.trackName + " but it wasn't found." );
-			return;
-		}
+						console.error( '  can not bind to material as node does not have a material', this );
+						return;
 
 
-		if ( this.objectName ) {
-			// special case were we need to reach deeper into the hierarchy to get the face materials....
-			if ( this.objectName === "materials" ) {
-				if ( ! targetObject.material ) {
-					console.error( '  can not bind to material as node does not have a material', this );
-					return;
-				}
-				if ( ! targetObject.material.materials ) {
-					console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
-					return;
-				}
-				targetObject = targetObject.material.materials;
-			} else if ( this.objectName === "bones" ) {
-				if ( ! targetObject.skeleton ) {
-					console.error( '  can not bind to bones as node does not have a skeleton', this );
-					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;
+					if( ! targetObject.material.materials ) {
+
+						console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
+						return;
 
 
-				// support resolving morphTarget names into indices.
-				for ( var i = 0; i < targetObject.length; i ++ ) {
-					if ( targetObject[i].name === this.objectIndex ) {
-						this.objectIndex = i;
-						break;
 					}
 					}
-				}
-			} else {
 
 
-				if ( targetObject[ this.objectName ] === undefined ) {
-					console.error( '  can not bind to objectName of node, undefined', this );
-					return;
-				}
-				targetObject = targetObject[ this.objectName ];
+					targetObject = targetObject.material.materials;
+
+					break;
+
+				case 'bones':
+
+					if( ! targetObject.skeleton ) {
+
+						console.error( '  can not bind to bones as node does not have a skeleton', this );
+						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.
+					for ( var i = 0; i < targetObject.length; i ++ ) {
+
+						if ( targetObject[i].name === objectIndex ) {
+
+							objectIndex = i;
+							break;
+
+						}
+
+					}
+
+					break;
+
+				default:
+
+					if ( targetObject[ objectName ] === undefined ) {
+
+						console.error( '  can not bind to objectName of node, undefined', this );
+						return;
+
+					}
+
+					targetObject = targetObject[ objectName ];
+
 			}
 			}
 
 
-			if ( this.objectIndex !== undefined ) {
-				if ( targetObject[ this.objectIndex ] === undefined ) {
+
+			if ( objectIndex !== undefined ) {
+
+				if( targetObject[ objectIndex ] === undefined ) {
+
 					console.error( "  trying to bind to objectIndex of objectName, but is undefined:", this, targetObject );
 					console.error( "  trying to bind to objectIndex of objectName, but is undefined:", this, targetObject );
 					return;
 					return;
+
 				}
 				}
 
 
-				targetObject = targetObject[ this.objectIndex ];
+				targetObject = targetObject[ objectIndex ];
+
 			}
 			}
 
 
 		}
 		}
 
 
- 		// special case mappings
- 		var nodeProperty = targetObject[ this.propertyName ];
+		// resolve property
+		var nodeProperty = targetObject[ propertyName ];
+
 		if ( ! nodeProperty ) {
 		if ( ! nodeProperty ) {
-			console.error( "  trying to update property for track: " + this.nodeName + '.' + this.propertyName + " but it wasn't found.", targetObject );
+
+			var nodeName = parsedPath.nodeName;
+
+			console.error( "  trying to update property for track: " + nodeName +
+					'.' + propertyName + " but it wasn't found.", targetObject );
 			return;
 			return;
+
 		}
 		}
 
 
-		// access a sub element of the property array (only primitives are supported right now)
-		if ( this.propertyIndex !== undefined ) {
+		// determine versioning scheme
+		var versioning = this.Versioning.None;
 
 
-			if ( this.propertyName === "morphTargetInfluences" ) {
+		if ( targetObject.needsUpdate !== undefined ) { // material
+
+			versioning = this.Versioning.NeedsUpdate;
+			this.targetObject = targetObject;
+
+		} else if ( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
+
+			versioning = this.Versioning.MatrixWorldNeedsUpdate;
+			this.targetObject = targetObject;
+
+		}
+
+		// determine how the property gets bound
+		var bindingType = this.BindingType.Direct;
+
+		if ( propertyIndex !== undefined ) {
+			// access a sub element of the property array (only primitives are supported right now)
+
+			if ( propertyName === "morphTargetInfluences" ) {
 				// potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
 				// 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.
 				// support resolving morphTarget names into indices.
 				if ( ! targetObject.geometry ) {
 				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 );
+					return;
+
 				}
 				}
+
 				if ( ! targetObject.geometry.morphTargets ) {
 				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 );
+					return;
+
 				}
 				}
 
 
 				for ( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
 				for ( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
-					if ( targetObject.geometry.morphTargets[i].name === this.propertyIndex ) {
-						this.propertyIndex = i;
+
+					if ( targetObject.geometry.morphTargets[i].name === propertyIndex ) {
+
+						propertyIndex = i;
 						break;
 						break;
+
 					}
 					}
+
 				}
 				}
+
 			}
 			}
 
 
-			this.setValue = function setValue_propertyIndexed( value ) {
-				if ( ! this.equalsValue( nodeProperty[ this.propertyIndex ], value ) ) {
-					nodeProperty[ this.propertyIndex ] = value;
-					return true;
-				}
-				return false;
-			};
+			bindingType = this.BindingType.ArrayElement;
+
+			this.resolvedProperty = nodeProperty;
+			this.propertyIndex = propertyIndex;
 
 
-			this.getValue = function getValue_propertyIndexed() {
-				return nodeProperty[ this.propertyIndex ];
-			};
+		} else if ( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {
+			// must use copy for Object3D.Euler/Quaternion
+
+			bindingType = this.BindingType.HasFromToArray;
+
+			this.resolvedProperty = nodeProperty;
+
+		} else {
+
+			this.propertyName = propertyName;
 
 
 		}
 		}
-		// 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;
-			}
+		// select getter / setter
+		this.getValue = this.GetterByBindingType[ bindingType ];
+		this.setValue = this.SetterByBindingTypeAndVersioning[ bindingType ][ versioning ];
+
+	},
+
+	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, { // prototype, continued
+
+	// 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,
+
+	BindingType: {
+		Direct: 0,
+		ArrayElement: 1,
+		HasFromToArray: 2
+	},
+
+	Versioning: {
+		None: 0,
+		NeedsUpdate: 1,
+		MatrixWorldNeedsUpdate: 2
+	},
+
+	GetterByBindingType: [
+
+		function getValue_direct( buffer, offset ) {
+
+			buffer[ offset ] = this.node[ this.propertyName ];
+
+		},
+
+		function getValue_arrayElement( buffer, offset ) {
+
+			buffer[ offset ] = this.resolvedProperty[ this.propertyIndex ];
 
 
-			this.getValue = function getValue_propertyObject() {
-				return nodeProperty;
-			};
+		},
+
+		function getValue_toArray( buffer, offset ) {
+
+			this.resolvedProperty.toArray( 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;
+	],
+
+	SetterByBindingTypeAndVersioning: [
+
+		[
+			// Direct
+
+			function setValue_direct( buffer, offset ) {
+
+				this.node[ this.propertyName ] = buffer[ offset ];
+
+			},
+
+			function setValue_direct_setNeedsUpdate( buffer, offset ) {
+
+				this.node[ this.propertyName ] = buffer[ offset ];
+				this.targetObject.needsUpdate = true;
+
+			},
+
+			function setValue_direct_setMatrixWorldNeedsUpdate( buffer, offset ) {
+
+				this.node[ this.propertyName ] = buffer[ offset ];
+				this.targetObject.matrixWorldNeedsUpdate = true;
+
 			}
 			}
 
 
-			this.getValue = function getValue_property() {
-				return targetObject[ this.propertyName ];
-			};
+		], [
 
 
-		}
+			// ArrayElement
 
 
-		// trigger node dirty
-		if ( targetObject.needsUpdate !== undefined ) { // material
+			function setValue_arrayElement( buffer, offset ) {
+
+				this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
+
+			},
+
+			function setValue_arrayElement_setNeedsUpdate( buffer, offset ) {
+
+				this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
+				this.targetObject.needsUpdate = true;
+
+			},
+
+			function setValue_arrayElement_setMatrixWorldNeedsUpdate( buffer, offset ) {
+
+				this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
+				this.targetObject.matrixWorldNeedsUpdate = true;
 
 
-			this.triggerDirty = function triggerDirty_needsUpdate() {
-				this.node.needsUpdate = true;
 			}
 			}
 
 
-		} else if ( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
+		], [
+
+			// HasToFromArray
+
+			function setValue_fromArray( buffer, offset ) {
+
+				this.resolvedProperty.fromArray( buffer, offset );
+
+			},
+
+			function setValue_fromArray_setNeedsUpdate( buffer, offset ) {
+
+				this.resolvedProperty.fromArray( buffer, offset );
+				this.targetObject.needsUpdate = true;
+
+			},
+
+			function setValue_fromArray_setMatrixWorldNeedsUpdate( buffer, offset ) {
+
+				this.resolvedProperty.fromArray( buffer, offset );
+				this.targetObject.matrixWorldNeedsUpdate = true;
 
 
-			this.triggerDirty = function triggerDirty_matrixWorldNeedsUpdate() {
-				targetObject.matrixWorldNeedsUpdate = true;
 			}
 			}
 
 
-		}
+		]
+
+	]
+
+} );
+
+THREE.PropertyBinding.Composite =
+		function( targetGroup, path, optionalParsedPath ) {
+
+	var parsedPath = optionalParsedPath ||
+			THREE.PropertyBinding.parseTrackName( path );
+
+	this._targetGroup = targetGroup;
+	this._bindings = targetGroup.subscribe_( path, parsedPath );
+
+};
+
+THREE.PropertyBinding.Composite.prototype = {
+
+	constructor: THREE.PropertyBinding.Composite,
+
+	getValue: function( array, offset ) {
 
 
-		this.originalValue = this.getValue();
+		this.bind(); // bind all binding
 
 
-		this.equalsValue = THREE.AnimationUtils.getEqualsFunc( this.originalValue );
-		this.lerpValue = THREE.AnimationUtils.getLerpFunc( this.originalValue, true );
+		var firstValidIndex = this._targetGroup.nCachedObjects_,
+			binding = this._bindings[ firstValidIndex ];
 
 
-		this.isBound = true;
+		// and only call .getValue on the first
+		if ( binding !== undefined ) binding.getValue( array, offset );
 
 
 	},
 	},
 
 
-	apply: function() {
+	setValue: function( array, offset ) {
 
 
-		// 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();
+		var bindings = this._bindings;
 
 
-		// early exit if there is nothing to apply.
-		if ( this.cumulativeWeight > 0 ) {
+		for ( var i = this._targetGroup.nCachedObjects_,
+				n = bindings.length; i !== n; ++ i ) {
 
 
-			// blend with original value
-			if ( this.cumulativeWeight < 1 ) {
+			bindings[ i ].setValue( array, offset );
 
 
-				var remainingWeight = 1 - this.cumulativeWeight;
-				var lerpAlpha = remainingWeight / ( this.cumulativeWeight + remainingWeight );
-				this.cumulativeValue = this.lerpValue( this.cumulativeValue, this.originalValue, lerpAlpha );
+		}
 
 
-			}
+	},
 
 
-			var valueChanged = this.setValue( this.cumulativeValue );
+	bind: function() {
 
 
-			if ( valueChanged && this.triggerDirty ) {
-				this.triggerDirty();
-			}
+		var bindings = this._bindings;
 
 
-			// reset accumulator
-			this.cumulativeValue = null;
-			this.cumulativeWeight = 0;
+		for ( var i = this._targetGroup.nCachedObjects_,
+				n = bindings.length; i !== n; ++ i ) {
+
+			bindings[ i ].bind();
 
 
 		}
 		}
+
+	},
+
+	unbind: function() {
+
+		var bindings = this._bindings;
+
+		for ( var i = this._targetGroup.nCachedObjects_,
+				n = bindings.length; i !== n; ++ i ) {
+
+			bindings[ i ].unbind();
+
+		}
+
 	}
 	}
 
 
 };
 };
 
 
+THREE.PropertyBinding.create = function( root, path, parsedPath ) {
+
+	if ( ! ( root instanceof THREE.AnimationObjectGroup ) ) {
+
+		return new THREE.PropertyBinding( root, path, parsedPath );
+
+	} else {
+
+		return new THREE.PropertyBinding.Composite( root, path, parsedPath );
+
+	}
+
+};
 
 
 THREE.PropertyBinding.parseTrackName = function( trackName ) {
 THREE.PropertyBinding.parseTrackName = function( trackName ) {
 
 
@@ -288,7 +483,7 @@ THREE.PropertyBinding.parseTrackName = function( trackName ) {
 	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);
 	var matches = re.exec(trackName);
 
 
-	if ( ! matches ) {
+	if( ! matches ) {
 		throw new Error( "cannot parse trackName at all: " + trackName );
 		throw new Error( "cannot parse trackName at all: " + trackName );
 	}
 	}
 
 
@@ -297,7 +492,7 @@ THREE.PropertyBinding.parseTrackName = function( trackName ) {
     }
     }
 
 
 	var results = {
 	var results = {
-		directoryName: matches[1],
+		// directoryName: matches[1], // (tschw) currently unused
 		nodeName: matches[3], 	// allowed to be null, specified root node.
 		nodeName: matches[3], 	// allowed to be null, specified root node.
 		objectName: matches[5],
 		objectName: matches[5],
 		objectIndex: matches[7],
 		objectIndex: matches[7],
@@ -305,7 +500,7 @@ THREE.PropertyBinding.parseTrackName = function( trackName ) {
 		propertyIndex: matches[11]	// allowed to be null, specifies that the whole property is set.
 		propertyIndex: matches[11]	// allowed to be null, specifies that the whole property is set.
 	};
 	};
 
 
-	if ( results.propertyName === null || results.propertyName.length === 0 ) {
+	if( results.propertyName === null || results.propertyName.length === 0 ) {
 		throw new Error( "can not parse propertyName from trackName: " + trackName );
 		throw new Error( "can not parse propertyName from trackName: " + trackName );
 	}
 	}
 
 
@@ -315,71 +510,69 @@ THREE.PropertyBinding.parseTrackName = function( trackName ) {
 
 
 THREE.PropertyBinding.findNode = function( root, nodeName ) {
 THREE.PropertyBinding.findNode = function( root, nodeName ) {
 
 
-	function searchSkeleton( skeleton ) {
-
-		for ( var i = 0; i < skeleton.bones.length; i ++ ) {
+	if( ! nodeName || nodeName === "" || nodeName === "root" || nodeName === "." || nodeName === -1 || nodeName === root.name || nodeName === root.uuid ) {
 
 
-			var bone = skeleton.bones[i];
+		return root;
 
 
-			if ( bone.name === nodeName ) {
+	}
 
 
-				return bone;
+	// search into skeleton bones.
+	if( root.skeleton ) {
 
 
-			}
-		}
+		var searchSkeleton = function( skeleton ) {
 
 
-		return null;
+			for( var i = 0; i < skeleton.bones.length; i ++ ) {
 
 
-	}
+				var bone = skeleton.bones[i];
 
 
-	function searchNodeSubtree( children ) {
+				if( bone.name === nodeName ) {
 
 
-		for ( var i = 0; i < children.length; i ++ ) {
+					return bone;
 
 
-			var childNode = children[i];
+				}
+			}
 
 
-			if ( childNode.name === nodeName || childNode.uuid === nodeName ) {
+			return null;
 
 
-				return childNode;
+		};
 
 
-			}
+		var bone = searchSkeleton( root.skeleton );
 
 
-			var result = searchNodeSubtree( childNode.children );
+		if( bone ) {
 
 
-			if ( result ) return result;
+			return bone;
 
 
 		}
 		}
+	}
 
 
-		return null;
+	// search into node subtree.
+	if( root.children ) {
 
 
-	}
+		var searchNodeSubtree = function( children ) {
 
 
-	//
+			for( var i = 0; i < children.length; i ++ ) {
 
 
-	if ( ! nodeName || nodeName === "" || nodeName === "root" || nodeName === "." || nodeName === -1 || nodeName === root.name || nodeName === root.uuid ) {
+				var childNode = children[i];
 
 
-		return root;
+				if( childNode.name === nodeName || childNode.uuid === nodeName ) {
 
 
-	}
+					return childNode;
 
 
-	// search into skeleton bones.
-	if ( root.skeleton ) {
+				}
 
 
-		var bone = searchSkeleton( root.skeleton );
+				var result = searchNodeSubtree( childNode.children );
 
 
-		if ( bone ) {
+				if( result ) return result;
 
 
-			return bone;
+			}
 
 
-		}
-	}
+			return null;
 
 
-	// search into node subtree.
-	if ( root.children ) {
+		};
 
 
 		var subTreeNode = searchNodeSubtree( root.children );
 		var subTreeNode = searchNodeSubtree( root.children );
 
 
-		if ( subTreeNode ) {
+		if( subTreeNode ) {
 
 
 			return subTreeNode;
 			return subTreeNode;
 
 
@@ -388,4 +581,5 @@ THREE.PropertyBinding.findNode = function( root, nodeName ) {
 	}
 	}
 
 
 	return null;
 	return null;
+
 }
 }

+ 203 - 0
src/animation/PropertyMixer.js

@@ -0,0 +1,203 @@
+/**
+ *
+ * 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 ( binding, typeName, valueSize ) {
+
+	this.binding = binding;
+	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.useCount = 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.Quaternion.slerpFlat( buffer, dstOffset,
+				buffer, dstOffset, buffer, 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;
+
+		}
+
+	}
+
+};

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

@@ -1,63 +1,34 @@
 /**
 /**
  *
  *
- * A Track that interpolates Boolean
+ * A Track of Boolean keyframe values.
+ *
  *
  *
  * @author Ben Houston / http://clara.io/
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @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;
-
-};
-
-// 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 = function ( name, times, values ) {
 
 
-THREE.BooleanKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
-
-	return ( value0 === value1 );
+	THREE.KeyframeTrack.call( this, name, times, values );
 
 
 };
 };
 
 
-THREE.BooleanKeyframeTrack.prototype.clone = function() {
-
-	var clonedKeys = [];
+THREE.BooleanKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 
-	for ( var i = 0; i < this.keys.length; i ++ ) {
+	constructor: THREE.BooleanKeyframeTrack,
 
 
-		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 - 55
src/animation/tracks/ColorKeyframeTrack.js

@@ -1,73 +1,32 @@
 /**
 /**
  *
  *
- * A Track that interpolates Color
+ * A Track of keyframe values that represent color.
+ *
  *
  *
  * @author Ben Houston / http://clara.io/
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @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 );
-
-THREE.ColorKeyframeTrack.prototype.constructor = THREE.ColorKeyframeTrack;
+THREE.ColorKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 
-THREE.ColorKeyframeTrack.prototype.setResult = function( value ) {
-
-	this.result.copy( value );
-
-};
+	constructor: THREE.ColorKeyframeTrack,
 
 
-// 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 ) {
+	ValueTypeName: 'color'
 
 
-	return value0.lerp( value1, alpha );
+	// ValueBufferType is inherited
 
 
-};
-
-THREE.ColorKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
-
-	return value0.equals( value1 );
-
-};
+	// DefaultInterpolation is inherited
 
 
-THREE.ColorKeyframeTrack.prototype.clone = function() {
 
 
-	var clonedKeys = [];
+	// Note: Very basic implementation and nothing special yet.
+	// However, this is the place for color space parameterization.
 
 
-	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 - 47
src/animation/tracks/NumberKeyframeTrack.js

@@ -1,63 +1,27 @@
 /**
 /**
  *
  *
- * A Track that interpolates Numbers
+ * A Track of numeric keyframe values.
  *
  *
  * @author Ben Houston / http://clara.io/
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
  */
 
 
-THREE.NumberKeyframeTrack = function ( name, keys ) {
+THREE.NumberKeyframeTrack = function ( name, times, values, interpolation ) {
 
 
-	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.prototype.compareValues = function( value0, value1 ) {
-
-	return ( value0 === value1 );
+	THREE.KeyframeTrack.call( this, name, times, values, interpolation );
 
 
 };
 };
 
 
-THREE.NumberKeyframeTrack.prototype.clone = function() {
+THREE.NumberKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 
-	var clonedKeys = [];
+	constructor: THREE.NumberKeyframeTrack,
 
 
-	for ( var i = 0; i < this.keys.length; i ++ ) {
+	ValueTypeName: 'number',
 
 
-		var key = this.keys[i];
-		clonedKeys.push( {
-			time: key.time,
-			value: key.value
-		} );
-	}
+	// ValueBufferType is inherited
 
 
-	return new THREE.NumberKeyframeTrack( this.name, clonedKeys );
+	// DefaultInterpolation is inherited
 
 
-};
-
-THREE.NumberKeyframeTrack.parse = function( json ) {
-
-	return new THREE.NumberKeyframeTrack( json.name, json.keys );
-
-};
+} );

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

@@ -1,85 +1,36 @@
 /**
 /**
  *
  *
- * A Track that interpolates Quaternion
+ * A Track of quaternion keyframe values.
  *
  *
  * @author Ben Houston / http://clara.io/
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @author David Sarno / http://lighthaus.us/
+ * @author tschw
  */
  */
 
 
-THREE.QuaternionKeyframeTrack = function ( name, keys ) {
+THREE.QuaternionKeyframeTrack = function ( name, times, values, interpolation ) {
 
 
-	THREE.KeyframeTrack.call( this, name, keys );
-
-	// local cache of value type to avoid allocations during runtime.
-	this.result = this.keys[0].value.clone();
+	THREE.KeyframeTrack.call( this, name, times, values, interpolation );
 
 
 };
 };
 
 
-THREE.QuaternionKeyframeTrack.prototype = Object.create( THREE.KeyframeTrack.prototype );
-
-THREE.QuaternionKeyframeTrack.prototype.constructor = THREE.QuaternionKeyframeTrack;
+THREE.QuaternionKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 
-THREE.QuaternionKeyframeTrack.prototype.setResult = function( value ) {
+	constructor: THREE.QuaternionKeyframeTrack,
 
 
-	this.result.copy( value );
+	ValueTypeName: 'quaternion',
 
 
-};
+	// ValueBufferType is inherited
 
 
-// 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 ) {
+	DefaultInterpolation: THREE.InterpolateLinear,
 
 
-	return value0.slerp( value1, alpha );
+	InterpolantFactoryMethodLinear: function( result ) {
 
 
-};
+		return new THREE.QuaternionLinearInterpolant(
+				this.times, this.values, this.getValueSize(), result );
 
 
-THREE.QuaternionKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
-
-	return value0.equals( value1 );
-
-};
+	},
 
 
-THREE.QuaternionKeyframeTrack.prototype.multiply = function( quat ) {
-
-	for ( var i = 0; i < this.keys.length; i ++ ) {
-
-		this.keys[i].value.multiply( quat );
-
-	}
-
-	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 - 45
src/animation/tracks/StringKeyframeTrack.js

@@ -2,62 +2,30 @@
  *
  *
  * A Track that interpolates Strings
  * A Track that interpolates Strings
  *
  *
+ *
  * @author Ben Houston / http://clara.io/
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @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;
-
-};
-
-// 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 = function ( name, times, values, interpolation ) {
 
 
-THREE.StringKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
-
-	return ( value0 === value1 );
+	THREE.KeyframeTrack.call( this, name, times, values, interpolation );
 
 
 };
 };
 
 
-THREE.StringKeyframeTrack.prototype.clone = function() {
-
-	var clonedKeys = [];
+THREE.StringKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 
-	for ( var i = 0; i < this.keys.length; i ++ ) {
+	constructor: THREE.StringKeyframeTrack,
 
 
-		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 - 60
src/animation/tracks/VectorKeyframeTrack.js

@@ -1,76 +1,28 @@
 /**
 /**
  *
  *
- * A Track that interpolates Vectors
+ * A Track of vectored keyframe values.
+ *
  *
  *
  * @author Ben Houston / http://clara.io/
  * @author Ben Houston / http://clara.io/
  * @author David Sarno / http://lighthaus.us/
  * @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 ) {
+THREE.VectorKeyframeTrack = function ( name, times, values, interpolation ) {
 
 
-	return value0.lerp( value1, alpha );
+	THREE.KeyframeTrack.call( this, name, times, values, interpolation );
 
 
 };
 };
 
 
-THREE.VectorKeyframeTrack.prototype.compareValues = function( value0, value1 ) {
+THREE.VectorKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 
-	return value0.equals( value1 );
+	constructor: THREE.VectorKeyframeTrack,
 
 
-};
-
-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 );
+	ValueTypeName: 'vector'
 
 
-};
-
-THREE.VectorKeyframeTrack.parse = function( json ) {
-
-	var elementCount = json.keys[0].value.length;
-	var valueType = THREE[ 'Vector' + elementCount ];
+	// ValueBufferType is inherited
 
 
-	var keys = [];
+	// DefaultInterpolation is inherited
 
 
-	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
-		} );
-	}
-
-	return new THREE.VectorKeyframeTrack( json.name, keys );
-
-};
+} );

+ 257 - 0
src/math/Interpolant.js

@@ -0,0 +1,257 @@
+/**
+ * Abstract base class of interpolants over parametric samples.
+ *
+ * The parameter domain is one dimensional, typically the time or a path
+ * along a curve defined by the data.
+ *
+ * The sample values can have any dimensionality and derived classes may
+ * apply special interpretations to the data.
+ *
+ * This class provides the interval seek in a Template Method, deferring
+ * the actual interpolation to derived classes.
+ *
+ * Time complexity is O(1) for linear access crossing at most two points
+ * and O(log N) for random access, where N is the number of positions.
+ *
+ * References:
+ *
+ * 		http://www.oodesign.com/template-method-pattern.html
+ *
+ * @author tschw
+ */
+
+THREE.Interpolant = function(
+		parameterPositions, sampleValues, sampleSize, resultBuffer ) {
+
+	this.parameterPositions = parameterPositions;
+	this._cachedIndex = 0;
+
+	this.resultBuffer = resultBuffer !== undefined ?
+			resultBuffer : new sampleValues.constructor( sampleSize );
+	this.sampleValues = sampleValues;
+	this.valueSize = sampleSize;
+
+};
+
+THREE.Interpolant.prototype = {
+
+	constructor: THREE.Intepolant,
+
+	evaluate: function( t ) {
+
+		var pp = this.parameterPositions,
+			i1 = this._cachedIndex,
+
+			t1 = pp[   i1   ],
+			t0 = pp[ i1 - 1 ];
+
+		validate_interval: {
+
+			seek: {
+
+				var right;
+
+				linear_scan: {
+//- See http://jsperf.com/comparison-to-undefined/3
+//- slower code:
+//-
+//- 				if ( t >= t1 || t1 === undefined ) {
+					forward_scan: if ( ! ( t < t1 ) ) {
+
+						for ( var giveUpAt = i1 + 2; ;) {
+
+							if ( t1 === undefined ) {
+
+								if ( t < t0 ) break forward_scan;
+
+								// after end
+
+								i1 = pp.length;
+								this._cachedIndex = i1;
+								return this.afterEnd_( i1 - 1, t, t0 );
+
+							}
+
+							if ( i1 === giveUpAt ) break; // this loop
+
+							t0 = t1;
+							t1 = pp[ ++ i1 ];
+
+							if ( t < t1 ) {
+
+								// we have arrived at the sought interval
+								break seek;
+
+							}
+
+						}
+
+						// prepare binary search on the right side of the index
+						right = pp.length;
+						break linear_scan;
+
+					}
+
+//- slower code:
+//-					if ( t < t0 || t0 === undefined ) {
+					if ( ! ( t >= t0 ) ) {
+
+						// looping?
+
+						var t1global = pp[ 1 ];
+
+						if ( t < t1global ) {
+
+							i1 = 2; // + 1, using the scan for the details
+							t0 = t1global;
+
+						}
+
+						// linear reverse scan
+
+						for ( var giveUpAt = i1 - 2; ;) {
+
+							if ( t0 === undefined ) {
+
+								// before start
+
+								this._cachedIndex = 0;
+								return this.beforeStart_( 0, t, t1 );
+
+							}
+
+							if ( i1 === giveUpAt ) break; // this loop
+
+							t1 = t0;
+							t0 = pp[ -- i1 - 1 ];
+
+							if ( t >= t0 ) {
+
+								// we have arrived at the sought interval
+								break seek;
+
+							}
+
+						}
+
+						// prepare binary search on the left side of the index
+						right = i1;
+						i1 = 0;
+						break linear_scan;
+
+					}
+
+					// the interval is valid
+
+					break validate_interval;
+
+				} // linear scan
+
+				// binary search
+
+				while ( i1 < right ) {
+
+					var mid = ( i1 + right ) >>> 1;
+
+					if ( t < pp[ mid ] ) {
+
+						right = mid;
+
+					} else {
+
+						i1 = mid + 1;
+
+					}
+
+				}
+
+				t1 = pp[   i1   ];
+				t0 = pp[ i1 - 1 ];
+
+				// check boundary cases, again
+
+				if ( t0 === undefined ) {
+
+					this._cachedIndex = 0;
+					return this.beforeStart_( 0, t, t1 );
+
+				}
+
+				if ( t1 === undefined ) {
+
+					i1 = pp.length;
+					this._cachedIndex = i1;
+					return this.afterEnd_( i1 - 1, t0, t );
+
+				}
+
+			} // seek
+
+			this._cachedIndex = i1;
+
+			this.intervalChanged_( i1, t0, t1 );
+
+		} // validate_interval
+
+		return this.interpolate_( i1, t0, t, t1 );
+
+	},
+
+	settings: null, // optional, subclass-specific settings structure
+	// Note: The indirection allows central control of many interpolants.
+
+	// --- Protected interface
+
+	DefaultSettings_: {},
+
+	getSettings_: function() {
+
+		return this.settings || this.DefaultSettings_;
+
+	},
+
+	copySampleValue_: function( index ) {
+
+		// copies a sample value to the result buffer
+
+		var result = this.resultBuffer,
+			values = this.sampleValues,
+			stride = this.valueSize,
+			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" );
+		// implementations shall return this.resultBuffer
+
+	},
+
+	intervalChanged_: function( i1, t0, t1 ) {
+
+		// empty
+
+	}
+
+};
+
+Object.assign( THREE.Interpolant.prototype, {
+
+	beforeStart_: //( 0, t, t0 ), returns this.resultBuffer
+		THREE.Interpolant.prototype.copySampleValue_,
+
+	afterEnd_: //( N-1, tN-1, t ), returns this.resultBuffer
+		THREE.Interpolant.prototype.copySampleValue_
+
+} );

+ 71 - 3
src/math/Quaternion.js

@@ -516,8 +516,76 @@ THREE.Quaternion.prototype = {
 
 
 };
 };
 
 
-THREE.Quaternion.slerp = function ( qa, qb, qm, t ) {
+Object.assign( THREE.Quaternion, {
 
 
-	return qm.copy( qa ).slerp( qb, t );
+	slerp: function( qa, qb, qm, t ) {
+
+		return qm.copy( qa ).slerp( qb, t );
+
+	},
+
+	slerpFlat: function(
+			dst, dstOffset, src0, srcOffset0, src1, srcOffset1, t ) {
+
+		// fuzz-free, array-based Quaternion SLERP operation
+
+		var x0 = src0[   srcOffset0   ],
+			y0 = src0[ srcOffset0 + 1 ],
+			z0 = src0[ srcOffset0 + 2 ],
+			w0 = src0[ srcOffset0 + 3 ],
+
+			x1 = src1[   srcOffset1   ],
+			y1 = src1[ srcOffset1 + 1 ],
+			z1 = src1[ srcOffset1 + 2 ],
+			w1 = src1[ srcOffset1 + 3 ];
+
+		if ( w0 !== w1 || x0 !== x1 || y0 !== y1 || z0 !== z1 ) {
+
+			var s = 1 - t,
+
+				cos = x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1,
+
+				dir = ( cos >= 0 ? 1 : -1 ),
+				sqrSin = 1 - cos * cos;
+
+			// 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;
+
+			}
+
+			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;
+
+	}
+
+} );
 
 
-};

+ 152 - 0
src/math/interpolants/CubicInterpolant.js

@@ -0,0 +1,152 @@
+/**
+ * Fast and simple cubic spline interpolant.
+ *
+ * It was derived from a Hermitian construction setting the first derivative
+ * at each sample position to the linear slope between neighboring positions
+ * over their parameter interval.
+ *
+ * @author tschw
+ */
+
+THREE.CubicInterpolant = function(
+		parameterPositions, sampleValues, sampleSize, resultBuffer ) {
+
+	THREE.Interpolant.call(
+			this, parameterPositions, sampleValues, sampleSize, resultBuffer );
+
+	this._weightPrev = -0;
+	this._offsetPrev = -0;
+	this._weightNext = -0;
+	this._offsetNext = -0;
+
+};
+
+THREE.CubicInterpolant.prototype =
+		Object.assign( Object.create( THREE.Interpolant.prototype ), {
+
+	constructor: THREE.CubicInterpolant,
+
+	DefaultSettings_: {
+
+		endingStart: 	THREE.ZeroCurvatureEnding,
+		endingEnd:		THREE.ZeroCurvatureEnding
+
+	},
+
+	intervalChanged_: function( i1, t0, t1 ) {
+
+		var pp = this.parameterPositions,
+			iPrev = i1 - 2,
+			iNext = i1 + 1,
+
+			tPrev = pp[ iPrev ],
+			tNext = pp[ iNext ];
+
+		if ( tPrev === undefined ) {
+
+			switch ( this.getSettings_().endingStart ) {
+
+				case THREE.ZeroSlopeEnding:
+
+					// f'(t0) = 0
+					iPrev = i1;
+					tPrev = 2 * t0 - t1;
+
+					break;
+
+				case THREE.WrapAroundEnding:
+
+					// use the other end of the curve
+					iPrev = pp.length - 2;
+					tPrev = t0 + pp[ iPrev ] - pp[ iPrev + 1 ];
+
+					break;
+
+				default: // ZeroCurvatureEnding
+
+					// f''(t0) = 0 a.k.a. Natural Spline
+					iPrev = i1;
+					tPrev = t1;
+
+			}
+
+		}
+
+		if ( tNext === undefined ) {
+
+			switch ( this.getSettings_().endingEnd ) {
+
+				case THREE.ZeroSlopeEnding:
+
+					// f'(tN) = 0
+					iNext = i1;
+					tNext = 2 * t1 - t0;
+
+					break;
+
+				case THREE.WrapAroundEnding:
+
+					// use the other end of the curve
+					iNext = 1;
+					tNext = t1 + pp[ 1 ] - pp[ 0 ];
+
+					break;
+
+				default: // ZeroCurvatureEnding
+
+					// f''(tN) = 0, a.k.a. Natural Spline
+					iNext = i1 - 1;
+					tNext = t0;
+
+			}
+
+		}
+
+		var halfDt = ( t1 - t0 ) * 0.5,
+			stride = this.valueSize;
+
+		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 result = this.resultBuffer,
+			values = this.sampleValues,
+			stride = this.valueSize,
+
+			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;
+
+		// combine data linearly
+
+		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;
+
+	}
+
+} );

+ 28 - 0
src/math/interpolants/DiscreteInterpolant.js

@@ -0,0 +1,28 @@
+/**
+ *
+ * Interpolant that evaluates to the sample value at the position preceeding
+ * the parameter.
+ *
+ * @author tschw
+ */
+
+THREE.DiscreteInterpolant = function(
+		parameterPositions, sampleValues, sampleSize, resultBuffer ) {
+
+	THREE.Interpolant.call(
+			this, parameterPositions, sampleValues, sampleSize, resultBuffer );
+
+};
+
+THREE.DiscreteInterpolant.prototype =
+		Object.assign( Object.create( THREE.Interpolant.prototype ), {
+
+	constructor: THREE.DiscreteInterpolant,
+
+	interpolate_: function( i1, t0, t, t1 ) {
+
+		return this.copySampleValue_( i1 - 1 );
+
+	}
+
+} );

+ 42 - 0
src/math/interpolants/LinearInterpolant.js

@@ -0,0 +1,42 @@
+/**
+ * @author tschw
+ */
+
+THREE.LinearInterpolant = function(
+		parameterPositions, sampleValues, sampleSize, resultBuffer ) {
+
+	THREE.Interpolant.call(
+			this, parameterPositions, sampleValues, sampleSize, resultBuffer );
+
+};
+
+THREE.LinearInterpolant.prototype =
+		Object.assign( Object.create( THREE.Interpolant.prototype ), {
+
+	constructor: THREE.LinearInterpolant,
+
+	interpolate_: function( i1, t0, t, t1 ) {
+
+		var result = this.resultBuffer,
+			values = this.sampleValues,
+			stride = this.valueSize,
+
+			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;
+
+	}
+
+} );

+ 41 - 0
src/math/interpolants/QuaternionLinearInterpolant.js

@@ -0,0 +1,41 @@
+/**
+ * Spherical linear unit quaternion interpolant.
+ *
+ * @author tschw
+ */
+
+THREE.QuaternionLinearInterpolant = function(
+		parameterPositions, sampleValues, sampleSize, resultBuffer ) {
+
+	THREE.Interpolant.call(
+			this, parameterPositions, sampleValues, sampleSize, resultBuffer );
+
+};
+
+THREE.QuaternionLinearInterpolant.prototype =
+		Object.assign( Object.create( THREE.Interpolant.prototype ), {
+
+	constructor: THREE.QuaternionLinearInterpolant,
+
+	interpolate_: function( i1, t0, t, t1 ) {
+
+		var result = this.resultBuffer,
+			values = this.sampleValues,
+			stride = this.valueSize,
+
+			offset = i1 * stride,
+
+			alpha = ( t - t0 ) / ( t1 - t0 );
+
+		for ( var end = offset + stride; offset !== end; offset += 4 ) {
+
+			THREE.Quaternion.slerpFlat( result, 0,
+					values, offset - stride, values, offset, alpha );
+
+		}
+
+		return result;
+
+	}
+
+} );

+ 107 - 0
test/unit/animation/AnimationObjectGroup.js

@@ -0,0 +1,107 @@
+/**
+ * @author tschw
+ */
+
+module( "AnimationObjectGroup" );
+
+var ObjectA = new THREE.Object3D(),
+	ObjectB = new THREE.Object3D(),
+	ObjectC = new THREE.Object3D(),
+
+	PathA = 'object.position',
+	PathB = 'object.rotation',
+	PathC = 'object.scale',
+
+	ParsedPathA = THREE.PropertyBinding.parseTrackName( PathA ),
+	ParsedPathB = THREE.PropertyBinding.parseTrackName( PathB ),
+	ParsedPathC = THREE.PropertyBinding.parseTrackName( PathC );
+
+
+test( "smoke test", function() {
+
+	var expect = function expect( testIndex, group, bindings, path, cached, roots ) {
+
+		var rootNodes = [], pathsOk = true, nodesOk = true;
+
+		for ( var i = group.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {
+
+			if ( bindings[ i ].path !== path ) pathsOk = false;
+			rootNodes.push( bindings[ i ].rootNode );
+
+		}
+
+		for ( var i = 0, n = roots.length; i !== n; ++ i ) {
+
+			if ( rootNodes.indexOf( roots[ i ] ) === -1 ) nodesOk = false;
+
+		}
+
+		ok( pathsOk, testIndex + " paths" );
+		ok( nodesOk, testIndex + " nodes");
+		ok( group.nCachedObjects_ === cached, testIndex + " cache size" );
+		ok( bindings.length - group.nCachedObjects_ === roots.length, testIndex + " object count" );
+
+	};
+
+	// initial state
+
+	var groupA = new THREE.AnimationObjectGroup();
+	ok( groupA instanceof THREE.AnimationObjectGroup, "constructor (w/o args)" );
+
+	var bindingsAA = groupA.subscribe_( PathA, ParsedPathA );
+	expect( 0, groupA, bindingsAA, PathA, 0, [] );
+
+	var groupB = new THREE.AnimationObjectGroup( ObjectA, ObjectB );
+	ok( groupB instanceof THREE.AnimationObjectGroup, "constructor (with args)" );
+
+	var bindingsBB = groupB.subscribe_( PathB, ParsedPathB );
+	expect( 1, groupB, bindingsBB, PathB, 0, [ ObjectA, ObjectB ] );
+
+	// add
+
+	groupA.add( ObjectA, ObjectB );
+	expect( 2, groupA, bindingsAA, PathA, 0, [ ObjectA, ObjectB ] );
+
+	groupB.add( ObjectC );
+	expect( 3, groupB, bindingsBB, PathB, 0, [ ObjectA, ObjectB, ObjectC ] );
+
+	// remove
+
+	groupA.remove( ObjectA, ObjectC );
+	expect( 4, groupA, bindingsAA, PathA, 1, [ ObjectB ] );
+
+	groupB.remove( ObjectA, ObjectB, ObjectC );
+	expect( 5, groupB, bindingsBB, PathB, 3, [] );
+
+	// subscribe after re-add
+
+	groupA.add( ObjectC );
+	expect( 6, groupA, bindingsAA, PathA, 1, [ ObjectB, ObjectC ] );
+	var bindingsAC = groupA.subscribe_( PathC, ParsedPathC );
+	expect( 7, groupA, bindingsAC, PathC, 1, [ ObjectB, ObjectC ] );
+
+	// re-add after subscribe
+
+	var bindingsBC = groupB.subscribe_( PathC, ParsedPathC );
+	groupB.add( ObjectA, ObjectB );
+	expect( 8, groupB, bindingsBB, PathB, 1, [ ObjectA, ObjectB ] );
+
+	// unsubscribe
+
+	var copyOfBindingsBC = bindingsBC.slice();
+	groupB.unsubscribe_( PathC );
+	groupB.add( ObjectC );
+	deepEqual( bindingsBC, copyOfBindingsBC, "no more update after unsubscribe" );
+
+	// uncache active
+
+	groupB.uncache( ObjectA );
+	expect( 9, groupB, bindingsBB, PathB, 0, [ ObjectB, ObjectC ] );
+
+	// uncache cached
+
+	groupA.uncache( ObjectA );
+	expect( 10, groupA, bindingsAC, PathC, 0, [ ObjectB, ObjectC ] );
+
+} );
+

+ 387 - 0
test/unit/math/Interpolant.js

@@ -0,0 +1,387 @@
+/**
+ * @author tschw
+ */
+
+module( "Interpolant" );
+
+// Since this is an abstract base class, we have to make it concrete in order
+// to test its functionality...
+
+function Mock( parameterPositions, sampleValues, sampleSize, resultBuffer ) {
+
+	THREE.Interpolant.call(
+			this, parameterPositions, sampleValues, sampleSize, resultBuffer );
+
+}
+
+Mock.prototype = Object.create( THREE.Interpolant.prototype );
+
+Mock.prototype.intervalChanged_ = function intervalChanged( i1, t0, t1 ) {
+
+	Mock.captureCall( arguments );
+
+};
+
+Mock.prototype.interpolate_ = function interpolate( i1, t0, t, t1 ) {
+
+	Mock.captureCall( arguments );
+	return this.copySampleValue_( i1 - 1 );
+
+};
+
+Mock.prototype.beforeStart_ = function beforeStart( i, t, t0 ) {
+
+	Mock.captureCall( arguments );
+	return this.copySampleValue_( i );
+
+};
+
+Mock.prototype.afterEnd_ = function afterEnd( i, tN, t ) {
+
+	Mock.captureCall( arguments );
+	return this.copySampleValue_( i );
+
+};
+
+// Call capturing facility
+
+Mock.calls = null;
+
+Mock.captureCall = function( args ) {
+
+	if ( Mock.calls !== null ) {
+
+		Mock.calls.push( {
+			func: Mock.captureCall.caller.name,
+			args: Array.prototype.slice.call( args )
+		} );
+
+	}
+
+};
+
+// Tests
+
+test( "copySampleValue_", function() {
+
+	var interpolant = new Mock( null, [ 1, 11, 2, 22, 3, 33 ], 2, [] );
+
+	deepEqual( interpolant.copySampleValue_( 0 ), [ 1, 11 ], "sample fetch (0)" );
+	deepEqual( interpolant.copySampleValue_( 1 ), [ 2, 22 ], "sample fetch (1)" );
+	deepEqual( interpolant.copySampleValue_( 2 ), [ 3, 33 ], "first sample (2)" );
+
+} );
+
+test( "evaluate -> intervalChanged_ / interpolate_", function() {
+
+	var actual, expect;
+
+	var interpolant = new Mock( [ 11, 22, 33, 44, 55, 66, 77, 88, 99 ], null, 0, null );
+
+	Mock.calls = [];
+	interpolant.evaluate( 11 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 1, 11, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 1, 11, 11, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 12 ); // same interval
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'interpolate', args: [ 1, 11, 12, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 22 ); // step forward
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 2, 22, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 2, 22, 22, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2 );
+
+	Mock.calls = [];
+	interpolant.evaluate( 21 ); // step back
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 1, 11, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 1, 11, 21, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 20 ); // same interval
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'interpolate', args: [ 1, 11, 20, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 43 ); // two steps forward
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 3, 33, 44 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 3, 33, 43, 44 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 12 ); // two steps back
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 1, 11, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 1, 11, 12, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 77 ); // random access
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 7, 77, 88 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 7, 77, 77, 88 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 80 ); // same interval
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'interpolate', args: [ 7, 77, 80, 88 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 36 ); // random access
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 3, 33, 44 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 3, 33, 36, 44 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 24 ); // fast reset / loop (2nd)
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 2, 22, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 2, 22, 24, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+	Mock.calls = [];
+	interpolant.evaluate( 16 ); // fast reset / loop (2nd)
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 1, 11, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 1, 11, 16, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+} );
+
+test( "evaulate -> beforeStart_ [once]", function() {
+
+	var actual, expect;
+
+	var interpolant = new Mock( [ 11, 22, 33 ], null, 0, null );
+
+	Mock.calls = [];
+	interpolant.evaluate( 10 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'beforeStart', args: [ 0, 10, 11 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+	// Check operation resumes normally and intervalChanged gets called
+	Mock.calls = [];
+	interpolant.evaluate( 11 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 1, 11, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 1, 11, 11, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+	// Back off-bounds
+	Mock.calls = [];
+	interpolant.evaluate( 10 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'beforeStart', args: [ 0, 10, 11 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+} );
+
+test( "evaluate -> beforeStart_ [twice]", function() {
+
+	var actual, expect;
+
+	var interpolant = new Mock( [ 11, 22, 33 ], null, 0, null );
+
+	Mock.calls = [];
+	interpolant.evaluate( 10 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'beforeStart', args: [ 0, 10, 11 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+	Mock.calls = []; // again - consider changed state
+	interpolant.evaluate( 10 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'beforeStart', args: [ 0, 10, 11 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+	// Check operation resumes normally and intervalChanged gets called
+	Mock.calls = [];
+	interpolant.evaluate( 11 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 1, 11, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 1, 11, 11, 22 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+} );
+
+test( "evaluate -> afterEnd_ [once]", function() {
+
+	var actual, expect;
+
+	var interpolant = new Mock( [ 11, 22, 33 ], null, 0, null );
+
+	Mock.calls = [];
+	interpolant.evaluate( 33 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'afterEnd', args: [ 2, 33, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+	// Check operation resumes normally and intervalChanged gets called
+	Mock.calls = [];
+	interpolant.evaluate( 32 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 2, 22, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 2, 22, 32, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+	// Back off-bounds
+	Mock.calls = [];
+	interpolant.evaluate( 33 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'afterEnd', args: [ 2, 33, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+} );
+
+test( "evaluate -> afterEnd_ [twice]", function() {
+
+	var actual, expect;
+
+	var interpolant = new Mock( [ 11, 22, 33 ], null, 0, null );
+
+	Mock.calls = [];
+	interpolant.evaluate( 33 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'afterEnd', args: [ 2, 33, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+	Mock.calls = []; // again - consider changed state
+	interpolant.evaluate( 33 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'afterEnd', args: [ 2, 33, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 1, "no further calls" );
+
+	// Check operation resumes normally and intervalChanged gets called
+	Mock.calls = [];
+	interpolant.evaluate( 32 );
+
+	actual = Mock.calls[ 0 ];
+	expect = { func: 'intervalChanged', args: [ 2, 22, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	actual = Mock.calls[ 1 ];
+	expect = { func: 'interpolate', args: [ 2, 22, 32, 33 ] };
+	deepEqual( actual, expect, JSON.stringify( expect ) );
+
+	ok( Mock.calls.length === 2, "no further calls" );
+
+} );

+ 143 - 8
test/unit/math/Quaternion.js

@@ -1,5 +1,6 @@
 /**
 /**
  * @author bhouston / http://exocortex.com
  * @author bhouston / http://exocortex.com
+ * @author tschw
  */
  */
 
 
 module( "Quaternion" );
 module( "Quaternion" );
@@ -173,7 +174,7 @@ test( "multiplyQuaternions/multiply", function() {
 });
 });
 
 
 test( "multiplyVector3", function() {
 test( "multiplyVector3", function() {
-	
+
 	var angles = [ new THREE.Euler( 1, 0, 0 ), new THREE.Euler( 0, 1, 0 ), new THREE.Euler( 0, 0, 1 ) ];
 	var angles = [ new THREE.Euler( 1, 0, 0 ), new THREE.Euler( 0, 1, 0 ), new THREE.Euler( 0, 0, 1 ) ];
 
 
 	// ensure euler conversion for Quaternion matches that of Matrix4
 	// ensure euler conversion for Quaternion matches that of Matrix4
@@ -185,7 +186,7 @@ test( "multiplyVector3", function() {
 			var v0 = new THREE.Vector3(1, 0, 0);
 			var v0 = new THREE.Vector3(1, 0, 0);
 			var qv = v0.clone().applyQuaternion( q );
 			var qv = v0.clone().applyQuaternion( q );
 			var mv = v0.clone().applyMatrix4( m );
 			var mv = v0.clone().applyMatrix4( m );
-		
+
 			ok( qv.distanceTo( mv ) < 0.001, "Passed!" );
 			ok( qv.distanceTo( mv ) < 0.001, "Passed!" );
 		}
 		}
 	}
 	}
@@ -195,7 +196,7 @@ test( "multiplyVector3", function() {
 test( "equals", function() {
 test( "equals", function() {
 	var a = new THREE.Quaternion( x, y, z, w );
 	var a = new THREE.Quaternion( x, y, z, w );
 	var b = new THREE.Quaternion( -x, -y, -z, -w );
 	var b = new THREE.Quaternion( -x, -y, -z, -w );
-	
+
 	ok( a.x != b.x, "Passed!" );
 	ok( a.x != b.x, "Passed!" );
 	ok( a.y != b.y, "Passed!" );
 	ok( a.y != b.y, "Passed!" );
 
 
@@ -210,10 +211,144 @@ test( "equals", function() {
 	ok( b.equals( a ), "Passed!" );
 	ok( b.equals( a ), "Passed!" );
 });
 });
 
 
+
+function doSlerpObject( aArr, bArr, t ) {
+
+	var a = new THREE.Quaternion().fromArray( aArr ),
+		b = new THREE.Quaternion().fromArray( bArr ),
+		c = new THREE.Quaternion().fromArray( aArr );
+
+	c.slerp( b, t );
+
+	return {
+
+		equals: function( x, y, z, w, maxError ) {
+
+			if ( maxError === undefined ) maxError = Number.EPSILON;
+
+			return 	Math.abs( x - c.x ) <= maxError &&
+					Math.abs( y - c.y ) <= maxError &&
+					Math.abs( z - c.z ) <= maxError &&
+					Math.abs( w - c.w ) <= maxError;
+
+		},
+
+		length: c.length(),
+
+		dotA: c.dot( a ),
+		dotB: c.dot( b )
+
+	};
+
+};
+
+function doSlerpArray( a, b, t ) {
+
+	var result = [ 0, 0, 0, 0 ];
+
+	THREE.Quaternion.slerpFlat( result, 0, a, 0, b, 0, t );
+
+	function arrDot( a, b ) {
+
+		return 	a[ 0 ] * b[ 0 ] + a[ 1 ] * b[ 1 ] +
+				a[ 2 ] * b[ 2 ] + a[ 3 ] * b[ 3 ];
+
+	}
+
+	return {
+
+		equals: function( x, y, z, w, maxError ) {
+
+			if ( maxError === undefined ) maxError = Number.EPSILON;
+
+			return 	Math.abs( x - result[ 0 ] ) <= maxError &&
+					Math.abs( y - result[ 1 ] ) <= maxError &&
+					Math.abs( z - result[ 2 ] ) <= maxError &&
+					Math.abs( w - result[ 3 ] ) <= maxError;
+
+		},
+
+		length: Math.sqrt( arrDot( result, result ) ),
+
+		dotA: arrDot( result, a ),
+		dotB: arrDot( result, b )
+
+	};
+
+}
+
+function slerpTestSkeleton( doSlerp, maxError ) {
+
+	var a, b, result;
+
+	a = [
+		0.6753410084407496,
+		0.4087830051091744,
+		0.32856700410659473,
+		0.5185120064806223,
+	];
+
+	b = [
+		0.6602792107657797,
+		0.43647413932562285,
+		0.35119011210236006,
+		0.5001871596632682
+	];
+
+	var maxNormError = 0;
+
+	function isNormal( result ) {
+
+		var normError = Math.abs( 1 - result.length );
+		maxNormError = Math.max( maxNormError, normError );
+		return normError <= maxError;
+
+	}
+
+	result = doSlerp( a, b, 0 );
+	ok( result.equals(
+			a[ 0 ], a[ 1 ], a[ 2 ], a[ 3 ], 0 ), "Exactly A @ t = 0" );
+
+	result = doSlerp( a, b, 1 );
+	ok( result.equals(
+			b[ 0 ], b[ 1 ], b[ 2 ], b[ 3 ], 0 ), "Exactly B @ t = 1" );
+
+	result = doSlerp( a, b, 0.5 );
+	ok( Math.abs( result.dotA - result.dotB ) <= Number.EPSILON, "Symmetry at 0.5" );
+	ok( isNormal( result ), "Approximately normal (at 0.5)" );
+
+	result = doSlerp( a, b, 0.25 );
+	ok( result.dotA > result.dotB, "Interpolating at 0.25" );
+	ok( isNormal( result ), "Approximately normal (at 0.25)" );
+
+	result = doSlerp( a, b, 0.75 );
+	ok( result.dotA < result.dotB, "Interpolating at 0.75" );
+	ok( isNormal( result ), "Approximately normal (at 0.75)" );
+
+	var D = Math.SQRT1_2;
+
+	result = doSlerp( [ 1, 0, 0, 0 ], [ 0, 0, 1, 0 ], 0.5 );
+	ok( result.equals( D, 0, D, 0 ), "X/Z diagonal from axes" );
+	ok( isNormal( result ), "Approximately normal (X/Z diagonal)" );
+
+	result = doSlerp( [ 0, D, 0, D ], [ 0, -D, 0, D ], 0.5 );
+	ok( result.equals( 0, 0, 0, 1 ), "W-Unit from diagonals" );
+	ok( isNormal( result ), "Approximately normal (W-Unit)" );
+
+	console.log( "maxNormError", maxNormError );
+
+}
+
+
 test( "slerp", function() {
 test( "slerp", function() {
-	var a = new THREE.Quaternion( 0.675341, 0.408783, 0.328567, 0.518512 );
-	var b = new THREE.Quaternion( 0.660279, 0.436474, 0.35119, 0.500187 );
 
 
-	ok( a.slerp( b, 0 ).equals( a ), "Passed!" );
-	ok( a.slerp( b, 1 ).equals( b ), "Passed!" );
-});
+	var ErrorMargin = Number.EPSILON * 64; // probably improvable
+	slerpTestSkeleton( doSlerpObject, ErrorMargin );
+
+} );
+
+test( "slerpFlat", function() {
+
+	slerpTestSkeleton( doSlerpArray, Number.EPSILON );
+
+} );

+ 4 - 0
test/unit/unittests_sources.html

@@ -6,6 +6,7 @@
   <link rel="stylesheet" href="qunit-1.18.0.css">
   <link rel="stylesheet" href="qunit-1.18.0.css">
 </head>
 </head>
 <body>
 <body>
+
   <div id="qunit"></div>
   <div id="qunit"></div>
   <script src="qunit-1.18.0.js"></script>
   <script src="qunit-1.18.0.js"></script>
   <script src="qunit-utils.js"></script>
   <script src="qunit-utils.js"></script>
@@ -30,6 +31,7 @@
   <script src="../../src/math/Plane.js"></script>
   <script src="../../src/math/Plane.js"></script>
   <script src="../../src/math/Math.js"></script>
   <script src="../../src/math/Math.js"></script>
   <script src="../../src/math/Triangle.js"></script>
   <script src="../../src/math/Triangle.js"></script>
+  <script src="../../src/math/Interpolant.js"></script>
 
 
   <!-- add class-based unit tests below -->
   <!-- add class-based unit tests below -->
 
 
@@ -51,5 +53,7 @@
   <script src="math/Matrix4.js"></script>
   <script src="math/Matrix4.js"></script>
   <script src="math/Frustum.js"></script>
   <script src="math/Frustum.js"></script>
   <script src="math/Color.js"></script>
   <script src="math/Color.js"></script>
+  <script src="math/Interpolant.js"></script>
+
 </body>
 </body>
 </html>
 </html>

+ 3 - 0
test/unit/unittests_three-math.html

@@ -6,6 +6,7 @@
   <link rel="stylesheet" href="qunit-1.18.0.css">
   <link rel="stylesheet" href="qunit-1.18.0.css">
 </head>
 </head>
 <body>
 <body>
+
   <div id="qunit"></div>
   <div id="qunit"></div>
   <script src="qunit-1.18.0.js"></script>
   <script src="qunit-1.18.0.js"></script>
   <script src="qunit-utils.js"></script>
   <script src="qunit-utils.js"></script>
@@ -33,5 +34,7 @@
   <script src="math/Matrix3.js"></script>
   <script src="math/Matrix3.js"></script>
   <script src="math/Matrix4.js"></script>
   <script src="math/Matrix4.js"></script>
   <script src="math/Frustum.js"></script>
   <script src="math/Frustum.js"></script>
+  <script src="math/Interpolant.js"></script>
+
 </body>
 </body>
 </html>
 </html>

+ 6 - 2
test/unit/unittests_three.html

@@ -45,14 +45,18 @@
   <script src="math/Matrix3.js"></script>
   <script src="math/Matrix3.js"></script>
   <script src="math/Matrix4.js"></script>
   <script src="math/Matrix4.js"></script>
   <script src="math/Frustum.js"></script>
   <script src="math/Frustum.js"></script>
+  <script src="math/Interpolant.js"></script>
+
+  <script src="animation/AnimationObjectGroup.js"></script>
 
 
-  <script src="geometry/EdgesGeometry.js"></script>
-  <script src="extras/ImageUtils.test.js"></script>
   <script src="lights/AmbientLight.tests.js"></script>
   <script src="lights/AmbientLight.tests.js"></script>
   <script src="lights/DirectionalLight.tests.js"></script>
   <script src="lights/DirectionalLight.tests.js"></script>
   <script src="lights/HemisphereLight.tests.js"></script>
   <script src="lights/HemisphereLight.tests.js"></script>
   <script src="lights/PointLight.tests.js"></script>
   <script src="lights/PointLight.tests.js"></script>
   <script src="lights/SpotLight.tests.js"></script>
   <script src="lights/SpotLight.tests.js"></script>
+  <script src="geometry/EdgesGeometry.js"></script>
+
+  <script src="extras/ImageUtils.test.js"></script>
 
 
   <script src="extras/geometries/BoxGeometry.tests.js"></script>
   <script src="extras/geometries/BoxGeometry.tests.js"></script>
   <script src="extras/geometries/CircleBufferGeometry.tests.js"></script>
   <script src="extras/geometries/CircleBufferGeometry.tests.js"></script>

+ 2 - 0
test/unit/unittests_three.min.html

@@ -33,5 +33,7 @@
   <script src="math/Matrix3.js"></script>
   <script src="math/Matrix3.js"></script>
   <script src="math/Matrix4.js"></script>
   <script src="math/Matrix4.js"></script>
   <script src="math/Frustum.js"></script>
   <script src="math/Frustum.js"></script>
+  <script src="math/Interpolant.js"></script>
+
 </body>
 </body>
 </html>
 </html>

+ 10 - 4
utils/build/includes/common.json

@@ -18,6 +18,11 @@
 	"src/math/Math.js",
 	"src/math/Math.js",
 	"src/math/Spline.js",
 	"src/math/Spline.js",
 	"src/math/Triangle.js",
 	"src/math/Triangle.js",
+	"src/math/Interpolant.js",
+	"src/math/interpolants/CubicInterpolant.js",
+	"src/math/interpolants/DiscreteInterpolant.js",
+	"src/math/interpolants/LinearInterpolant.js",
+	"src/math/interpolants/QuaternionLinearInterpolant.js",
 	"src/core/Clock.js",
 	"src/core/Clock.js",
 	"src/core/EventDispatcher.js",
 	"src/core/EventDispatcher.js",
 	"src/core/Layers.js",
 	"src/core/Layers.js",
@@ -34,17 +39,18 @@
 	"src/core/DirectGeometry.js",
 	"src/core/DirectGeometry.js",
 	"src/core/BufferGeometry.js",
 	"src/core/BufferGeometry.js",
 	"src/core/InstancedBufferGeometry.js",
 	"src/core/InstancedBufferGeometry.js",
-	"src/animation/AnimationAction.js",
 	"src/animation/AnimationClip.js",
 	"src/animation/AnimationClip.js",
 	"src/animation/AnimationMixer.js",
 	"src/animation/AnimationMixer.js",
+	"src/animation/AnimationObjectGroup.js",
 	"src/animation/AnimationUtils.js",
 	"src/animation/AnimationUtils.js",
 	"src/animation/KeyframeTrack.js",
 	"src/animation/KeyframeTrack.js",
 	"src/animation/PropertyBinding.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/BooleanKeyframeTrack.js",
 	"src/animation/tracks/NumberKeyframeTrack.js",
 	"src/animation/tracks/NumberKeyframeTrack.js",
+	"src/animation/tracks/QuaternionKeyframeTrack.js",
+	"src/animation/tracks/StringKeyframeTrack.js",
+	"src/animation/tracks/VectorKeyframeTrack.js",
 	"src/cameras/Camera.js",
 	"src/cameras/Camera.js",
 	"src/cameras/CubeCamera.js",
 	"src/cameras/CubeCamera.js",
 	"src/cameras/OrthographicCamera.js",
 	"src/cameras/OrthographicCamera.js",

+ 6 - 1
utils/build/includes/math.json

@@ -17,5 +17,10 @@
 	"src/math/Plane.js",
 	"src/math/Plane.js",
 	"src/math/Math.js",
 	"src/math/Math.js",
 	"src/math/Spline.js",
 	"src/math/Spline.js",
-	"src/math/Triangle.js"
+	"src/math/Triangle.js",
+	"src/math/Interpolant.js",
+	"src/math/interpolants/CubicInterpolant.js",
+	"src/math/interpolants/DiscreteInterpolant.js",
+	"src/math/interpolants/LinearInterpolant.js",
+	"src/math/interpolants/QuaternionLinearInterpolant.js"
 ]
 ]