소스 검색

Adding support for additive animation

Christine Morten 5 년 전
부모
커밋
a070068b08
5개의 변경된 파일502개의 추가작업 그리고 81개의 파일을 삭제
  1. 181 78
      src/Animations/animatable.ts
  2. 202 1
      src/Animations/animation.ts
  3. 60 2
      src/Animations/animationGroup.ts
  4. 7 0
      src/Animations/runtimeAnimation.ts
  5. 52 0
      src/Bones/skeleton.ts

+ 181 - 78
src/Animations/animatable.ts

@@ -121,7 +121,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);
@@ -462,10 +464,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 +482,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 +502,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 +519,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 +535,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 +604,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 +614,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 +625,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 +649,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 +671,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 +750,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 && !holder.totalAdditiveWeight) {
+        return holder.originalValue;
+    }
+
     let normalizer = 1.0;
     let finalPosition = TmpVectors.Vector3[0];
     let finalScaling = TmpVectors.Vector3[1];
@@ -764,100 +784,161 @@ Scene.prototype._processLateAnimationBindingsForMatrices = function(holder: {
     let startIndex = 0;
     let originalAnimation = holder.animations[0];
     let originalValue = holder.originalValue;
+    originalValue.decompose(finalScaling, finalQuaternion, finalPosition);
 
     var scale = 1;
+    var skipOverride = false;
     if (holder.totalWeight < 1.0) {
-        // We need to mix the original value in
-        originalValue.decompose(finalScaling, finalQuaternion, finalPosition);
+        // We need to mix the original value in      
         scale = 1.0 - holder.totalWeight;
     } 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;
+            }
+        }
+
+        startIndex = 1;
+        // We need to normalize the weights
+        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) {
+                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 (var animIndex = 0; animIndex < holder.additiveAnimations.length; animIndex++) {
+        var runtimeAnimation = holder.additiveAnimations[animIndex];
+        if (!runtimeAnimation.weight) {
+            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 && !holder.totalAdditiveWeight) {
+        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 && holder.totalAdditiveWeight) {
+        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) {
+            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) {
+                    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 (var animIndex = 0; animIndex < holder.additiveAnimations.length; animIndex++) {
+        let runtimeAnimation = holder.additiveAnimations[animIndex];
+        if (!runtimeAnimation.weight) {
             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 +972,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 +998,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 (var 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 (var index = 0; index < sceneAnimatables.length; index++) {
+            var 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 (var index = 0; index < animatables.length; index++) {
+            let animatable = animatables[index];
+            var 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;