瀏覽代碼

Animation: New API, caching and masses.

tschw 9 年之前
父節點
當前提交
c32f31a587
共有 37 個文件被更改,包括 2488 次插入743 次删除
  1. 1 1
      examples/canvas_morphtargets_horse.html
  2. 23 28
      examples/js/BlendCharacter.js
  3. 3 10
      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 11
      examples/webgl_animation_skinning_blending.html
  10. 372 48
      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. 0 190
      src/animation/AnimationAction.js
  22. 1 1
      src/animation/AnimationClip.js
  23. 1187 216
      src/animation/AnimationMixer.js
  24. 370 0
      src/animation/AnimationObjectGroup.js
  25. 1 2
      src/animation/AnimationUtils.js
  26. 6 6
      src/animation/KeyframeTrack.js
  27. 293 167
      src/animation/PropertyBinding.js
  28. 3 2
      src/animation/PropertyMixer.js
  29. 2 1
      src/animation/tracks/BooleanKeyframeTrack.js
  30. 2 1
      src/animation/tracks/ColorKeyframeTrack.js
  31. 2 1
      src/animation/tracks/NumberKeyframeTrack.js
  32. 2 1
      src/animation/tracks/QuaternionKeyframeTrack.js
  33. 2 1
      src/animation/tracks/StringKeyframeTrack.js
  34. 2 1
      src/animation/tracks/VectorKeyframeTrack.js
  35. 107 0
      test/unit/animation/AnimationObjectGroup.js
  36. 5 2
      test/unit/unittests_three.html
  37. 1 1
      utils/build/includes/common.json

+ 1 - 1
examples/canvas_morphtargets_horse.html

@@ -78,7 +78,7 @@
 					mixer = new THREE.AnimationMixer( mesh );
 
 					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 () {
 
-	this.animations = {};
 	this.weightSchedule = [];
 	this.warpSchedule = [];
 
@@ -20,13 +19,13 @@ THREE.BlendCharacter = function () {
 
 			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 ) {
 
-				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.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.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.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 ) {
 
-		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.mixer.timeScale = 0;
@@ -103,7 +98,7 @@ THREE.BlendCharacter = function () {
 
 	this.stopAll = function() {
 
-		this.mixer.removeAllActions();
+		this.mixer.stopAllAction();
 
 	};
 

+ 3 - 10
examples/js/BlendCharacterGui.js

@@ -40,16 +40,9 @@ function BlendCharacterGui( blendMesh ) {
 
 	this.update = function( time ) {
 
-		var getWeight = function( actionName ) {
-
-			var action = blendMesh.mixer.findActionByName( actionName );
-			return ( action !== null) ? action.getWeightAt( time ) : 0;
-
-		}
-
-		controls[ 'idle' ] = getWeight( 'idle' );
-		controls[ 'walk' ] = getWeight( 'walk' );
-		controls[ 'run' ] = getWeight( 'run' );
+		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 = [];
 		for ( var i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
-
 		// SKINS
 
 		this.skinsBody = loadTextures( config.baseUrl + "skins/", config.skins );
@@ -81,6 +80,21 @@ THREE.MD2Character = function () {
 				scope.weapons[ index ] = 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();
 
 			}
@@ -154,17 +168,15 @@ THREE.MD2Character = function () {
 		if ( this.meshBody ) {
 
 			if( this.meshBody.activeAction ) {
-				scope.mixer.removeAction( this.meshBody.activeAction );
+				this.meshBody.activeAction.stop();
 				this.meshBody.activeAction = null;
 			}
 
 			var clip = THREE.AnimationClip.findByName( this.meshBody.geometry.animations, clipName );
 			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( this.meshWeapon.activeAction ) {
-				scope.mixer.removeAction( this.meshWeapon.activeAction );
+				this.meshWeapon.activeAction.stop();
 				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 ) {
 
-		this.mixer.removeAction( this.activeAction );
+		this.activeAction.stop();
 		this.activeAction = null;
 		
 	}
@@ -40,10 +40,9 @@ THREE.MorphAnimMesh.prototype.playAnimation = function ( label, fps ) {
 
 	if ( clip ) {
 
-		var action = new THREE.AnimationAction( clip );
+		var action = this.mixer.clipAction( clip );
 		action.timeScale = ( clip.tracks.length * fps ) / clip.duration;
-		this.mixer.addAction( action );
-		this.activeAction = action;
+		this.activeAction = action.play();
 
 	} else {
 

+ 1 - 1
examples/js/UCSCharacter.js

@@ -55,7 +55,7 @@ THREE.UCSCharacter = function() {
 			mesh.castShadow = 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 );
 			

+ 1 - 1
examples/webgl_animation_blend.html

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

+ 1 - 11
examples/webgl_animation_skinning_blending.html

@@ -156,17 +156,7 @@
 				var data = event.detail;
 				for ( var i = 0; i < data.anims.length; ++i ) {
 
-					var action = blendMesh.mixer.findActionByName( data.anims[i] );
-
-					if ( action !== null ) {
-
-						if( action.getWeightAt( blendMesh.mixer.time ) !== data.weights[i] ) {
-
-							action.weight = data.weights[i];
-
-						}
-
-					}
+					blendMesh.applyWeight( data.anims[ i ], data.weights[ i ] );
 
 				}
 

+ 372 - 48
examples/webgl_animation_skinning_morph.html

@@ -22,6 +22,12 @@
 				padding: 5px;
 			}
 
+			#meminfo {
+				margin-top: 8px;
+				font-size: 10px;
+				display: none;
+			}
+
 			a {
 				color: #0af;
 			}
@@ -40,6 +46,7 @@
 		<div id="info">
 		<a href="http://threejs.org" target="_blank">three.js</a> webgl - clip system
 		- knight by <a href="http://vimeo.com/36113323">apendua</a>
+			<div id="meminfo"></div>
 		</div>
 
 		<script src="../build/three.min.js"></script>
@@ -59,9 +66,9 @@
 			var camera, scene;
 			var renderer;
 
-			var mesh, helper;
+			var mesh, mesh2, helper;
 
-			var mixer, facesAction, bonesAction;
+			var mixer, facesClip, bonesClip;
 
 			var mouseX = 0, mouseY = 0;
 
@@ -70,6 +77,9 @@
 
 			var clock = new THREE.Clock();
 
+			var domMemInfo = document.getElementById( 'meminfo' ),
+				showMemInfo = false;
+
 			document.addEventListener( 'mousemove', onDocumentMouseMove, false );
 
 			init();
@@ -212,6 +222,7 @@
 				}
 
 				mesh = new THREE.SkinnedMesh( geometry, new THREE.MeshFaceMaterial( materials ) );
+				mesh.name = "Knight Mesh";
 				mesh.position.set( x, y - bb.min.y * s, z );
 				mesh.scale.set( s, s, s );
 				scene.add( mesh );
@@ -219,6 +230,19 @@
 				mesh.castShadow = 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.material.linewidth = 3;
 				helper.visible = false;
@@ -226,87 +250,375 @@
 
 				mixer = new THREE.AnimationMixer( mesh );
 
-				var clipBones = geometry.animations[0];
-				bonesAction = new THREE.AnimationAction( clipBones );
-
-				var clipMorpher = THREE.AnimationClip.CreateFromMorphTargetSequence( 'facialExpressions', mesh.geometry.morphTargets, 3 );
-				facesAction = new THREE.AnimationAction( clipMorpher );
+				bonesClip = geometry.animations[0];
+				facesClip = THREE.AnimationClip.CreateFromMorphTargetSequence( 'facialExpressions', mesh.geometry.morphTargets, 3 );
 			}
 
 			function initGUI() {
 
 				var API = {
-					'show model'    : true,
-					'show skeleton' : false,
-					'bones action' : true, // use false to see initial allocation
-					'bones enable' : true,
-					'faces action' : true,
-					'faces enable' : true,
-					'release props' : function() { mixer.releaseCachedBindings( true ); },
-					'purge cache' : function() { mixer.releaseCachedBindings(); }
+					'show model'    	: true,
+					'show skeleton'		: false,
+					'show 2nd model'	: false,
+					'show mem. info'	: false
 				};
 
 				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 skeleton' ).onChange( function() { helper.visible = API[ 'show skeleton' ]; } );
+				gui.add( API, 'show 2nd model' ).onChange( function() {
+						mesh2.visible = API[ 'show 2nd model' ];
+				} );
 
 
-				// Note: .add/removeAction and .enabled = true / false have
-				// different performance characteristics:
-				//
-				// The former changes dynamic data structures in the mixer,
-				// therefore the switch is more expensive but removes the
-				// per-action  base cost caused by the unique property
-				// bindings it uses.
-				//
-				// The latter is a zero-cost switch, but the per-frame base
-				// cost for having the action added to the mixer remains.
+				gui.add( API, 'show mem. info' ).onChange( function() {
 
-				function actionControls( key, action ) {
+					showMemInfo = API[ 'show mem. info' ];
+					domMemInfo.style.display = showMemInfo ? 'block' : 'none';
 
-					var guiNameAddRemove = key + ' action';
-					var guiNameEnabled = key + ' enable';
+				} );
+
+				// utility function used for drop-down options lists in the GUI
+				var objectNames = function( objects ) {
 
-					// set initial state
+					var result = [];
 
-					if ( API[ guiNameAddRemove ] ) {
+					for ( var i = 0, n = objects.length; i !== n; ++ i ) {
 
-						action.enabled = API[ guiNameEnabled ];
-						mixer.addAction( action );
+						var obj = objects[ i ];
+						result.push( obj && obj.name || '&lt;null&gt;' );
 
 					}
 
-					// attach controls
+					return result;
 
-					gui.add( API, guiNameAddRemove ).onChange( function() {
+				};
 
-						if ( API[ guiNameAddRemove ] ) {
 
-							mixer.addAction( action );
+				// creates gui folder with tests / examples for the action API
+				var clipControl = function clipControl( gui, mixer, clip, rootObjects ) {
 
-						} else {
+					var folder = gui.addFolder( "Clip '" + clip.name + "'" ),
 
-							mixer.removeAction( action );
+						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 : true;
+
+							},
+
+							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 );
+
+							},
 
-					gui.add( API, guiNameEnabled ).onChange( function() {
+							'fade in': function() {
 
-						action.enabled = API[ guiNameEnabled ];
+								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' );
 
 				}
 
-				actionControls( 'bones', bonesAction );
-				actionControls( 'faces', facesAction );
+				memoryControl( gui, mixer,
+						[ bonesClip, facesClip ], [ mesh, mesh2 ] );
 
-				gui.add( API, 'release props' );
-				gui.add( API, 'purge cache' );
 
 			}
 
@@ -326,6 +638,18 @@
 				render();
 				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() {

+ 1 - 1
examples/webgl_lights_hemisphere.html

@@ -214,7 +214,7 @@
 					scene.add( 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 );
 
 				} );

+ 8 - 13
examples/webgl_loader_json_blender.html

@@ -59,7 +59,7 @@
 			var dae;
 
 			var clock = new THREE.Clock();
-			var mixers = [];
+			var mixer;
 
 			// Collada model
 
@@ -102,6 +102,8 @@
 
 				// Add Blender exported Collada model
 
+				mixer = new THREE.AnimationMixer( scene );
+
 				var loader = new THREE.JSONLoader();
 				loader.load( 'models/animated/monster/monster.js', function ( geometry, materials ) {
 
@@ -137,13 +139,10 @@
 
 						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 );
 
-				for ( var i = 0; i < mixers.length; i ++ ) {
-
-					mixers[ i ].update( delta );
-
-				}
+				mixer.update( delta );
 
 
 				render();

+ 1 - 1
examples/webgl_loader_scene.html

@@ -214,7 +214,7 @@
 							if( object.geometry && object.geometry.animations && object.geometry.animations.length > 0 ) {
 
 								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 );
 
 							}

+ 2 - 2
examples/webgl_morphnormals.html

@@ -93,7 +93,7 @@
 					scene.add( 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 );
 
@@ -120,7 +120,7 @@
 					scene.add( 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 );
 

+ 1 - 1
examples/webgl_morphtargets_horse.html

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

+ 12 - 7
examples/webgl_shadowmap.html

@@ -64,7 +64,7 @@
 
 			var sceneHUD, cameraOrtho, hudMesh;
 
-			var morphs = [];
+			var mixer, morphs = [];
 
 			var light;
 
@@ -306,6 +306,8 @@
 
 				// MORPHS
 
+				mixer = new THREE.AnimationMixer( scene );
+
 				function addMorph( geometry, speed, duration, x, y, z, fudgeColor ) {
 
 					var material = new THREE.MeshLambertMaterial( { color: 0xffaa55, morphTargets: true, vertexColors: THREE.FaceColors } );
@@ -319,10 +321,13 @@
 					var mesh = new THREE.Mesh( geometry, material );
 					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.rotation.y = Math.PI/2;
@@ -383,12 +388,12 @@
 
 				var delta = clock.getDelta();
 
+				mixer.update( delta );
+
 				for ( var i = 0; i < morphs.length; i ++ ) {
 
 					morph = morphs[ i ];
 
-					morph.mixer.update( delta );
-
 					morph.position.x += morph.speed * delta;
 
 					if ( morph.position.x  > 2000 )  {

+ 42 - 5
examples/webgl_shadowmap_performance.html

@@ -54,12 +54,14 @@
 			var SCREEN_HEIGHT = window.innerHeight;
 			var FLOOR = -250;
 
+			var ANIMATION_GROUPS = 25;
+
 			var camera, controls, scene, renderer;
 			var container, stats;
 
 			var NEAR = 5, FAR = 3000;
 
-			var morph, morphs = [], mixer;
+			var morph, morphs = [], mixer, animGroups = [];
 
 			var light;
 
@@ -240,9 +242,16 @@
 
 				mixer = new THREE.AnimationMixer( scene );
 
+				for ( var i = 0; i !== ANIMATION_GROUPS; ++ i ) {
+
+					var group = new THREE.AnimationObjectGroup();
+					animGroups.push( new THREE.AnimationObjectGroup() );
+
+				}
+
 				// 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 } );
 
@@ -255,7 +264,35 @@
 					var mesh = new THREE.Mesh( geometry, material );
 					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.rotation.y = Math.PI/2;
@@ -275,7 +312,7 @@
 
 					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();
 
-				if( mixer ) mixer.update( delta );
+				if ( mixer ) mixer.update( delta );
 
 				for ( var i = 0; i < morphs.length; i ++ ) {
 

+ 1 - 1
examples/webgl_skinning_simple.html

@@ -76,7 +76,7 @@
 					scene.add( 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;
 
 					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() );
 					mesh.mixer = mixer;
 

+ 0 - 190
src/animation/AnimationAction.js

@@ -1,190 +0,0 @@
-/**
- *
- * Runnable instance of an AnimationClip.
- *
- * Multiple Actions are required to add the same clip with the (same or
- * different) mixer(s) simultaneously.
- *
- *
- * @author Ben Houston / http://clara.io/
- * @author David Sarno / http://lighthaus.us/
- */
-
-THREE.AnimationAction = function ( clip, startTime, timeScale, weight, loop ) {
-
-	if( clip === undefined ) throw new Error( 'clip is null' );
-	this.name = '';
-	this.clip = clip;
-	this.localRoot = null;
-	this.startTime = startTime || 0;
-	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.mixer = null;
-
-	var tracks = clip.tracks,
-		nTracks = tracks.length,
-		interpolants = new Array( nTracks );
-
-	for ( var i = 0; i !== nTracks; ++ i ) {
-
-		interpolants[ i ] = tracks[ i ].createInterpolant( null );
-
-	}
-
-	this._interpolants = interpolants;
-	this._propertyBindings = new Array( nTracks );
-
-	this._prevRootUuid = '';
-	this._prevMixerUuid = '';
-
-};
-
-/*
-THREE.LoopOnce = 2200;
-THREE.LoopRepeat = 2201;
-THREE.LoopPingPing = 2202;
-*/
-
-THREE.AnimationAction.prototype = {
-
-	constructor: THREE.AnimationAction,
-
-	getName: function() {
-
-		return this.name || this.clip.name;
-
-	},
-
-	setLocalRoot: function( localRoot ) {
-
-		this.localRoot = localRoot;
-
-		return this;
-
-	},
-
-	updateTime: function( clipDeltaTime ) {
-
-		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;
-
-	},
-
-	// pass in time, not clip time, allows for fadein/fadeout across multiple loops of the clip
-	getTimeScaleAt: function( time ) {
-
-		var timeScale = this.timeScale;
-
-		if( timeScale.evaluate !== undefined ) {
-
-			return timeScale.evaluate( time )[ 0 ];
-
-		}
-
-		return timeScale;
-
-	},
-
-	// pass in time, not clip time, allows for fadein/fadeout across multiple loops of the clip
-	getWeightAt: function( time ) {
-
-		var weight = this.weight;
-
-		if( weight.evaluate !== undefined ) {
-
-			return weight.evaluate( time )[ 0 ];
-
-		}
-
-		return weight;
-
-	}
-
-};

+ 1 - 1
src/animation/AnimationClip.js

@@ -8,7 +8,7 @@
 
 THREE.AnimationClip = function ( name, duration, tracks ) {
 
-	this.name = name;
+	this.name = name || THREE.Math.generateUUID();
 	this.tracks = tracks;
 	this.duration = ( duration !== undefined ) ? duration : -1;
 

+ 1187 - 216
src/animation/AnimationMixer.js

@@ -1,7 +1,6 @@
 /**
  *
- * Sequencer that performs AnimationActions, mixes their results and updates
- * the scene graph.
+ * Player for AnimationClips.
  *
  *
  * @author Ben Houston / http://clara.io/
@@ -11,306 +10,545 @@
 
 THREE.AnimationMixer = function( root ) {
 
-	this.root = root;
-	this.uuid = THREE.Math.generateUUID();
+	this._root = root;
+	this._initMemoryManager();
+	this._accuIndex = 0;
 
 	this.time = 0;
+
 	this.timeScale = 1.0;
 
-	this._actions = [];
+};
 
-	this._bindingsMaps = {}; // contains a path -> prop_mixer map per root.uuid
+THREE.AnimationMixer.prototype = {
 
-	this._bindings = []; // array of all bindings with refCnt != 0
-	this._bindingsDirty = false; // whether we need to rebuild the array
+	constructor: THREE.AnimationMixer,
 
-	this._accuIndex = 0;
+	// 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,
 
-THREE.AnimationMixer.prototype = {
+			actionsForClip = this._actionsByClip[ clipName ],
+			prototypeAction;
 
-	constructor: THREE.AnimationMixer,
+		if ( actionsForClip !== undefined ) {
+
+			var existingAction =
+					actionsForClip.actionByRoot[ rootUuid ];
+
+			if ( existingAction !== undefined ) {
+
+				return existingAction;
+
+			}
+
+			// we know the clip, so we don't have to parse all
+			// the bindings again but can just copy
+			prototypeAction = actionsForClip.knownActions[ 0 ];
 
-	addAction: function( action ) {
+			// also, take the clip from the prototype action
+			clipObject = prototypeAction._clip;
 
-		if ( this._actions.indexOf( action ) !== -1 ) {
+			if ( clip !== clipName && clip !== clipObject ) {
 
-			return; // action is already added - do nothing
+				throw new Error(
+						"Different clips with the same name detected!" );
+
+			}
 
 		}
 
-		var root = action.localRoot || this.root,
-			rootUuid = root.uuid,
+		// 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 ) {
 
-			bindingsMap = this._bindingsMaps[ rootUuid ];
+		var root = optionalRoot || this._root,
+			rootUuid = root.uuid,
+			clipName = ( typeof clip === 'string' ) ? clip : clip.name,
+			actionsForClip = this._actionsByClip[ clipName ];
 
-		if ( bindingsMap === undefined ) {
+		if ( actionsForClip !== undefined ) {
 
-			bindingsMap = {};
-			this._bindingsMaps[ rootUuid ] = bindingsMap;
+			return actionsForClip.actionByRoot[ rootUuid ] || null;
 
 		}
 
-		var interpolants = action._interpolants,
-			actionBindings = action._propertyBindings,
+		return null;
+
+	},
+
+	// deactivates all previously scheduled actions
+	stopAllAction: function() {
 
-			tracks = action.clip.tracks,
-			bindingsChanged = false,
+		var actions = this._actions,
+			nActions = this._nActiveActions,
+			bindings = this._bindings,
+			nBindings = this._nActiveBindings;
 
-			myUuid = this.uuid,
-			prevRootUuid = action._prevRootUuid,
+		this._nActiveActions = 0;
+		this._nActiveBindings = 0;
 
-			rootSwitchValid =
-				prevRootUuid !== rootUuid &&
-				action._prevMixerUuid === myUuid,
+		for ( var i = 0; i !== nActions; ++ i ) {
 
-			prevRootBindingsMap;
+			actions[ i ].reset();
 
-		if ( rootSwitchValid ) {
+		}
 
-			// in this case we try to transfer currently unused
-			// context infrastructure from the previous root
+		for ( var i = 0; i !== nBindings; ++ i ) {
 
-			prevRootBindingsMap = this._bindingsMaps[ prevRootUuid ];
+			bindings[ i ].useCount = 0;
 
 		}
 
-		for ( var i = 0, n = tracks.length; i !== n; ++ i ) {
+		return this;
 
-			var track = tracks[ i ];
+	},
 
-			var trackName = track.name;
-			var propertyMixer = bindingsMap[ trackName ];
+	// advance the time and update apply the animation
+	update: function( deltaTime ) {
 
-			if ( rootSwitchValid && propertyMixer === undefined ) {
+		var actions = this._actions,
+			nActions = this._nActiveActions,
+			mixerDeltaTime = deltaTime * this.timeScale,
+			direction = Math.sign( deltaTime );
 
-				var candidate = prevRootBindingsMap[ trackName ];
+		var time = this.time += mixerDeltaTime;
+		var accuIndex = this._accuIndex ^= 1;
 
-				if ( candidate !== undefined &&
-						candidate.referenceCount === 0 ) {
+		// perform all actions
 
-					propertyMixer = candidate;
+		for ( var i = 0; i !== nActions; ++ i ) {
 
-					// no longer use with the old root!
-					delete prevRootBindingsMap[ trackName ];
+			var action = actions[ i ],
+				actionDeltaTime = mixerDeltaTime;
 
-					propertyMixer.binding.setRootNode( root );
+			if ( ! action.enabled ) continue;
 
-					bindingsMap[ trackName ] = propertyMixer;
+			var startTime = action.startTime_;
 
-				}
+			if ( startTime !== null ) {
 
-			}
+				// check for scheduled start of action
 
-			if ( propertyMixer === undefined ) {
+				var timeRunning = ( time - startTime ) * direction;
+				if ( timeRunning < 0 ) continue; // yet to come
 
-				propertyMixer = new THREE.PropertyMixer(
-						root, trackName,
-						track.ValueTypeName, track.getValueSize() );
+				// start
 
-				bindingsMap[ trackName ] = propertyMixer;
+				action.startTime_ = null; // unschedule
+				actionDeltaTime = direction * timeRunning;
 
 			}
 
-			if ( propertyMixer.referenceCount === 0 ) {
+			// run this action
 
-				propertyMixer.saveOriginalState();
-				bindingsChanged = true;
+			actionDeltaTime *= action.updateTimeScale_( time );
+			var clipTime = action.updateTime_( actionDeltaTime );
 
-			}
+			// note: updateTime_ may disable the action resulting in
+			// an effective weight of 0
 
-			++ propertyMixer.referenceCount;
+			var weight = action.updateWeight_( time );
 
-			interpolants[ i ].resultBuffer = propertyMixer.buffer;
-			actionBindings[ i ] = propertyMixer;
+			if ( weight > 0 ) {
 
-		}
+				var interpolants = action._interpolants;
+				var propertyMixers = action._propertyBindings;
+
+				for ( var j = 0, m = interpolants.length; j !== m; ++ j ) {
+
+					interpolants[ j ].evaluate( clipTime );
+					propertyMixers[ j ].accumulate( accuIndex, weight );
 
-		if ( bindingsChanged ) {
+				}
 
-			this._bindingsDirty = true; // invalidates this._bindings
+			}
 
 		}
 
-		action.mixer = this;
-		action._prevRootUuid = rootUuid;
-		action._prevMixerUuid = myUuid;
+		// update scene graph
+
+		var bindings = this._bindings,
+			nBindings = this._nActiveBindings;
+
+		for ( var i = 0; i !== nBindings; ++ i ) {
 
-		// TODO: check for duplicate action names?
-		// Or provide each action with a UUID?
-		this._actions.push( action );
+			bindings[ i ].apply( accuIndex );
 
-		action.init( this.time );
+		}
 
 		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 actions = this._actions,
-			index = actions.indexOf( action );
+			clipName = clip.name,
+			actionsByClip = this._actionsByClip,
+			actionsForClip = actionsByClip[ clipName ];
 
-		if ( index === - 1 ) {
+		if ( actionsForClip !== undefined ) {
 
-			return this; // we don't know this action - do nothing
+			// 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;
 
-		// unreference all property mixers
-		var interpolants = action._interpolants,
-			actionBindings = action._propertyBindings,
-			rootUuid = ( action.localRoot || this.root ).uuid,
-			bindings = this._bindingsMaps[ rootUuid ],
+			for ( var i = 0, n = actionsToRemove.length; i !== n; ++ i ) {
 
-			bindingsChanged = false;
+				var action = actionsToRemove[ i ];
 
-		for( var i = 0, n = actionBindings.length; i !== n; ++ i ) {
+				this._deactivateAction( action );
 
-			var propertyMixer = actionBindings[ i ];
-			actionBindings[ i ] = null;
+				var cacheIndex = action._cacheIndex,
+					lastInactiveAction = actions[ actions.length - 1 ];
 
-			interpolants[ i ].resultBuffer = null;
+				action._cacheIndex = null;
+				action._byClipCacheIndex = null;
 
-			// eventually remove the binding from the array
-			if( -- propertyMixer.referenceCount === 0 ) {
+				lastInactiveAction._cacheIndex = cacheIndex;
+				actions[ cacheIndex ] = lastInactiveAction;
+				actions.pop();
 
-				propertyMixer.restoreOriginalState();
-				bindingsChanged = true;
+				this._removeInactiveBindingsForAction( action );
 
 			}
 
+			delete actionsByClip[ clipName ];
+
 		}
 
-		if ( bindingsChanged ) {
+	},
 
-			this._bindingsDirty = true; // invalidates this._bindings
+	// free all resources specific to a particular root target object
+	uncacheRoot: function( root ) {
 
-		}
+		var rootUuid = root.uuid,
+			actionsByClip = this._actionsByClip;
 
-		// remove from array-based unordered set
-		actions[ index ] = actions[ actions.length - 1 ];
-		actions.pop();
+		for ( var clipName in actionsByClip ) {
 
-		action.mixer = null;
+			var actionByRoot = actionsByClip[ clipName ].actionByRoot,
+				action = actionByRoot[ rootUuid ];
 
-		return this;
+			if ( action !== undefined ) {
 
-	},
+				this._deactivateAction( action );
+				this._removeInactiveAction( action );
 
-	removeAllActions: function() {
+			}
 
-		if ( this._bindingsDirty ) {
+		}
 
-			this._updateBindings();
+		var bindingsByRoot = this._bindingsByRootAndName,
+			bindingByName = bindingsByRoot[ rootUuid ];
 
-		}
+		if ( bindingByName !== undefined ) {
 
-		var bindings = this._bindings; // all bindings currently in use
+			for ( var trackName in bindingByName ) {
 
-		for ( var i = 0, n = bindings.length; i !== n; ++ i ) {
+				var binding = bindingByName[ trackName ];
+				binding.restoreOriginalState();
+				this._removeInactiveBinding( binding );
 
-			var binding = bindings[ i ];
-			binding.referenceCount = 0;
-			binding.restoreOriginalState();
+			}
 
 		}
 
-		bindings.length = 0;
+	},
 
-		this._bindingsDirty = false;
+	// remove a targeted clip from the cache
+	uncacheAction: function( clip, optionalRoot ) {
 
-		var actions = this._actions;
+		var action = this.existingAction( clip, optionalRoot );
 
-		for ( var i = 0, n = actions.length; i !== n; ++ i ) {
+		if ( action !== null ) {
 
-			actions[ i ].mixer = null;
+			this._deactivateAction( action );
+			this._removeInactiveAction( action );
 
 		}
 
-		actions.length = 0;
+	},
 
-		return this;
+	// DEPRECATED
+
+	findActionByName: function( name ) {
+
+		console.assert( false, "DEPRECATED: AnimationMixer.findActionByName" );
+
+		return this.clipAction( name );
 
 	},
 
+	play: function( action ) {
 
-	// can be optimized if needed
-	findActionByName: function( name ) {
+		console.assert( false, "DEPRECATED: AnimationMixer.play" );
+
+		action.play();
 
-		var actions = this._actions;
+		return this;
 
-		for ( var i = 0, n = actions.length; i !== n; ++ i ) {
+	},
 
-			var action = actions[ i ];
-			var actionName = action.getName();
+	fadeOut: function( action, duration ) {
 
-			if( name === actionName ) return action;
+		console.assert( false, "DEPRECATED: AnimationMixer.fadeOut" );
 
-		}
+		action.fadeOut( duration );
 
-		return null;
+		return this;
 
 	},
 
-	play: function( action, optionalFadeInDuration ) {
+	fadeIn: function( action, duration ) {
+
+		console.assert( false, "DEPRECATED: AnimationMixer.fadeIn" );
 
-		action.startTime = this.time;
-		this.addAction( action );
+		action.fadeIn( duration );
 
 		return this;
 
 	},
 
-	fadeOut: function( action, duration ) {
+	warp: function( action, startTimeScale, endTimeScale, duration ) {
 
-		var time = this.time,
-			times = Float64Array.of( time, time + duration );
+		console.assert( false, "DEPRECATED: AnimationMixer.warp" );
 
-		action.weight = new THREE.LinearInterpolant(
-				times, this._FadeOutValues, 1, this._tmp );
+		action.warp( startTimeScale, endTimeScale, duration );
 
 		return this;
 
 	},
 
-	fadeIn: function( action, duration ) {
+	crossFade: function( fadeOutAction, fadeInAction, duration, warp ) {
+
+		console.assert( false, "DEPRECATED: AnimationMixer.crossFade" );
+
+		fadeOutAction.crossFadeTo( fadeInAction, duration, warp );
+
+		return this;
 
-		var time = this.time,
-			times = Float64Array.of( time, time + duration );
+	}
 
-		action.weight = new THREE.LinearInterpolant(
-				times, this._FadeInValues, 1, this._tmp );
+};
+
+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;
+	this._propertyBindings = new Array( nTracks );
+
+	this._timeScaleInterpolant = null;
+	this._weightInterpolant = null;
+
+	this._cacheIndex = null;
+	this._byClipCacheIndex = null;
+
+	this._effectiveTimeScale = 1;
+	this._effectiveWeight = 1;
+
+	this.loop = THREE.LoopRepeat;
+	this._loopCount = -1;
+
+	// global mixer time when the action is to be started
+	// it's set back to 'null' when the mixer has started this 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.weight = 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 	= true;		// 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;
 
 	},
 
-	warp: function( action, startTimeScale, endTimeScale, duration ) {
+	stop: function() {
+
+		this._mixer._deactivateAction( this );
 
-		var time = this.time,
-			times = Float64Array.of( time, time + duration ),
-			values = Float64Array.of( startTimeScale, endTimeScale );
+		return this.reset();
 
-		action.timeScale = new THREE.LinearInterpolant( times, values, 1, this._tmp );
+	},
+
+	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();
+
+	},
+
+	isRunning: function() {
+
+		var start = this.startTime_;
+
+		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 );
+
+	},
+
+	startAt: function( time ) {
+
+		this.startTime_ = time;
 
 		return this;
 
 	},
 
-	crossFade: function( fadeOutAction, fadeInAction, duration, warp ) {
+	setLoop: function( mode, repetitions ) {
+
+		this.loop = mode;
+		this.repetitions = repetitions;
+
+		return this;
+
+	},
+
+	// 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() {
 
-		this.fadeOut( fadeOutAction, duration );
-		this.fadeIn( fadeInAction, duration );
+		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 startEndRatio = fadeOutAction.clip.duration / fadeInAction.clip.duration;
-			var endStartRatio = 1.0 / startEndRatio;
+			var fadeInDuration = this._clip.duration,
+				fadeOutDuration = fadeOutAction._clip.duration,
+
+				startEndRatio = fadeOutDuration / fadeInDuration,
+				endStartRatio = fadeInDuration / fadeOutDuration;
 
-			this.warp( fadeOutAction, 1.0, startEndRatio, duration );
-			this.warp( fadeInAction, endStartRatio, 1.0, duration );
+			fadeOutAction.warp( 1.0, startEndRatio, duration );
+			this.warp( endStartRatio, 1.0, duration );
 
 		}
 
@@ -318,50 +556,106 @@ THREE.AnimationMixer.prototype = {
 
 	},
 
-	update: function( deltaTime ) {
+	crossFadeTo: function( fadeInAction, duration, warp ) {
 
-		var actions = this._actions,
-			mixerDeltaTime = deltaTime * this.timeScale;
+		return fadeInAction.crossFadeFrom( this, duration, warp );
 
-		var time = this.time += mixerDeltaTime;
-		var accuIndex = this.accuIndex ^= 1;
+	},
 
-		// perform all actions
+	stopFading: function() {
 
-		for ( var i = 0, n = actions.length; i !== n; ++ i ) {
+		var weightInterpolant = this._weightInterpolant;
 
-			var action = actions[ i ];
-			if ( ! action.enabled ) continue;
+		if ( weightInterpolant !== null ) {
 
-			var weight = action.getWeightAt( time );
-			if ( weight <= 0 ) continue;
+			this._weightInterpolant = null;
+			this._mixer._takeBackControlInterpolant( weightInterpolant );
 
-			var actionTimeScale = action.getTimeScaleAt( time );
-			var actionTime = action.updateTime( mixerDeltaTime * actionTimeScale );
+		}
 
-			var interpolants = action._interpolants;
-			var propertyMixers = action._propertyBindings;
+		return this;
 
-			for ( var j = 0, m = interpolants.length; j !== m; ++ j ) {
+	},
 
-				interpolants[ j ].evaluate( actionTime );
-				propertyMixers[ j ].accumulate( accuIndex, weight );
+	// 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;
+
+	},
 
-		if ( this._bindingsDirty ) {
+	setDuration: function( duration ) {
 
-			this._updateBindings();
-			this._bindingsDirty = false;
+		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 ) {
+
+		return this.warp( this._currentTimeScale, 0, duration );
+
+	},
+
+	warp: function( startTimeScale, endTimeScale, duration ) {
+
+		var mixer = this._mixer, now = mixer.time,
+			interpolant = this._timeScaleInterpolant,
+
+			timeScale = this.timeScale;
+
+		if ( interpolant === null ) {
+
+			interpolant = mixer._lendControlInterpolant(),
+			this._timeScaleInterpolant = interpolant;
 
 		}
 
-		var bindings = this._bindings;
-		for ( var i = 0, n = bindings.length; i !== n; ++ i ) {
+		var times = interpolant.parameterPositions,
+			values = interpolant.sampleValues;
 
-			bindings[ i ].apply( accuIndex );
+		times[ 0 ] = now;
+		times[ 1 ] = now + duration;
+
+		values[ 0 ] = startTimeScale / timeScale;
+		values[ 1 ] = endTimeScale / timeScale;
+
+		return this;
+
+	},
+
+	stopWarping: function() {
+
+		var timeScaleInterpolant = this._timeScaleInterpolant;
+
+		if ( timeScaleInterpolant !== null ) {
+
+			this._timeScaleInterpolant = null;
+			this._mixer._takeBackControlInterpolant( timeScaleInterpolant );
 
 		}
 
@@ -369,49 +663,53 @@ THREE.AnimationMixer.prototype = {
 
 	},
 
-	// releases cached references to scene graph nodes
-	// pass 'true' for 'unbindOnly' to allow a quick rebind at
-	// the expense of higher cost add / removeAction operations
-	releaseCachedBindings: function( unbindOnly ) {
+	// Object Accessors
 
-		var bindingsMaps = this._bindingsMaps;
+	getMixer: function() {
 
-		for ( var rootUuid in bindingsMaps ) {
+		return this._mixer;
 
-			var bindingsMap = bindingsMaps[ rootUuid ];
+	},
 
-			var mapChanged = false;
+	getClip: function() {
 
-			for ( var trackName in bindingsMap ) {
+		return this._clip;
 
-				var propertyMixer = bindingsMap[ trackName ];
+	},
 
-				if ( propertyMixer.referenceCount === 0 ) {
+	getRoot: function() {
 
-					if ( unbindOnly ) {
+		return this._localRoot || this._mixer._root;
 
-						propertyMixer.binding.unbind();
+	},
 
-					} else {
+	// Interface used by the mixer:
 
-						delete bindingsMap[ trackName ];
-						mapChanged = true;
+	updateWeight_: function( time ) {
 
-					}
+		var weight = 0;
 
-				}
+		if ( this.enabled ) {
 
-			}
+			weight = this.weight;
+			var interpolant = this._weightInterpolant;
+
+			if ( interpolant !== null ) {
 
-			if ( mapChanged ) {
+				var interpolantValue = interpolant.evaluate( time )[ 0 ];
 
-				// when bindingsMap became empty, remove it from bindingsMaps
+				weight *= interpolantValue;
 
-				remove_empty_map: {
+				if ( time > interpolant.parameterPositions[ 1 ] ) {
 
-					for ( var k in bindingsMap ) break remove_empty_map;
+					this.stopFading();
 
-					delete bindingsMaps[ rootUuid ];
+					if ( interpolantValue === 0 ) {
+
+						// faded out, disable
+						this.enabled = false;
+
+					}
 
 				}
 
@@ -419,25 +717,42 @@ THREE.AnimationMixer.prototype = {
 
 		}
 
+		this._effectiveWeight = weight;
+		return weight;
+
 	},
 
-	_updateBindings: function() {
+	updateTimeScale_: function( time ) {
 
-		var bindingsMaps = this._bindingsMaps,
-			bindings = this._bindings,
-			writeIndex = 0;
+		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 ] ) {
 
-		for ( var rootUuid in bindingsMaps ) {
+					this.stopWarping();
 
-			var bindingsMap = bindingsMaps[ rootUuid ];
+					if ( timeScale === 0 ) {
 
-			for ( var trackName in bindingsMap ) {
+						// motion has halted, pause
+						this.pause = true;
 
-				var propertyMixer = bindingsMap[ trackName ];
+					} else {
 
-				if ( propertyMixer.referenceCount !== 0 ) {
+						// warp done - apply final time scale
+						this.timeScale = timeScale;
 
-					bindings[ writeIndex ++ ] = propertyMixer;
+					}
 
 				}
 
@@ -445,15 +760,671 @@ THREE.AnimationMixer.prototype = {
 
 		}
 
-		bindings.length = writeIndex;
+		this._effectiveTimeScale = timeScale;
+		return timeScale;
 
 	},
 
-	_FadeInValues: Float64Array.of( 0, 1 ),
-	_FadeOutValues: Float64Array.of( 1, 0 ),
+	updateTime_: function( actionDeltaTime ) {
 
-	_tmp: new Float64Array( 1 )
+		var time = this.time + actionDeltaTime,
+			duration = this._clip.duration,
 
-};
+			direction = Math.sign( actionDeltaTime ),
+
+			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: direction
+				} );
+
+				break;
+
+			case THREE.LoopPingPong:
+
+				pingPong = true;
+
+			case THREE.LoopRepeat:
+
+				if ( loopCount === -1 ) {
+
+					// just started
+
+					loopCount = 0;
+
+					var atStart = direction > 0;
+					this._setEndings( atStart, ! atStart, pingPong );
+
+				}
+
+				if ( time >= duration || time < 0 ) {
+
+					// wrap around
+
+					var loopDelta = Math.floor( time / duration ); // signed
+					time -= duration * loopDelta;
+
+					loopCount += loopDelta * direction;
+
+					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 = direction < 0 ? 0 : duration;
+
+						this._mixer.dispatchEvent( {
+							type: 'finished', action: this,
+							direction: direction
+						} );
+
+						break;
+
+					} else if ( pending === 0 ) {
+
+						// transition to last round
+
+						var atStart = direction < 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;
+
+	},
+
+	// Interna
+
+	_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();
+
+		}
+
+	}
+
+};
+

+ 1 - 2
src/animation/AnimationUtils.js

@@ -99,8 +99,7 @@ THREE.AnimationUtils = {
 		var value = key[ valuePropertyName ];
 		if ( value === undefined ) return; // no data
 
-		if ( value[ 'splice' ] !== undefined ) {
-			// ...assume array
+		if ( Array.isArray( value ) ) {
 
 			do {
 

+ 6 - 6
src/animation/KeyframeTrack.js

@@ -34,8 +34,8 @@ THREE.KeyframeTrack.prototype = {
 
 	constructor: THREE.KeyframeTrack,
 
-	TimeBufferType: Float64Array,
-	ValueBufferType: Float64Array,
+	TimeBufferType: Float32Array,
+	ValueBufferType: Float32Array,
 
 	DefaultInterpolation: THREE.InterpolateLinear,
 
@@ -250,7 +250,7 @@ THREE.KeyframeTrack.prototype = {
 
 			var currTime = times[ i ];
 
-			if ( Number.isNaN( currTime ) ) {
+			if ( typeof currTime === 'number' && isNaN( currTime ) ) {
 
 				console.error( "time is not a valid number", this, i, currTime );
 				valid = false;
@@ -278,7 +278,7 @@ THREE.KeyframeTrack.prototype = {
 
 					var value = values[ i ];
 
-					if ( Number.isNaN( value ) ) {
+					if ( isNaN( value ) ) {
 
 						console.error( "value is not a valid number", this, i, value );
 						valid = false;
@@ -392,7 +392,7 @@ Object.assign( THREE.KeyframeTrack, {
 
 		}
 
-		var trackType = THREE.KeyframeTrack.GetTrackTypeForValueTypeName( json.type );
+		var trackType = THREE.KeyframeTrack._getTrackTypeForValueTypeName( json.type );
 
 		if ( json[ 'times' ] === undefined ) {
 
@@ -460,7 +460,7 @@ Object.assign( THREE.KeyframeTrack, {
 
 	},
 
-	GetTrackTypeForValueTypeName: function( typeName ) {
+	_getTrackTypeForValueTypeName: function( typeName ) {
 
 		switch( typeName.toLowerCase() ) {
 

+ 293 - 167
src/animation/PropertyBinding.js

@@ -8,21 +8,16 @@
  * @author tschw
  */
 
-THREE.PropertyBinding = function ( rootNode, path ) {
+THREE.PropertyBinding = function ( rootNode, path, parsedPath ) {
 
-	this.rootNode = rootNode;
 	this.path = path;
+	this.parsedPath = parsedPath ||
+			THREE.PropertyBinding.parseTrackName( path );
 
-	var parseResults = 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.setRootNode( rootNode );
+	this.rootNode = rootNode;
 
 };
 
@@ -50,31 +45,20 @@ THREE.PropertyBinding.prototype = {
 
 	},
 
-	// change the root used for binding
-	setRootNode: function( rootNode ) {
-
-		var oldNode = this.node,
-			newNode = THREE.PropertyBinding.findNode( rootNode, this.nodeName ) || rootNode;
-
-		if ( oldNode && oldNode !== newNode ) {
-
-			this.unbind(); // for the change to take effect on the next call
-
-		}
-
-		this.rootNode = rootNode;
-		this.node = newNode;
-
-	},
-
 	// create getter / setter pair for a property in the scene graph
 	bind: function() {
 
-		var targetObject = this.node;
+		var targetObject = this.node,
+			parsedPath = this.parsedPath,
+
+			objectName = parsedPath.objectName,
+			propertyName = parsedPath.propertyName,
+			propertyIndex = parsedPath.propertyIndex;
 
 		if ( ! targetObject ) {
 
-			targetObject = THREE.PropertyBinding.findNode( this.rootNode, this.nodeName ) || this.rootNode;
+			targetObject = THREE.PropertyBinding.findNode(
+					this.rootNode, parsedPath.nodeName ) || this.rootNode;
 
 			this.node = targetObject;
 
@@ -85,262 +69,403 @@ THREE.PropertyBinding.prototype = {
 		this.setValue = this._setValue_unavailable;
 
  		// ensure there is a value node
-		if( ! targetObject ) {
+		if ( ! targetObject ) {
+
 			console.error( "  trying to update node for track: " + this.path + " but it wasn't found." );
 			return;
+
 		}
 
-		if( this.objectName ) {
-			// special case were we need to reach deeper into the hierarchy to get the face materials....
-			if( this.objectName === "materials" ) {
-				if( ! targetObject.material ) {
-					console.error( '  can not bind to material as node does not have a material', this );
-					return;
-				}
-				if( ! targetObject.material.materials ) {
-					console.error( '  can not bind to material.materials as node.material does not have a materials array', this );
-					return;
-				}
-				targetObject = targetObject.material.materials;
-			}
-			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.
+		if( objectName ) {
 
-				targetObject = targetObject.skeleton.bones;
+			var objectIndex = parsedPath.objectIndex;
+
+			// special cases were we need to reach deeper into the hierarchy to get the face materials....
+			switch ( objectName ) {
+
+				case 'materials':
+
+					if( ! targetObject.material ) {
+
+						console.error( '  can not bind to material as node does not have a material', 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 ];
+					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;
+
+					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 );
 					return;
+
 				}
 
-				targetObject = targetObject[ this.objectIndex ];
+				targetObject = targetObject[ objectIndex ];
+
 			}
 
 		}
 
- 		// special case mappings
- 		var nodeProperty = targetObject[ this.propertyName ];
-		if( ! nodeProperty ) {
-			console.error( "  trying to update property for track: " + this.nodeName + '.' + this.propertyName + " but it wasn't found.", targetObject );
+		// resolve property
+		var nodeProperty = targetObject[ propertyName ];
+
+		if ( ! nodeProperty ) {
+
+			var nodeName = parsedPath.nodeName;
+
+			console.error( "  trying to update property for track: " + nodeName +
+					'.' + propertyName + " but it wasn't found.", targetObject );
 			return;
+
 		}
 
 		// determine versioning scheme
-		var versioning = 0;
-		var NeedsUpdate = 1;
-		var MatrixWorldNeedsUpdate = 2;
+		var versioning = this.Versioning.None;
 
-		if( targetObject.needsUpdate !== undefined ) { // material
+		if ( targetObject.needsUpdate !== undefined ) { // material
 
-			versioning = NeedsUpdate;
+			versioning = this.Versioning.NeedsUpdate;
+			this.targetObject = targetObject;
 
-		} else if( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
+		} else if ( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform
 
-			versioning = MatrixWorldNeedsUpdate;
+			versioning = this.Versioning.MatrixWorldNeedsUpdate;
+			this.targetObject = targetObject;
 
 		}
 
-		// access a sub element of the property array (only primitives are supported right now)
-		if( this.propertyIndex !== undefined ) {
+		// 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( this.propertyName === "morphTargetInfluences" ) {
+			if ( propertyName === "morphTargetInfluences" ) {
 				// potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
 
 				// support resolving morphTarget names into indices.
-				if( ! targetObject.geometry ) {
+				if ( ! targetObject.geometry ) {
+
 					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 );
+					return;
+
 				}
 
-				for( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
-					if( targetObject.geometry.morphTargets[i].name === this.propertyIndex ) {
-						this.propertyIndex = i;
+				for ( var i = 0; i < this.node.geometry.morphTargets.length; i ++ ) {
+
+					if ( targetObject.geometry.morphTargets[i].name === propertyIndex ) {
+
+						propertyIndex = i;
 						break;
+
 					}
+
 				}
+
 			}
 
-			var propertyIndex = this.propertyIndex;
+			bindingType = this.BindingType.ArrayElement;
 
-			this.getValue = function getValue_propertyIndexed( buffer, offset ) {
+			this.resolvedProperty = nodeProperty;
+			this.propertyIndex = propertyIndex;
 
-				buffer[ offset ] = nodeProperty[ this.propertyIndex ];
+		} else if ( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {
+			// must use copy for Object3D.Euler/Quaternion
 
-			};
+			bindingType = this.BindingType.HasFromToArray;
 
-			switch ( versioning ) {
+			this.resolvedProperty = nodeProperty;
 
-				case NeedsUpdate:
+		} else {
 
-					this.setValue = function setValue_propertyIndexed( buffer, offset ) {
+			this.propertyName = propertyName;
 
-						nodeProperty[ propertyIndex ] = buffer[ offset ];
-						targetObject.needsUpdate = true;
+		}
 
-					};
+		// select getter / setter
+		this.getValue = this.GetterByBindingType[ bindingType ];
+		this.setValue = this.SetterByBindingTypeAndVersioning[ bindingType ][ versioning ];
 
-					break;
+	},
 
-				case MatrixWorldNeedsUpdate:
+	unbind: function() {
 
-					this.setValue = function setValue_propertyIndexed( buffer, offset ) {
+		this.node = null;
 
-						nodeProperty[ propertyIndex ] = buffer[ offset ];
-						targetObject.matrixWorldNeedsUpdate = true;
+		// 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;
 
-					};
+	}
 
-					break;
+};
 
-				default:
+Object.assign( THREE.PropertyBinding.prototype, { // prototype, continued
 
-					this.setValue = function setValue_propertyIndexed( buffer, offset ) {
+	// these are used to "bind" a nonexistent property
+	_getValue_unavailable: function() {},
+	_setValue_unavailable: function() {},
 
-						nodeProperty[ propertyIndex ] = buffer[ offset ];
+	// 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
+	},
 
-		}
-		// must use copy for Object3D.Euler/Quaternion
-		else if( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {
+	GetterByBindingType: [
 
-			this.getValue = function getValue_propertyObject( buffer, offset ) {
+		function getValue_direct( buffer, offset ) {
 
-				nodeProperty.toArray( buffer, offset );
+			buffer[ offset ] = this.node[ this.propertyName ];
 
-			};
+		},
 
+		function getValue_arrayElement( buffer, offset ) {
 
-			switch ( versioning ) {
+			buffer[ offset ] = this.resolvedProperty[ this.propertyIndex ];
 
-				case NeedsUpdate:
+		},
 
-					this.setValue = function setValue_propertyObject( buffer, offset ) {
+		function getValue_toArray( buffer, offset ) {
 
-						nodeProperty.fromArray( buffer, offset );
-						targetObject.needsUpdate = true;
+			this.resolvedProperty.toArray( buffer, offset );
 
-					}
+		}
 
-				case MatrixWorldNeedsUpdate:
+	],
 
-					this.setValue = function setValue_propertyObject( buffer, offset ) {
+	SetterByBindingTypeAndVersioning: [
 
-						nodeProperty.fromArray( buffer, offset );
-						targetObject.matrixWorldNeedsUpdate = true;
+		[
+			// Direct
 
-					}
+			function setValue_direct( buffer, offset ) {
 
-				default:
+				this.node[ this.propertyName ] = buffer[ offset ];
 
-					this.setValue = function setValue_propertyObject( buffer, offset ) {
+			},
 
-						nodeProperty.fromArray( 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;
 
 			}
 
-		}
-		// otherwise just set the property directly on the node (do not use nodeProperty as it may not be a reference object)
-		else {
+		], [
 
-			var propertyName = this.propertyName;
+			// ArrayElement
 
-			this.getValue = function getValue_property( buffer, offset ) {
+			function setValue_arrayElement( buffer, offset ) {
 
-				buffer[ offset ] = nodeProperty[ propertyName ];
+				this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
 
-			};
+			},
 
-			switch ( versioning ) {
+			function setValue_arrayElement_setNeedsUpdate( buffer, offset ) {
 
-				case NeedsUpdate:
+				this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
+				this.targetObject.needsUpdate = true;
 
-					this.setValue = function setValue_property( buffer, offset ) {
+			},
 
-						nodeProperty[ propertyName ] = buffer[ offset ];
-						targetObject.needsUpdate = true;
+			function setValue_arrayElement_setMatrixWorldNeedsUpdate( buffer, offset ) {
 
-					}
+				this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
+				this.targetObject.matrixWorldNeedsUpdate = true;
 
-					break;
+			}
 
-				case MatrixWorldNeedsUpdate:
+		], [
 
-					this.setValue = function setValue_property( buffer, offset ) {
+			// HasToFromArray
 
-						nodeProperty[ propertyName ] = buffer[ offset ];
-						targetObject.matrixWorldNeedsUpdate = true;
+			function setValue_fromArray( buffer, offset ) {
 
-					}
+				this.resolvedProperty.fromArray( buffer, offset );
 
-					break;
+			},
 
-				default:
+			function setValue_fromArray_setNeedsUpdate( buffer, offset ) {
 
-					this.setValue = function setValue_property( buffer, offset ) {
+				this.resolvedProperty.fromArray( buffer, offset );
+				this.targetObject.needsUpdate = true;
 
-						nodeProperty[ propertyName ] = buffer[ offset ];
+			},
 
-					}
+			function setValue_fromArray_setMatrixWorldNeedsUpdate( buffer, offset ) {
+
+				this.resolvedProperty.fromArray( buffer, offset );
+				this.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.bind(); // bind all binding
+
+		var firstValidIndex = this._targetGroup.nCachedObjects_,
+			binding = this._bindings[ firstValidIndex ];
+
+		// and only call .getValue on the first
+		if ( binding !== undefined ) binding.getValue( array, offset );
+
+	},
+
+	setValue: function( array, offset ) {
+
+		var bindings = this._bindings;
+
+		for ( var i = this._targetGroup.nCachedObjects_,
+				n = bindings.length; i !== n; ++ i ) {
+
+			bindings[ i ].setValue( array, offset );
+
+		}
+
+	},
+
+	bind: function() {
+
+		var bindings = this._bindings;
+
+		for ( var i = this._targetGroup.nCachedObjects_,
+				n = bindings.length; i !== n; ++ i ) {
+
+			bindings[ i ].bind();
+
 		}
 
 	},
 
 	unbind: function() {
 
-		this.node = null;
+		var bindings = this._bindings;
 
-		// 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;
+		for ( var i = this._targetGroup.nCachedObjects_,
+				n = bindings.length; i !== n; ++ i ) {
+
+			bindings[ i ].unbind();
+
+		}
 
 	}
 
 };
 
-Object.assign( THREE.PropertyBinding.prototype, {
+THREE.PropertyBinding.create = function( root, path, parsedPath ) {
 
-	// these are used to "bind" a nonexistent property
-	_getValue_unavailable: function() {},
-	_setValue_unavailable: function() {},
+	if ( ! ( root instanceof THREE.AnimationObjectGroup ) ) {
 
-	// initial state of these methods that calls 'bind'
-	_getValue_unbound: THREE.PropertyBinding.prototype.getValue,
-	_setValue_unbound: THREE.PropertyBinding.prototype.setValue
+		return new THREE.PropertyBinding( root, path, parsedPath );
 
-} );
+	} else {
+
+		return new THREE.PropertyBinding.Composite( root, path, parsedPath );
+
+	}
+
+};
 
 THREE.PropertyBinding.parseTrackName = function( trackName ) {
 
@@ -367,7 +492,7 @@ THREE.PropertyBinding.parseTrackName = function( trackName ) {
     }
 
 	var results = {
-		directoryName: matches[1],
+		// directoryName: matches[1], // (tschw) currently unused
 		nodeName: matches[3], 	// allowed to be null, specified root node.
 		objectName: matches[5],
 		objectIndex: matches[7],
@@ -456,4 +581,5 @@ THREE.PropertyBinding.findNode = function( root, nodeName ) {
 	}
 
 	return null;
+
 }

+ 3 - 2
src/animation/PropertyMixer.js

@@ -8,9 +8,9 @@
  * @author tschw
  */
 
-THREE.PropertyMixer = function ( rootNode, path, typeName, valueSize ) {
+THREE.PropertyMixer = function ( binding, typeName, valueSize ) {
 
-	this.binding = new THREE.PropertyBinding( rootNode, path );
+	this.binding = binding;
 	this.valueSize = valueSize;
 
 	var bufferType = Float64Array,
@@ -45,6 +45,7 @@ THREE.PropertyMixer = function ( rootNode, path, typeName, valueSize ) {
 
 	this.cumulativeWeight = 0;
 
+	this.useCount = 0;
 	this.referenceCount = 0;
 
 };

+ 2 - 1
src/animation/tracks/BooleanKeyframeTrack.js

@@ -14,7 +14,8 @@ THREE.BooleanKeyframeTrack = function ( name, times, values ) {
 
 };
 
-Object.assign( THREE.BooleanKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
+THREE.BooleanKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 	constructor: THREE.BooleanKeyframeTrack,
 

+ 2 - 1
src/animation/tracks/ColorKeyframeTrack.js

@@ -14,7 +14,8 @@ THREE.ColorKeyframeTrack = function ( name, times, values, interpolation ) {
 
 };
 
-Object.assign( THREE.ColorKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
+THREE.ColorKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 	constructor: THREE.ColorKeyframeTrack,
 

+ 2 - 1
src/animation/tracks/NumberKeyframeTrack.js

@@ -13,7 +13,8 @@ THREE.NumberKeyframeTrack = function ( name, times, values, interpolation ) {
 
 };
 
-Object.assign( THREE.NumberKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
+THREE.NumberKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 	constructor: THREE.NumberKeyframeTrack,
 

+ 2 - 1
src/animation/tracks/QuaternionKeyframeTrack.js

@@ -13,7 +13,8 @@ THREE.QuaternionKeyframeTrack = function ( name, times, values, interpolation )
 
 };
 
-Object.assign( THREE.QuaternionKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
+THREE.QuaternionKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 	constructor: THREE.QuaternionKeyframeTrack,
 

+ 2 - 1
src/animation/tracks/StringKeyframeTrack.js

@@ -14,7 +14,8 @@ THREE.StringKeyframeTrack = function ( name, times, values, interpolation ) {
 
 };
 
-Object.assign( THREE.StringKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
+THREE.StringKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 	constructor: THREE.StringKeyframeTrack,
 

+ 2 - 1
src/animation/tracks/VectorKeyframeTrack.js

@@ -14,7 +14,8 @@ THREE.VectorKeyframeTrack = function ( name, times, values, interpolation ) {
 
 };
 
-Object.assign( THREE.VectorKeyframeTrack.prototype, THREE.KeyframeTrack.prototype, {
+THREE.VectorKeyframeTrack.prototype =
+		Object.assign( Object.create( THREE.KeyframeTrack.prototype ), {
 
 	constructor: THREE.VectorKeyframeTrack,
 

+ 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 ] );
+
+} );
+

+ 5 - 2
test/unit/unittests_three.html

@@ -47,13 +47,16 @@
   <script src="math/Frustum.js"></script>
   <script src="math/Interpolant.js"></script>
 
-  <script src="geometry/EdgesGeometry.js"></script>
-  <script src="extras/ImageUtils.test.js"></script>
+  <script src="animation/AnimationObjectGroup.js"></script>
+
   <script src="lights/AmbientLight.tests.js"></script>
   <script src="lights/DirectionalLight.tests.js"></script>
   <script src="lights/HemisphereLight.tests.js"></script>
   <script src="lights/PointLight.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/CircleBufferGeometry.tests.js"></script>

+ 1 - 1
utils/build/includes/common.json

@@ -39,9 +39,9 @@
 	"src/core/DirectGeometry.js",
 	"src/core/BufferGeometry.js",
 	"src/core/InstancedBufferGeometry.js",
-	"src/animation/AnimationAction.js",
 	"src/animation/AnimationClip.js",
 	"src/animation/AnimationMixer.js",
+	"src/animation/AnimationObjectGroup.js",
 	"src/animation/AnimationUtils.js",
 	"src/animation/KeyframeTrack.js",
 	"src/animation/PropertyBinding.js",