Browse Source

Adding support for additive animation

- Added a clip conversion method to AnimationUtils to convert to additive based on a reference frame

- Added an additive buffer and additive blending methods to PropertyMixer

- Added an isAdditive property to AnimationAction so it can choose the correct PropertyMixer accumulation method

- Added isAdditive input to AnimationMixer.clipAction so it can create the correct type of action

- AnimationMixer looks at AnimationAction's isAdditive property to choose which PropertyMixer buffer to use for Interpolant.resultBuffer
Christine Morten 5 years ago
parent
commit
a81bf052d5

+ 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


+ 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();
+
+				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 );
+
+				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, undefined, false );
+							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, undefined, true );
+							activateAction( action );
+							additiveActions[ name ].action = action;
+							allActions.push( action );
+
+						}
+
+					}
+
+					createPanel();
+
+					animate();
+
+				} );
+
+			}
+
+			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 ];
+
+					const panelName = `modify ${name} weight`;
+					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>

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

@@ -6,8 +6,9 @@ import { Object3D } from '../core/Object3D';
 
 export class AnimationAction {
 
-	constructor( mixer: AnimationMixer, clip: AnimationClip, localRoot?: Object3D );
+	constructor( mixer: AnimationMixer, clip: AnimationClip, localRoot?: Object3D, isAdditive?: boolean );
 
+	isAdditive: boolean;
 	loop: AnimationActionLoopStyles;
 	time: number;
 	timeScale: number;

+ 18 - 3
src/animation/AnimationAction.js

@@ -11,11 +11,12 @@ import { WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, LoopPingPong, L
  *
  */
 
-function AnimationAction( mixer, clip, localRoot ) {
+function AnimationAction( mixer, clip, localRoot, isAdditive = false ) {
 
 	this._mixer = mixer;
 	this._clip = clip;
 	this._localRoot = localRoot || null;
+	this.isAdditive = isAdditive;
 
 	var tracks = clip.tracks,
 		nTracks = tracks.length,
@@ -367,7 +368,21 @@ Object.assign( AnimationAction.prototype, {
 		// note: _updateTime may disable the action resulting in
 		// an effective weight of 0
 
-		var weight = this._updateWeight( time );
+		var accuParamA, accuParamB, accuFn,
+			weight = this._updateWeight( time );
+
+		if ( this.isAdditive ) {
+
+			accuFn = 'accumulateAdditive';
+			accuParamA = weight;
+
+		} else {
+
+			accuFn = 'accumulate';
+			accuParamA = accuIndex;
+			accuParamB = weight;
+
+		}
 
 		if ( weight > 0 ) {
 
@@ -377,7 +392,7 @@ Object.assign( AnimationAction.prototype, {
 			for ( var j = 0, m = interpolants.length; j !== m; ++ j ) {
 
 				interpolants[ j ].evaluate( clipTime );
-				propertyMixers[ j ].accumulate( accuIndex, weight );
+				propertyMixers[ j ][ accuFn ]( accuParamA, accuParamB );
 
 			}
 

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

@@ -10,7 +10,7 @@ export class AnimationMixer extends EventDispatcher {
 	time: number;
 	timeScale: number;
 
-	clipAction( clip: AnimationClip, root?: Object3D ): AnimationAction;
+	clipAction( clip: AnimationClip, root?: Object3D, isAdditive?: boolean ): AnimationAction;
 	existingAction( clip: AnimationClip, root?: Object3D ): AnimationAction | null;
 	stopAllAction(): AnimationMixer;
 	update( deltaTime: number ): AnimationMixer;

+ 6 - 4
src/animation/AnimationMixer.js

@@ -92,7 +92,9 @@ AnimationMixer.prototype = Object.assign( Object.create( EventDispatcher.prototy
 
 			}
 
-			interpolants[ i ].resultBuffer = binding.buffer;
+			if ( action.isAdditive ) interpolants[ i ].resultBuffer = binding.bufferAdditive;
+
+			else interpolants[ i ].resultBuffer = binding.buffer;
 
 		}
 
@@ -516,7 +518,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, isAdditive = false ) {
 
 		var root = optionalRoot || this._root,
 			rootUuid = root.uuid,
@@ -534,7 +536,7 @@ AnimationMixer.prototype = Object.assign( Object.create( EventDispatcher.prototy
 			var existingAction =
 					actionsForClip.actionByRoot[ rootUuid ];
 
-			if ( existingAction !== undefined ) {
+			if ( existingAction !== undefined && existingAction.isAdditive === isAdditive ) {
 
 				return existingAction;
 
@@ -554,7 +556,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, isAdditive );
 
 		this._bindAction( newAction, prototypeAction );
 

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

@@ -23,4 +23,11 @@ export namespace AnimationUtils {
 		endFrame: number,
 		fps?: number
 	): AnimationClip;
+	export function makeClipAdditive(
+		sourceClip: AnimationClip,
+		referenceFrame?: number,
+		cloneOriginal?: boolean,
+		clonedName?: string,
+		fps?: number
+	): AnimationClip;
 }

+ 101 - 0
src/animation/AnimationUtils.js

@@ -4,6 +4,8 @@
  * @author David Sarno / http://lighthaus.us/
  */
 
+import { Quaternion } from '../math/Quaternion.js';
+
 var AnimationUtils = {
 
 	// same as Array.prototype.slice, but also works on typed arrays
@@ -231,6 +233,105 @@ var AnimationUtils = {
 
 		return clip;
 
+	},
+
+	makeClipAdditive: function ( sourceClip, referenceFrame = 0, cloneOriginal = false, clonedName, fps = 30 ) {
+
+		let clip = sourceClip;
+		if ( cloneOriginal ) {
+
+			clip = sourceClip.clone();
+			clip.name = clonedName || clip.name;
+
+		}
+		const numTracks = clip.tracks.length;
+
+		fps = fps || 30;
+		const referenceTime = referenceFrame / fps;
+
+		// Make each track's values relative to the values at the reference frame
+		for ( let i = 0; i !== numTracks; ++ i ) {
+
+			const track = clip.tracks[ i ];
+			const trackType = track.ValueTypeName;
+
+			// Skip this track if it's non-numeric
+			if ( trackType === 'bool' || trackType === 'string' ) continue;
+
+			const valueSize = track.getValueSize();
+			const lastIndex = track.times.length - 1;
+			const numTimes = track.times.length;
+			let referenceValue;
+
+			// Find the value to subtract out of the track
+			if ( referenceTime <= track.times[ 0 ] ) {
+
+				// Reference frame is earlier than the first keyframe, so just use the first keyframe
+				referenceValue = AnimationUtils.arraySlice( track.values, 0, track.valueSize );
+
+			} else if ( referenceTime >= track.times[ lastIndex ] ) {
+
+				// Reference frame is after the last keyframe, so just use the last keyframe
+				const startIndex = lastIndex * valueSize;
+				referenceValue = AnimationUtils.arraySlice( track.values, startIndex );
+
+			} else {
+
+				// Interpolate to the reference value
+				const interpolant = track.createInterpolant();
+				interpolant.evaluate( referenceTime );
+				referenceValue = interpolant.resultBuffer;
+
+			}
+
+			// Conjugate the quaternion
+			if ( trackType === 'quaternion' ) {
+
+				const 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
+
+			for ( let j = 0; j !== numTimes; ++ j ) {
+
+				const valueStart = j * valueSize;
+
+				if ( trackType === 'quaternion' ) {
+
+					// Multiply the conjugate for quaternion track types
+					Quaternion.multiplyQuaternionsFlat(
+						track.values,
+						valueStart,
+						referenceValue,
+						0,
+						track.values,
+						valueStart
+					);
+
+				} else {
+
+					// Subtract each value for all other numeric track types
+					for ( let k = 0; k !== valueSize; ++ k ) {
+
+						track.values[ valueStart + k ] -= referenceValue[ k ];
+
+					}
+
+				}
+
+			}
+
+		}
+
+		return clip;
+
 	}
 
 };

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

@@ -5,11 +5,14 @@ export class PropertyMixer {
 	binding: any;
 	valueSize: number;
 	buffer: any;
+	bufferAdditive: 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;

+ 110 - 3
src/animation/PropertyMixer.js

@@ -16,27 +16,45 @@ function PropertyMixer( binding, typeName, valueSize ) {
 	this.valueSize = valueSize;
 
 	var bufferType = Float64Array,
-		mixFunction;
+		mixFunction,
+		mixFunctionAdditive;
 
 	switch ( typeName ) {
 
 		case 'quaternion':
 			mixFunction = this._slerp;
+			mixFunctionAdditive = this._slerpAdditive;
+
+			this.bufferAdditive = new bufferType( 16 );
+			this.bufferAdditive.fill( 0 );
+			this.bufferAdditive[ 3 ] = 1;
+			this.bufferAdditive[ 7 ] = 1;
+			this.bufferAdditive[ 11 ] = 1;
+			this.bufferAdditive[ 15 ] = 1;
 			break;
 
 		case 'string':
 		case 'bool':
 			bufferType = Array;
 			mixFunction = this._select;
+
+			// Use the regular mix function for additive on these types,
+			// additive is not relevant for non-numeric types
+			mixFunctionAdditive = this._select;
+			this.bufferAdditive = [];
 			break;
 
 		default:
 			mixFunction = this._lerp;
+			mixFunctionAdditive = this._lerpAdditive;
+
+			this.bufferAdditive = new bufferType( valueSize * 3 );
+			this.bufferAdditive.fill( 0 );
 
 	}
 
 	this.buffer = new bufferType( valueSize * 4 );
-	// layout: [ incoming | accu0 | accu1 | orig ]
+	// buffer layout: [ incoming | accu0 | accu1 | orig ]
 	//
 	// interpolators can use .buffer as their .result
 	// the data then goes to 'incoming'
@@ -47,9 +65,25 @@ function PropertyMixer( binding, typeName, valueSize ) {
 	//
 	// 'orig' stores the original state of the property
 
+
+	// additiveBuffer layout: [ incoming | accu | identity | ( optional work ) ]
+	//
+	// interpolators can use .additiveBuffer as their .result
+	// the data then goes to 'incoming'
+	//
+	// 'accu' is used frame-interleaved for the cumulative result
+	//
+	// 'identity' stores the zeroed out state of the property
+	//
+	// optional work is only valid for quaternions. It is used to store
+	// intermediate quaternion muliplication results.
+
+
 	this._mixBufferRegion = mixFunction;
+	this._mixBufferRegionAdditive = mixFunctionAdditive;
 
 	this.cumulativeWeight = 0;
+	this.cumulativeWeightAdditive = 0;
 
 	this.useCount = 0;
 	this.referenceCount = 0;
@@ -96,18 +130,51 @@ Object.assign( PropertyMixer.prototype, {
 
 	},
 
+	accumulateAdditive: function ( weight ) {
+
+		// note: happily accumulating nothing when weight = 0, the caller knows
+		// the weight and shouldn't have made the call in the first place
+
+		var buffer = this.bufferAdditive,
+			stride = this.valueSize,
+			offset = stride;
+
+		if ( this.cumulativeWeightAdditive === 0 ) {
+
+			// accuN = original
+
+			var originalValueOffset = stride * 2;
+
+			for ( var i = 0; i !== stride; ++ i ) {
+
+				buffer[ offset + i ] = buffer[ originalValueOffset + i ];
+
+			}
+
+		}
+
+		// accuN := accuN + incoming * weight
+
+		this._mixBufferRegionAdditive( buffer, offset, buffer, 0, weight, stride );
+		this.cumulativeWeightAdditive += weight;
+
+	},
+
 	// apply the state of 'accu<i>' to the binding when accus differ
 	apply: function ( accuIndex ) {
 
 		var stride = this.valueSize,
 			buffer = this.buffer,
+			bufferAdditive = this.bufferAdditive,
 			offset = accuIndex * stride + stride,
 
 			weight = this.cumulativeWeight,
+			weightAdditive = this.cumulativeWeightAdditive,
 
 			binding = this.binding;
 
 		this.cumulativeWeight = 0;
+		this.cumulativeWeightAdditive = 0;
 
 		if ( weight < 1 ) {
 
@@ -120,6 +187,14 @@ Object.assign( PropertyMixer.prototype, {
 
 		}
 
+		if ( weightAdditive > 0 ) {
+
+			// accuN := accuN + additive accuN
+
+			this._mixBufferRegionAdditive( buffer, offset, bufferAdditive, stride, 1, stride );
+
+		}
+
 		for ( var i = stride, e = stride + stride; i !== e; ++ i ) {
 
 			if ( buffer[ i ] !== buffer[ i + stride ] ) {
@@ -141,20 +216,30 @@ Object.assign( PropertyMixer.prototype, {
 		var binding = this.binding;
 
 		var buffer = this.buffer,
+			bufferAdditive = this.bufferAdditive,
 			stride = this.valueSize,
 
-			originalValueOffset = stride * 3;
+			originalValueOffset = stride * 3,
+			originalValueOffsetAdditive = stride * 2;
 
 		binding.getValue( buffer, originalValueOffset );
 
 		// accu[0..1] := orig -- initially detect changes against the original
+		// original for additive is identity
 		for ( var i = stride, e = originalValueOffset; i !== e; ++ i ) {
 
 			buffer[ i ] = buffer[ originalValueOffset + ( i % stride ) ];
 
+			if ( originalValueOffset <= originalValueOffsetAdditive ) {
+
+				bufferAdditive[ i ] = bufferAdditive[ originalValueOffset + ( i % stride ) ];
+
+			}
+
 		}
 
 		this.cumulativeWeight = 0;
+		this.cumulativeWeightAdditive = 0;
 
 	},
 
@@ -189,6 +274,16 @@ Object.assign( PropertyMixer.prototype, {
 
 	},
 
+	_slerpAdditive: function ( dstBuffer, dstOffset, srcBuffer, srcOffset, t ) {
+
+		// Store result in intermediate buffer offset
+		Quaternion.multiplyQuaternionsFlat( srcBuffer, 12, dstBuffer, dstOffset, srcBuffer, srcOffset );
+
+		// Slerp to the intermediate result
+		Quaternion.slerpFlat( dstBuffer, dstOffset, dstBuffer, dstOffset, srcBuffer, 12, t );
+
+	},
+
 	_lerp: function ( buffer, dstOffset, srcOffset, t, stride ) {
 
 		var s = 1 - t;
@@ -201,6 +296,18 @@ Object.assign( PropertyMixer.prototype, {
 
 		}
 
+	},
+
+	_lerpAdditive: function ( dstBuffer, dstOffset, srcBuffer, srcOffset, t, stride ) {
+
+		for ( var i = 0; i !== stride; ++ i ) {
+
+			var j = dstOffset + i;
+
+			dstBuffer[ j ] = dstBuffer[ j ] + srcBuffer[ srcOffset + i ] * t;
+
+		}
+
 	}
 
 } );

+ 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 ) {
+
+		const x0 = src0[ srcOffset0 ];
+		const y0 = src0[ srcOffset0 + 1 ];
+		const z0 = src0[ srcOffset0 + 2 ];
+		const w0 = src0[ srcOffset0 + 3 ];
+
+		const x1 = src1[ srcOffset1 ];
+		const y1 = src1[ srcOffset1 + 1 ];
+		const z1 = src1[ srcOffset1 + 2 ];
+		const 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;
+
 	}
 
 } );