瀏覽代碼

Add GLTF Serialization of morph targets and morph target animation (#8780)

* Initial implementation of morph target export, added accessor and buffer view creation for morph targets, added target json syntax

* initial approach of adding morph target position min max attributes

* Initial implementation of morph target export, added accessor and buffer view creation for morph targets, added target json syntax

* initial approach of adding morph target position min max attributes

* Fixes to morph target glTF generation, fix to tangent export logic, remove change to normal attribute export logic

* Adding implementation for morph target and morph target animation export, added logic for combination of a mesh's morph target manager's morph target animation frames for each target to comply with glTF spec. Added utility method for specifying glTF accesor type expected amount of elements.

* Formatting

* remove .orig

* Fix formatting in gltfAnimation.ts, add morph target round trip test

* what's new
Nicholas Barlow 5 年之前
父節點
當前提交
4a9c72df97

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

@@ -130,6 +130,7 @@
 
 - Added support for KHR_materials_unlit to glTF serializer ([Popov72](https://github.com/Popov72))
 - Added support for glTF Skins to glTF serializer ([Drigax](https://github.com/Drigax))
+- Added support for glTF Morph Target serialization ([Drigax](https://github.com/Drigax))
 
 ### Navigation
 

+ 233 - 35
serializers/src/glTF/2.0/glTFAnimation.ts

@@ -6,6 +6,8 @@ import { Tools } from "babylonjs/Misc/tools";
 import { Animation } from "babylonjs/Animations/animation";
 import { TransformNode } from "babylonjs/Meshes/transformNode";
 import { Scene } from "babylonjs/scene";
+import { MorphTarget } from "babylonjs/Morph/morphTarget";
+import { Mesh } from "babylonjs/Meshes/mesh";
 
 import { _BinaryWriter } from "./glTFExporter";
 import { _GLTFUtilities } from "./glTFUtilities";
@@ -49,7 +51,7 @@ export interface _IAnimationInfo {
     /**
      * The glTF accessor type for the data.
      */
-    dataAccessorType: AccessorType.VEC3 | AccessorType.VEC4;
+    dataAccessorType: AccessorType.VEC3 | AccessorType.VEC4 | AccessorType.SCALAR;
     /**
      * Specifies if quaternions should be used.
      */
@@ -153,6 +155,11 @@ export class _GLTFAnimation {
                 animationChannelTargetPath = AnimationChannelTargetPath.ROTATION;
                 break;
             }
+            case 'influence': {
+                dataAccessorType = AccessorType.SCALAR;
+                animationChannelTargetPath = AnimationChannelTargetPath.WEIGHTS;
+                break;
+            }
             default: {
                 Tools.Error(`Unsupported animatable property ${property[0]}`);
             }
@@ -216,8 +223,83 @@ export class _GLTFAnimation {
     }
 
     /**
+ * @ignore
+ * Create individual morph animations from the mesh's morph target animation tracks
+ * @param babylonNode
+ * @param runtimeGLTFAnimation
+ * @param idleGLTFAnimations
+ * @param nodeMap
+ * @param nodes
+ * @param binaryWriter
+ * @param bufferViews
+ * @param accessors
+ * @param convertToRightHandedSystem
+ * @param animationSampleRate
+ */
+    public static _CreateMorphTargetAnimationFromMorphTargets(babylonNode: Node, runtimeGLTFAnimation: IAnimation, idleGLTFAnimations: IAnimation[], nodeMap: { [key: number]: number }, nodes: INode[], binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystem: boolean, animationSampleRate: number) {
+        let glTFAnimation: IAnimation;
+        if (babylonNode instanceof Mesh) {
+            let morphTargetManager = babylonNode.morphTargetManager;
+            if (morphTargetManager) {
+                for (let i = 0; i < morphTargetManager.numTargets; ++i) {
+                    let morphTarget = morphTargetManager.getTarget(i);
+                    for (let j = 0; j < morphTarget.animations.length; ++j) {
+                        let animation = morphTarget.animations[j];
+                        let combinedAnimation = new Animation(`${animation.name}`,
+                            "influence",
+                            animation.framePerSecond,
+                            animation.dataType,
+                            animation.loopMode,
+                            animation.enableBlending);
+                        let combinedAnimationKeys: IAnimationKey[] = [];
+                        let animationKeys = animation.getKeys();
+
+                        for (let k = 0; k < animationKeys.length; ++k) {
+                            let animationKey = animationKeys[k];
+                            for (let l = 0; l < morphTargetManager.numTargets; ++l) {
+                                if (l == i) {
+                                    combinedAnimationKeys.push(animationKey);
+                                } else {
+                                    combinedAnimationKeys.push({ frame: animationKey.frame, value: morphTargetManager.getTarget(l).influence });
+                                }
+                            }
+                        }
+                        combinedAnimation.setKeys(combinedAnimationKeys);
+                        let animationInfo = _GLTFAnimation._DeduceAnimationInfo(combinedAnimation);
+                        if (animationInfo) {
+                            glTFAnimation = {
+                                name: combinedAnimation.name,
+                                samplers: [],
+                                channels: []
+                            };
+                            _GLTFAnimation.AddAnimation(`${animation.name}`,
+                                animation.hasRunningRuntimeAnimations ? runtimeGLTFAnimation : glTFAnimation,
+                                babylonNode,
+                                animation,
+                                animationInfo.dataAccessorType,
+                                animationInfo.animationChannelTargetPath,
+                                nodeMap,
+                                binaryWriter,
+                                bufferViews,
+                                accessors,
+                                convertToRightHandedSystem,
+                                animationInfo.useQuaternion,
+                                animationSampleRate,
+                                morphTargetManager.numTargets
+                            );
+                            if (glTFAnimation.samplers.length && glTFAnimation.channels.length) {
+                                idleGLTFAnimations.push(glTFAnimation);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
      * @ignore
-     * Create node animations from the animation groups
+     * Create node and morph animations from the animation groups
      * @param babylonScene
      * @param glTFAnimations
      * @param nodeMap
@@ -232,14 +314,18 @@ export class _GLTFAnimation {
         let glTFAnimation: IAnimation;
         if (babylonScene.animationGroups) {
             let animationGroups = babylonScene.animationGroups;
-
             for (let animationGroup of animationGroups) {
+                let morphAnimations: Map<Mesh, Map<MorphTarget, Animation>> = new Map();
+                let sampleAnimations: Map<Mesh, Animation> = new Map();
+                let morphAnimationMeshes: Set<Mesh> = new Set();
+                let animationGroupFrameDiff = animationGroup.to - animationGroup.from;
                 glTFAnimation = {
                     name: animationGroup.name,
                     channels: [],
                     samplers: []
                 };
-                for (let targetAnimation of animationGroup.targetedAnimations) {
+                for (let i = 0; i < animationGroup.targetedAnimations.length; ++i) {
+                    let targetAnimation = animationGroup.targetedAnimations[i];
                     let target = targetAnimation.target;
                     let animation = targetAnimation.animation;
                     if (target instanceof TransformNode || target.length === 1 && target[0] instanceof TransformNode) {
@@ -262,8 +348,90 @@ export class _GLTFAnimation {
                                 animationSampleRate
                             );
                         }
+                    } else if (target instanceof MorphTarget || target.length === 1 && target[0] instanceof MorphTarget) {
+                        let animationInfo = _GLTFAnimation._DeduceAnimationInfo(targetAnimation.animation);
+                        if (animationInfo) {
+                            let babylonMorphTarget = target instanceof MorphTarget ? target as MorphTarget : target[0] as MorphTarget;
+                            if (babylonMorphTarget) {
+                                let babylonMorphTargetManager = babylonScene.morphTargetManagers.find((morphTargetManager) => {
+                                    for (let j = 0; j < morphTargetManager.numTargets; ++j) {
+                                        if (morphTargetManager.getTarget(j) === babylonMorphTarget) {
+                                            return true;
+                                        }
+                                    }
+                                    return false;
+                                });
+                                if (babylonMorphTargetManager) {
+                                    let babylonMesh = <Mesh>babylonScene.meshes.find((mesh) => {
+                                        return (mesh as Mesh).morphTargetManager === babylonMorphTargetManager;
+                                    });
+                                    if (babylonMesh) {
+                                        if (!morphAnimations.has(babylonMesh)) {
+                                            morphAnimations.set(babylonMesh, new Map());
+                                        }
+                                        morphAnimations.get(babylonMesh)?.set(babylonMorphTarget, animation);
+                                        morphAnimationMeshes.add(babylonMesh);
+                                        sampleAnimations.set(babylonMesh, animation);
+                                    }
+                                }
+                            }
+                        }
                     }
                 }
+                morphAnimationMeshes.forEach((mesh) => {
+                    // for each mesh that we want to export a Morph target animation track for...
+                    let morphTargetManager = mesh.morphTargetManager;
+                    let combinedAnimationGroup: Nullable<Animation> = null;
+                    let animationKeys: IAnimationKey[] = [];
+                    let sampleAnimation = sampleAnimations.get(mesh)!;
+                    let numAnimationKeys = sampleAnimation.getKeys().length;
+                    // for each frame of this mesh's animation group track
+                    for (let i = 0; i < numAnimationKeys; ++i) {
+                        // construct a new Animation track by interlacing the frames of each morph target animation track
+                        if (morphTargetManager) {
+                            for (let j = 0; j < morphTargetManager.numTargets; ++j) {
+                                let morphTarget = morphTargetManager.getTarget(j);
+                                let animationsByMorphTarget = morphAnimations.get(mesh);
+                                if (animationsByMorphTarget) {
+                                    let morphTargetAnimation = animationsByMorphTarget.get(morphTarget);
+                                    if (morphTargetAnimation) {
+                                        if (!combinedAnimationGroup) {
+                                            combinedAnimationGroup = new Animation(`${animationGroup.name}_${mesh.name}_MorphWeightAnimation`,
+                                                "influence",
+                                                morphTargetAnimation.framePerSecond,
+                                                Animation.ANIMATIONTYPE_FLOAT,
+                                                morphTargetAnimation.loopMode,
+                                                morphTargetAnimation.enableBlending);
+                                        }
+                                        animationKeys.push(morphTargetAnimation.getKeys()[i]);
+                                    }
+                                    else {
+                                        animationKeys.push({ frame: animationGroup.from + (animationGroupFrameDiff / numAnimationKeys) * i, value: morphTarget.influence });
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    combinedAnimationGroup!.setKeys(animationKeys);
+                    let animationInfo = _GLTFAnimation._DeduceAnimationInfo(combinedAnimationGroup!);
+                    if (animationInfo) {
+                        _GLTFAnimation.AddAnimation(`${animationGroup.name}_${mesh.name}_MorphWeightAnimation`,
+                            glTFAnimation,
+                            mesh,
+                            combinedAnimationGroup!,
+                            animationInfo.dataAccessorType,
+                            animationInfo.animationChannelTargetPath,
+                            nodeMap,
+                            binaryWriter,
+                            bufferViews,
+                            accessors,
+                            false,
+                            animationInfo.useQuaternion,
+                            animationSampleRate,
+                            morphTargetManager?.numTargets
+                        );
+                    }
+                });
                 if (glTFAnimation.channels.length && glTFAnimation.samplers.length) {
                     glTFAnimations.push(glTFAnimation);
                 }
@@ -271,8 +439,10 @@ export class _GLTFAnimation {
         }
     }
 
-    private static AddAnimation(name: string, glTFAnimation: IAnimation, babylonTransformNode: TransformNode, 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(babylonTransformNode, animation, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion, animationSampleRate);
+    private static AddAnimation(name: string, glTFAnimation: IAnimation, babylonTransformNode: TransformNode, animation: Animation, dataAccessorType: AccessorType, animationChannelTargetPath: AnimationChannelTargetPath, nodeMap: { [key: number]: number }, binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystem: boolean, useQuaternion: boolean, animationSampleRate: number, morphAnimationChannels?: number) {
+        let animationData;
+        animationData = _GLTFAnimation._CreateNodeAnimation(babylonTransformNode, animation, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion, animationSampleRate);
+
         let bufferView: IBufferView;
         let accessor: IAccessor;
         let keyframeAccessorIndex: number;
@@ -282,13 +452,27 @@ export class _GLTFAnimation {
         let animationChannel: IAnimationChannel;
 
         if (animationData) {
+            if (morphAnimationChannels) {
+                let index = 0;
+                let currentInput: number = 0;
+                let newInputs: number[] = [];
+                while (animationData.inputs.length > 0) {
+                    currentInput = animationData.inputs.shift()!;
+                    if (index % morphAnimationChannels == 0) {
+                        newInputs.push(currentInput);
+                    }
+                    index++;
+                }
+                animationData.inputs = newInputs;
+            }
+
             let nodeIndex = nodeMap[babylonTransformNode.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) {
+            animationData.inputs.forEach(function (input) {
                 binaryWriter.setFloat32(input);
             });
 
@@ -298,14 +482,14 @@ export class _GLTFAnimation {
 
             // create bufferview and accessor for keyed values.
             outputLength = animationData.outputs.length;
-            byteLength = dataAccessorType === AccessorType.VEC3 ? animationData.outputs.length * 12 : animationData.outputs.length * 16;
+            byteLength = _GLTFUtilities._GetDataAccessorElementCount(dataAccessorType) * 4 * animationData.outputs.length;
 
             // 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) {
+            animationData.outputs.forEach(function (output) {
+                output.forEach(function (entry) {
                     binaryWriter.setFloat32(entry);
                 });
             });
@@ -451,9 +635,9 @@ export class _GLTFAnimation {
 
     private static _SetInterpolatedValue(babylonTransformNode: TransformNode, value: Nullable<number | Vector3 | Quaternion>, time: number, animation: Animation, animationChannelTargetPath: AnimationChannelTargetPath, quaternionCache: Quaternion, inputs: number[], outputs: number[][], convertToRightHandedSystem: boolean, useQuaternion: boolean) {
         const animationType = animation.dataType;
-        let cacheValue: Vector3 | Quaternion;
+        let cacheValue: Vector3 | Quaternion | number;
         inputs.push(time);
-        if (typeof value === "number") {
+        if (typeof value === "number" && babylonTransformNode instanceof TransformNode) {
             value = this._ConvertFactorToVector3OrQuaternion(value as number, babylonTransformNode, animation, animationType, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion);
         }
         if (value) {
@@ -474,7 +658,10 @@ export class _GLTFAnimation {
                 }
                 outputs.push(quaternionCache.asArray());
             }
-            else {
+            else if (animationChannelTargetPath === AnimationChannelTargetPath.WEIGHTS) {
+                outputs.push([value as number]);
+            }
+            else { // scaling and position animation
                 cacheValue = value as Vector3;
                 if (convertToRightHandedSystem && (animationChannelTargetPath !== AnimationChannelTargetPath.SCALE)) {
                     _GLTFUtilities._GetRightHandedPositionVector3FromRef(cacheValue);
@@ -519,7 +706,7 @@ export class _GLTFAnimation {
      * @param useQuaternion Specifies if quaternions are used in the animation
      */
     private static _CreateCubicSplineAnimation(babylonTransformNode: TransformNode, animation: Animation, animationChannelTargetPath: AnimationChannelTargetPath, frameDelta: number, inputs: number[], outputs: number[][], convertToRightHandedSystem: boolean, useQuaternion: boolean) {
-        animation.getKeys().forEach(function(keyFrame) {
+        animation.getKeys().forEach(function (keyFrame) {
             inputs.push(keyFrame.frame / animation.framePerSecond); // keyframes in seconds.
             _GLTFAnimation.AddSplineTangent(
                 babylonTransformNode,
@@ -594,7 +781,7 @@ export class _GLTFAnimation {
      */
     private static _AddKeyframeValue(keyFrame: IAnimationKey, animation: Animation, outputs: number[][], animationChannelTargetPath: AnimationChannelTargetPath, babylonTransformNode: TransformNode, convertToRightHandedSystem: boolean, useQuaternion: boolean) {
         let value: number[];
-        let newPositionRotationOrScale: Nullable<Vector3 | Quaternion>;
+        let newPositionRotationOrScale: Nullable<Vector3 | Quaternion | number>;
         const animationType = animation.dataType;
         if (animationType === Animation.ANIMATIONTYPE_VECTOR3) {
             value = keyFrame.value.asArray();
@@ -622,31 +809,35 @@ export class _GLTFAnimation {
             outputs.push(value); // scale  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.
-            newPositionRotationOrScale = this._ConvertFactorToVector3OrQuaternion(keyFrame.value as number, babylonTransformNode, animation, animationType, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion);
-            if (newPositionRotationOrScale) {
-                if (animationChannelTargetPath === AnimationChannelTargetPath.ROTATION) {
-                    let posRotScale = useQuaternion ? newPositionRotationOrScale as Quaternion : Quaternion.RotationYawPitchRoll(newPositionRotationOrScale.y, newPositionRotationOrScale.x, newPositionRotationOrScale.z).normalize();
-                    if (convertToRightHandedSystem) {
-                        _GLTFUtilities._GetRightHandedQuaternionFromRef(posRotScale);
+        else if (animationType === Animation.ANIMATIONTYPE_FLOAT) {
+            if (animationChannelTargetPath === AnimationChannelTargetPath.WEIGHTS) {
+                outputs.push([keyFrame.value]);
+            } else {  // handles single component x, y, z or w component animation by using a base property and animating over a component.
+                newPositionRotationOrScale = this._ConvertFactorToVector3OrQuaternion(keyFrame.value as number, babylonTransformNode, animation, animationType, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion);
+                if (newPositionRotationOrScale) {
+                    if (animationChannelTargetPath === AnimationChannelTargetPath.ROTATION) {
+                        let posRotScale = useQuaternion ? newPositionRotationOrScale as Quaternion : Quaternion.RotationYawPitchRoll(newPositionRotationOrScale.y, newPositionRotationOrScale.x, newPositionRotationOrScale.z).normalize();
+                        if (convertToRightHandedSystem) {
+                            _GLTFUtilities._GetRightHandedQuaternionFromRef(posRotScale);
 
-                        if (!babylonTransformNode.parent) {
-                            posRotScale = Quaternion.FromArray([0, 1, 0, 0]).multiply(posRotScale);
+                            if (!babylonTransformNode.parent) {
+                                posRotScale = Quaternion.FromArray([0, 1, 0, 0]).multiply(posRotScale);
+                            }
                         }
+                        outputs.push(posRotScale.asArray());
                     }
-                    outputs.push(posRotScale.asArray());
-                }
-                else if (animationChannelTargetPath === AnimationChannelTargetPath.TRANSLATION) {
-                    if (convertToRightHandedSystem) {
-                        _GLTFUtilities._GetRightHandedNormalVector3FromRef(newPositionRotationOrScale as Vector3);
+                    else if (animationChannelTargetPath === AnimationChannelTargetPath.TRANSLATION) {
+                        if (convertToRightHandedSystem) {
+                            _GLTFUtilities._GetRightHandedNormalVector3FromRef(newPositionRotationOrScale as Vector3);
 
-                        if (!babylonTransformNode.parent) {
-                            newPositionRotationOrScale.x *= -1;
-                            newPositionRotationOrScale.z *= -1;
+                            if (!babylonTransformNode.parent) {
+                                newPositionRotationOrScale.x *= -1;
+                                newPositionRotationOrScale.z *= -1;
+                            }
                         }
                     }
+                    outputs.push(newPositionRotationOrScale.asArray());
                 }
-                outputs.push(newPositionRotationOrScale.asArray());
             }
         }
         else if (animationType === Animation.ANIMATIONTYPE_QUATERNION) {
@@ -736,7 +927,7 @@ export class _GLTFAnimation {
      */
     private static AddSplineTangent(babylonTransformNode: TransformNode, 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;
+        let tangentValue: Vector3 | Quaternion | number = tangentType === _TangentType.INTANGENT ? keyFrame.inTangent : keyFrame.outTangent;
         if (interpolation === AnimationSamplerInterpolation.CUBICSPLINE) {
             if (animationChannelTargetPath === AnimationChannelTargetPath.ROTATION) {
                 if (tangentValue) {
@@ -759,6 +950,13 @@ export class _GLTFAnimation {
                     tangent = [0, 0, 0, 0];
                 }
             }
+            else if (animationChannelTargetPath === AnimationChannelTargetPath.WEIGHTS) {
+                if (tangentValue) {
+                    tangent = [tangentValue as number];
+                } else {
+                    tangent = [0];
+                }
+            }
             else {
                 if (tangentValue) {
                     tangent = (tangentValue as Vector3).asArray();
@@ -789,7 +987,7 @@ export class _GLTFAnimation {
     private static calculateMinMaxKeyFrames(keyFrames: IAnimationKey[]): { min: number, max: number } {
         let min: number = Infinity;
         let max: number = -Infinity;
-        keyFrames.forEach(function(keyFrame) {
+        keyFrames.forEach(function (keyFrame) {
             min = Math.min(min, keyFrame.frame);
             max = Math.max(max, keyFrame.frame);
         });

+ 230 - 14
serializers/src/glTF/2.0/glTFExporter.ts

@@ -10,6 +10,7 @@ import { TransformNode } from "babylonjs/Meshes/transformNode";
 import { AbstractMesh } from "babylonjs/Meshes/abstractMesh";
 import { SubMesh } from "babylonjs/Meshes/subMesh";
 import { Mesh } from "babylonjs/Meshes/mesh";
+import { MorphTarget } from "babylonjs/Morph/morphTarget";
 import { LinesMesh } from "babylonjs/Meshes/linesMesh";
 import { InstancedMesh } from "babylonjs/Meshes/instancedMesh";
 import { BaseTexture } from "babylonjs/Materials/Textures/baseTexture";
@@ -195,7 +196,7 @@ export class _Exporter {
         return this._applyExtensions(meshPrimitive, (extension, node) => extension.postExportMeshPrimitiveAsync && extension.postExportMeshPrimitiveAsync(context, node, babylonSubMesh, binaryWriter));
     }
 
-    public _extensionsPostExportNodeAsync(context: string, node: Nullable<INode>, babylonNode: Node, nodeMap?: {[key: number]: number}): Promise<Nullable<INode>> {
+    public _extensionsPostExportNodeAsync(context: string, node: Nullable<INode>, babylonNode: Node, nodeMap?: { [key: number]: number }): Promise<Nullable<INode>> {
         return this._applyExtensions(node, (extension, node) => extension.postExportNodeAsync && extension.postExportNodeAsync(context, node, babylonNode, nodeMap));
     }
 
@@ -763,7 +764,106 @@ export class _Exporter {
         }
 
         let writeBinaryFunc;
-        switch (attributeComponentKind){
+        switch (attributeComponentKind) {
+            case AccessorComponentType.UNSIGNED_BYTE: {
+                writeBinaryFunc = binaryWriter.setUInt8.bind(binaryWriter);
+                break;
+            }
+            case AccessorComponentType.UNSIGNED_SHORT: {
+                writeBinaryFunc = binaryWriter.setUInt16.bind(binaryWriter);
+                break;
+            }
+            case AccessorComponentType.UNSIGNED_INT: {
+                writeBinaryFunc = binaryWriter.setUInt32.bind(binaryWriter);
+            }
+            case AccessorComponentType.FLOAT: {
+                writeBinaryFunc = binaryWriter.setFloat32.bind(binaryWriter);
+                break;
+            }
+            default: {
+                Tools.Warn("Unsupported Attribute Component kind: " + attributeComponentKind);
+                return;
+            }
+        }
+
+        for (let vertexAttribute of vertexAttributes) {
+            for (let component of vertexAttribute) {
+                writeBinaryFunc(component);
+            }
+        }
+    }
+
+    /**
+     * Writes mesh attribute data to a data buffer
+     * Returns the bytelength of the data
+     * @param vertexBufferKind Indicates what kind of vertex data is being passed in
+     * @param meshAttributeArray Array containing the attribute data
+     * @param byteStride Specifies the space between data
+     * @param binaryWriter The buffer to write the binary data to
+     * @param convertToRightHandedSystem Converts the values to right-handed
+     */
+    public writeMorphTargetAttributeData(vertexBufferKind: string, attributeComponentKind: AccessorComponentType, meshPrimitive: SubMesh, morphTarget: MorphTarget, meshAttributeArray: FloatArray, morphTargetAttributeArray: FloatArray, stride: number, binaryWriter: _BinaryWriter, convertToRightHandedSystem: boolean, minMax?: any) {
+        let vertexAttributes: number[][] = [];
+        let index: number;
+        let difference: Vector3 = new Vector3();
+        let difference4: Vector4 = new Vector4(0, 0, 0, 0);
+
+        switch (vertexBufferKind) {
+            case VertexBuffer.PositionKind: {
+                for (let k = meshPrimitive.verticesStart; k < meshPrimitive.verticesCount; ++k) {
+                    index = meshPrimitive.indexStart + k * stride;
+                    const vertexData = Vector3.FromArray(meshAttributeArray, index);
+                    const morphData = Vector3.FromArray(morphTargetAttributeArray, index);
+                    difference = morphData.subtractToRef(vertexData, difference);
+                    if (convertToRightHandedSystem) {
+                        _GLTFUtilities._GetRightHandedPositionVector3FromRef(difference);
+                    }
+                    if (minMax) {
+                        minMax.min.copyFromFloats(Math.min(difference.x, minMax.min.x), Math.min(difference.y, minMax.min.y), Math.min(difference.z, minMax.min.z));
+                        minMax.max.copyFromFloats(Math.max(difference.x, minMax.max.x), Math.max(difference.y, minMax.max.y), Math.max(difference.z, minMax.max.z));
+                    }
+                    vertexAttributes.push(difference.asArray());
+                }
+                break;
+            }
+            case VertexBuffer.NormalKind: {
+                for (let k = meshPrimitive.verticesStart; k < meshPrimitive.verticesCount; ++k) {
+                    index = meshPrimitive.indexStart + k * stride;
+                    const vertexData = Vector3.FromArray(meshAttributeArray, index);
+                    vertexData.normalize();
+                    const morphData = Vector3.FromArray(morphTargetAttributeArray, index);
+                    morphData.normalize();
+                    difference = morphData.subtractToRef(vertexData, difference);
+                    if (convertToRightHandedSystem) {
+                        _GLTFUtilities._GetRightHandedNormalVector3FromRef(difference);
+                    }
+                    vertexAttributes.push(difference.asArray());
+                }
+                break;
+            }
+            case VertexBuffer.TangentKind: {
+                for (let k = meshPrimitive.verticesStart; k < meshPrimitive.verticesCount; ++k) {
+                    index = meshPrimitive.indexStart + k * (stride + 1);
+                    const vertexData = Vector4.FromArray(meshAttributeArray, index);
+                    _GLTFUtilities._NormalizeTangentFromRef(vertexData);
+                    const morphData = Vector4.FromArray(morphTargetAttributeArray, index);
+                    _GLTFUtilities._NormalizeTangentFromRef(morphData);
+                    difference4 = morphData.subtractToRef(vertexData, difference4);
+                    if (convertToRightHandedSystem) {
+                        _GLTFUtilities._GetRightHandedVector4FromRef(difference4);
+                    }
+                    vertexAttributes.push([difference4.x, difference4.y, difference4.z]);
+                }
+                break;
+            }
+            default: {
+                Tools.Warn("Unsupported Vertex Buffer Type: " + vertexBufferKind);
+                vertexAttributes = [];
+            }
+        }
+
+        let writeBinaryFunc;
+        switch (attributeComponentKind) {
             case AccessorComponentType.UNSIGNED_BYTE: {
                 writeBinaryFunc = binaryWriter.setUInt8.bind(binaryWriter);
                 break;
@@ -1110,6 +1210,105 @@ export class _Exporter {
     }
 
     /**
+ * Creates a bufferview based on the vertices type for the Babylon mesh
+ * @param babylonSubMesh The Babylon submesh that the morph target is applied to
+ * @param babylonMorphTarget the morph target to be exported
+ * @param binaryWriter The buffer to write the bufferview data to
+ * @param convertToRightHandedSystem Converts the values to right-handed
+ */
+    private setMorphTargetAttributes(babylonSubMesh: SubMesh, meshPrimitive: IMeshPrimitive, babylonMorphTarget: MorphTarget, binaryWriter: _BinaryWriter, convertToRightHandedSystem: boolean) {
+        if (babylonMorphTarget) {
+            if (!meshPrimitive.targets) {
+                meshPrimitive.targets = [];
+            }
+            let target: { [attribute: string]: number } = {};
+            if (babylonMorphTarget.hasNormals) {
+                const vertexNormals = babylonSubMesh.getMesh().getVerticesData(VertexBuffer.NormalKind)!;
+                const morphNormals = babylonMorphTarget.getNormals()!;
+                const count = babylonSubMesh.verticesCount;
+                const byteStride = 12; // 3 x 4 byte floats
+                const byteLength = count * byteStride;
+                const bufferView = _GLTFUtilities._CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, byteStride, babylonMorphTarget.name + "_NORMAL");
+                this._bufferViews.push(bufferView);
+
+                let bufferViewIndex = this._bufferViews.length - 1;
+                const accessor = _GLTFUtilities._CreateAccessor(bufferViewIndex, babylonMorphTarget.name + " - " + "NORMAL", AccessorType.VEC3, AccessorComponentType.FLOAT, count, 0, null, null);
+                this._accessors.push(accessor);
+                target.NORMAL = this._accessors.length - 1;
+
+                this.writeMorphTargetAttributeData(
+                    VertexBuffer.NormalKind,
+                    AccessorComponentType.FLOAT,
+                    babylonSubMesh,
+                    babylonMorphTarget,
+                    vertexNormals,
+                    morphNormals,
+                    byteStride / 4,
+                    binaryWriter,
+                    convertToRightHandedSystem
+                );
+            }
+            if (babylonMorphTarget.hasPositions) {
+                const vertexPositions = babylonSubMesh.getMesh().getVerticesData(VertexBuffer.PositionKind)!;
+                const morphPositions = babylonMorphTarget.getPositions()!;
+                const count = babylonSubMesh.verticesCount;
+                const byteStride = 12; // 3 x 4 byte floats
+                const byteLength = count * byteStride;
+                const bufferView = _GLTFUtilities._CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, byteStride, babylonMorphTarget.name + "_POSITION");
+                this._bufferViews.push(bufferView);
+
+                let bufferViewIndex = this._bufferViews.length - 1;
+                let minMax = { min: new Vector3(Infinity, Infinity, Infinity), max: new Vector3(-Infinity, -Infinity, -Infinity) };
+                const accessor = _GLTFUtilities._CreateAccessor(bufferViewIndex, babylonMorphTarget.name + " - " + "POSITION", AccessorType.VEC3, AccessorComponentType.FLOAT, count, 0, null, null);
+                this._accessors.push(accessor);
+                target.POSITION = this._accessors.length - 1;
+
+                this.writeMorphTargetAttributeData(
+                    VertexBuffer.PositionKind,
+                    AccessorComponentType.FLOAT,
+                    babylonSubMesh,
+                    babylonMorphTarget,
+                    vertexPositions,
+                    morphPositions,
+                    byteStride / 4,
+                    binaryWriter,
+                    convertToRightHandedSystem,
+                    minMax
+                );
+                accessor.min = minMax.min!.asArray();
+                accessor.max = minMax.max!.asArray();
+            }
+            if (babylonMorphTarget.hasTangents) {
+                const vertexTangents = babylonSubMesh.getMesh().getVerticesData(VertexBuffer.TangentKind)!;
+                const morphTangents = babylonMorphTarget.getTangents()!;
+                const count = babylonSubMesh.verticesCount;
+                const byteStride = 12; // 3 x 4 byte floats
+                const byteLength = count * byteStride;
+                const bufferView = _GLTFUtilities._CreateBufferView(0, binaryWriter.getByteOffset(), byteLength, byteStride, babylonMorphTarget.name + "_NORMAL");
+                this._bufferViews.push(bufferView);
+
+                let bufferViewIndex = this._bufferViews.length - 1;
+                const accessor = _GLTFUtilities._CreateAccessor(bufferViewIndex, babylonMorphTarget.name + " - " + "TANGENT", AccessorType.VEC3, AccessorComponentType.FLOAT, count, 0, null, null);
+                this._accessors.push(accessor);
+                target.TANGENT = this._accessors.length - 1;
+
+                this.writeMorphTargetAttributeData(
+                    VertexBuffer.TangentKind,
+                    AccessorComponentType.FLOAT,
+                    babylonSubMesh,
+                    babylonMorphTarget,
+                    vertexTangents,
+                    morphTangents,
+                    byteStride / 4,
+                    binaryWriter,
+                    convertToRightHandedSystem,
+                );
+            }
+            meshPrimitive.targets.push(target);
+        }
+    }
+
+    /**
      * The primitive mode of the Babylon mesh
      * @param babylonMesh The BabylonJS mesh
      */
@@ -1239,8 +1438,8 @@ export class _Exporter {
             { kind: VertexBuffer.NormalKind, accessorType: AccessorType.VEC3, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 12 },
             { kind: VertexBuffer.ColorKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 16 },
             { kind: VertexBuffer.TangentKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 16 },
-            { kind: VertexBuffer.UVKind, accessorType: AccessorType.VEC2, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 8  },
-            { kind: VertexBuffer.UV2Kind, accessorType: AccessorType.VEC2, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 8  },
+            { kind: VertexBuffer.UVKind, accessorType: AccessorType.VEC2, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 8 },
+            { kind: VertexBuffer.UV2Kind, accessorType: AccessorType.VEC2, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 8 },
             { kind: VertexBuffer.MatricesIndicesKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.UNSIGNED_SHORT, byteStride: 8 },
             { kind: VertexBuffer.MatricesIndicesExtraKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.UNSIGNED_SHORT, byteStride: 8 },
             { kind: VertexBuffer.MatricesWeightsKind, accessorType: AccessorType.VEC4, accessorComponentType: AccessorComponentType.FLOAT, byteStride: 16 },
@@ -1251,6 +1450,7 @@ export class _Exporter {
             let indexBufferViewIndex: Nullable<number> = null;
             const primitiveMode = this.getMeshPrimitiveMode(bufferMesh);
             let vertexAttributeBufferViews: { [attributeKind: string]: number } = {};
+            let morphTargetManager = bufferMesh.morphTargetManager;
 
             // For each BabylonMesh, create bufferviews for each 'kind'
             for (const attribute of attributeData) {
@@ -1382,6 +1582,14 @@ export class _Exporter {
                         meshPrimitive.material = materialIndex;
 
                     }
+                    if (morphTargetManager) {
+                        let target;
+                        for (let i = 0; i < morphTargetManager.numTargets; ++i) {
+                            target = morphTargetManager.getTarget(i);
+                            this.setMorphTargetAttributes(submesh, meshPrimitive, target, binaryWriter, convertToRightHandedSystem);
+                        }
+                    }
+
                     mesh.primitives.push(meshPrimitive);
 
                     const promise = this._extensionsPostExportMeshPrimitiveAsync("postExport", meshPrimitive, submesh, binaryWriter);
@@ -1402,8 +1610,7 @@ export class _Exporter {
      * @returns True if the node is used to convert its descendants from right-handed to left-handed. False otherwise
      */
     private isBabylonCoordinateSystemConvertingNode(node: Node): boolean {
-        if (node instanceof TransformNode)
-        {
+        if (node instanceof TransformNode) {
             if (node.name !== "__root__") {
                 return false;
             }
@@ -1519,7 +1726,7 @@ export class _Exporter {
                             }
 
                             if (babylonNode instanceof Mesh) {
-                                let babylonMesh : Mesh = babylonNode;
+                                let babylonMesh: Mesh = babylonNode;
                                 if (babylonMesh.skeleton) {
                                     glTFNode.skin = skinMap[babylonMesh.skeleton.uniqueId];
                                 }
@@ -1586,6 +1793,7 @@ export class _Exporter {
 
                                 if (!babylonScene.animationGroups.length && babylonNode.animations.length) {
                                     _GLTFAnimation._CreateNodeAnimationFromNodeAnimations(babylonNode, runtimeGLTFAnimation, idleGLTFAnimations, nodeMap, this._nodes, binaryWriter, this._bufferViews, this._accessors, convertToRightHandedSystem, this._animationSampleRate);
+                                    _GLTFAnimation._CreateMorphTargetAnimationFromMorphTargets(babylonNode, runtimeGLTFAnimation, idleGLTFAnimations, nodeMap, this._nodes, binaryWriter, this._bufferViews, this._accessors, convertToRightHandedSystem, this._animationSampleRate);
                                 }
                             });
                         }
@@ -1623,7 +1831,7 @@ export class _Exporter {
      * @param nodeMap Node mapping of unique id to glTF node index
      * @returns glTF node
      */
-    private createNodeAsync(babylonNode: Node, binaryWriter: _BinaryWriter, convertToRightHandedSystem: boolean, nodeMap?: {[key: number]: number}): Promise<INode> {
+    private createNodeAsync(babylonNode: Node, binaryWriter: _BinaryWriter, convertToRightHandedSystem: boolean, nodeMap?: { [key: number]: number }): Promise<INode> {
         return Promise.resolve().then(() => {
             // create node to hold translation/rotation/scale and the mesh
             const node: INode = {};
@@ -1637,7 +1845,15 @@ export class _Exporter {
             if (babylonNode instanceof TransformNode) {
                 // Set transformation
                 this.setNodeTransformation(node, babylonNode, convertToRightHandedSystem);
-
+                if (babylonNode instanceof Mesh) {
+                    let morphTargetManager = babylonNode.morphTargetManager;
+                    if (morphTargetManager && morphTargetManager.numTargets > 0) {
+                        mesh.weights = [];
+                        for (let i = 0; i < morphTargetManager.numTargets; ++i) {
+                            mesh.weights.push(morphTargetManager.getTarget(i).influence);
+                        }
+                    }
+                }
                 return this.setPrimitiveAttributesAsync(mesh, babylonNode, binaryWriter, convertToRightHandedSystem).then(() => {
                     if (mesh.primitives.length) {
                         this._meshes.push(mesh);
@@ -1664,9 +1880,9 @@ export class _Exporter {
         const skinMap: { [key: number]: number } = {};
         for (let skeleton of babylonScene.skeletons) {
             // create skin
-            const skin: ISkin = { joints: []};
-            let inverseBindMatrices : Matrix[] = [];
-            let skeletonMesh = babylonScene.meshes.find((mesh) => {mesh.skeleton === skeleton; });
+            const skin: ISkin = { joints: [] };
+            let inverseBindMatrices: Matrix[] = [];
+            let skeletonMesh = babylonScene.meshes.find((mesh) => { mesh.skeleton === skeleton; });
             skin.skeleton = skeleton.overrideMesh === null ? (skeletonMesh ? nodeMap[skeletonMesh.uniqueId] : undefined) : nodeMap[skeleton.overrideMesh.uniqueId];
             for (let bone of skeleton.bones) {
                 if (bone._index != -1) {
@@ -1687,7 +1903,7 @@ export class _Exporter {
             let bufferViewOffset = binaryWriter.getByteOffset();
             let bufferView = _GLTFUtilities._CreateBufferView(0, bufferViewOffset, byteLength, byteStride, "InverseBindMatrices" + " - " + skeleton.name);
             this._bufferViews.push(bufferView);
-            let bufferViewIndex =  this._bufferViews.length - 1;
+            let bufferViewIndex = this._bufferViews.length - 1;
             let bindMatrixAccessor = _GLTFUtilities._CreateAccessor(bufferViewIndex, "InverseBindMatrices" + " - " + skeleton.name, AccessorType.MAT4, AccessorComponentType.FLOAT, inverseBindMatrices.length, null, null, null);
             let inverseBindAccessorIndex = this._accessors.push(bindMatrixAccessor) - 1;
             skin.inverseBindMatrices = inverseBindAccessorIndex;
@@ -1916,4 +2132,4 @@ export class _BinaryWriter {
             this._byteOffset += 4;
         }
     }
-}
+}

+ 19 - 0
serializers/src/glTF/2.0/glTFUtilities.ts

@@ -204,4 +204,23 @@ export class _GLTFUtilities {
             matrix
         );
     }
+
+    public static _GetDataAccessorElementCount(accessorType: AccessorType) {
+        switch (accessorType) {
+            case AccessorType.MAT2:
+                return 4;
+            case AccessorType.MAT3:
+                return 9;
+            case AccessorType.MAT4:
+                return 16;
+            case AccessorType.SCALAR:
+                return 1;
+            case AccessorType.VEC2:
+                return 2;
+            case AccessorType.VEC3:
+                return 3;
+            case AccessorType.VEC4:
+                return 4;
+        }
+    }
 }

二進制
tests/validation/ReferenceImages/gltfSerializerMorphTargetAnimation.png


+ 5 - 0
tests/validation/config.json

@@ -603,6 +603,11 @@
             "referenceImage": "gltfSerializerSkinningAndAnimation.png"
         },
         {
+            "title": "GLTF Serializer Morph Target Animation",
+            "playgroundId": "#T087A8#27",
+            "referenceImage": "gltfSerializerMorphTargetAnimation.png"
+        },
+        {
             "title": "GLTF Buggy with Draco Mesh Compression",
             "playgroundId": "#JNW207#1",
             "referenceImage": "gltfBuggyDraco.png"