فهرست منبع

Merge pull request #7773 from c-morten/Additive_Animation

Additive animation
David Catuhe 5 سال پیش
والد
کامیت
450484d0fa

BIN
Playground/scenes/Xbot.glb


+ 3 - 0
dist/preview release/what's new.md

@@ -61,6 +61,9 @@
 ### Collisions
 - Added an option to optimize collision detection performance ([jsdream](https://github.com/jsdream)) - [PR](https://github.com/BabylonJS/Babylon.js/pull/7810)
 
+### Animation
+- Added support for Additive Animation Blending. Existing animations can be converted to additive using the new MakeAnimationAdditive method for Skeletons, AnimationGroups and Animations. Animations can be played additively using the new isAdditive input parameter to the begin animation methods. ([c-morten](https://github.com/c-morten))
+
 ## Bugs
 
 - Fix infinite loop in `GlowLayer.unReferenceMeshFromUsingItsOwnMaterial` ([Popov72](https://github.com/Popov72)

+ 183 - 75
src/Animations/animatable.ts

@@ -106,6 +106,7 @@ export class Animatable {
      * @param onAnimationEnd defines a callback to call when animation ends if it is not looping
      * @param animations defines a group of animation to add to the new Animatable
      * @param onAnimationLoop defines a callback to call when animation loops
+     * @param isAdditive defines whether the animation should be evaluated additively
      */
     constructor(scene: Scene,
         /** defines the target object */
@@ -121,7 +122,9 @@ export class Animatable {
         public onAnimationEnd?: Nullable<() => void>,
         animations?: Animation[],
         /** defines a callback to call when animation loops */
-        public onAnimationLoop?: Nullable<() => void>) {
+        public onAnimationLoop?: Nullable<() => void>,
+        /** defines whether the animation should be evaluated additively */
+        public isAdditive: boolean = false) {
         this._scene = scene;
         if (animations) {
             this.appendAnimations(target, animations);
@@ -436,14 +439,18 @@ declare module "../scene" {
         /** @hidden */
         _processLateAnimationBindingsForMatrices(holder: {
             totalWeight: number,
+            totalAdditiveWeight: number,
             animations: RuntimeAnimation[],
+            additiveAnimations: RuntimeAnimation[],
             originalValue: Matrix
         }): any;
 
         /** @hidden */
         _processLateAnimationBindingsForQuaternions(holder: {
             totalWeight: number,
+            totalAdditiveWeight: number,
             animations: RuntimeAnimation[],
+            additiveAnimations: RuntimeAnimation[],
             originalValue: Quaternion
         }, refQuaternion: Quaternion): Quaternion;
 
@@ -462,10 +469,11 @@ declare module "../scene" {
          * @param animatable defines an animatable object. If not provided a new one will be created from the given params
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
          * @param onAnimationLoop defines the callback to call when an animation loops
+         * @param isAdditive defines whether the animation should be evaluated additively (false by default)
          * @returns the animatable object created for this animation
          */
         beginWeightedAnimation(target: any, from: number, to: number, weight: number, loop?: boolean, speedRatio?: number,
-            onAnimationEnd?: () => void, animatable?: Animatable, targetMask?: (target: any) => boolean, onAnimationLoop?: () => void): Animatable;
+            onAnimationEnd?: () => void, animatable?: Animatable, targetMask?: (target: any) => boolean, onAnimationLoop?: () => void, isAdditive?: boolean): Animatable;
 
         /**
          * Will start the animation sequence of a given target
@@ -479,11 +487,12 @@ declare module "../scene" {
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
          * @param targetMask defines if the target should be animate if animations are present (this is called recursively on descendant animatables regardless of return value)
          * @param onAnimationLoop defines the callback to call when an animation loops
+         * @param isAdditive defines whether the animation should be evaluated additively (false by default)
          * @returns the animatable object created for this animation
          */
         beginAnimation(target: any, from: number, to: number, loop?: boolean, speedRatio?: number,
             onAnimationEnd?: () => void, animatable?: Animatable, stopCurrent?: boolean,
-            targetMask?: (target: any) => boolean, onAnimationLoop?: () => void): Animatable;
+            targetMask?: (target: any) => boolean, onAnimationLoop?: () => void, isAdditive?: boolean): Animatable;
 
         /**
          * Will start the animation sequence of a given target and its hierarchy
@@ -498,11 +507,12 @@ declare module "../scene" {
          * @param stopCurrent defines if the current animations must be stopped first (true by default)
          * @param targetMask defines if the target should be animated if animations are present (this is called recursively on descendant animatables regardless of return value)
          * @param onAnimationLoop defines the callback to call when an animation loops
+         * @param isAdditive defines whether the animation should be evaluated additively (false by default)
          * @returns the list of created animatables
          */
         beginHierarchyAnimation(target: any, directDescendantsOnly: boolean, from: number, to: number, loop?: boolean, speedRatio?: number,
             onAnimationEnd?: () => void, animatable?: Animatable, stopCurrent?: boolean,
-            targetMask?: (target: any) => boolean, onAnimationLoop?: () => void): Animatable[];
+            targetMask?: (target: any) => boolean, onAnimationLoop?: () => void, isAdditive?: boolean): Animatable[];
 
         /**
          * Begin a new animation on a given node
@@ -514,9 +524,10 @@ declare module "../scene" {
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
          * @param onAnimationLoop defines the callback to call when an animation loops
+         * @param isAdditive defines whether the animation should be evaluated additively (false by default)
          * @returns the list of created animatables
          */
-        beginDirectAnimation(target: any, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void): Animatable;
+        beginDirectAnimation(target: any, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void, isAdditive?: boolean): Animatable;
 
         /**
          * Begin a new animation on a given node and its hierarchy
@@ -529,9 +540,10 @@ declare module "../scene" {
          * @param speedRatio defines the speed ratio to apply to all animations
          * @param onAnimationEnd defines the callback to call when an animation ends (will be called once per node)
          * @param onAnimationLoop defines the callback to call when an animation loops
+         * @param isAdditive defines whether the animation should be evaluated additively (false by default)
          * @returns the list of animatables created for all nodes
          */
-        beginDirectHierarchyAnimation(target: Node, directDescendantsOnly: boolean, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void): Animatable[];
+        beginDirectHierarchyAnimation(target: Node, directDescendantsOnly: boolean, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void, isAdditive?: boolean): Animatable[];
 
         /**
          * Gets the animatable associated with a specific target
@@ -597,9 +609,9 @@ Scene.prototype._animate = function(): void {
 };
 
 Scene.prototype.beginWeightedAnimation = function(target: any, from: number, to: number, weight = 1.0, loop?: boolean, speedRatio: number = 1.0,
-    onAnimationEnd?: () => void, animatable?: Animatable, targetMask?: (target: any) => boolean, onAnimationLoop?: () => void): Animatable {
+    onAnimationEnd?: () => void, animatable?: Animatable, targetMask?: (target: any) => boolean, onAnimationLoop?: () => void, isAdditive = false): Animatable {
 
-    let returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask, onAnimationLoop);
+    let returnedAnimatable = this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, false, targetMask, onAnimationLoop, isAdditive);
     returnedAnimatable.weight = weight;
 
     return returnedAnimatable;
@@ -607,7 +619,7 @@ Scene.prototype.beginWeightedAnimation = function(target: any, from: number, to:
 
 Scene.prototype.beginAnimation = function(target: any, from: number, to: number, loop?: boolean, speedRatio: number = 1.0,
     onAnimationEnd?: () => void, animatable?: Animatable, stopCurrent = true,
-    targetMask?: (target: any) => boolean, onAnimationLoop?: () => void): Animatable {
+    targetMask?: (target: any) => boolean, onAnimationLoop?: () => void, isAdditive = false): Animatable {
 
     if (from > to && speedRatio > 0) {
         speedRatio *= -1;
@@ -618,7 +630,7 @@ Scene.prototype.beginAnimation = function(target: any, from: number, to: number,
     }
 
     if (!animatable) {
-        animatable = new Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, undefined, onAnimationLoop);
+        animatable = new Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, undefined, onAnimationLoop, isAdditive);
     }
 
     const shouldRunTargetAnimations = targetMask ? targetMask(target) : true;
@@ -642,20 +654,20 @@ Scene.prototype.beginAnimation = function(target: any, from: number, to: number,
 
 Scene.prototype.beginHierarchyAnimation = function(target: any, directDescendantsOnly: boolean, from: number, to: number, loop?: boolean, speedRatio: number = 1.0,
     onAnimationEnd?: () => void, animatable?: Animatable, stopCurrent = true,
-    targetMask?: (target: any) => boolean, onAnimationLoop?: () => void): Animatable[] {
+    targetMask?: (target: any) => boolean, onAnimationLoop?: () => void, isAdditive = false): Animatable[] {
 
     let children = target.getDescendants(directDescendantsOnly);
 
     let result = [];
-    result.push(this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask));
+    result.push(this.beginAnimation(target, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, undefined, isAdditive));
     for (var child of children) {
-        result.push(this.beginAnimation(child, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask));
+        result.push(this.beginAnimation(child, from, to, loop, speedRatio, onAnimationEnd, animatable, stopCurrent, targetMask, undefined, isAdditive));
     }
 
     return result;
 };
 
-Scene.prototype.beginDirectAnimation = function(target: any, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void): Animatable {
+Scene.prototype.beginDirectAnimation = function(target: any, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void, isAdditive = false): Animatable {
     if (speedRatio === undefined) {
         speedRatio = 1.0;
     }
@@ -664,18 +676,18 @@ Scene.prototype.beginDirectAnimation = function(target: any, animations: Animati
         speedRatio *= -1;
     }
 
-    var animatable = new Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations, onAnimationLoop);
+    var animatable = new Animatable(this, target, from, to, loop, speedRatio, onAnimationEnd, animations, onAnimationLoop, isAdditive);
 
     return animatable;
 };
 
-Scene.prototype.beginDirectHierarchyAnimation = function(target: Node, directDescendantsOnly: boolean, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void): Animatable[] {
+Scene.prototype.beginDirectHierarchyAnimation = function(target: Node, directDescendantsOnly: boolean, animations: Animation[], from: number, to: number, loop?: boolean, speedRatio?: number, onAnimationEnd?: () => void, onAnimationLoop?: () => void, isAdditive = false): Animatable[] {
     let children = target.getDescendants(directDescendantsOnly);
 
     let result = [];
-    result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
+    result.push(this.beginDirectAnimation(target, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop, isAdditive));
     for (var child of children) {
-        result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop));
+        result.push(this.beginDirectAnimation(child, animations, from, to, loop, speedRatio, onAnimationEnd, onAnimationLoop, isAdditive));
     }
 
     return result;
@@ -743,20 +755,33 @@ Scene.prototype._registerTargetForLateAnimationBinding = function(runtimeAnimati
     if (!target._lateAnimationHolders[runtimeAnimation.targetPath]) {
         target._lateAnimationHolders[runtimeAnimation.targetPath] = {
             totalWeight: 0,
+            totalAdditiveWeight: 0,
             animations: [],
+            additiveAnimations: [],
             originalValue: originalValue
         };
     }
 
-    target._lateAnimationHolders[runtimeAnimation.targetPath].animations.push(runtimeAnimation);
-    target._lateAnimationHolders[runtimeAnimation.targetPath].totalWeight += runtimeAnimation.weight;
+    if (runtimeAnimation.isAdditive) {
+        target._lateAnimationHolders[runtimeAnimation.targetPath].additiveAnimations.push(runtimeAnimation);
+        target._lateAnimationHolders[runtimeAnimation.targetPath].totalAdditiveWeight += runtimeAnimation.weight;
+    } else {
+        target._lateAnimationHolders[runtimeAnimation.targetPath].animations.push(runtimeAnimation);
+        target._lateAnimationHolders[runtimeAnimation.targetPath].totalWeight += runtimeAnimation.weight;
+    }
 };
 
 Scene.prototype._processLateAnimationBindingsForMatrices = function(holder: {
     totalWeight: number,
+    totalAdditiveWeight: number,
     animations: RuntimeAnimation[],
+    additiveAnimations: RuntimeAnimation[],
     originalValue: Matrix
 }): any {
+    if (holder.totalWeight === 0 && holder.totalAdditiveWeight === 0) {
+        return holder.originalValue;
+    }
+
     let normalizer = 1.0;
     let finalPosition = TmpVectors.Vector3[0];
     let finalScaling = TmpVectors.Vector3[1];
@@ -766,98 +791,159 @@ Scene.prototype._processLateAnimationBindingsForMatrices = function(holder: {
     let originalValue = holder.originalValue;
 
     var scale = 1;
+    let skipOverride = false;
     if (holder.totalWeight < 1.0) {
         // We need to mix the original value in
-        originalValue.decompose(finalScaling, finalQuaternion, finalPosition);
         scale = 1.0 - holder.totalWeight;
+        originalValue.decompose(finalScaling, finalQuaternion, finalPosition);
     } else {
         startIndex = 1;
         // We need to normalize the weights
         normalizer = holder.totalWeight;
-        originalAnimation.currentValue.decompose(finalScaling, finalQuaternion, finalPosition);
         scale = originalAnimation.weight / normalizer;
         if (scale == 1) {
-            return originalAnimation.currentValue;
+            if (holder.totalAdditiveWeight) {
+                skipOverride = true;
+            } else {
+                return originalAnimation.currentValue;
+            }
+        }
+
+        originalAnimation.currentValue.decompose(finalScaling, finalQuaternion, finalPosition);
+    }
+
+    // Add up the override animations
+    if (!skipOverride) {
+        finalScaling.scaleInPlace(scale);
+        finalPosition.scaleInPlace(scale);
+        finalQuaternion.scaleInPlace(scale);
+
+        for (var animIndex = startIndex; animIndex < holder.animations.length; animIndex++) {
+            var runtimeAnimation = holder.animations[animIndex];
+            if (runtimeAnimation.weight === 0) {
+                continue;
+            }
+
+            var scale = runtimeAnimation.weight / normalizer;
+            let currentPosition = TmpVectors.Vector3[2];
+            let currentScaling = TmpVectors.Vector3[3];
+            let currentQuaternion = TmpVectors.Quaternion[1];
+
+            runtimeAnimation.currentValue.decompose(currentScaling, currentQuaternion, currentPosition);
+            currentScaling.scaleAndAddToRef(scale, finalScaling);
+            currentQuaternion.scaleAndAddToRef(scale, finalQuaternion);
+            currentPosition.scaleAndAddToRef(scale, finalPosition);
         }
     }
 
-    finalScaling.scaleInPlace(scale);
-    finalPosition.scaleInPlace(scale);
-    finalQuaternion.scaleInPlace(scale);
+    // Add up the additive animations
+    for (let animIndex = 0; animIndex < holder.additiveAnimations.length; animIndex++) {
+        var runtimeAnimation = holder.additiveAnimations[animIndex];
+        if (runtimeAnimation.weight === 0) {
+            continue;
+        }
 
-    for (var animIndex = startIndex; animIndex < holder.animations.length; animIndex++) {
-        var runtimeAnimation = holder.animations[animIndex];
-        var scale = runtimeAnimation.weight / normalizer;
         let currentPosition = TmpVectors.Vector3[2];
         let currentScaling = TmpVectors.Vector3[3];
         let currentQuaternion = TmpVectors.Quaternion[1];
 
         runtimeAnimation.currentValue.decompose(currentScaling, currentQuaternion, currentPosition);
-        currentScaling.scaleAndAddToRef(scale, finalScaling);
-        currentQuaternion.scaleAndAddToRef(scale, finalQuaternion);
-        currentPosition.scaleAndAddToRef(scale, finalPosition);
+        currentScaling.multiplyToRef(finalScaling, currentScaling);
+        Vector3.LerpToRef(finalScaling, currentScaling, runtimeAnimation.weight, finalScaling);
+        finalQuaternion.multiplyToRef(currentQuaternion, currentQuaternion);
+        Quaternion.SlerpToRef(finalQuaternion, currentQuaternion, runtimeAnimation.weight, finalQuaternion);
+        currentPosition.scaleAndAddToRef(runtimeAnimation.weight, finalPosition);
+
     }
-    let workValue = originalAnimation._animationState.workValue;
+
+    let workValue = originalAnimation ? originalAnimation._animationState.workValue : TmpVectors.Matrix[0].clone();
     Matrix.ComposeToRef(finalScaling, finalQuaternion, finalPosition, workValue);
     return workValue;
 };
 
 Scene.prototype._processLateAnimationBindingsForQuaternions = function(holder: {
     totalWeight: number,
+    totalAdditiveWeight: number,
     animations: RuntimeAnimation[],
+    additiveAnimations: RuntimeAnimation[],
     originalValue: Quaternion
 }, refQuaternion: Quaternion): Quaternion {
+    if (holder.totalWeight === 0 && holder.totalAdditiveWeight === 0) {
+        return refQuaternion;
+    }
+
     let originalAnimation = holder.animations[0];
     let originalValue = holder.originalValue;
+    let cumulativeQuaternion = refQuaternion;
 
-    if (holder.animations.length === 1) {
-        Quaternion.SlerpToRef(originalValue, originalAnimation.currentValue, Math.min(1.0, holder.totalWeight), refQuaternion);
-        return refQuaternion;
-    }
+    if (holder.totalWeight === 0 && holder.totalAdditiveWeight > 0) {
+        cumulativeQuaternion.copyFrom(originalValue);
+    } else if (holder.animations.length === 1) {
+        Quaternion.SlerpToRef(originalValue, originalAnimation.currentValue, Math.min(1.0, holder.totalWeight), cumulativeQuaternion);
 
-    let normalizer = 1.0;
-    let quaternions: Array<Quaternion>;
-    let weights: Array<number>;
+        if (holder.totalAdditiveWeight === 0) {
+            return cumulativeQuaternion;
+        }
+    } else if (holder.animations.length > 1) {
+        // Add up the override animations
+        let normalizer = 1.0;
+        let quaternions: Array<Quaternion>;
+        let weights: Array<number>;
 
-    if (holder.totalWeight < 1.0) {
-        let scale = 1.0 - holder.totalWeight;
+        if (holder.totalWeight < 1.0) {
+            let scale = 1.0 - holder.totalWeight;
 
-        quaternions = [];
-        weights = [];
+            quaternions = [];
+            weights = [];
 
-        quaternions.push(originalValue);
-        weights.push(scale);
-    } else {
-        if (holder.animations.length === 2) { // Slerp as soon as we can
-            Quaternion.SlerpToRef(holder.animations[0].currentValue, holder.animations[1].currentValue, holder.animations[1].weight / holder.totalWeight, refQuaternion);
-            return refQuaternion;
+            quaternions.push(originalValue);
+            weights.push(scale);
+        } else {
+            if (holder.animations.length === 2) { // Slerp as soon as we can
+                Quaternion.SlerpToRef(holder.animations[0].currentValue, holder.animations[1].currentValue, holder.animations[1].weight / holder.totalWeight, refQuaternion);
+
+                if (holder.totalAdditiveWeight === 0) {
+                    return refQuaternion;
+                }
+            }
+
+            quaternions = [];
+            weights = [];
+            normalizer = holder.totalWeight;
         }
-        quaternions = [];
-        weights = [];
 
-        normalizer = holder.totalWeight;
-    }
-    for (var animIndex = 0; animIndex < holder.animations.length; animIndex++) {
-        let runtimeAnimation = holder.animations[animIndex];
-        quaternions.push(runtimeAnimation.currentValue);
-        weights.push(runtimeAnimation.weight / normalizer);
-    }
+        for (var animIndex = 0; animIndex < holder.animations.length; animIndex++) {
+            let runtimeAnimation = holder.animations[animIndex];
+            quaternions.push(runtimeAnimation.currentValue);
+            weights.push(runtimeAnimation.weight / normalizer);
+        }
 
-    // https://gamedev.stackexchange.com/questions/62354/method-for-interpolation-between-3-quaternions
+        // https://gamedev.stackexchange.com/questions/62354/method-for-interpolation-between-3-quaternions
 
-    let cumulativeAmount = 0;
-    let cumulativeQuaternion: Nullable<Quaternion> = null;
-    for (var index = 0; index < quaternions.length;) {
-        if (!cumulativeQuaternion) {
-            Quaternion.SlerpToRef(quaternions[index], quaternions[index + 1], weights[index + 1] / (weights[index] + weights[index + 1]), refQuaternion);
-            cumulativeQuaternion = refQuaternion;
-            cumulativeAmount = weights[index] + weights[index + 1];
-            index += 2;
+        let cumulativeAmount = 0;
+        for (var index = 0; index < quaternions.length;) {
+            if (!index) {
+                Quaternion.SlerpToRef(quaternions[index], quaternions[index + 1], weights[index + 1] / (weights[index] + weights[index + 1]), refQuaternion);
+                cumulativeQuaternion = refQuaternion;
+                cumulativeAmount = weights[index] + weights[index + 1];
+                index += 2;
+                continue;
+            }
+            cumulativeAmount += weights[index];
+            Quaternion.SlerpToRef(cumulativeQuaternion, quaternions[index], weights[index] / cumulativeAmount, cumulativeQuaternion);
+            index++;
+        }
+    }
+
+    // Add up the additive animations
+    for (let animIndex = 0; animIndex < holder.additiveAnimations.length; animIndex++) {
+        let runtimeAnimation = holder.additiveAnimations[animIndex];
+        if (runtimeAnimation.weight === 0) {
             continue;
         }
-        cumulativeAmount += weights[index];
-        Quaternion.SlerpToRef(cumulativeQuaternion, quaternions[index], weights[index] / cumulativeAmount, cumulativeQuaternion);
-        index++;
+
+        cumulativeQuaternion.multiplyToRef(runtimeAnimation.currentValue, TmpVectors.Quaternion[0]);
+        Quaternion.SlerpToRef(cumulativeQuaternion, TmpVectors.Quaternion[0], runtimeAnimation.weight, cumulativeQuaternion);
     }
 
     return cumulativeQuaternion!;
@@ -891,12 +977,16 @@ Scene.prototype._processLateAnimationBindings = function(): void {
 
                     if (holder.totalWeight < 1.0) {
                         // We need to mix the original value in
-                        if (originalValue.scale) {
+                        if (originalAnimation && originalValue.scale) {
                             finalValue = originalValue.scale(1.0 - holder.totalWeight);
-                        } else {
+                        } else if (originalAnimation) {
                             finalValue = originalValue * (1.0 - holder.totalWeight);
+                        } else if (originalValue.clone) {
+                            finalValue = originalValue.clone();
+                        } else {
+                            finalValue = originalValue;
                         }
-                    } else {
+                    } else if (originalAnimation) {
                         // We need to normalize the weights
                         normalizer = holder.totalWeight;
                         let scale = originalAnimation.weight / normalizer;
@@ -913,10 +1003,28 @@ Scene.prototype._processLateAnimationBindings = function(): void {
                         startIndex = 1;
                     }
 
+                    // Add up the override animations
                     for (var animIndex = startIndex; animIndex < holder.animations.length; animIndex++) {
                         var runtimeAnimation = holder.animations[animIndex];
                         var scale = runtimeAnimation.weight / normalizer;
-                        if (runtimeAnimation.currentValue.scaleAndAddToRef) {
+
+                        if (!scale) {
+                            continue;
+                        } else if (runtimeAnimation.currentValue.scaleAndAddToRef) {
+                            runtimeAnimation.currentValue.scaleAndAddToRef(scale, finalValue);
+                        } else {
+                            finalValue += runtimeAnimation.currentValue * scale;
+                        }
+                    }
+
+                    // Add up the additive animations
+                    for (let animIndex = 0; animIndex < holder.additiveAnimations.length; animIndex++) {
+                        var runtimeAnimation = holder.additiveAnimations[animIndex];
+                        var scale: number = runtimeAnimation.weight;
+
+                        if (!scale) {
+                            continue;
+                        } else if (runtimeAnimation.currentValue.scaleAndAddToRef) {
                             runtimeAnimation.currentValue.scaleAndAddToRef(scale, finalValue);
                         } else {
                             finalValue += runtimeAnimation.currentValue * scale;

+ 202 - 1
src/Animations/animation.ts

@@ -1,5 +1,5 @@
 import { IEasingFunction, EasingFunction } from "./easing";
-import { Vector3, Quaternion, Vector2, Matrix } from "../Maths/math.vector";
+import { Vector3, Quaternion, Vector2, Matrix, TmpVectors } from "../Maths/math.vector";
 import { Color3, Color4 } from '../Maths/math.color';
 import { Scalar } from "../Maths/math.scalar";
 
@@ -224,6 +224,207 @@ export class Animation {
     }
 
     /**
+     * Convert the keyframes for all animations belonging to the group to be relative to a given reference frame.
+     * @param sourceAnimation defines the Animation containing keyframes to convert
+     * @param referenceFrame defines the frame that keyframes in the range will be relative to
+     * @param range defines the name of the AnimationRange belonging to the Animation to convert
+     * @param cloneOriginal defines whether or not to clone the animation and convert the clone or convert the original animation (default is false)
+     * @param clonedName defines the name of the resulting cloned Animation if cloneOriginal is true
+     * @returns a new Animation if cloneOriginal is true or the original Animation if cloneOriginal is false
+     */
+    public static MakeAnimationAdditive(sourceAnimation: Animation, referenceFrame = 0, range?: string, cloneOriginal = false, clonedName?: string): Animation {
+        let animation = sourceAnimation;
+
+        if (cloneOriginal) {
+            animation = sourceAnimation.clone();
+            animation.name = clonedName || animation.name;
+        }
+
+        if (!animation._keys.length) {
+            return animation;
+        }
+
+        referenceFrame = referenceFrame >= 0 ? referenceFrame : 0;
+        let startIndex = 0;
+        let firstKey = animation._keys[0];
+        let endIndex = animation._keys.length - 1;
+        let lastKey = animation._keys[endIndex];
+        let valueStore = {
+            referenceValue: firstKey.value,
+            referencePosition: TmpVectors.Vector3[0],
+            referenceQuaternion: TmpVectors.Quaternion[0],
+            referenceScaling: TmpVectors.Vector3[1],
+            keyPosition: TmpVectors.Vector3[2],
+            keyQuaternion: TmpVectors.Quaternion[1],
+            keyScaling: TmpVectors.Vector3[3]
+        };
+        let referenceFound = false;
+        let from = firstKey.frame;
+        let to = lastKey.frame;
+        if (range) {
+            let rangeValue = animation.getRange(range);
+
+            if (rangeValue) {
+                from = rangeValue.from;
+                to = rangeValue.to;
+            }
+        }
+        let fromKeyFound = firstKey.frame === from;
+        let toKeyFound = lastKey.frame === to;
+
+        // There's only one key, so use it
+        if (animation._keys.length === 1) {
+            let value = animation._getKeyValue(animation._keys[0]);
+            valueStore.referenceValue = value.clone ? value.clone() : value;
+            referenceFound = true;
+        }
+
+        // Reference frame is before the first frame, so just use the first frame
+        else if (referenceFrame <= firstKey.frame) {
+            let value = animation._getKeyValue(firstKey.value);
+            valueStore.referenceValue = value.clone ? value.clone() : value;
+            referenceFound = true;
+        }
+
+        // Reference frame is after the last frame, so just use the last frame
+        else if (referenceFrame >= lastKey.frame) {
+            let value = animation._getKeyValue(lastKey.value);
+            valueStore.referenceValue = value.clone ? value.clone() : value;
+            referenceFound = true;
+        }
+
+        // Find key bookends, create them if they don't exist
+        var index = 0;
+        while (!referenceFound || !fromKeyFound || !toKeyFound && index < animation._keys.length - 1) {
+            let currentKey = animation._keys[index];
+            let nextKey = animation._keys[index + 1];
+
+            // If reference frame wasn't found yet, check if we can interpolate to it
+            if (!referenceFound && referenceFrame >= currentKey.frame && referenceFrame <= nextKey.frame) {
+                let value;
+
+                if (referenceFrame === currentKey.frame) {
+                    value = animation._getKeyValue(currentKey.value);
+                } else if (referenceFrame === nextKey.frame) {
+                    value = animation._getKeyValue(nextKey.value);
+                } else {
+                    let animationState = {
+                        key: index,
+                        repeatCount: 0,
+                        loopMode: this.ANIMATIONLOOPMODE_CONSTANT
+                    };
+                    value = animation._interpolate(referenceFrame, animationState);
+                }
+
+                valueStore.referenceValue = value.clone ? value.clone() : value;
+                referenceFound = true;
+            }
+
+            // If from key wasn't found yet, check if we can interpolate to it
+            if (!fromKeyFound && from >= currentKey.frame && from <= nextKey.frame) {
+                if (from === currentKey.frame) {
+                    startIndex = index;
+                } else if (from === nextKey.frame) {
+                    startIndex = index + 1;
+                } else {
+                    let animationState = {
+                        key: index,
+                        repeatCount: 0,
+                        loopMode: this.ANIMATIONLOOPMODE_CONSTANT
+                    };
+                    let value = animation._interpolate(from, animationState);
+                    let key: IAnimationKey = {
+                        frame: from,
+                        value: value.clone ? value.clone() : value
+                    };
+                    animation._keys.splice(index + 1, 0, key);
+                    startIndex = index + 1;
+                }
+
+                fromKeyFound = true;
+            }
+
+            // If to key wasn't found yet, check if we can interpolate to it
+            if (!toKeyFound && to >= currentKey.frame && to <= nextKey.frame) {
+                if (to === currentKey.frame) {
+                    endIndex = index;
+                } else if (to === nextKey.frame) {
+                    endIndex = index + 1;
+                } else {
+                    let animationState = {
+                        key: index,
+                        repeatCount: 0,
+                        loopMode: this.ANIMATIONLOOPMODE_CONSTANT
+                    };
+                    let value = animation._interpolate(to, animationState);
+                    let key: IAnimationKey = {
+                        frame: to,
+                        value: value.clone ? value.clone() : value
+                    };
+                    animation._keys.splice(index + 1, 0, key);
+                    endIndex = index + 1;
+                }
+
+                toKeyFound = true;
+            }
+
+            index++;
+        }
+
+        // Conjugate the quaternion
+        if (animation.dataType === Animation.ANIMATIONTYPE_QUATERNION) {
+            valueStore.referenceValue.normalize().conjugateInPlace();
+        }
+
+        // Decompose matrix and conjugate the quaternion
+        else if (animation.dataType === Animation.ANIMATIONTYPE_MATRIX) {
+            valueStore.referenceValue.decompose(valueStore.referenceScaling, valueStore.referenceQuaternion, valueStore.referencePosition);
+            valueStore.referenceQuaternion.normalize().conjugateInPlace();
+        }
+
+        // Subtract the reference value from all of the key values
+        for (var index = startIndex; index <= endIndex; index++) {
+            let key = animation._keys[index];
+
+            // If this key was duplicated to create a frame 0 key, skip it because its value has already been updated
+            if (index && animation.dataType !== Animation.ANIMATIONTYPE_FLOAT && key.value === firstKey.value) {
+                continue;
+            }
+
+            switch (animation.dataType) {
+                case Animation.ANIMATIONTYPE_MATRIX:
+                    key.value.decompose(valueStore.keyScaling, valueStore.keyQuaternion, valueStore.keyPosition);
+                    valueStore.keyPosition.subtractInPlace(valueStore.referencePosition);
+                    valueStore.keyScaling.divideInPlace(valueStore.referenceScaling);
+                    valueStore.referenceQuaternion.multiplyToRef(valueStore.keyQuaternion, valueStore.keyQuaternion);
+                    Matrix.ComposeToRef(valueStore.keyScaling, valueStore.keyQuaternion, valueStore.keyPosition, key.value);
+                    break;
+
+                case Animation.ANIMATIONTYPE_QUATERNION:
+                    valueStore.referenceValue.multiplyToRef(key.value, key.value);
+                    break;
+
+                case Animation.ANIMATIONTYPE_VECTOR2:
+                case Animation.ANIMATIONTYPE_VECTOR3:
+                case Animation.ANIMATIONTYPE_COLOR3:
+                case Animation.ANIMATIONTYPE_COLOR4:
+                    key.value.subtractToRef(valueStore.referenceValue, key.value);
+                    break;
+
+                case Animation.ANIMATIONTYPE_SIZE:
+                    key.value.width -= valueStore.referenceValue.width;
+                    key.value.height -= valueStore.referenceValue.height;
+                    break;
+
+                default:
+                    key.value -= valueStore.referenceValue;
+            }
+        }
+
+        return animation;
+    }
+
+    /**
      * Transition property of an host to the target Value
      * @param property The property to transition
      * @param targetValue The target Value of the property

+ 60 - 2
src/Animations/animationGroup.ts

@@ -49,6 +49,7 @@ export class AnimationGroup implements IDisposable {
     private _isPaused: boolean;
     private _speedRatio = 1;
     private _loopAnimation = false;
+    private _isAdditive = false;
 
     /**
      * Gets or sets the unique id of the node
@@ -157,6 +158,26 @@ export class AnimationGroup implements IDisposable {
     }
 
     /**
+     * Gets or sets if all animations should be evaluated additively
+     */
+    public get isAdditive(): boolean {
+        return this._isAdditive;
+    }
+
+    public set isAdditive(value: boolean) {
+        if (this._isAdditive === value) {
+            return;
+        }
+
+        this._isAdditive = value;
+
+        for (var index = 0; index < this._animatables.length; index++) {
+            let animatable = this._animatables[index];
+            animatable.isAdditive = this._isAdditive;
+        }
+    }
+
+    /**
      * Gets the targeted animations for this animation group
      */
     public get targetedAnimations(): Array<TargetedAnimation> {
@@ -286,9 +307,10 @@ export class AnimationGroup implements IDisposable {
      * @param speedRatio defines the ratio to apply to animation speed (1 by default)
      * @param from defines the from key (optional)
      * @param to defines the to key (optional)
+     * @param isAdditive defines the additive state for the resulting animatables (optional)
      * @returns the current animation group
      */
-    public start(loop = false, speedRatio = 1, from?: number, to?: number): AnimationGroup {
+    public start(loop = false, speedRatio = 1, from?: number, to?: number, isAdditive?: boolean): AnimationGroup {
         if (this._isStarted || this._targetedAnimations.length === 0) {
             return this;
         }
@@ -300,7 +322,17 @@ export class AnimationGroup implements IDisposable {
 
         for (var index = 0; index < this._targetedAnimations.length; index++) {
             const targetedAnimation = this._targetedAnimations[index];
-            let animatable = this._scene.beginDirectAnimation(targetedAnimation.target, [targetedAnimation.animation], from !== undefined ? from : this._from, to !== undefined ? to : this._to, loop, speedRatio);
+            let animatable = this._scene.beginDirectAnimation(
+                targetedAnimation.target,
+                [targetedAnimation.animation],
+                from !== undefined ? from : this._from,
+                to !== undefined ? to : this._to,
+                loop,
+                speedRatio,
+                undefined,
+                undefined,
+                isAdditive !== undefined ? isAdditive : this._isAdditive
+            );
             animatable.onAnimationEnd = () => {
                 this.onAnimationEndObservable.notifyObservers(targetedAnimation);
                 this._checkAnimationGroupEnded(animatable);
@@ -583,6 +615,32 @@ export class AnimationGroup implements IDisposable {
     }
 
     /**
+     * Convert the keyframes for all animations belonging to the group to be relative to a given reference frame.
+     * @param sourceAnimationGroup defines the AnimationGroup containing animations to convert
+     * @param referenceFrame defines the frame that keyframes in the range will be relative to
+     * @param range defines the name of the AnimationRange belonging to the animations in the group to convert
+     * @param cloneOriginal defines whether or not to clone the group and convert the clone or convert the original group (default is false)
+     * @param clonedName defines the name of the resulting cloned AnimationGroup if cloneOriginal is true
+     * @returns a new AnimationGroup if cloneOriginal is true or the original AnimationGroup if cloneOriginal is false
+     */
+    public static MakeAnimationAdditive(sourceAnimationGroup: AnimationGroup, referenceFrame = 0, range?: string, cloneOriginal = false, clonedName?: string): AnimationGroup {
+        let animationGroup = sourceAnimationGroup;
+        if (cloneOriginal) {
+            animationGroup = sourceAnimationGroup.clone(clonedName || animationGroup.name);
+        }
+
+        let targetedAnimations = animationGroup.targetedAnimations;
+        for (var index = 0; index < targetedAnimations.length; index++) {
+            let targetedAnimation = targetedAnimations[index];
+            Animation.MakeAnimationAdditive(targetedAnimation.animation, referenceFrame, range);
+        }
+
+        animationGroup.isAdditive = true;
+
+        return animationGroup;
+    }
+
+    /**
      * Returns the string "AnimationGroup"
      * @returns "AnimationGroup"
      */

+ 7 - 0
src/Animations/runtimeAnimation.ts

@@ -172,6 +172,13 @@ export class RuntimeAnimation {
         return this._currentActiveTarget;
     }
 
+    /**
+     * Gets the additive state of the runtime animation
+     */
+    public get isAdditive(): boolean {
+        return this._host && this._host.isAdditive;
+    }
+
     /** @hidden */
     public _onLoop: () => void;
 

+ 52 - 0
src/Bones/skeleton.ts

@@ -390,6 +390,58 @@ export class Skeleton implements IAnimatable {
         return this._scene.beginAnimation(this, range.from, range.to, loop, speedRatio, onAnimationEnd);
     }
 
+    /**
+     * Convert the keyframes for a range of animation on a skeleton to be relative to a given reference frame.
+     * @param skeleton defines the Skeleton containing the animation range to convert
+     * @param referenceFrame defines the frame that keyframes in the range will be relative to
+     * @param range defines the name of the AnimationRange belonging to the Skeleton to convert
+     * @returns the original skeleton
+     */
+    public static MakeAnimationAdditive(skeleton: Skeleton, referenceFrame = 0, range: string): Nullable<Skeleton> {
+        var rangeValue = skeleton.getAnimationRange(name);
+
+        // We can't make a range additive if it doesn't exist
+        if (!rangeValue) {
+            return null;
+        }
+
+        // Find any current scene-level animatable belonging to the target that matches the range
+        var sceneAnimatables = skeleton._scene.getAllAnimatablesByTarget(skeleton);
+        var rangeAnimatable: Nullable<Animatable> = null;
+
+        for (let index = 0; index < sceneAnimatables.length; index++) {
+            let sceneAnimatable = sceneAnimatables[index];
+
+            if (sceneAnimatable.fromFrame === rangeValue?.from && sceneAnimatable.toFrame === rangeValue?.to) {
+                rangeAnimatable = sceneAnimatable;
+                break;
+            }
+        }
+
+        // Convert the animations belonging to the skeleton to additive keyframes
+        var animatables = skeleton.getAnimatables();
+
+        for (let index = 0; index < animatables.length; index++) {
+            let animatable = animatables[index];
+            let animations = animatable.animations;
+
+            if (!animations) {
+                continue;
+            }
+
+            for (var animIndex = 0; animIndex < animations.length; animIndex++) {
+                Animation.MakeAnimationAdditive(animations[animIndex], referenceFrame, range);
+            }
+        }
+
+        // Mark the scene-level animatable as additive
+        if (rangeAnimatable) {
+            rangeAnimatable.isAdditive = true;
+        }
+
+        return skeleton;
+    }
+
     /** @hidden */
     public _markAsDirty(): void {
         this._isDirty = true;