Browse Source

Added TRS animation support to glTF serializer; refactorings

Kacey Coley 7 years ago
parent
commit
114c1d215e

+ 3 - 1
Tools/Gulp/config.json

@@ -1592,7 +1592,9 @@
                     "../../serializers/src/glTF/2.0/babylon.glTFSerializer.ts",
                     "../../serializers/src/glTF/2.0/babylon.glTFExporter.ts",
                     "../../serializers/src/glTF/2.0/babylon.glTFData.ts",
-                    "../../serializers/src/glTF/2.0/babylon.glTFMaterial.ts"
+                    "../../serializers/src/glTF/2.0/babylon.glTFMaterial.ts",
+                    "../../serializers/src/glTF/2.0/babylon.glTFAnimation.ts",
+                    "../../serializers/src/glTF/2.0/babylon.glTFUtilities.ts"
                 ],
                 "output": "babylon.glTF2Serializer.js"
             }

+ 718 - 0
serializers/src/glTF/2.0/babylon.glTFAnimation.ts

@@ -0,0 +1,718 @@
+/// <reference path="../../../../dist/babylon.glTF2Interface.d.ts"/>
+
+module BABYLON.GLTF2 {
+    /**
+     * @hidden
+     * Interface to store animation data.
+     */
+    export interface _IAnimationData {
+        /**
+         * Keyframe data.
+         */
+        inputs: number[],
+        /**
+         * Value data.
+         */
+        outputs: number[][],
+        /**
+         * Animation interpolation data.
+         */
+        samplerInterpolation: AnimationSamplerInterpolation,
+        /**
+         * Minimum keyframe value.
+         */
+        inputsMin: number,
+        /**
+         * Maximum keyframe value.
+         */
+        inputsMax: number,
+    }
+
+    /**
+     * @hidden
+     */
+    export interface _IAnimationInfo {
+        /**
+         * The target channel for the animation
+         */
+        animationChannelTargetPath: AnimationChannelTargetPath,
+        /**
+         * The glTF accessor type for the data.
+         */
+        dataAccessorType: AccessorType.VEC3 | AccessorType.VEC4,
+        /**
+         * Specifies if quaternions should be used.
+         */
+        useQuaternion: boolean
+    }
+
+    /**
+     * @hidden
+     * Enum for handling in tangent and out tangent.
+     */
+    enum _TangentType {
+        /**
+         * Specifies that input tangents are used.
+         */
+        INTANGENT,
+        /**
+         * Specifies that output tangents are used.
+         */
+        OUTTANGENT
+    }
+    /**
+     * @hidden
+     * Utility class for generating glTF animation data from BabylonJS.
+     */
+    export class _GLTFAnimation {
+        /**
+         * @ignore
+         * 
+         * Creates glTF channel animation from BabylonJS animation.
+         * @param babylonMesh - BabylonJS mesh.
+         * @param animation - animation.
+         * @param animationChannelTargetPath - The target animation channel. 
+         * @param convertToRightHandedSystem - Specifies if the values should be converted to right-handed.
+         * @param useQuaternion - Specifies if quaternions are used.
+         * @returns nullable IAnimationData
+         */
+        public static _CreateNodeAnimation(babylonMesh: BABYLON.Mesh, animation: Animation, animationChannelTargetPath: AnimationChannelTargetPath, convertToRightHandedSystem: boolean, useQuaternion: boolean, animationSampleRate: number): Nullable<_IAnimationData> {
+            const inputs: number[] = [];
+            const outputs: number[][] = [];
+            const keyFrames = animation.getKeys();
+            const minMaxKeyFrames = _GLTFAnimation.calculateMinMaxKeyFrames(keyFrames);
+            const interpolationOrBake = _GLTFAnimation._DeduceInterpolation(keyFrames, animationChannelTargetPath, useQuaternion);
+            const frameDelta = minMaxKeyFrames.max - minMaxKeyFrames.min;
+
+            const interpolation = interpolationOrBake.interpolationType;
+            const shouldBakeAnimation = interpolationOrBake.shouldBakeAnimation;
+
+            if (shouldBakeAnimation) {
+                _GLTFAnimation._CreateBakedAnimation(babylonMesh, animation, animationChannelTargetPath, minMaxKeyFrames.min, minMaxKeyFrames.max, animation.framePerSecond, animationSampleRate, inputs, outputs, minMaxKeyFrames, convertToRightHandedSystem, useQuaternion);
+            }
+            else {
+                if (interpolation === AnimationSamplerInterpolation.LINEAR || interpolation === AnimationSamplerInterpolation.STEP) {
+                    _GLTFAnimation._CreateLinearOrStepAnimation(babylonMesh, animation, animationChannelTargetPath, frameDelta, inputs, outputs, convertToRightHandedSystem, useQuaternion);
+
+                }
+                else if (interpolation === AnimationSamplerInterpolation.CUBICSPLINE) {
+                    _GLTFAnimation._CreateCubicSplineAnimation(babylonMesh, animation, animationChannelTargetPath, frameDelta, inputs, outputs, convertToRightHandedSystem, useQuaternion);
+                }
+                else {
+                    Tools.Error('Unsupported animation interpolation type!');
+                }
+            }
+
+            if (inputs.length && outputs.length) {
+                const result: _IAnimationData = {
+                    inputs: inputs,
+                    outputs: outputs,
+                    samplerInterpolation: interpolation,
+                    inputsMin: shouldBakeAnimation ? minMaxKeyFrames.min : this.fround(minMaxKeyFrames.min / animation.framePerSecond),
+                    inputsMax: shouldBakeAnimation ? minMaxKeyFrames.max : this.fround(minMaxKeyFrames.max / animation.framePerSecond)
+                }
+
+                return result;
+            }
+
+            return null;
+        }
+
+        /**
+         * Math.fround is not supported in IE or PhantomJS, so this helper function is used instead
+         * @param num value to fround
+         */
+        private static fround(num: number) {
+            return new Float32Array([num])[0];
+        }
+
+        private static _DeduceAnimationInfo(animation: Animation): Nullable<_IAnimationInfo> {
+            let animationChannelTargetPath: AnimationChannelTargetPath | undefined;
+            let dataAccessorType = AccessorType.VEC3;
+            let useQuaternion: boolean = false;
+            let property = animation.targetProperty.split('.');
+            switch (property[0]) {
+                case 'scaling': {
+                    animationChannelTargetPath = AnimationChannelTargetPath.SCALE;
+                    break;
+                }
+                case 'position': {
+                    animationChannelTargetPath = AnimationChannelTargetPath.TRANSLATION;
+                    break;
+                }
+                case 'rotation': {
+                    dataAccessorType = AccessorType.VEC4;
+                    animationChannelTargetPath = AnimationChannelTargetPath.ROTATION;
+                    break;
+                }
+                case 'rotationQuaternion': {
+                    dataAccessorType = AccessorType.VEC4;
+                    useQuaternion = true;
+                    animationChannelTargetPath = AnimationChannelTargetPath.ROTATION;
+                    break;
+                }
+                default: {
+                    Tools.Error(`Unsupported animatable property ${property[0]}`);
+                }
+            }
+            if (animationChannelTargetPath) {
+                return { animationChannelTargetPath: animationChannelTargetPath, dataAccessorType: dataAccessorType, useQuaternion: useQuaternion };
+            }
+            else {
+                Tools.Error('animation channel target path and data accessor type could be deduced');
+            }
+            return null;
+        }
+
+        /**
+         * @ignore
+         * 
+         * @param babylonMesh 
+         * @param runtimeGLTFAnimation 
+         * @param idleGLTFAnimations 
+         * @param nodeMap 
+         * @param nodes 
+         * @param binaryWriter 
+         * @param bufferViews 
+         * @param accessors 
+         * @param convertToRightHandedSystem 
+         */
+        public static _CreateNodeAnimationFromMeshAnimations(babylonMesh: Mesh, runtimeGLTFAnimation: IAnimation, idleGLTFAnimations: IAnimation[], nodeMap: { [key: number]: number }, nodes: INode[], binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystem: boolean, animationSampleRate: number) {
+            let glTFAnimation: IAnimation;
+            if (babylonMesh.animations) {
+                babylonMesh.animations.forEach(function (animation) {
+                    let animationInfo = _GLTFAnimation._DeduceAnimationInfo(animation);
+                    if (animationInfo) {
+                        glTFAnimation = {
+                            name: animation.name,
+                            samplers: [],
+                            channels: []
+                        }
+                        _GLTFAnimation.AddAnimation(`${animation.name}`,
+                            animation.hasRunningRuntimeAnimations ? runtimeGLTFAnimation : glTFAnimation,
+                            babylonMesh,
+                            animation,
+                            animationInfo.dataAccessorType,
+                            animationInfo.animationChannelTargetPath,
+                            nodeMap,
+                            binaryWriter,
+                            bufferViews,
+                            accessors,
+                            convertToRightHandedSystem,
+                            animationInfo.useQuaternion,
+                            animationSampleRate
+                        );
+                        if (glTFAnimation.samplers.length && glTFAnimation.channels.length) {
+                            idleGLTFAnimations.push(glTFAnimation);
+                        }
+                    }
+                });
+            }
+        }
+
+        /**
+         * @ignore
+         * 
+         * @param babylonScene 
+         * @param glTFAnimations 
+         * @param nodeMap 
+         * @param nodes 
+         * @param binaryWriter 
+         * @param bufferViews 
+         * @param accessors 
+         * @param convertToRightHandedSystem 
+         */
+        public static _CreateNodeAnimationFromAnimationGroups(babylonScene: Scene, glTFAnimations: IAnimation[], nodeMap: { [key: number]: number }, nodes: INode[], binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystem: boolean, animationSampleRate: number) {
+            let glTFAnimation: IAnimation;
+            if (babylonScene.animationGroups) {
+                let animationGroups = babylonScene.animationGroups;
+
+                animationGroups.forEach(function (animationGroup) {
+                    glTFAnimation = {
+                        name: animationGroup.name,
+                        channels: [],
+                        samplers: []
+                    }
+                    animationGroup.targetedAnimations.forEach(function (targetAnimation) {
+                        let target = targetAnimation.target;
+                        let animation = targetAnimation.animation;
+                        if (target instanceof Mesh) {
+                            let animationInfo = _GLTFAnimation._DeduceAnimationInfo(targetAnimation.animation);
+                            if (animationInfo) {
+                                let babylonMesh = target as Mesh;
+                                _GLTFAnimation.AddAnimation(`${animation.name}`,
+                                    glTFAnimation,
+                                    babylonMesh,
+                                    animation,
+                                    animationInfo.dataAccessorType,
+                                    animationInfo.animationChannelTargetPath,
+                                    nodeMap,
+                                    binaryWriter,
+                                    bufferViews,
+                                    accessors,
+                                    convertToRightHandedSystem,
+                                    animationInfo.useQuaternion,
+                                    animationSampleRate
+                                );
+                            }
+                        }
+                    });
+                    if (glTFAnimation.channels.length && glTFAnimation.samplers.length) {
+                        glTFAnimations.push(glTFAnimation);
+                    }
+                });
+            }
+        }
+
+        private static AddAnimation(name: string, glTFAnimation: IAnimation, babylonMesh: Mesh, animation: Animation, dataAccessorType: AccessorType, animationChannelTargetPath: AnimationChannelTargetPath, nodeMap: { [key: number]: number }, binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystem: boolean, useQuaternion: boolean, animationSampleRate: number) {
+            let animationData = _GLTFAnimation._CreateNodeAnimation(babylonMesh, animation, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion, animationSampleRate);
+            let bufferView: IBufferView;
+            let accessor: IAccessor;
+            let keyframeAccessorIndex: number;
+            let dataAccessorIndex: number;
+            let outputLength: number;
+            let animationSampler: IAnimationSampler;
+            let animationChannel: IAnimationChannel;
+
+            if (animationData) {
+                let nodeIndex = nodeMap[babylonMesh.uniqueId];
+
+                // Creates buffer view and accessor for key frames.
+                let byteLength = animationData.inputs.length * 4;
+                bufferView = _GLTFUtilities.CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, undefined, `${name}  keyframe data view`);
+                bufferViews.push(bufferView);
+
+                animationData.inputs.forEach(function (input) {
+                    binaryWriter.setFloat32(input);
+                });
+
+                accessor = _GLTFUtilities.CreateAccessor(bufferViews.length - 1, `${name}  keyframes`, AccessorType.SCALAR, AccessorComponentType.FLOAT, animationData.inputs.length, null, [animationData.inputsMin], [animationData.inputsMax]);
+                accessors.push(accessor);
+                keyframeAccessorIndex = accessors.length - 1;
+
+                // create bufferview and accessor for keyed values.
+                outputLength = animationData.outputs.length;
+                byteLength = dataAccessorType === AccessorType.VEC3 ? animationData.outputs.length * 12 : animationData.outputs.length * 16;
+
+                // check for in and out tangents
+                bufferView = _GLTFUtilities.CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, undefined, `${name}  data view`);
+                bufferViews.push(bufferView);
+
+                animationData.outputs.forEach(function (output) {
+                    output.forEach(function (entry) {
+                        binaryWriter.setFloat32(entry);
+                    });
+                });
+
+                accessor = _GLTFUtilities.CreateAccessor(bufferViews.length - 1, `${name}  data`, dataAccessorType, AccessorComponentType.FLOAT, outputLength, null, null, null);
+                accessors.push(accessor);
+                dataAccessorIndex = accessors.length - 1;
+
+                // create sampler
+                animationSampler = {
+                    interpolation: animationData.samplerInterpolation,
+                    input: keyframeAccessorIndex,
+                    output: dataAccessorIndex
+                }
+                glTFAnimation.samplers.push(animationSampler);
+
+                // create channel
+                animationChannel = {
+                    sampler: glTFAnimation.samplers.length - 1,
+                    target: {
+                        node: nodeIndex,
+                        path: animationChannelTargetPath
+                    }
+                }
+                glTFAnimation.channels.push(animationChannel);
+            }
+        }
+
+        /**
+         * Create a baked animation
+         * @param babylonMesh BabylonJS mesh
+         * @param animation BabylonJS animation corresponding to the BabylonJS mesh
+         * @param animationChannelTargetPath animation target channel
+         * @param minFrame minimum animation frame
+         * @param maxFrame maximum animation frame
+         * @param fps frames per second of the animation
+         * @param inputs input key frames of the animation
+         * @param outputs output key frame data of the animation
+         * @param convertToRightHandedSystem converts the values to right-handed
+         * @param useQuaternion specifies if quaternions should be used
+         */
+        private static _CreateBakedAnimation(babylonMesh: Mesh, animation: Animation, animationChannelTargetPath: AnimationChannelTargetPath, minFrame: number, maxFrame: number, fps: number, sampleRate: number, inputs: number[], outputs: number[][], minMaxFrames: { min: number, max: number }, convertToRightHandedSystem: boolean, useQuaternion: boolean) {
+            let value: Vector3 | Quaternion;
+            let quaternionCache: Quaternion = Quaternion.Identity();
+            let previousTime: Nullable<number> = null;
+            const frameDelta = maxFrame - minFrame;
+            let time: number;
+            let maxUsedFrame: Nullable<number> = null;
+            let currKeyFrame: Nullable<IAnimationKey> = null;
+            let nextKeyFrame: Nullable<IAnimationKey> = null;
+            let prevKeyFrame: Nullable<IAnimationKey> = null;
+            let endFrame: Nullable<number> = null;
+            minMaxFrames.min = this.fround(minFrame / fps);
+
+            if (frameDelta / fps > sampleRate) {
+                let keyFrames = animation.getKeys();
+                for (let i = 0, length = keyFrames.length; i < length; ++i) {
+                    endFrame = null;
+                    currKeyFrame = keyFrames[i];
+
+                    if (i + 1 < length) {
+                        nextKeyFrame = keyFrames[i + 1];
+                        if (_GLTFAnimation._ValuesAreEqual(currKeyFrame.value, nextKeyFrame.value)) {
+                            if (i === 0) { // set the first frame to itself
+                                endFrame = currKeyFrame.frame;
+                            }
+                            else {
+                                continue;
+                            }
+                        }
+                        else {
+                            endFrame = nextKeyFrame.frame;
+                        }
+                    }
+                    else {
+                        // at the last key frame
+                        prevKeyFrame = keyFrames[i - 1];
+                        if (_GLTFAnimation._ValuesAreEqual(currKeyFrame.value, prevKeyFrame.value)) {
+                            continue;
+                        }
+                        else {
+                            endFrame = maxFrame;
+                        }
+                    }
+                    if (endFrame) {
+                        for (let f = currKeyFrame.frame; f <= endFrame; f += sampleRate) {
+                            time = this.fround(f / fps);
+                            if (time === previousTime) {
+                                continue;
+                            }
+                            previousTime = time;
+                            maxUsedFrame = time;
+                            value = animation._interpolate(f, 0, undefined, animation.loopMode);
+                            _GLTFAnimation._SetInterpolatedValue(value, time, animationChannelTargetPath, quaternionCache, inputs, outputs, convertToRightHandedSystem, useQuaternion);
+                        }
+                    }
+                }
+                if (maxUsedFrame) {
+                    minMaxFrames.max = maxUsedFrame;
+                }
+            }
+            else {
+                Tools.Warn('Frame range is too small to bake animation');
+            }
+        }
+
+        /**
+         * Checks if two values are equal to each other
+         * @param value1 First value as a Vector3 or Quaternion
+         * @param value2 Second value as a Vector3 or Quaternion
+         */
+        private static _ValuesAreEqual(value1: Vector3 | Quaternion, value2: Vector3 | Quaternion): boolean {
+            return (value1 instanceof Vector3) ? (value1 as Vector3).equals(value2 as Vector3) : (value1 as Quaternion).equals(value2 as Quaternion);
+        }
+
+        private static _SetInterpolatedValue(value: Vector3 | Quaternion, time: number, animationChannelTargetPath: AnimationChannelTargetPath, quaternionCache: Quaternion, inputs: number[], outputs: number[][], convertToRightHandedSystem: boolean, useQuaternion: boolean) {
+            inputs.push(time);
+            if (animationChannelTargetPath === AnimationChannelTargetPath.ROTATION) {
+                if (useQuaternion) {
+                    quaternionCache = value as Quaternion;
+                }
+                else {
+                    Quaternion.RotationYawPitchRollToRef(value.y, value.x, value.z, quaternionCache);
+                }
+                if (convertToRightHandedSystem) {
+                    quaternionCache.x *= -1;
+                    quaternionCache.y *= -1;
+                    outputs.push(quaternionCache.asArray());
+                }
+            }
+            else {
+                if (convertToRightHandedSystem && (animationChannelTargetPath !== AnimationChannelTargetPath.SCALE)) {
+                    value.z *= -1;
+                }
+
+                outputs.push(value.asArray());
+            }
+        }
+
+        /**
+         * Creates linear animation from the animation key frames
+         * @param babylonMesh BabylonJS mesh
+         * @param animation BabylonJS animation
+         * @param animationChannelTargetPath The target animation channel
+         * @param frameDelta The difference between the last and first frame of the animation
+         * @param inputs Array to store the key frame times
+         * @param outputs Array to store the key frame data
+         * @param convertToRightHandedSystem Specifies if the position data should be converted to right handed
+         * @param useQuaternion Specifies if quaternions are used in the animation
+         */
+        private static _CreateLinearOrStepAnimation(babylonMesh: Mesh, animation: Animation, animationChannelTargetPath: AnimationChannelTargetPath, frameDelta: number, inputs: number[], outputs: number[][], convertToRightHandedSystem: boolean, useQuaternion: boolean) {
+            animation.getKeys().forEach(function (keyFrame) {
+                inputs.push(keyFrame.frame / animation.framePerSecond); // keyframes in seconds.
+                let basePositionRotationOrScale = _GLTFAnimation._GetBasePositionRotationOrScale(babylonMesh, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion)
+                _GLTFAnimation._AddKeyframeValue(keyFrame, animation, outputs, animationChannelTargetPath, basePositionRotationOrScale, convertToRightHandedSystem, useQuaternion);
+            });
+        }
+
+        /**
+         * Creates cubic spline animation from the animation key frames
+         * @param babylonMesh BabylonJS mesh
+         * @param animation BabylonJS animation
+         * @param animationChannelTargetPath The target animation channel
+         * @param frameDelta The difference between the last and first frame of the animation
+         * @param inputs Array to store the key frame times
+         * @param outputs Array to store the key frame data
+         * @param convertToRightHandedSystem Specifies if the position data should be converted to right handed
+         * @param useQuaternion Specifies if quaternions are used in the animation
+         */
+        private static _CreateCubicSplineAnimation(babylonMesh: Mesh, animation: Animation, animationChannelTargetPath: AnimationChannelTargetPath, frameDelta: number, inputs: number[], outputs: number[][], convertToRightHandedSystem: boolean, useQuaternion: boolean) {
+            animation.getKeys().forEach(function (keyFrame) {
+                inputs.push(keyFrame.frame / animation.framePerSecond); // keyframes in seconds.
+                _GLTFAnimation.AddSplineTangent(
+                    _TangentType.INTANGENT,
+                    outputs,
+                    animationChannelTargetPath,
+                    AnimationSamplerInterpolation.CUBICSPLINE,
+                    keyFrame,
+                    frameDelta,
+                    useQuaternion,
+                    convertToRightHandedSystem
+                );
+                let basePositionRotationOrScale = _GLTFAnimation._GetBasePositionRotationOrScale(babylonMesh, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion)
+                _GLTFAnimation._AddKeyframeValue(keyFrame, animation, outputs, animationChannelTargetPath, basePositionRotationOrScale, convertToRightHandedSystem, useQuaternion);
+
+                _GLTFAnimation.AddSplineTangent(
+                    _TangentType.OUTTANGENT,
+                    outputs,
+                    animationChannelTargetPath,
+                    AnimationSamplerInterpolation.CUBICSPLINE,
+                    keyFrame,
+                    frameDelta,
+                    useQuaternion,
+                    convertToRightHandedSystem
+                );
+            });
+        }
+
+        private static _GetBasePositionRotationOrScale(babylonMesh: Mesh, animationChannelTargetPath: AnimationChannelTargetPath, convertToRightHandedSystem: boolean, useQuaternion: boolean) {
+            let basePositionRotationOrScale: number[];
+            if (animationChannelTargetPath === AnimationChannelTargetPath.ROTATION) {
+                if (useQuaternion) {
+                    if (babylonMesh.rotationQuaternion) {
+                        basePositionRotationOrScale = babylonMesh.rotationQuaternion.asArray();
+                        if (convertToRightHandedSystem) {
+                            basePositionRotationOrScale[0] *= -1;
+                            basePositionRotationOrScale[1] *= -1;
+                        }
+                    }
+                    else {
+                        basePositionRotationOrScale = BABYLON.Quaternion.Identity().asArray();
+                    }
+                }
+                else {
+                    basePositionRotationOrScale = babylonMesh.rotation.asArray();
+                    basePositionRotationOrScale[2] *= -1;
+                }
+            }
+            else if (animationChannelTargetPath === AnimationChannelTargetPath.TRANSLATION) {
+                basePositionRotationOrScale = babylonMesh.position.asArray();
+                if (convertToRightHandedSystem) {
+                    basePositionRotationOrScale[2] *= -1;
+                }
+            }
+            else { // scale
+                basePositionRotationOrScale = babylonMesh.scaling.asArray();
+            }
+            return basePositionRotationOrScale;
+        }
+
+        /**
+         * Adds a key frame value
+         * @param keyFrame 
+         * @param animation 
+         * @param outputs 
+         * @param animationChannelTargetPath 
+         * @param basePositionRotationOrScale 
+         * @param convertToRightHandedSystem 
+         * @param useQuaternion 
+         */
+        private static _AddKeyframeValue(keyFrame: IAnimationKey, animation: Animation, outputs: number[][], animationChannelTargetPath: AnimationChannelTargetPath, basePositionRotationOrScale: number[], convertToRightHandedSystem: boolean, useQuaternion: boolean) {
+            let value: number[];
+            let property: Nullable<RegExpMatchArray>;
+            let factor: number;
+            let componentName: string;
+            let newPositionRotationOrScale: Vector3 | Quaternion;
+            const animationType = animation.dataType;
+            if (animationType === Animation.ANIMATIONTYPE_VECTOR3) {
+                value = keyFrame.value.asArray();
+                if (convertToRightHandedSystem && !(animationChannelTargetPath === AnimationChannelTargetPath.SCALE)) {
+                    value[2] *= -1;
+                }
+                if (animationChannelTargetPath === AnimationChannelTargetPath.ROTATION) {
+                    outputs.push(Vector3.FromArray(value).toQuaternion().asArray());
+                }
+                else {
+                    outputs.push(value); // scale or position vector.
+                }
+            }
+            else if (animationType === Animation.ANIMATIONTYPE_FLOAT) { // handles single component x, y, z or w component animation by using a base property and animating over a component.
+                property = animation.targetProperty.split('.');
+                factor = keyFrame.value as number; // the value for the x, y or z component
+                componentName = property ? property[1] : ''; // x, y, or z component
+                newPositionRotationOrScale = useQuaternion ? BABYLON.Quaternion.FromArray(basePositionRotationOrScale).normalize() : BABYLON.Vector3.FromArray(basePositionRotationOrScale);
+
+                switch (componentName) {
+                    case 'x':
+                    case 'y': {
+                        newPositionRotationOrScale[componentName] = (convertToRightHandedSystem && useQuaternion) ? -factor : factor;
+                        break;
+                    }
+                    case 'z': {
+                        newPositionRotationOrScale[componentName] = (convertToRightHandedSystem && !useQuaternion && !(animationChannelTargetPath === AnimationChannelTargetPath.SCALE)) ? -factor : factor;
+                        break;
+                    }
+                    case 'w': {
+                        (newPositionRotationOrScale as Quaternion).w = factor;
+                        break;
+                    }
+                    default: {
+                        throw new Error(`glTFAnimation: Unsupported component type "${componentName}" for scale animation!`);
+                    }
+                }
+
+                if (animationChannelTargetPath === AnimationChannelTargetPath.ROTATION) {
+                    useQuaternion ? outputs.push(newPositionRotationOrScale.normalize().asArray()) : outputs.push((newPositionRotationOrScale as Vector3).toQuaternion().normalize().asArray());
+                }
+                else {
+                    outputs.push(newPositionRotationOrScale.asArray());
+                }
+            }
+            else if (animationType === Animation.ANIMATIONTYPE_QUATERNION) {
+                value = (keyFrame.value as Quaternion).normalize().asArray();
+
+                if (convertToRightHandedSystem) {
+                    value[0] *= -1;
+                    value[1] *= -1;
+                }
+                outputs.push(value);
+            }
+            else {
+                throw new Error('glTFAnimation: Unsupported key frame values for animation!');
+            }
+        }
+
+        private static _DeduceInterpolation(keyFrames: IAnimationKey[], animationChannelTargetPath: AnimationChannelTargetPath, useQuaternion: boolean): { interpolationType: AnimationSamplerInterpolation, shouldBakeAnimation: boolean } {
+            let interpolationType: AnimationSamplerInterpolation | undefined;
+            let shouldBakeAnimation = false;
+            let key: IAnimationKey;
+
+            if (animationChannelTargetPath === AnimationChannelTargetPath.ROTATION && !useQuaternion) {
+                return { interpolationType: AnimationSamplerInterpolation.LINEAR, shouldBakeAnimation: true };
+            }
+
+            for (let i = 0, length = keyFrames.length; i < length; ++i) {
+                key = keyFrames[i];
+                if (key.inTangent || key.outTangent) {
+                    if (interpolationType) {
+                        if (interpolationType !== AnimationSamplerInterpolation.CUBICSPLINE) {
+                            interpolationType = AnimationSamplerInterpolation.LINEAR;
+                            shouldBakeAnimation = true;
+                            break;
+                        }
+                    }
+                    else {
+                        interpolationType = AnimationSamplerInterpolation.CUBICSPLINE;
+                    }
+                }
+                else {
+                    if (interpolationType) {
+                        if (interpolationType === AnimationSamplerInterpolation.CUBICSPLINE || 
+                            (key.interpolation && (key.interpolation === AnimationKeyInterpolation.STEP) && interpolationType !== AnimationSamplerInterpolation.STEP)) {
+                            interpolationType = AnimationSamplerInterpolation.LINEAR;
+                            shouldBakeAnimation = true;
+                            break;
+                        }
+                    }
+                    else {
+                        if (key.interpolation && (key.interpolation === AnimationKeyInterpolation.STEP)) {
+                            interpolationType = AnimationSamplerInterpolation.STEP;
+                        }
+                        else {
+                            interpolationType = AnimationSamplerInterpolation.LINEAR;
+                        }
+                    }
+                }
+            }
+            if (!interpolationType) {
+                interpolationType = AnimationSamplerInterpolation.LINEAR;
+            }
+
+            return { interpolationType: interpolationType, shouldBakeAnimation: shouldBakeAnimation };
+        }
+
+        /**
+         * Adds an input tangent or output tangent to the output data
+         * If an input tangent or output tangent is missing, it uses the zero vector or zero quaternion
+         * @param tangentType Specifies which type of tangent to handle (inTangent or outTangent)
+         * @param outputs The animation data by keyframe
+         * @param animationChannelTargetPath The target animation channel
+         * @param interpolation The interpolation type
+         * @param keyFrame The key frame with the animation data
+         * @param frameDelta Time difference between two frames used to scale the tangent by the frame delta
+         * @param useQuaternion Specifies if quaternions are used
+         * @param convertToRightHandedSystem Specifies if the values should be converted to right-handed
+         */
+        private static AddSplineTangent(tangentType: _TangentType, outputs: number[][], animationChannelTargetPath: AnimationChannelTargetPath, interpolation: AnimationSamplerInterpolation, keyFrame: IAnimationKey, frameDelta: number, useQuaternion: boolean, convertToRightHandedSystem: boolean) {
+            let tangent: number[];
+            let tangentValue: Vector3 | Quaternion = tangentType === _TangentType.INTANGENT ? keyFrame.inTangent : keyFrame.outTangent;
+            if (interpolation === AnimationSamplerInterpolation.CUBICSPLINE) {
+                if (animationChannelTargetPath === AnimationChannelTargetPath.ROTATION) {
+                    if (tangentValue) {
+                        tangent = useQuaternion ? (tangentValue as Quaternion).scale(frameDelta).asArray() : (tangentValue as Vector3).scale(frameDelta).toQuaternion().asArray();
+                        if (convertToRightHandedSystem) {
+                            tangent[0] *= -1;
+                            tangent[1] *= -1;
+                        }
+                    }
+                    else {
+                        tangent = [0, 0, 0, 0];
+                    }
+                }
+                else {
+                    if (tangentValue) {
+                        tangent = (tangentValue as Vector3).scale(frameDelta).asArray();
+                        if (convertToRightHandedSystem) {
+                            tangent[2] *= -1;
+                        }
+                    }
+                    else {
+                        tangent = [0, 0, 0];
+                    }
+                }
+
+                outputs.push(tangent);
+            }
+        }
+
+        /**
+         * Get the minimum and maximum key frames' frame values
+         * @param keyFrames animation key frames
+         * @returns the minimum and maximum key frame value
+         */
+        private static calculateMinMaxKeyFrames(keyFrames: IAnimationKey[]): { min: number, max: number } {
+            let min: number = Infinity;
+            let max: number = -Infinity;
+            keyFrames.forEach(function (keyFrame) {
+                min = Math.min(min, keyFrame.frame);
+                max = Math.max(max, keyFrame.frame);
+            });
+
+            return { min: min, max: max };
+
+        }
+    }
+}

+ 8 - 8
serializers/src/glTF/2.0/babylon.glTFData.ts

@@ -4,28 +4,28 @@ module BABYLON {
     /**
      * Class for holding and downloading glTF file data
      */
-    export class _GLTFData {
+    export class GLTFData {
         /**
-         * Object which contains the file name as the key and its data as the value.
+         * Object which contains the file name as the key and its data as the value
          */
         glTFFiles: { [fileName: string]: string | Blob };
 
         /**
-         * Initializes the glTF file object.
+         * Initializes the glTF file object
          */
         public constructor() {
             this.glTFFiles = {};
         }
 
         /**
-         * Downloads the glTF data as files based on their names and data.
+         * Downloads the glTF data as files based on their names and data
          */
         public downloadFiles(): void {
             /**
-            * Checks for a matching suffix at the end of a string (for ES5 and lower).
-            * @param str - Source string.
-            * @param suffix - Suffix to search for in the source string.
-            * @returns - Boolean indicating whether the suffix was found (true) or not (false).
+            * Checks for a matching suffix at the end of a string (for ES5 and lower)
+            * @param str Source string
+            * @param suffix Suffix to search for in the source string
+            * @returns Boolean indicating whether the suffix was found (true) or not (false)
             */
             function endsWith(str: string, suffix: string): boolean {
                 return str.indexOf(suffix, str.length - suffix.length) !== -1;

File diff suppressed because it is too large
+ 415 - 389
serializers/src/glTF/2.0/babylon.glTFExporter.ts


+ 167 - 141
serializers/src/glTF/2.0/babylon.glTFMaterial.ts

@@ -2,63 +2,66 @@
 
 module BABYLON.GLTF2 {
     /** 
-     * Interface for storing specular glossiness factors.
+     * Interface for storing specular glossiness factors
      * @hidden
      */
+
     interface _IPBRSpecularGlossiness {
         /** 
-         * Represents the linear diffuse factors of the material.
+         * Represents the linear diffuse factors of the material
         */
         diffuseColor: BABYLON.Color3;
         /** 
-         * Represents the linear specular factors of the material.
+         * Represents the linear specular factors of the material
         */
         specularColor: BABYLON.Color3;
         /** 
-         * Represents the smoothness of the material.
+         * Represents the smoothness of the material
         */
         glossiness: number;
     }
 
     /** 
-     * Interface for storing metallic roughness factors.
+     * Interface for storing metallic roughness factors
      * @hidden
      */
+
     interface _IPBRMetallicRoughness {
         /** 
-         * Represents the albedo color of the material.
+         * Represents the albedo color of the material
         */
         baseColor: BABYLON.Color3;
         /** 
-         * Represents the metallness of the material.
+         * Represents the metallness of the material
         */
         metallic: number;
         /** 
-         * Represents the roughness of the material.
+         * Represents the roughness of the material
         */
         roughness: number;
         /** 
-         * The metallic roughness texture as a base64 string.
+         * The metallic roughness texture as a base64 string
         */
         metallicRoughnessTextureBase64?: Nullable<string>;
         /** 
-         * The base color texture as a base64 string.
+         * The base color texture as a base64 string
         */
         baseColorTextureBase64?: Nullable<string>;
     }
 
     /**
-     * Utility methods for working with glTF material conversion properties.  This class should only be used internally.
+     * Utility methods for working with glTF material conversion properties.  This class should only be used internally
      * @hidden
+
      */
     export class _GLTFMaterial {
         /**
-         * Represents the dielectric specular values for R, G and B.
+         * Represents the dielectric specular values for R, G and B
          */
         private static readonly _dielectricSpecular: Color3 = new Color3(0.04, 0.04, 0.04);
 
         /**
-         * Allows the maximum specular power to be defined for material calculations.
+         * Allows the maximum specular power to be defined for material calculations
          */
         private static _maxSpecularPower = 1024;
 
@@ -68,10 +71,10 @@ module BABYLON.GLTF2 {
         private static _epsilon = 1e-6;
 
         /**
-         * Specifies if two colors are approximately equal in value.
-         * @param color1 - first color to compare to.
-         * @param color2 - second color to compare to.
-         * @param epsilon - threshold value
+         * Specifies if two colors are approximately equal in value
+         * @param color1 first color to compare to
+         * @param color2 second color to compare to
+         * @param epsilon threshold value
          */
         private static FuzzyEquals(color1: Color3, color2: Color3, epsilon: number): boolean {
             return Scalar.WithinEpsilon(color1.r, color2.r, epsilon) &&
@@ -80,14 +83,16 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Gets the materials from a Babylon scene and converts them to glTF materials.
-         * @param scene - babylonjs scene.
-         * @param mimeType - texture mime type.
-         * @param images - array of images.
-         * @param textures - array of textures.
-         * @param materials - array of materials.
-         * @param imageData - mapping of texture names to base64 textures
-         * @param hasTextureCoords - specifies if texture coordinates are present on the material.
+         * @ignore
+         * 
+         * Gets the materials from a Babylon scene and converts them to glTF materials
+         * @param scene babylonjs scene
+         * @param mimeType texture mime type
+         * @param images array of images
+         * @param textures array of textures
+         * @param materials array of materials
+         * @param imageData mapping of texture names to base64 textures
+         * @param hasTextureCoords specifies if texture coordinates are present on the material
          */
         public static _ConvertMaterialsToGLTF(babylonMaterials: Material[], mimeType: ImageMimeType, images: IImage[], textures: ITexture[], materials: IMaterial[], imageData: { [fileName: string]: { data: Uint8Array, mimeType: ImageMimeType } }, hasTextureCoords: boolean) {
             for (let i = 0; i < babylonMaterials.length; ++i) {
@@ -108,8 +113,10 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Makes a copy of the glTF material without the texture parameters.
-         * @param originalMaterial - original glTF material.
+         * @ignore
+         * 
+         * Makes a copy of the glTF material without the texture parameters
+         * @param originalMaterial original glTF material
          * @returns glTF material without texture parameters
          */
         public static _StripTexturesFromMaterial(originalMaterial: IMaterial): IMaterial {
@@ -132,8 +139,10 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Specifies if the material has any texture parameters present.
-         * @param material - glTF Material.
+         * @ignore
+         * 
+         * Specifies if the material has any texture parameters present
+         * @param material glTF Material
          * @returns boolean specifying if texture parameters are present
          */
         public static _HasTexturesPresent(material: IMaterial): boolean {
@@ -151,9 +160,11 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Converts a Babylon StandardMaterial to a glTF Metallic Roughness Material.
+         * @ignore
+         * 
+         * Converts a Babylon StandardMaterial to a glTF Metallic Roughness Material
          * @param babylonStandardMaterial 
-         * @returns - glTF Metallic Roughness Material representation
+         * @returns glTF Metallic Roughness Material representation
          */
         public static _ConvertToGLTFPBRMetallicRoughness(babylonStandardMaterial: StandardMaterial): IMaterialPbrMetallicRoughness {
             const P0 = new BABYLON.Vector2(0, 1);
@@ -162,13 +173,13 @@ module BABYLON.GLTF2 {
             const P3 = new BABYLON.Vector2(1300, 0.1);
 
             /**
-             * Given the control points, solve for x based on a given t for a cubic bezier curve.
-             * @param t - a value between 0 and 1.
-             * @param p0 - first control point.
-             * @param p1 - second control point.
-             * @param p2 - third control point.
-             * @param p3 - fourth control point.
-             * @returns - number result of cubic bezier curve at the specified t.
+             * Given the control points, solve for x based on a given t for a cubic bezier curve
+             * @param t a value between 0 and 1
+             * @param p0 first control point
+             * @param p1 second control point
+             * @param p2 third control point
+             * @param p3 fourth control point
+             * @returns number result of cubic bezier curve at the specified t
              */
             function _cubicBezierCurve(t: number, p0: number, p1: number, p2: number, p3: number): number {
                 return (
@@ -182,9 +193,9 @@ module BABYLON.GLTF2 {
             /**
              * Evaluates a specified specular power value to determine the appropriate roughness value, 
              * based on a pre-defined cubic bezier curve with specular on the abscissa axis (x-axis) 
-             * and roughness on the ordinant axis (y-axis).
-             * @param specularPower - specular power of standard material.
-             * @returns - Number representing the roughness value.
+             * and roughness on the ordinant axis (y-axis)
+             * @param specularPower specular power of standard material
+             * @returns Number representing the roughness value
              */
             function _solveForRoughness(specularPower: number): number {
                 var t = Math.pow(specularPower / P3.x, 0.333333);
@@ -212,11 +223,13 @@ module BABYLON.GLTF2 {
         }
 
         /**
+         * @ignore
+         * 
          * Computes the metallic factor
-         * @param diffuse - diffused value
-         * @param specular - specular value
-         * @param oneMinusSpecularStrength - one minus the specular strength
-         * @returns - metallic value
+         * @param diffuse diffused value
+         * @param specular specular value
+         * @param oneMinusSpecularStrength one minus the specular strength
+         * @returns metallic value
          */
         public static _SolveMetallic(diffuse: number, specular: number, oneMinusSpecularStrength: number): number {
             if (specular < _GLTFMaterial._dielectricSpecular.r) {
@@ -232,9 +245,11 @@ module BABYLON.GLTF2 {
         }
 
         /**
+         * @ignore
+         * 
          * Gets the glTF alpha mode from the Babylon Material
-         * @param babylonMaterial - Babylon Material
-         * @returns - The Babylon alpha mode value
+         * @param babylonMaterial Babylon Material
+         * @returns The Babylon alpha mode value
          */
         public static _GetAlphaMode(babylonMaterial: Material): MaterialAlphaMode {
             if (babylonMaterial instanceof StandardMaterial) {
@@ -298,17 +313,18 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Converts a Babylon Standard Material to a glTF Material.
-         * @param babylonStandardMaterial - BJS Standard Material.
-         * @param mimeType - mime type to use for the textures.
-         * @param images - array of glTF image interfaces.
-         * @param textures - array of glTF texture interfaces.
-         * @param materials - array of glTF material interfaces.
-         * @param imageData - map of image file name to data.
-         * @param hasTextureCoords - specifies if texture coordinates are present on the submesh to determine if textures should be applied.
+         * @ignore
+         * 
+         * Converts a Babylon Standard Material to a glTF Material
+         * @param babylonStandardMaterial BJS Standard Material
+         * @param mimeType mime type to use for the textures
+         * @param images array of glTF image interfaces
+         * @param textures array of glTF texture interfaces
+         * @param materials array of glTF material interfaces
+         * @param imageData map of image file name to data
+         * @param hasTextureCoords specifies if texture coordinates are present on the submesh to determine if textures should be applied
          */
         public static _ConvertStandardMaterial(babylonStandardMaterial: StandardMaterial, mimeType: ImageMimeType, images: IImage[], textures: ITexture[], materials: IMaterial[], imageData: { [fileName: string]: { data: Uint8Array, mimeType: ImageMimeType } }, hasTextureCoords: boolean) {
-            Tools.Warn(babylonStandardMaterial.name + ": Standard Material is currently not fully supported/implemented in glTF serializer");
             const glTFPbrMetallicRoughness = _GLTFMaterial._ConvertToGLTFPBRMetallicRoughness(babylonStandardMaterial);
 
             const glTFMaterial: IMaterial = { name: babylonStandardMaterial.name };
@@ -369,14 +385,16 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Converts a Babylon PBR Metallic Roughness Material to a glTF Material.
-         * @param babylonPBRMetalRoughMaterial - BJS PBR Metallic Roughness Material.
-         * @param mimeType - mime type to use for the textures.
-         * @param images - array of glTF image interfaces.
-         * @param textures - array of glTF texture interfaces.
-         * @param materials - array of glTF material interfaces.
-         * @param imageData - map of image file name to data.
-         * @param hasTextureCoords - specifies if texture coordinates are present on the submesh to determine if textures should be applied.
+         * @ignore
+         * 
+         * Converts a Babylon PBR Metallic Roughness Material to a glTF Material
+         * @param babylonPBRMetalRoughMaterial BJS PBR Metallic Roughness Material
+         * @param mimeType mime type to use for the textures
+         * @param images array of glTF image interfaces
+         * @param textures array of glTF texture interfaces
+         * @param materials array of glTF material interfaces
+         * @param imageData map of image file name to data
+         * @param hasTextureCoords specifies if texture coordinates are present on the submesh to determine if textures should be applied
          */
         public static _ConvertPBRMetallicRoughnessMaterial(babylonPBRMetalRoughMaterial: PBRMetallicRoughnessMaterial, mimeType: ImageMimeType, images: IImage[], textures: ITexture[], materials: IMaterial[], imageData: { [fileName: string]: { data: Uint8Array, mimeType: ImageMimeType } }, hasTextureCoords: boolean) {
             const glTFPbrMetallicRoughness: IMaterialPbrMetallicRoughness = {};
@@ -454,14 +472,14 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Converts an image typed array buffer to a base64 image.
-         * @param buffer - typed array buffer.
-         * @param width - width of the image.
-         * @param height - height of the image.
-         * @param mimeType - mimetype of the image.
-         * @returns - base64 image string.
+         * Converts an image typed array buffer to a base64 image
+         * @param buffer typed array buffer
+         * @param width width of the image
+         * @param height height of the image
+         * @param mimeType mimetype of the image
+         * @returns base64 image string
          */
-        private static _CreateBase64FromCanvas(buffer: Uint8ClampedArray, width: number, height: number, mimeType: ImageMimeType): string {
+        private static _CreateBase64FromCanvas(buffer: Uint8ClampedArray | Float32Array, width: number, height: number, mimeType: ImageMimeType): string {
             const imageCanvas = document.createElement('canvas');
             imageCanvas.id = "WriteCanvas";
             const ctx = imageCanvas.getContext('2d') as CanvasRenderingContext2D;
@@ -477,11 +495,11 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Generates a white texture based on the specified width and height.
-         * @param width - width of the texture in pixels.
-         * @param height - height of the texture in pixels.
-         * @param scene - babylonjs scene.
-         * @returns - white texture.
+         * Generates a white texture based on the specified width and height
+         * @param width width of the texture in pixels
+         * @param height height of the texture in pixels
+         * @param scene babylonjs scene
+         * @returns white texture
          */
         private static _CreateWhiteTexture(width: number, height: number, scene: Scene): Texture {
             const data = new Uint8Array(width * height * 4);
@@ -496,11 +514,11 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Resizes the two source textures to the same dimensions.  If a texture is null, a default white texture is generated.  If both textures are null, returns null.
-         * @param texture1 - first texture to resize.
-         * @param texture2 - second texture to resize.
-         * @param scene - babylonjs scene.
-         * @returns resized textures or null.
+         * Resizes the two source textures to the same dimensions.  If a texture is null, a default white texture is generated.  If both textures are null, returns null
+         * @param texture1 first texture to resize
+         * @param texture2 second texture to resize
+         * @param scene babylonjs scene
+         * @returns resized textures or null
          */
         private static _ResizeTexturesToSameDimensions(texture1: BaseTexture, texture2: BaseTexture, scene: Scene): { "texture1": BaseTexture, "texture2": BaseTexture } {
             let texture1Size = texture1 ? texture1.getSize() : { width: 0, height: 0 };
@@ -538,14 +556,14 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Convert Specular Glossiness Textures to Metallic Roughness.
+         * Convert Specular Glossiness Textures to Metallic Roughness
          * See link below for info on the material conversions from PBR Metallic/Roughness and Specular/Glossiness
          * @link https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness/examples/convert-between-workflows-bjs/js/babylon.pbrUtilities.js
-         * @param diffuseTexture - texture used to store diffuse information.
-         * @param specularGlossinessTexture - texture used to store specular and glossiness information.
-         * @param factors - specular glossiness material factors.
-         * @param mimeType - the mime type to use for the texture.
-         * @returns pbr metallic roughness interface or null.
+         * @param diffuseTexture texture used to store diffuse information
+         * @param specularGlossinessTexture texture used to store specular and glossiness information
+         * @param factors specular glossiness material factors
+         * @param mimeType the mime type to use for the texture
+         * @returns pbr metallic roughness interface or null
          */
         private static _ConvertSpecularGlossinessTexturesToMetallicRoughness(diffuseTexture: BaseTexture, specularGlossinessTexture: BaseTexture, factors: _IPBRSpecularGlossiness, mimeType: ImageMimeType): Nullable<_IPBRMetallicRoughness> {
             if (!(diffuseTexture || specularGlossinessTexture)) {
@@ -678,9 +696,9 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Converts specular glossiness material properties to metallic roughness.
-         * @param specularGlossiness - interface with specular glossiness material properties.
-         * @returns - interface with metallic roughness material properties.
+         * Converts specular glossiness material properties to metallic roughness
+         * @param specularGlossiness interface with specular glossiness material properties
+         * @returns interface with metallic roughness material properties
          */
         private static _ConvertSpecularGlossinessToMetallicRoughness(specularGlossiness: _IPBRSpecularGlossiness): _IPBRMetallicRoughness {
             const diffusePerceivedBrightness = _GLTFMaterial._GetPerceivedBrightness(specularGlossiness.diffuseColor);
@@ -702,9 +720,9 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Calculates the surface reflectance, independent of lighting conditions.
-         * @param color - Color source to calculate brightness from.
-         * @returns number representing the perceived brightness, or zero if color is undefined. 
+         * Calculates the surface reflectance, independent of lighting conditions
+         * @param color Color source to calculate brightness from
+         * @returns number representing the perceived brightness, or zero if color is undefined
          */
         private static _GetPerceivedBrightness(color: Color3): number {
             if (color) {
@@ -714,9 +732,9 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Returns the maximum color component value.
+         * Returns the maximum color component value
          * @param color 
-         * @returns maximum color component value, or zero if color is null or undefined.
+         * @returns maximum color component value, or zero if color is null or undefined
          */
         private static _GetMaxComponent(color: Color3): number {
             if (color) {
@@ -726,15 +744,15 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Convert a PBRMaterial (Metallic/Roughness) to Metallic Roughness factors.
-         * @param babylonPBRMaterial - BJS PBR Metallic Roughness Material.
-         * @param mimeType - mime type to use for the textures.
-         * @param images - array of glTF image interfaces.
-         * @param textures - array of glTF texture interfaces.
-         * @param glTFPbrMetallicRoughness - glTF PBR Metallic Roughness interface.
-         * @param imageData - map of image file name to data.
-         * @param hasTextureCoords - specifies if texture coordinates are present on the submesh to determine if textures should be applied.
-         * @returns - glTF PBR Metallic Roughness factors.
+         * Convert a PBRMaterial (Metallic/Roughness) to Metallic Roughness factors
+         * @param babylonPBRMaterial BJS PBR Metallic Roughness Material
+         * @param mimeType mime type to use for the textures
+         * @param images array of glTF image interfaces
+         * @param textures array of glTF texture interfaces
+         * @param glTFPbrMetallicRoughness glTF PBR Metallic Roughness interface
+         * @param imageData map of image file name to data
+         * @param hasTextureCoords specifies if texture coordinates are present on the submesh to determine if textures should be applied
+         * @returns glTF PBR Metallic Roughness factors
          */
         private static _ConvertMetalRoughFactorsToMetallicRoughness(babylonPBRMaterial: PBRMaterial, mimeType: ImageMimeType, images: IImage[], textures: ITexture[], glTFPbrMetallicRoughness: IMaterialPbrMetallicRoughness, imageData: { [fileName: string]: { data: Uint8Array, mimeType: ImageMimeType } }, hasTextureCoords: boolean): _IPBRMetallicRoughness {
             const metallicRoughness = {
@@ -761,15 +779,15 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Convert a PBRMaterial (Specular/Glossiness) to Metallic Roughness factors.
-         * @param babylonPBRMaterial - BJS PBR Metallic Roughness Material.
-         * @param mimeType - mime type to use for the textures.
-         * @param images - array of glTF image interfaces.
-         * @param textures - array of glTF texture interfaces.
-         * @param glTFPbrMetallicRoughness - glTF PBR Metallic Roughness interface.
-         * @param imageData - map of image file name to data.
-         * @param hasTextureCoords - specifies if texture coordinates are present on the submesh to determine if textures should be applied.
-         * @returns - glTF PBR Metallic Roughness factors.
+         * Convert a PBRMaterial (Specular/Glossiness) to Metallic Roughness factors
+         * @param babylonPBRMaterial BJS PBR Metallic Roughness Material
+         * @param mimeType mime type to use for the textures
+         * @param images array of glTF image interfaces
+         * @param textures array of glTF texture interfaces
+         * @param glTFPbrMetallicRoughness glTF PBR Metallic Roughness interface
+         * @param imageData map of image file name to data
+         * @param hasTextureCoords specifies if texture coordinates are present on the submesh to determine if textures should be applied
+         * @returns glTF PBR Metallic Roughness factors
          */
         private static _ConvertSpecGlossFactorsToMetallicRoughness(babylonPBRMaterial: PBRMaterial, mimeType: ImageMimeType, images: IImage[], textures: ITexture[], glTFPbrMetallicRoughness: IMaterialPbrMetallicRoughness, imageData: { [fileName: string]: { data: Uint8Array, mimeType: ImageMimeType } }, hasTextureCoords: boolean): _IPBRMetallicRoughness {
             const specGloss: _IPBRSpecularGlossiness = {
@@ -806,14 +824,16 @@ module BABYLON.GLTF2 {
         }
 
         /**
-         * Converts a Babylon PBR Metallic Roughness Material to a glTF Material.
-         * @param babylonPBRMaterial - BJS PBR Metallic Roughness Material.
-         * @param mimeType - mime type to use for the textures.
-         * @param images - array of glTF image interfaces.
-         * @param textures - array of glTF texture interfaces.
-         * @param materials - array of glTF material interfaces.
-         * @param imageData - map of image file name to data.
-         * @param hasTextureCoords - specifies if texture coordinates are present on the submesh to determine if textures should be applied.
+         * @ignore
+         * 
+         * Converts a Babylon PBR Metallic Roughness Material to a glTF Material
+         * @param babylonPBRMaterial BJS PBR Metallic Roughness Material
+         * @param mimeType mime type to use for the textures
+         * @param images array of glTF image interfaces
+         * @param textures array of glTF texture interfaces
+         * @param materials array of glTF material interfaces
+         * @param imageData map of image file name to data
+         * @param hasTextureCoords specifies if texture coordinates are present on the submesh to determine if textures should be applied
          */
         public static _ConvertPBRMaterial(babylonPBRMaterial: PBRMaterial, mimeType: ImageMimeType, images: IImage[], textures: ITexture[], materials: IMaterial[], imageData: { [fileName: string]: { data: Uint8Array, mimeType: ImageMimeType } }, hasTextureCoords: boolean) {
             const glTFPbrMetallicRoughness: IMaterialPbrMetallicRoughness = {};
@@ -898,14 +918,19 @@ module BABYLON.GLTF2 {
             materials.push(glTFMaterial);
         }
 
+        private static GetPixelsFromTexture(babylonTexture: Texture): Uint8Array | Float32Array {
+            let pixels = babylonTexture.textureType === Engine.TEXTURETYPE_UNSIGNED_INT ? babylonTexture.readPixels() as Uint8Array : babylonTexture.readPixels() as Float32Array;
+            return pixels;
+        }
+
         /**
-         * Extracts a texture from a Babylon texture into file data and glTF data.
-         * @param babylonTexture - Babylon texture to extract.
-         * @param mimeType - Mime Type of the babylonTexture.
-         * @param images - Array of glTF images.
-         * @param textures - Array of glTF textures.
-         * @param imageData - map of image file name and data.
-         * @return - glTF texture info, or null if the texture format is not supported.
+         * Extracts a texture from a Babylon texture into file data and glTF data
+         * @param babylonTexture Babylon texture to extract
+         * @param mimeType Mime Type of the babylonTexture
+         * @param images Array of glTF images
+         * @param textures Array of glTF textures
+         * @param imageData map of image file name and data
+         * @return glTF texture info, or null if the texture format is not supported
          */
         private static _ExportTexture(babylonTexture: BaseTexture, mimeType: ImageMimeType, images: IImage[], textures: ITexture[], imageData: { [fileName: string]: { data: Uint8Array, mimeType: ImageMimeType } }): Nullable<ITextureInfo> {
             let textureName = "texture_" + (textures.length - 1).toString();
@@ -932,24 +957,24 @@ module BABYLON.GLTF2 {
             textureName = baseFile + extension;
 
 
-            const pixels = babylonTexture.readPixels() as Uint8Array;
+            const pixels = _GLTFMaterial.GetPixelsFromTexture(babylonTexture as Texture);
 
             const size = babylonTexture.getSize();
 
             const base64Data = this._CreateBase64FromCanvas(pixels, size.width, size.height, mimeType);
-
+   
             return this._GetTextureInfoFromBase64(base64Data, textureName, mimeType, images, textures, imageData);
         }
 
         /**
-         * Builds a texture from base64 string.
-         * @param base64Texture - base64 texture string.
-         * @param textureName - Name to use for the texture.
-         * @param mimeType - image mime type for the texture.
-         * @param images - array of images.
-         * @param textures - array of textures.
-         * @param imageData - map of image data.
-         * @returns - glTF texture info, or null if the texture format is not supported.
+         * Builds a texture from base64 string
+         * @param base64Texture base64 texture string
+         * @param textureName Name to use for the texture
+         * @param mimeType image mime type for the texture
+         * @param images array of images
+         * @param textures array of textures
+         * @param imageData map of image data
+         * @returns glTF texture info, or null if the texture format is not supported
          */
         private static _GetTextureInfoFromBase64(base64Texture: string, textureName: string, mimeType: ImageMimeType, images: IImage[], textures: ITexture[], imageData: { [fileName: string]: { data: Uint8Array, mimeType: ImageMimeType } }): Nullable<ITextureInfo> {
             let textureInfo: Nullable<ITextureInfo> = null;
@@ -959,8 +984,9 @@ module BABYLON.GLTF2 {
             };
 
             const binStr = atob(base64Texture.split(',')[1]);
-            const arr = new Uint8Array(binStr.length);
-            for (let i = 0; i < binStr.length; ++i) {
+            let arrBuff = new ArrayBuffer(binStr.length);
+            const arr = new Uint8Array(arrBuff);
+            for (let i = 0, length = binStr.length; i < length; ++i) {
                 arr[i] = binStr.charCodeAt(i);
             }
             const imageValues = { data: arr, mimeType: mimeType };
@@ -970,7 +996,7 @@ module BABYLON.GLTF2 {
                 const glTFImage: IImage = {
                     uri: textureName
                 }
-                let foundIndex = -1;
+                let foundIndex: number = -1;
                 for (let i = 0; i < images.length; ++i) {
                     if (images[i].uri === textureName) {
                         foundIndex = i;

+ 19 - 16
serializers/src/glTF/2.0/babylon.glTFSerializer.ts

@@ -6,12 +6,15 @@ module BABYLON {
      */
     export interface IExporterOptions {
         /**
-         * Function which indicates whether a babylon mesh should be exported or not.
-         * @param mesh - source Babylon mesh. It is used to check whether it should be 
-         * exported to glTF or not.
+         * Function which indicates whether a babylon mesh should be exported or not
+         * @param mesh source Babylon mesh. It is used to check whether it should be exported to glTF or not
          * @returns boolean, which indicates whether the mesh should be exported (true) or not (false)
          */
         shouldExportMesh?(mesh: AbstractMesh): boolean;
+        /**
+         * The sample rate to bake animation curves
+         */
+        animationSampleRate?: number;
     };
 
     /**
@@ -19,14 +22,14 @@ module BABYLON {
      */
     export class GLTF2Export {
         /**
-         * Exports the geometry of the scene to .gltf file format.
-         * @param scene - Babylon scene with scene hierarchy information.
-         * @param filePrefix - File prefix to use when generating the glTF file.
-         * @param options - Exporter options.
-         * @returns - Returns an object with a .gltf file and associates texture names
-         * as keys and their data and paths as values.
+         * Exports the geometry of the scene to .gltf file format
+         * @param scene Babylon scene with scene hierarchy information
+         * @param filePrefix File prefix to use when generating the glTF file
+         * @param options Exporter options
+         * @returns Returns an object with a .gltf file and associates texture names
+         * as keys and their data and paths as values
          */
-        public static GLTF(scene: Scene, filePrefix: string, options?: IExporterOptions): _GLTFData {
+        public static GLTF(scene: Scene, filePrefix: string, options?: IExporterOptions): GLTFData {
             const glTFPrefix = filePrefix.replace(/\.[^/.]+$/, "");
             const gltfGenerator = new GLTF2._Exporter(scene, options);
             if (scene.isReady) {
@@ -38,13 +41,13 @@ module BABYLON {
         }
 
         /**
-         * Exports the geometry of the scene to .glb file format.
-         * @param scene - Babylon scene with scene hierarchy information.
-         * @param filePrefix - File prefix to use when generating glb file.
-         * @param options - Exporter options.
-         * @returns - Returns an object with a .glb filename as key and data as value
+         * Exports the geometry of the scene to .glb file format
+         * @param scene Babylon scene with scene hierarchy information
+         * @param filePrefix File prefix to use when generating glb file
+         * @param options Exporter options
+         * @returns Returns an object with a .glb filename as key and data as value
          */
-        public static GLB(scene: Scene, filePrefix: string, options?: IExporterOptions): _GLTFData {
+        public static GLB(scene: Scene, filePrefix: string, options?: IExporterOptions): GLTFData {
             const glTFPrefix = filePrefix.replace(/\.[^/.]+$/, "");        
             const gltfGenerator = new GLTF2._Exporter(scene, options);
             if (scene.isReady) {

+ 149 - 0
serializers/src/glTF/2.0/babylon.glTFUtilities.ts

@@ -0,0 +1,149 @@
+/// <reference path="../../../../dist/babylon.glTF2Interface.d.ts"/>
+
+module BABYLON.GLTF2 {
+    /**
+     * @hidden
+     */
+    export class _GLTFUtilities {
+        /**
+         * @ignore
+         * 
+         * Creates a buffer view based on the supplied arguments
+         * @param bufferIndex index value of the specified buffer
+         * @param byteOffset byte offset value
+         * @param byteLength byte length of the bufferView
+         * @param byteStride byte distance between conequential elements
+         * @param name name of the buffer view
+         * @returns bufferView for glTF
+         */
+        public static CreateBufferView(bufferIndex: number, byteOffset: number, byteLength: number, byteStride?: number, name?: string): IBufferView {
+            let bufferview: IBufferView = { buffer: bufferIndex, byteLength: byteLength };
+            if (byteOffset) {
+                bufferview.byteOffset = byteOffset;
+            }
+            if (name) {
+                bufferview.name = name;
+            }
+            if (byteStride) {
+                bufferview.byteStride = byteStride;
+            }
+
+            return bufferview;
+        }
+
+        /**
+         * @ignore
+         * 
+         * Creates an accessor based on the supplied arguments
+         * @param bufferviewIndex The index of the bufferview referenced by this accessor
+         * @param name The name of the accessor
+         * @param type The type of the accessor
+         * @param componentType The datatype of components in the attribute
+         * @param count The number of attributes referenced by this accessor
+         * @param byteOffset The offset relative to the start of the bufferView in bytes
+         * @param min Minimum value of each component in this attribute
+         * @param max Maximum value of each component in this attribute
+         * @returns accessor for glTF 
+         */
+        public static CreateAccessor(bufferviewIndex: number, name: string, type: AccessorType, componentType: AccessorComponentType, count: number, byteOffset: Nullable<number>, min: Nullable<number[]>, max: Nullable<number[]>): IAccessor {
+            let accessor: IAccessor = { name: name, bufferView: bufferviewIndex, componentType: componentType, count: count, type: type };
+
+            if (min) {
+                accessor.min = min;
+            }
+            if (max) {
+                accessor.max = max;
+            }
+            if (byteOffset) {
+                accessor.byteOffset = byteOffset;
+            }
+
+            return accessor;
+        }
+
+        /**
+         * @ignore
+         * 
+         * Calculates the minimum and maximum values of an array of position floats
+         * @param positions Positions array of a mesh
+         * @param vertexStart Starting vertex offset to calculate min and max values
+         * @param vertexCount Number of vertices to check for min and max values
+         * @returns min number array and max number array
+         */
+        public static CalculateMinMaxPositions(positions: FloatArray, vertexStart: number, vertexCount: number, convertToRightHandedSystem: boolean): { min: number[], max: number[] } {
+            const min = [Infinity, Infinity, Infinity];
+            const max = [-Infinity, -Infinity, -Infinity];
+            const positionStrideSize = 3;
+            let indexOffset: number;
+            let position: Vector3;
+            let vector: number[];
+
+            if (vertexCount) {
+                for (let i = vertexStart, length = vertexStart + vertexCount; i < length; ++i) {
+                    indexOffset = positionStrideSize * i;
+
+                    position = Vector3.FromArray(positions, indexOffset);
+                    if (convertToRightHandedSystem) {
+                        _GLTFUtilities.GetRightHandedVector3FromRef(position);
+                    }
+                    vector = position.asArray();
+
+                    for (let j = 0; j < positionStrideSize; ++j) {
+                        let num = vector[j];
+                        if (num < min[j]) {
+                            min[j] = num;
+                        }
+                        if (num > max[j]) {
+                            max[j] = num;
+                        }
+                        ++indexOffset;
+                    }
+                }
+            }
+            return { min, max };
+        }
+
+        /**
+         * @ignore
+         * 
+         * Converts a new right-handed Vector3
+         * @param vector vector3 array
+         * @returns right-handed Vector3
+         */
+        public static GetRightHandedVector3(vector: Vector3): Vector3 {
+            return new Vector3(vector.x, vector.y, -vector.z);
+        }
+
+        /**
+         * @ignore
+         * 
+         * Converts a Vector3 to right-handed
+         * @param vector Vector3 to convert to right-handed
+         */
+        public static GetRightHandedVector3FromRef(vector: Vector3) {
+            vector.z *= -1;
+        }
+
+        /**
+         * @ignore
+         * 
+         * Converts a Vector4 to right-handed
+         * @param vector Vector4 to convert to right-handed
+         */
+        public static GetRightHandedVector4FromRef(vector: Vector4) {
+            vector.z *= -1;
+            vector.w *= -1;
+        }
+
+        /**
+         * @ignore
+         * 
+         * Converts a Quaternion to right-handed
+         * @param quaternion Source quaternion to convert to right-handed
+         */
+        public static GetRightHandedQuaternionFromRef(quaternion: Quaternion) {
+            quaternion.x *= -1;
+            quaternion.y *= -1;
+        }
+    }
+}

+ 281 - 0
tests/unit/babylon/serializers/babylon.glTFSerializer.tests.ts

@@ -163,5 +163,286 @@ describe('Babylon glTF Serializer', () => {
                 done();
             });
         });
+        it('should serialize single component translation animation to glTF', (done) => {
+            mocha.timeout(10000);
+            const scene = new BABYLON.Scene(subject);
+            const box = BABYLON.Mesh.CreateBox('box', 1, scene);
+            let keys: BABYLON.IAnimationKey[] = [];
+            keys.push({
+            frame: 0,
+            value: 1
+            });
+            keys.push({
+            frame: 20,
+            value: 0.2
+            });
+            keys.push({
+            frame: 40,
+            value: 1
+            });
+            let animationBoxT = new BABYLON.Animation('boxAnimation_translation', 'position.y', 30, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
+            animationBoxT.setKeys(keys);
+            box.animations.push(animationBoxT);
+            scene.executeWhenReady(function () {
+            const glTFExporter = new BABYLON.GLTF2._Exporter(scene);
+            const glTFData = glTFExporter._generateGLTF('test');
+            const jsonString = glTFData.glTFFiles['test.gltf'] as string;
+            const jsonData = JSON.parse(jsonString);
+            jsonData.animations.length.should.be.equal(1);
+            const animation = jsonData.animations[0];
+            animation.channels.length.should.be.equal(1);
+            animation.channels[0].sampler.should.be.equal(0);
+            animation.channels[0].target.node.should.be.equal(0);
+            animation.channels[0].target.path.should.be.equal('translation');
+            jsonData.animations[0].samplers.length.should.be.equal(1);
+            // accessors, asset, buffers, bufferViews, meshes, nodes, scene, scenes, materials, animations
+            Object.keys(jsonData).length.should.be.equal(10);
+            // positions, normals, texture coords, indices, animation keyframe data, animation data
+            jsonData.accessors.length.should.be.equal(6);
+            // generator, version
+            Object.keys(jsonData.asset).length.should.be.equal(2);
+            jsonData.buffers.length.should.be.equal(1);
+            // positions, normals, texture coords, indices, animation keyframe data, animation data
+            jsonData.bufferViews.length.should.be.equal(6);
+            jsonData.meshes.length.should.be.equal(1);
+            jsonData.nodes.length.should.be.equal(1);
+            jsonData.scenes.length.should.be.equal(1);
+            jsonData.scene.should.be.equal(0);
+            done();
+            });
+            });
+            it('should serialize translation animation to glTF', (done) => {
+            mocha.timeout(10000);
+            const scene = new BABYLON.Scene(subject);
+            const box = BABYLON.Mesh.CreateBox('box', 1, scene);
+            let keys: BABYLON.IAnimationKey[] = [];
+            keys.push({
+            frame: 0,
+            value: new BABYLON.Vector3(0.1, 0.1, 0.1)
+            });
+            keys.push({
+            frame: 20,
+            value: BABYLON.Vector3.One()
+            });
+            keys.push({
+            frame: 40,
+            value: new BABYLON.Vector3(0.1, 0.1, 0.1)
+            });
+            let animationBoxT = new BABYLON.Animation('boxAnimation_translation', 'position', 30, BABYLON.Animation.ANIMATIONTYPE_VECTOR3, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
+            animationBoxT.setKeys(keys);
+            box.animations.push(animationBoxT);
+            scene.executeWhenReady(function () {
+            const glTFExporter = new BABYLON.GLTF2._Exporter(scene);
+            const glTFData = glTFExporter._generateGLTF('test');
+            const jsonString = glTFData.glTFFiles['test.gltf'] as string;
+            const jsonData = JSON.parse(jsonString);
+            jsonData.animations.length.should.be.equal(1);
+            const animation = jsonData.animations[0];
+            animation.channels.length.should.be.equal(1);
+            animation.channels[0].sampler.should.be.equal(0);
+            animation.channels[0].target.node.should.be.equal(0);
+            animation.channels[0].target.path.should.be.equal('translation');
+            animation.samplers.length.should.be.equal(1);
+            animation.samplers[0].interpolation.should.be.equal('LINEAR');
+            animation.samplers[0].input.should.be.equal(4);
+            animation.samplers[0].output.should.be.equal(5);
+            jsonData.animations[0].samplers.length.should.be.equal(1);
+            // accessors, asset, buffers, bufferViews, meshes, nodes, scene, scenes, materials, animations
+            Object.keys(jsonData).length.should.be.equal(10);
+            // positions, normals, texture coords, indices, animation keyframe data, animation data
+            jsonData.accessors.length.should.be.equal(6);
+            // generator, version
+            Object.keys(jsonData.asset).length.should.be.equal(2);
+            jsonData.buffers.length.should.be.equal(1);
+            // positions, normals, texture coords, indices, animation keyframe data, animation data
+            jsonData.bufferViews.length.should.be.equal(6);
+            jsonData.meshes.length.should.be.equal(1);
+            jsonData.nodes.length.should.be.equal(1);
+            jsonData.scenes.length.should.be.equal(1);
+            jsonData.scene.should.be.equal(0);
+            done();
+            });
+            });
+            it('should serialize scale animation to glTF', (done) => {
+            mocha.timeout(10000);
+            const scene = new BABYLON.Scene(subject);
+            const box = BABYLON.Mesh.CreateBox('box', 1, scene);
+            let keys: BABYLON.IAnimationKey[] = [];
+            keys.push({
+            frame: 0,
+            value: new BABYLON.Vector3(0.1, 0.1, 0.1)
+            });
+            keys.push({
+            frame: 20,
+            value: BABYLON.Vector3.One()
+            });
+            keys.push({
+            frame: 40,
+            value: new BABYLON.Vector3(0.1, 0.1, 0.1)
+            });
+            let animationBoxT = new BABYLON.Animation('boxAnimation_translation', 'scaling', 30, BABYLON.Animation.ANIMATIONTYPE_VECTOR3, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
+            animationBoxT.setKeys(keys);
+            box.animations.push(animationBoxT);
+            scene.executeWhenReady(function () {
+            const glTFExporter = new BABYLON.GLTF2._Exporter(scene);
+            const glTFData = glTFExporter._generateGLTF('test');
+            const jsonString = glTFData.glTFFiles['test.gltf'] as string;
+            const jsonData = JSON.parse(jsonString);
+            jsonData.animations.length.should.be.equal(1);
+            const animation = jsonData.animations[0];
+            animation.channels.length.should.be.equal(1);
+            animation.channels[0].sampler.should.be.equal(0);
+            animation.channels[0].target.node.should.be.equal(0);
+            animation.channels[0].target.path.should.be.equal('scale');
+            animation.samplers.length.should.be.equal(1);
+            animation.samplers[0].interpolation.should.be.equal('LINEAR');
+            animation.samplers[0].input.should.be.equal(4);
+            animation.samplers[0].output.should.be.equal(5);
+            jsonData.animations[0].samplers.length.should.be.equal(1);
+            // accessors, asset, buffers, bufferViews, meshes, nodes, scene, scenes, materials, animations
+            Object.keys(jsonData).length.should.be.equal(10);
+            // positions, normals, texture coords, indices, animation keyframe data, animation data
+            jsonData.accessors.length.should.be.equal(6);
+            // generator, version
+            Object.keys(jsonData.asset).length.should.be.equal(2);
+            jsonData.buffers.length.should.be.equal(1);
+            // positions, normals, texture coords, indices, animation keyframe data, animation data
+            jsonData.bufferViews.length.should.be.equal(6);
+            jsonData.meshes.length.should.be.equal(1);
+            jsonData.nodes.length.should.be.equal(1);
+            jsonData.scenes.length.should.be.equal(1);
+            jsonData.scene.should.be.equal(0);
+            done();
+            });
+            });
+            it('should serialize rotation quaternion animation to glTF', (done) => {
+            mocha.timeout(10000);
+            const scene = new BABYLON.Scene(subject);
+            const box = BABYLON.Mesh.CreateBox('box', 1, scene);
+            let keys: BABYLON.IAnimationKey[] = [];
+            keys.push({
+            frame: 0,
+            value: new BABYLON.Quaternion(0.707, 0.0, 0.0, 0.707)
+            });
+            keys.push({
+            frame: 20,
+            value: BABYLON.Quaternion.Identity()
+            });
+            keys.push({
+            frame: 40,
+            value: new BABYLON.Quaternion(0.707, 0.0, 0.0, 0.707)
+            });
+            let animationBoxT = new BABYLON.Animation('boxAnimation_translation', 'rotationQuaternion', 30, BABYLON.Animation.ANIMATIONTYPE_QUATERNION, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
+            animationBoxT.setKeys(keys);
+            box.animations.push(animationBoxT);
+            scene.executeWhenReady(function () {
+            const glTFExporter = new BABYLON.GLTF2._Exporter(scene);
+            const glTFData = glTFExporter._generateGLTF('test');
+            const jsonString = glTFData.glTFFiles['test.gltf'] as string;
+            const jsonData = JSON.parse(jsonString);
+            jsonData.animations.length.should.be.equal(1);
+            const animation = jsonData.animations[0];
+            animation.channels.length.should.be.equal(1);
+            animation.channels[0].sampler.should.be.equal(0);
+            animation.channels[0].target.node.should.be.equal(0);
+            animation.channels[0].target.path.should.be.equal('rotation');
+            animation.samplers.length.should.be.equal(1);
+            animation.samplers[0].interpolation.should.be.equal('LINEAR');
+            animation.samplers[0].input.should.be.equal(4);
+            animation.samplers[0].output.should.be.equal(5);
+            jsonData.animations[0].samplers.length.should.be.equal(1);
+            // accessors, asset, buffers, bufferViews, meshes, nodes, scene, scenes, materials, animations
+            Object.keys(jsonData).length.should.be.equal(10);
+            // positions, normals, texture coords, indices, animation keyframe data, animation data
+            jsonData.accessors.length.should.be.equal(6);
+            // generator, version
+            Object.keys(jsonData.asset).length.should.be.equal(2);
+            jsonData.buffers.length.should.be.equal(1);
+            // positions, normals, texture coords, indices, animation keyframe data, animation data
+            jsonData.bufferViews.length.should.be.equal(6);
+            jsonData.meshes.length.should.be.equal(1);
+            jsonData.nodes.length.should.be.equal(1);
+            jsonData.scenes.length.should.be.equal(1);
+            jsonData.scene.should.be.equal(0);
+            done();
+            });
+            });
+            it('should serialize combined animations to glTF', (done) => {
+            mocha.timeout(10000);
+            const scene = new BABYLON.Scene(subject);
+            const box = BABYLON.Mesh.CreateBox('box', 1, scene);
+            const rotationKeyFrames: BABYLON.IAnimationKey[] = [];
+            rotationKeyFrames.push({
+            frame: 0,
+            value: new BABYLON.Quaternion(0.707, 0.0, 0.0, 0.707)
+            });
+            rotationKeyFrames.push({
+            frame: 20,
+            value: BABYLON.Quaternion.Identity()
+            });
+            rotationKeyFrames.push({
+            frame: 40,
+            value: new BABYLON.Quaternion(0.707, 0.0, 0.0, 0.707)
+            });
+            const scaleKeyFrames: BABYLON.IAnimationKey[] = [];
+            scaleKeyFrames.push({
+            frame: 0,
+            value: new BABYLON.Vector3(0.1, 0.1, 0.1)
+            });
+            scaleKeyFrames.push({
+            frame: 20,
+            value: BABYLON.Vector3.One()
+            });
+            scaleKeyFrames.push({
+            frame: 40,
+            value: new BABYLON.Vector3(0.1, 0.1, 0.1)
+            });
+            let rotationAnimationBox = new BABYLON.Animation('boxAnimation_rotation', 'rotationQuaternion', 30, BABYLON.Animation.ANIMATIONTYPE_QUATERNION, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
+            rotationAnimationBox.setKeys(rotationKeyFrames);
+            box.animations.push(rotationAnimationBox);
+            let scaleAnimationBox = new BABYLON.Animation('boxAnimation_scale', 'scaling', 30, BABYLON.Animation.ANIMATIONTYPE_VECTOR3, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
+            scaleAnimationBox.setKeys(scaleKeyFrames);
+            box.animations.push(scaleAnimationBox);
+            scene.executeWhenReady(function () {
+            const glTFExporter = new BABYLON.GLTF2._Exporter(scene);
+            const glTFData = glTFExporter._generateGLTF('test');
+            const jsonString = glTFData.glTFFiles['test.gltf'] as string;
+            const jsonData = JSON.parse(jsonString);
+            jsonData.animations.length.should.be.equal(2);
+            
+            let animation = jsonData.animations[0];
+            animation.channels.length.should.be.equal(1);
+            animation.channels[0].sampler.should.be.equal(0);
+            animation.channels[0].target.node.should.be.equal(0);
+            animation.channels[0].target.path.should.be.equal('rotation');
+            animation.samplers.length.should.be.equal(1);
+            animation.samplers[0].interpolation.should.be.equal('LINEAR');
+            animation.samplers[0].input.should.be.equal(4);
+            animation.samplers[0].output.should.be.equal(5);
+            animation = jsonData.animations[1];
+            animation.channels[0].sampler.should.be.equal(0);
+            animation.channels[0].target.node.should.be.equal(0);
+            animation.channels[0].target.path.should.be.equal('scale');
+            animation.samplers.length.should.be.equal(1);
+            animation.samplers[0].interpolation.should.be.equal('LINEAR');
+            animation.samplers[0].input.should.be.equal(6);
+            animation.samplers[0].output.should.be.equal(7);
+            // accessors, asset, buffers, bufferViews, meshes, nodes, scene, scenes, materials, animations
+            Object.keys(jsonData).length.should.be.equal(10);
+            // positions, normals, texture coords, indices, rotation animation keyframe data, rotation animation data, scale animation keyframe data, scale animation data
+            jsonData.accessors.length.should.be.equal(8);
+            // generator, version
+            Object.keys(jsonData.asset).length.should.be.equal(2);
+            jsonData.buffers.length.should.be.equal(1);
+            // positions, normals, texture coords, indices, rotation animation keyframe data, rotation animation data, scale animation keyframe data, scale animation data 
+            jsonData.bufferViews.length.should.be.equal(8);
+            jsonData.meshes.length.should.be.equal(1);
+            jsonData.nodes.length.should.be.equal(1);
+            jsonData.scenes.length.should.be.equal(1);
+            jsonData.scene.should.be.equal(0);
+            done();
+            });
+            });
+            
     });
 });