Преглед изворни кода

Merge pull request #18822 from c-morten/Additive_Animation

AnimationMixer: Additive Animations
Mr.doob пре 5 година
родитељ
комит
82b6c5a5cb

+ 1 - 0
examples/files.js

@@ -3,6 +3,7 @@ var files = {
 		"webgl_animation_cloth",
 		"webgl_animation_keyframes",
 		"webgl_animation_skinning_blending",
+		"webgl_animation_skinning_additive_blending",
 		"webgl_animation_skinning_morph",
 		"webgl_animation_multiple",
 		"webgl_camera",

BIN
examples/models/gltf/Xbot.glb


BIN
examples/screenshots/webgl_animation_skinning_additive_blending.png


+ 420 - 0
examples/webgl_animation_skinning_additive_blending.html

@@ -0,0 +1,420 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - additive animation - skinning</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+		<style>
+			a {
+				color: blue;
+			}
+			.ac {  /* prevent dat-gui from being selected */
+				-webkit-user-select: none;
+				-moz-user-select: none;
+				-ms-user-select: none;
+				user-select: none;
+			}
+			.control-inactive {
+				color: #888;
+			}
+		</style>
+	</head>
+	<body>
+		<div id="container"></div>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Skeletal Additive Animation Blending
+			(model from <a href="https://www.mixamo.com/" target="_blank" rel="noopener">mixamo.com</a>)<br/>
+		</div>
+
+		<script type="module">
+
+			import * as THREE from '../build/three.module.js';
+
+			import Stats from './jsm/libs/stats.module.js';
+			import { GUI } from './jsm/libs/dat.gui.module.js';
+			import { OrbitControls } from './jsm/controls/OrbitControls.js';
+			import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
+
+			var scene, renderer, camera, stats;
+			var model, skeleton, mixer, clock;
+
+			var crossFadeControls = [];
+
+			var currentBaseAction = 'idle';
+			const allActions = [];
+			const baseActions = {
+				idle: { weight: 1 },
+				walk: { weight: 0 },
+				run: { weight: 0 }
+			};
+			const additiveActions = {
+				sneak_pose: { weight: 0 },
+				sad_pose: { weight: 0 },
+				agree: { weight: 0 },
+				headShake: { weight: 0 }
+			};
+			var panelSettings, numAnimations;
+
+			init();
+
+			function init() {
+
+				var container = document.getElementById( 'container' );
+				clock = new THREE.Clock();
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xa0a0a0 );
+				scene.fog = new THREE.Fog( 0xa0a0a0, 10, 50 );
+
+				var hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444 );
+				hemiLight.position.set( 0, 20, 0 );
+				scene.add( hemiLight );
+
+				var dirLight = new THREE.DirectionalLight( 0xffffff );
+				dirLight.position.set( - 3, 10, - 10 );
+				dirLight.castShadow = true;
+				dirLight.shadow.camera.top = 2;
+				dirLight.shadow.camera.bottom = - 2;
+				dirLight.shadow.camera.left = - 2;
+				dirLight.shadow.camera.right = 2;
+				dirLight.shadow.camera.near = 0.1;
+				dirLight.shadow.camera.far = 40;
+				scene.add( dirLight );
+
+				// ground
+
+				var mesh = new THREE.Mesh( new THREE.PlaneBufferGeometry( 100, 100 ), new THREE.MeshPhongMaterial( { color: 0x999999, depthWrite: false } ) );
+				mesh.rotation.x = - Math.PI / 2;
+				mesh.receiveShadow = true;
+				scene.add( mesh );
+
+				var loader = new GLTFLoader();
+				loader.load( 'models/gltf/Xbot.glb', function ( gltf ) {
+
+					model = gltf.scene;
+					scene.add( model );
+
+					model.traverse( function ( object ) {
+
+						if ( object.isMesh ) object.castShadow = true;
+
+					} );
+
+					skeleton = new THREE.SkeletonHelper( model );
+					skeleton.visible = false;
+					scene.add( skeleton );
+
+					var animations = gltf.animations;
+					mixer = new THREE.AnimationMixer( model );
+
+					numAnimations = animations.length;
+
+					for ( let i = 0; i !== numAnimations; ++ i ) {
+
+						let clip = animations[ i ];
+						const name = clip.name;
+
+						if ( baseActions[ name ] ) {
+
+							const action = mixer.clipAction( clip );
+							activateAction( action );
+							baseActions[ name ].action = action;
+							allActions.push( action );
+
+						} else if ( additiveActions[ name ] ) {
+
+							// Make the clip additive and remove the reference frame
+
+							THREE.AnimationUtils.makeClipAdditive( clip );
+
+							if ( clip.name.endsWith( '_pose' ) ) {
+
+								clip = THREE.AnimationUtils.subclip( clip, clip.name, 2, 3, 30 );
+
+							}
+
+							const action = mixer.clipAction( clip );
+							activateAction( action );
+							additiveActions[ name ].action = action;
+							allActions.push( action );
+
+						}
+
+					}
+
+					createPanel();
+
+					animate();
+
+				} );
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.outputEncoding = THREE.sRGBEncoding;
+				renderer.shadowMap.enabled = true;
+				container.appendChild( renderer.domElement );
+
+				// camera
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
+				var controls = new OrbitControls( camera, renderer.domElement );
+				camera.position.set( - 1, 2, 3 );
+				camera.lookAt( 0, 1, 0 );
+				controls.target = new THREE.Vector3( 0, 1, 0 );
+				controls.update();
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			function createPanel() {
+
+				var panel = new GUI( { width: 310 } );
+
+				var folder1 = panel.addFolder( 'Base Actions' );
+				var folder2 = panel.addFolder( 'Additive Action Weights' );
+				var folder3 = panel.addFolder( 'General Speed' );
+
+				panelSettings = {
+					'modify time scale': 1.0
+				};
+
+				const baseNames = [ 'None', ...Object.keys( baseActions ) ];
+
+				for ( let i = 0, l = baseNames.length; i !== l; ++ i ) {
+
+					const name = baseNames[ i ];
+					const settings = baseActions[ name ];
+					panelSettings[ name ] = function () {
+
+						const currentSettings = baseActions[ currentBaseAction ];
+						const currentAction = currentSettings ? currentSettings.action : null;
+						const action = settings ? settings.action : null;
+
+						prepareCrossFade( currentAction, action, 0.35 );
+
+					};
+
+					crossFadeControls.push( folder1.add( panelSettings, name ) );
+
+				}
+
+				for ( const name of Object.keys( additiveActions ) ) {
+
+					const settings = additiveActions[ name ];
+
+					panelSettings[ name ] = settings.weight;
+					folder2.add( panelSettings, name, 0.0, 1.0, 0.01 ).listen().onChange( function ( weight ) {
+
+						setWeight( settings.action, weight );
+						settings.weight = weight;
+
+					} );
+
+				}
+
+				folder3.add( panelSettings, 'modify time scale', 0.0, 1.5, 0.01 ).onChange( modifyTimeScale );
+
+				folder1.open();
+				folder2.open();
+				folder3.open();
+
+				crossFadeControls.forEach( function ( control ) {
+
+					control.classList1 = control.domElement.parentElement.parentElement.classList;
+					control.classList2 = control.domElement.previousElementSibling.classList;
+
+					control.setInactive = function () {
+
+						control.classList2.add( 'control-inactive' );
+
+					};
+
+					control.setActive = function () {
+
+						control.classList2.remove( 'control-inactive' );
+
+					};
+
+					const settings = baseActions[ control.property ];
+
+					if ( ! settings || ! settings.weight ) {
+
+						control.setInactive();
+
+					}
+
+				} );
+
+			}
+
+			function activateAction( action ) {
+
+				const clip = action.getClip();
+				const settings = baseActions[ clip.name ] || additiveActions[ clip.name ];
+				setWeight( action, settings.weight );
+				action.play();
+
+			}
+
+			function modifyTimeScale( speed ) {
+
+				mixer.timeScale = speed;
+
+			}
+
+			function prepareCrossFade( startAction, endAction, duration ) {
+
+				// If the current action is 'idle', execute the crossfade immediately;
+				// else wait until the current action has finished its current loop
+
+				if ( currentBaseAction === 'idle' || ! startAction || ! endAction ) {
+
+					executeCrossFade( startAction, endAction, duration );
+
+				} else {
+
+					synchronizeCrossFade( startAction, endAction, duration );
+
+				}
+
+				// Update control colors
+
+				if ( endAction ) {
+
+					const clip = endAction.getClip();
+					currentBaseAction = clip.name;
+
+				} else {
+
+					currentBaseAction = 'None';
+
+				}
+
+				crossFadeControls.forEach( function ( control ) {
+
+					const name = control.property;
+
+					if ( name === currentBaseAction ) {
+
+						control.setActive();
+
+					} else {
+
+						control.setInactive();
+
+					}
+
+				} );
+
+			}
+
+			function synchronizeCrossFade( startAction, endAction, duration ) {
+
+				mixer.addEventListener( 'loop', onLoopFinished );
+
+				function onLoopFinished( event ) {
+
+					if ( event.action === startAction ) {
+
+						mixer.removeEventListener( 'loop', onLoopFinished );
+
+						executeCrossFade( startAction, endAction, duration );
+
+					}
+
+				}
+
+			}
+
+			function executeCrossFade( startAction, endAction, duration ) {
+
+				// Not only the start action, but also the end action must get a weight of 1 before fading
+				// (concerning the start action this is already guaranteed in this place)
+
+				if ( endAction ) {
+
+					setWeight( endAction, 1 );
+					endAction.time = 0;
+
+					if ( startAction ) {
+
+						// Crossfade with warping
+
+						startAction.crossFadeTo( endAction, duration, true );
+
+					} else {
+
+						// Fade in
+
+						endAction.fadeIn( duration );
+
+					}
+
+				} else {
+
+					// Fade out
+
+					startAction.fadeOut( duration );
+
+				}
+
+			}
+
+			// This function is needed, since animationAction.crossFadeTo() disables its start action and sets
+			// the start action's timeScale to ((start animation's duration) / (end animation's duration))
+
+			function setWeight( action, weight ) {
+
+				action.enabled = true;
+				action.setEffectiveTimeScale( 1 );
+				action.setEffectiveWeight( weight );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				// Render loop
+
+				requestAnimationFrame( animate );
+
+				for ( let i = 0; i !== numAnimations; ++ i ) {
+
+					const action = allActions[ i ];
+					const clip = action.getClip();
+					const settings = baseActions[ clip.name ] || additiveActions[ clip.name ];
+					settings.weight = action.getEffectiveWeight();
+
+				}
+
+				// Get the time elapsed since the last frame, used for mixer update
+
+				var mixerUpdateDelta = clock.getDelta();
+
+				// Update the animation mixer, the stats panel, and render this frame
+
+				mixer.update( mixerUpdateDelta );
+
+				stats.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 3 - 2
src/animation/AnimationAction.d.ts

@@ -1,13 +1,14 @@
 import { AnimationMixer } from './AnimationMixer';
 import { AnimationClip } from './AnimationClip';
-import { AnimationActionLoopStyles } from '../constants';
+import { AnimationActionLoopStyles, AnimationBlendMode } from '../constants';
 import { Object3D } from '../core/Object3D';
 // Animation ////////////////////////////////////////////////////////////////////////////////////////
 
 export class AnimationAction {
 
-	constructor( mixer: AnimationMixer, clip: AnimationClip, localRoot?: Object3D );
+	constructor( mixer: AnimationMixer, clip: AnimationClip, localRoot?: Object3D, blendMode?: AnimationBlendMode );
 
+	blendMode: AnimationBlendMode;
 	loop: AnimationActionLoopStyles;
 	time: number;
 	timeScale: number;

+ 24 - 5
src/animation/AnimationAction.js

@@ -1,4 +1,4 @@
-import { WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, LoopPingPong, LoopOnce, LoopRepeat } from '../constants.js';
+import { WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, LoopPingPong, LoopOnce, LoopRepeat, NormalAnimationBlendMode, AdditiveAnimationBlendMode } from '../constants.js';
 
 /**
  *
@@ -11,11 +11,12 @@ import { WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, LoopPingPong, L
  *
  */
 
-function AnimationAction( mixer, clip, localRoot ) {
+function AnimationAction( mixer, clip, localRoot, blendMode ) {
 
 	this._mixer = mixer;
 	this._clip = clip;
 	this._localRoot = localRoot || null;
+	this.blendMode = blendMode || clip.blendMode;
 
 	var tracks = clip.tracks,
 		nTracks = tracks.length,
@@ -374,10 +375,28 @@ Object.assign( AnimationAction.prototype, {
 			var interpolants = this._interpolants;
 			var propertyMixers = this._propertyBindings;
 
-			for ( var j = 0, m = interpolants.length; j !== m; ++ j ) {
+			switch ( this.blendMode ) {
 
-				interpolants[ j ].evaluate( clipTime );
-				propertyMixers[ j ].accumulate( accuIndex, weight );
+				case AdditiveAnimationBlendMode:
+
+					for ( var j = 0, m = interpolants.length; j !== m; ++ j ) {
+
+						interpolants[ j ].evaluate( clipTime );
+						propertyMixers[ j ].accumulateAdditive( weight );
+
+					}
+
+					break;
+
+				case NormalAnimationBlendMode:
+				default:
+
+					for ( var j = 0, m = interpolants.length; j !== m; ++ j ) {
+
+						interpolants[ j ].evaluate( clipTime );
+						propertyMixers[ j ].accumulate( accuIndex, weight );
+
+					}
 
 			}
 

+ 3 - 1
src/animation/AnimationClip.d.ts

@@ -1,13 +1,15 @@
 import { KeyframeTrack } from './KeyframeTrack';
 import { Bone } from './../objects/Bone';
 import { MorphTarget } from '../core/Geometry';
+import { AnimationBlendMode } from '../constants';
 
 export class AnimationClip {
 
-	constructor( name?: string, duration?: number, tracks?: KeyframeTrack[] );
+	constructor( name?: string, duration?: number, tracks?: KeyframeTrack[], blendMode?: AnimationBlendMode );
 
 	name: string;
 	tracks: KeyframeTrack[];
+	blendMode: AnimationBlendMode;
 	duration: number;
 	uuid: string;
 	results: any[];

+ 9 - 5
src/animation/AnimationClip.js

@@ -7,6 +7,7 @@ import { QuaternionKeyframeTrack } from './tracks/QuaternionKeyframeTrack.js';
 import { StringKeyframeTrack } from './tracks/StringKeyframeTrack.js';
 import { VectorKeyframeTrack } from './tracks/VectorKeyframeTrack.js';
 import { MathUtils } from '../math/MathUtils.js';
+import { NormalAnimationBlendMode } from '../constants.js';
 
 /**
  *
@@ -16,11 +17,12 @@ import { MathUtils } from '../math/MathUtils.js';
  * @author David Sarno / http://lighthaus.us/
  */
 
-function AnimationClip( name, duration, tracks ) {
+function AnimationClip( name, duration, tracks, blendMode ) {
 
 	this.name = name;
 	this.tracks = tracks;
 	this.duration = ( duration !== undefined ) ? duration : - 1;
+	this.blendMode = ( blendMode !== undefined ) ? blendMode : NormalAnimationBlendMode;
 
 	this.uuid = MathUtils.generateUUID();
 
@@ -124,7 +126,7 @@ Object.assign( AnimationClip, {
 
 		}
 
-		return new AnimationClip( json.name, json.duration, tracks );
+		return new AnimationClip( json.name, json.duration, tracks, json.blendMode );
 
 	},
 
@@ -138,7 +140,8 @@ Object.assign( AnimationClip, {
 			'name': clip.name,
 			'duration': clip.duration,
 			'tracks': tracks,
-			'uuid': clip.uuid
+			'uuid': clip.uuid,
+			'blendMode': clip.blendMode
 
 		};
 
@@ -300,6 +303,7 @@ Object.assign( AnimationClip, {
 		// automatic length determination in AnimationClip.
 		var duration = animation.length || - 1;
 		var fps = animation.fps || 30;
+		var blendMode = animation.blendMode;
 
 		var hierarchyTracks = animation.hierarchy || [];
 
@@ -381,7 +385,7 @@ Object.assign( AnimationClip, {
 
 		}
 
-		var clip = new AnimationClip( clipName, duration, tracks );
+		var clip = new AnimationClip( clipName, duration, tracks, blendMode );
 
 		return clip;
 
@@ -457,7 +461,7 @@ Object.assign( AnimationClip.prototype, {
 
 		}
 
-		return new AnimationClip( this.name, this.duration, tracks );
+		return new AnimationClip( this.name, this.duration, tracks, this.blendMode );
 
 	}
 

+ 2 - 1
src/animation/AnimationMixer.d.ts

@@ -1,5 +1,6 @@
 import { AnimationClip } from './AnimationClip';
 import { AnimationAction } from './AnimationAction';
+import { AnimationBlendMode } from '../constants';
 import { EventDispatcher } from './../core/EventDispatcher';
 import { Object3D } from '../core/Object3D';
 import { AnimationObjectGroup } from './AnimationObjectGroup';
@@ -11,7 +12,7 @@ export class AnimationMixer extends EventDispatcher {
 	time: number;
 	timeScale: number;
 
-	clipAction( clip: AnimationClip, root?: Object3D | AnimationObjectGroup ): AnimationAction;
+	clipAction( clip: AnimationClip, root?: Object3D | AnimationObjectGroup, blendMode?: AnimationBlendMode ): AnimationAction;
 	existingAction( clip: AnimationClip, root?: Object3D | AnimationObjectGroup ): AnimationAction | null;
 	stopAllAction(): AnimationMixer;
 	update( deltaTime: number ): AnimationMixer;

+ 18 - 3
src/animation/AnimationMixer.js

@@ -4,6 +4,7 @@ import { LinearInterpolant } from '../math/interpolants/LinearInterpolant.js';
 import { PropertyBinding } from './PropertyBinding.js';
 import { PropertyMixer } from './PropertyMixer.js';
 import { AnimationClip } from './AnimationClip.js';
+import { NormalAnimationBlendMode } from '../constants';
 
 /**
  *
@@ -516,7 +517,7 @@ AnimationMixer.prototype = Object.assign( Object.create( EventDispatcher.prototy
 	// 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 ) {
+	clipAction: function ( clip, optionalRoot, blendMode ) {
 
 		var root = optionalRoot || this._root,
 			rootUuid = root.uuid,
@@ -529,12 +530,26 @@ AnimationMixer.prototype = Object.assign( Object.create( EventDispatcher.prototy
 			actionsForClip = this._actionsByClip[ clipUuid ],
 			prototypeAction = null;
 
+		if ( blendMode === undefined ) {
+
+			if ( clipObject !== null ) {
+
+				blendMode = clipObject.blendMode;
+
+			} else {
+
+				blendMode = NormalAnimationBlendMode;
+
+			}
+
+		}
+
 		if ( actionsForClip !== undefined ) {
 
 			var existingAction =
 					actionsForClip.actionByRoot[ rootUuid ];
 
-			if ( existingAction !== undefined ) {
+			if ( existingAction !== undefined && existingAction.blendMode === blendMode ) {
 
 				return existingAction;
 
@@ -554,7 +569,7 @@ AnimationMixer.prototype = Object.assign( Object.create( EventDispatcher.prototy
 		if ( clipObject === null ) return null;
 
 		// allocate all resources required to run it
-		var newAction = new AnimationAction( this, clipObject, optionalRoot );
+		var newAction = new AnimationAction( this, clipObject, optionalRoot, blendMode );
 
 		this._bindAction( newAction, prototypeAction );
 

+ 6 - 0
src/animation/AnimationUtils.d.ts

@@ -23,4 +23,10 @@ export namespace AnimationUtils {
 		endFrame: number,
 		fps?: number
 	): AnimationClip;
+	export function makeClipAdditive(
+		targetClip: AnimationClip,
+		referenceFrame?: number,
+		referenceClip?: AnimationClip,
+		fps?: number
+	): AnimationClip;
 }

+ 109 - 0
src/animation/AnimationUtils.js

@@ -4,6 +4,9 @@
  * @author David Sarno / http://lighthaus.us/
  */
 
+import { Quaternion } from '../math/Quaternion.js';
+import { AdditiveAnimationBlendMode } from '../constants.js';
+
 var AnimationUtils = {
 
 	// same as Array.prototype.slice, but also works on typed arrays
@@ -231,6 +234,112 @@ var AnimationUtils = {
 
 		return clip;
 
+	},
+
+	makeClipAdditive: function ( targetClip, referenceFrame, referenceClip, fps ) {
+
+		if ( referenceFrame === undefined ) referenceFrame = 0;
+		if ( referenceClip === undefined ) referenceClip = targetClip;
+		if ( fps === undefined || fps <= 0 ) fps = 30;
+
+		var numTracks = targetClip.tracks.length;
+		var referenceTime = referenceFrame / fps;
+
+		// Make each track's values relative to the values at the reference frame
+		for ( var i = 0; i < numTracks; ++ i ) {
+
+			var referenceTrack = referenceClip.tracks[ i ];
+			var referenceTrackType = referenceTrack.ValueTypeName;
+
+			// Skip this track if it's non-numeric
+			if ( referenceTrackType === 'bool' || referenceTrackType === 'string' ) continue;
+
+			// Find the track in the target clip whose name and type matches the reference track
+			var targetTrack = targetClip.tracks.find( function ( track ) {
+
+				return track.name === referenceTrack.name
+				&& track.ValueTypeName === referenceTrackType;
+
+			} );
+
+			if ( targetTrack === undefined ) continue;
+
+			var valueSize = referenceTrack.getValueSize();
+			var lastIndex = referenceTrack.times.length - 1;
+			var referenceValue;
+
+			// Find the value to subtract out of the track
+			if ( referenceTime <= referenceTrack.times[ 0 ] ) {
+
+				// Reference frame is earlier than the first keyframe, so just use the first keyframe
+				referenceValue = AnimationUtils.arraySlice( referenceTrack.values, 0, referenceTrack.valueSize );
+
+			} else if ( referenceTime >= referenceTrack.times[ lastIndex ] ) {
+
+				// Reference frame is after the last keyframe, so just use the last keyframe
+				var startIndex = lastIndex * valueSize;
+				referenceValue = AnimationUtils.arraySlice( referenceTrack.values, startIndex );
+
+			} else {
+
+				// Interpolate to the reference value
+				var interpolant = referenceTrack.createInterpolant();
+				interpolant.evaluate( referenceTime );
+				referenceValue = interpolant.resultBuffer;
+
+			}
+
+			// Conjugate the quaternion
+			if ( referenceTrackType === 'quaternion' ) {
+
+				var referenceQuat = new Quaternion(
+					referenceValue[ 0 ],
+					referenceValue[ 1 ],
+					referenceValue[ 2 ],
+					referenceValue[ 3 ]
+				).normalize().conjugate();
+				referenceQuat.toArray( referenceValue );
+
+			}
+
+			// Subtract the reference value from all of the track values
+
+			var numTimes = targetTrack.times.length;
+			for ( var j = 0; j < numTimes; ++ j ) {
+
+				var valueStart = j * valueSize;
+
+				if ( referenceTrackType === 'quaternion' ) {
+
+					// Multiply the conjugate for quaternion track types
+					Quaternion.multiplyQuaternionsFlat(
+						targetTrack.values,
+						valueStart,
+						referenceValue,
+						0,
+						targetTrack.values,
+						valueStart
+					);
+
+				} else {
+
+					// Subtract each value for all other numeric track types
+					for ( var k = 0; k < valueSize; ++ k ) {
+
+						targetTrack.values[ valueStart + k ] -= referenceValue[ k ];
+
+					}
+
+				}
+
+			}
+
+		}
+
+		targetClip.blendMode = AdditiveAnimationBlendMode;
+
+		return targetClip;
+
 	}
 
 };

+ 2 - 0
src/animation/PropertyMixer.d.ts

@@ -6,10 +6,12 @@ export class PropertyMixer {
 	valueSize: number;
 	buffer: any;
 	cumulativeWeight: number;
+	cumulativeWeightAdditive: number;
 	useCount: number;
 	referenceCount: number;
 
 	accumulate( accuIndex: number, weight: number ): void;
+	accumulateAdditive( weight: number ): void;
 	apply( accuIndex: number ): void;
 	saveOriginalState(): void;
 	restoreOriginalState(): void;

+ 127 - 17
src/animation/PropertyMixer.js

@@ -15,41 +15,67 @@ function PropertyMixer( binding, typeName, valueSize ) {
 	this.binding = binding;
 	this.valueSize = valueSize;
 
-	var bufferType = Float64Array,
-		mixFunction;
+	var mixFunction,
+		mixFunctionAdditive,
+		setIdentity;
+
+	// buffer layout: [ incoming | accu0 | accu1 | orig | addAccu | (optional work) ]
+	//
+	// interpolators can use .buffer as their .result
+	// the data then goes to 'incoming'
+	//
+	// 'accu0' and 'accu1' are used frame-interleaved for
+	// the cumulative result and are compared to detect
+	// changes
+	//
+	// 'orig' stores the original state of the property
+	//
+	// 'add' is used for additive cumulative results
+	//
+	// 'work' is optional and is only present for quaternion types. It is used
+	// to store intermediate quaternion multiplication results
 
 	switch ( typeName ) {
 
 		case 'quaternion':
 			mixFunction = this._slerp;
+			mixFunctionAdditive = this._slerpAdditive;
+			setIdentity = this._setAdditiveIdentityQuaternion;
+
+			this.buffer = new Float64Array( 24 );
+			this._workIndex = 5;
 			break;
 
 		case 'string':
 		case 'bool':
-			bufferType = Array;
 			mixFunction = this._select;
+
+			// Use the regular mix function and for additive on these types,
+			// additive is not relevant for non-numeric types
+			mixFunctionAdditive = this._select;
+
+			setIdentity = this._setAdditiveIdentityOther;
+
+			this.buffer = new Array( valueSize * 5 );
 			break;
 
 		default:
 			mixFunction = this._lerp;
+			mixFunctionAdditive = this._lerpAdditive;
+			setIdentity = this._setAdditiveIdentityNumeric;
 
-	}
+			this.buffer = new Float64Array( valueSize * 5 );
 
-	this.buffer = new bufferType( valueSize * 4 );
-	// layout: [ incoming | accu0 | accu1 | orig ]
-	//
-	// interpolators can use .buffer as their .result
-	// the data then goes to 'incoming'
-	//
-	// 'accu0' and 'accu1' are used frame-interleaved for
-	// the cumulative result and are compared to detect
-	// changes
-	//
-	// 'orig' stores the original state of the property
+	}
 
 	this._mixBufferRegion = mixFunction;
+	this._mixBufferRegionAdditive = mixFunctionAdditive;
+	this._setIdentity = setIdentity;
+	this._origIndex = 3;
+	this._addIndex = 4;
 
 	this.cumulativeWeight = 0;
+	this.cumulativeWeightAdditive = 0;
 
 	this.useCount = 0;
 	this.referenceCount = 0;
@@ -96,6 +122,28 @@ Object.assign( PropertyMixer.prototype, {
 
 	},
 
+	// accumulate data in the 'incoming' region into 'add'
+	accumulateAdditive: function ( weight ) {
+
+		var buffer = this.buffer,
+			stride = this.valueSize,
+			offset = stride * this._addIndex;
+
+		if ( this.cumulativeWeightAdditive === 0 ) {
+
+			// add = identity
+
+			this._setIdentity();
+
+		}
+
+		// add := add + incoming * weight
+
+		this._mixBufferRegionAdditive( buffer, offset, 0, weight, stride );
+		this.cumulativeWeightAdditive += weight;
+
+	},
+
 	// apply the state of 'accu<i>' to the binding when accus differ
 	apply: function ( accuIndex ) {
 
@@ -104,22 +152,32 @@ Object.assign( PropertyMixer.prototype, {
 			offset = accuIndex * stride + stride,
 
 			weight = this.cumulativeWeight,
+			weightAdditive = this.cumulativeWeightAdditive,
 
 			binding = this.binding;
 
 		this.cumulativeWeight = 0;
+		this.cumulativeWeightAdditive = 0;
 
 		if ( weight < 1 ) {
 
 			// accuN := accuN + original * ( 1 - cumulativeWeight )
 
-			var originalValueOffset = stride * 3;
+			var originalValueOffset = stride * this._origIndex;
 
 			this._mixBufferRegion(
 				buffer, offset, originalValueOffset, 1 - weight, stride );
 
 		}
 
+		if ( weightAdditive > 0 ) {
+
+			// accuN := accuN + additive accuN
+
+			this._mixBufferRegionAdditive( buffer, offset, this._addIndex * stride, 1, stride );
+
+		}
+
 		for ( var i = stride, e = stride + stride; i !== e; ++ i ) {
 
 			if ( buffer[ i ] !== buffer[ i + stride ] ) {
@@ -143,7 +201,7 @@ Object.assign( PropertyMixer.prototype, {
 		var buffer = this.buffer,
 			stride = this.valueSize,
 
-			originalValueOffset = stride * 3;
+			originalValueOffset = stride * this._origIndex;
 
 		binding.getValue( buffer, originalValueOffset );
 
@@ -154,7 +212,11 @@ Object.assign( PropertyMixer.prototype, {
 
 		}
 
+		// Add to identity for additive
+		this._setIdentity();
+
 		this.cumulativeWeight = 0;
+		this.cumulativeWeightAdditive = 0;
 
 	},
 
@@ -166,6 +228,30 @@ Object.assign( PropertyMixer.prototype, {
 
 	},
 
+	_setAdditiveIdentityNumeric: function () {
+
+		var startIndex = this._addIndex * this.valueSize;
+
+		this.buffer.fill( 0, startIndex, startIndex + this.valueSize );
+
+	},
+
+	_setAdditiveIdentityQuaternion: function () {
+
+		this._setAdditiveIdentityNumeric();
+		this.buffer[ this._addIndex * 4 + 3 ] = 1;
+
+	},
+
+	_setAdditiveIdentityOther: function () {
+
+		var startIndex = this._origIndex * this.valueSize;
+		var targetIndex = this._addIndex * this.valueSize;
+
+		this.buffer.copyWithin( targetIndex, startIndex, this.valueSize );
+
+	},
+
 
 	// mix functions
 
@@ -189,6 +275,18 @@ Object.assign( PropertyMixer.prototype, {
 
 	},
 
+	_slerpAdditive: function ( buffer, dstOffset, srcOffset, t, stride ) {
+
+		var workOffset = this._workIndex * stride;
+
+		// Store result in intermediate buffer offset
+		Quaternion.multiplyQuaternionsFlat( buffer, workOffset, buffer, dstOffset, buffer, srcOffset );
+
+		// Slerp to the intermediate result
+		Quaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, workOffset, t );
+
+	},
+
 	_lerp: function ( buffer, dstOffset, srcOffset, t, stride ) {
 
 		var s = 1 - t;
@@ -201,6 +299,18 @@ Object.assign( PropertyMixer.prototype, {
 
 		}
 
+	},
+
+	_lerpAdditive: function ( buffer, dstOffset, srcOffset, t, stride ) {
+
+		for ( var i = 0; i !== stride; ++ i ) {
+
+			var j = dstOffset + i;
+
+			buffer[ j ] = buffer[ j ] + buffer[ srcOffset + i ] * t;
+
+		}
+
 	}
 
 } );

+ 5 - 0
src/constants.d.ts

@@ -306,6 +306,11 @@ export const ZeroCurvatureEnding: InterpolationEndingModes;
 export const ZeroSlopeEnding: InterpolationEndingModes;
 export const WrapAroundEnding: InterpolationEndingModes;
 
+// Animation blending modes
+export enum AnimationBlendMode { }
+export const NormalAnimationBlendMode: AnimationBlendMode;
+export const AdditiveAnimationBlendMode: AnimationBlendMode;
+
 // Triangle Draw modes
 export enum TrianglesDrawModes {}
 export const TrianglesDrawMode: TrianglesDrawModes;

+ 2 - 0
src/constants.js

@@ -153,6 +153,8 @@ export var InterpolateSmooth = 2302;
 export var ZeroCurvatureEnding = 2400;
 export var ZeroSlopeEnding = 2401;
 export var WrapAroundEnding = 2402;
+export var NormalAnimationBlendMode = 2500;
+export var AdditiveAnimationBlendMode = 2501;
 export var TrianglesDrawMode = 0;
 export var TriangleStripDrawMode = 1;
 export var TriangleFanDrawMode = 2;

+ 9 - 0
src/math/Quaternion.d.ts

@@ -149,6 +149,15 @@ export class Quaternion {
 		t: number
 	): Quaternion;
 
+	static multiplyQuaternionsFlat(
+		dst: number[],
+		dstOffset: number,
+		src0: number[],
+		srcOffset: number,
+		src1: number[],
+		stcOffset1: number
+	): number[];
+
 	/**
 	 * @deprecated Use {@link Vector#applyQuaternion vector.applyQuaternion( quaternion )} instead.
 	 */

+ 21 - 0
src/math/Quaternion.js

@@ -84,6 +84,27 @@ Object.assign( Quaternion, {
 		dst[ dstOffset + 2 ] = z0;
 		dst[ dstOffset + 3 ] = w0;
 
+	},
+
+	multiplyQuaternionsFlat: function ( dst, dstOffset, src0, srcOffset0, src1, srcOffset1 ) {
+
+		var x0 = src0[ srcOffset0 ];
+		var y0 = src0[ srcOffset0 + 1 ];
+		var z0 = src0[ srcOffset0 + 2 ];
+		var w0 = src0[ srcOffset0 + 3 ];
+
+		var x1 = src1[ srcOffset1 ];
+		var y1 = src1[ srcOffset1 + 1 ];
+		var z1 = src1[ srcOffset1 + 2 ];
+		var w1 = src1[ srcOffset1 + 3 ];
+
+		dst[ dstOffset ] = x0 * w1 + w0 * x1 + y0 * z1 - z0 * y1;
+		dst[ dstOffset + 1 ] = y0 * w1 + w0 * y1 + z0 * x1 - x0 * z1;
+		dst[ dstOffset + 2 ] = z0 * w1 + w0 * z1 + x0 * y1 - y0 * x1;
+		dst[ dstOffset + 3 ] = w0 * w1 - x0 * x1 - y0 * y1 - z0 * z1;
+
+		return dst;
+
 	}
 
 } );