فهرست منبع

Incorporate style and bugfixes for Morph target animation export logic. Added validation test for exporting in-engine created Morph targets to glTF

Nicholas Barlow 5 سال پیش
والد
کامیت
6984e437df
3فایلهای تغییر یافته به همراه96 افزوده شده و 79 حذف شده
  1. 86 74
      serializers/src/glTF/2.0/glTFAnimation.ts
  2. 4 4
      serializers/src/glTF/2.0/glTFExporter.ts
  3. 6 1
      tests/validation/config.json

+ 86 - 74
serializers/src/glTF/2.0/glTFAnimation.ts

@@ -223,41 +223,40 @@ 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 _CreateMorphTargetAnimationFromNode(babylonNode: Node, runtimeGLTFAnimation: IAnimation, idleGLTFAnimations: IAnimation[], nodeMap: { [key: number]: number }, nodes: INode[], binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystem: boolean, animationSampleRate: number) {
+     * @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 _CreateMorphTargetAnimationFromMorphTargetAnimations(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;
+            const 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}`,
+                    const morphTarget = morphTargetManager.getTarget(i);
+                    for (let animation of morphTarget.animations) {
+                        const combinedAnimation = new Animation(`${animation.name}`,
                             "influence",
                             animation.framePerSecond,
                             animation.dataType,
                             animation.loopMode,
                             animation.enableBlending);
-                        let combinedAnimationKeys: IAnimationKey[] = [];
-                        let animationKeys = animation.getKeys();
+                        const combinedAnimationKeys: IAnimationKey[] = [];
+                        const 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) {
+                        for (let j = 0; j < animationKeys.length; ++j) {
+                            let animationKey = animationKeys[j];
+                            for (let k = 0; k < morphTargetManager.numTargets; ++k) {
+                                if (k == i) {
                                     combinedAnimationKeys.push(animationKey);
                                 } else {
                                     combinedAnimationKeys.push({ frame: animationKey.frame, value: 0 });
@@ -265,14 +264,14 @@ export class _GLTFAnimation {
                             }
                         }
                         combinedAnimation.setKeys(combinedAnimationKeys);
-                        let animationInfo = _GLTFAnimation._DeduceAnimationInfo(combinedAnimation);
+                        const animationInfo = _GLTFAnimation._DeduceAnimationInfo(combinedAnimation);
                         if (animationInfo) {
                             glTFAnimation = {
                                 name: combinedAnimation.name,
                                 samplers: [],
                                 channels: []
                             };
-                            _GLTFAnimation.AddAnimation(`${animation.name}`,
+                            _GLTFAnimation.AddAnimation(animation.name,
                                 animation.hasRunningRuntimeAnimations ? runtimeGLTFAnimation : glTFAnimation,
                                 babylonNode,
                                 combinedAnimation,
@@ -310,29 +309,29 @@ export class _GLTFAnimation {
      * @param convertToRightHandedSystemMap
      * @param animationSampleRate
      */
-    public static _CreateNodeAnimationFromAnimationGroups(babylonScene: Scene, glTFAnimations: IAnimation[], nodeMap: { [key: number]: number }, nodes: INode[], binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystemMap: { [nodeId: number]: boolean }, animationSampleRate: number) {
+    public static _CreateNodeAndMorphAnimationFromAnimationGroups(babylonScene: Scene, glTFAnimations: IAnimation[], nodeMap: { [key: number]: number }, nodes: INode[], binaryWriter: _BinaryWriter, bufferViews: IBufferView[], accessors: IAccessor[], convertToRightHandedSystemMap: { [nodeId: number]: boolean }, animationSampleRate: number) {
         let glTFAnimation: IAnimation;
         if (babylonScene.animationGroups) {
-            let animationGroups = babylonScene.animationGroups;
+            const 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;
+                const morphAnimations: Map<Mesh, Map<MorphTarget, Animation>> = new Map();
+                const sampleAnimations: Map<Mesh, Animation> = new Map();
+                const morphAnimationMeshes: Set<Mesh> = new Set();
+                const animationGroupFrameDiff = animationGroup.to - animationGroup.from;
                 glTFAnimation = {
                     name: animationGroup.name,
                     channels: [],
                     samplers: []
                 };
                 for (let i = 0; i < animationGroup.targetedAnimations.length; ++i) {
-                    let targetAnimation = animationGroup.targetedAnimations[i];
-                    let target = targetAnimation.target;
-                    let animation = targetAnimation.animation;
+                    const targetAnimation = animationGroup.targetedAnimations[i];
+                    const target = targetAnimation.target;
+                    const animation = targetAnimation.animation;
                     if (target instanceof TransformNode || target.length === 1 && target[0] instanceof TransformNode) {
-                        let animationInfo = _GLTFAnimation._DeduceAnimationInfo(targetAnimation.animation);
+                        const animationInfo = _GLTFAnimation._DeduceAnimationInfo(targetAnimation.animation);
                         if (animationInfo) {
-                            let babylonTransformNode = target instanceof TransformNode ? target as TransformNode : target[0] as TransformNode;
-                            let convertToRightHandedSystem = convertToRightHandedSystemMap[babylonTransformNode.uniqueId];
+                            const babylonTransformNode = target instanceof TransformNode ? target as TransformNode : target[0] as TransformNode;
+                            const convertToRightHandedSystem = convertToRightHandedSystemMap[babylonTransformNode.uniqueId];
                             _GLTFAnimation.AddAnimation(`${animation.name}`,
                                 glTFAnimation,
                                 babylonTransformNode,
@@ -349,11 +348,11 @@ export class _GLTFAnimation {
                             );
                         }
                     } else if (target instanceof MorphTarget || target.length === 1 && target[0] instanceof MorphTarget) {
-                        let animationInfo = _GLTFAnimation._DeduceAnimationInfo(targetAnimation.animation);
+                        const animationInfo = _GLTFAnimation._DeduceAnimationInfo(targetAnimation.animation);
                         if (animationInfo) {
-                            let babylonMorphTarget = target instanceof MorphTarget ? target as MorphTarget : target[0] as MorphTarget;
+                            const babylonMorphTarget = target instanceof MorphTarget ? target as MorphTarget : target[0] as MorphTarget;
                             if (babylonMorphTarget) {
-                                let babylonMorphTargetManager = babylonScene.morphTargetManagers.find((morphTargetManager) => {
+                                const babylonMorphTargetManager = babylonScene.morphTargetManagers.find((morphTargetManager) => {
                                     for (let j = 0; j < morphTargetManager.numTargets; ++j) {
                                         if (morphTargetManager.getTarget(j) === babylonMorphTarget) {
                                             return true;
@@ -362,9 +361,9 @@ export class _GLTFAnimation {
                                     return false;
                                 });
                                 if (babylonMorphTargetManager) {
-                                    let babylonMesh = <Mesh>babylonScene.meshes.find((mesh) => {
+                                    const babylonMesh = babylonScene.meshes.find((mesh) => {
                                         return (mesh as Mesh).morphTargetManager === babylonMorphTargetManager;
-                                    });
+                                    }) as Mesh;
                                     if (babylonMesh) {
                                         if (!morphAnimations.has(babylonMesh)) {
                                             morphAnimations.set(babylonMesh, new Map());
@@ -379,41 +378,50 @@ export class _GLTFAnimation {
                     }
                 }
                 morphAnimationMeshes.forEach((mesh) => {
-                    // for each mesh that we want to export a Morph target animation track for...
-                    let morphTargetManager = mesh.morphTargetManager;
+                    const 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
+                    const animationKeys: IAnimationKey[] = [];
+                    const sampleAnimation = sampleAnimations.get(mesh)!;
+                    const sampleAnimationKeys = sampleAnimation.getKeys();
+                    const numAnimationKeys = sampleAnimationKeys.length;
+                    /*
+                        Due to how glTF expects morph target animation data to be formatted, we need to rearrange the individual morph target animation tracks,
+                        such that we have a single animation, where a given keyframe input value has successive output values for each morph target belonging to the manager.
+                        See: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
+
+                        We do this via constructing a new Animation track, and interleaving the frames of each morph target animation track in the current Animation Group
+                        We reuse the Babylon Animation data structure for ease of handling export of cubic spline animation keys, and to reuse the
+                        existing _GLTFAnimation.AddAnimation codepath with minimal modification, however the constructed Babylon Animation is NOT intended for use in-engine.
+                    */
                     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 });
+                        for (let j = 0; j < morphTargetManager.numTargets; ++j) {
+                            const morphTarget = morphTargetManager.getTarget(j);
+                            const animationsByMorphTarget = morphAnimations.get(mesh);
+                            if (animationsByMorphTarget) {
+                                const 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,
+                                                         inTangent: sampleAnimationKeys[0].inTangent ? 0 : undefined,
+                                                         outTangent: sampleAnimationKeys[0].outTangent ? 0 : undefined
+                                                        });
                                 }
                             }
                         }
                     }
                     combinedAnimationGroup!.setKeys(animationKeys);
-                    let animationInfo = _GLTFAnimation._DeduceAnimationInfo(combinedAnimationGroup!);
+                    const animationInfo = _GLTFAnimation._DeduceAnimationInfo(combinedAnimationGroup!);
                     if (animationInfo) {
                         _GLTFAnimation.AddAnimation(`${animationGroup.name}_${mesh.name}_MorphWeightAnimation`,
                             glTFAnimation,
@@ -440,9 +448,7 @@ 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, morphAnimationChannels?: number) {
-        let animationData;
-        animationData = _GLTFAnimation._CreateNodeAnimation(babylonTransformNode, animation, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion, animationSampleRate);
-
+        const animationData = _GLTFAnimation._CreateNodeAnimation(babylonTransformNode, animation, animationChannelTargetPath, convertToRightHandedSystem, useQuaternion, animationSampleRate);
         let bufferView: IBufferView;
         let accessor: IAccessor;
         let keyframeAccessorIndex: number;
@@ -452,6 +458,12 @@ export class _GLTFAnimation {
         let animationChannel: IAnimationChannel;
 
         if (animationData) {
+
+            /*
+            * Now that we have the glTF converted morph target animation data,
+            * we can remove redundant input data so that we have n input frames,
+            * and morphAnimationChannels * n output frames
+            */
             if (morphAnimationChannels) {
                 let index = 0;
                 let currentInput: number = 0;

+ 4 - 4
serializers/src/glTF/2.0/glTFExporter.ts

@@ -1797,11 +1797,11 @@ export class _Exporter {
                                 nodeIndex = this._nodes.length - 1;
                                 nodeMap[babylonNode.uniqueId] = nodeIndex;
 
-                                if (!babylonScene.animationGroups.length){
-                                    _GLTFAnimation._CreateMorphTargetAnimationFromNode(babylonNode, runtimeGLTFAnimation, idleGLTFAnimations, nodeMap, this._nodes, binaryWriter, this._bufferViews, this._accessors, convertToRightHandedSystem, this._animationSampleRate);
+                                if (!babylonScene.animationGroups.length) {
+                                    _GLTFAnimation._CreateMorphTargetAnimationFromMorphTargetAnimations(babylonNode, runtimeGLTFAnimation, idleGLTFAnimations, nodeMap, this._nodes, binaryWriter, this._bufferViews, this._accessors, convertToRightHandedSystem, this._animationSampleRate);
                                     if (babylonNode.animations.length) {
                                         _GLTFAnimation._CreateNodeAnimationFromNodeAnimations(babylonNode, runtimeGLTFAnimation, idleGLTFAnimations, nodeMap, this._nodes, binaryWriter, this._bufferViews, this._accessors, convertToRightHandedSystem, this._animationSampleRate);
-                                }
+                                    }
                                 }
                             });
                         }
@@ -1824,7 +1824,7 @@ export class _Exporter {
             });
 
             if (babylonScene.animationGroups.length) {
-                _GLTFAnimation._CreateNodeAnimationFromAnimationGroups(babylonScene, this._animations, nodeMap, this._nodes, binaryWriter, this._bufferViews, this._accessors, this._convertToRightHandedSystemMap, this._animationSampleRate);
+                _GLTFAnimation._CreateNodeAndMorphAnimationFromAnimationGroups(babylonScene, this._animations, nodeMap, this._nodes, binaryWriter, this._bufferViews, this._accessors, this._convertToRightHandedSystemMap, this._animationSampleRate);
             }
 
             return nodeMap;

+ 6 - 1
tests/validation/config.json

@@ -610,10 +610,15 @@
         },
         {
             "title": "GLTF Serializer Morph Target Animation",
-            "playgroundId": "#T087A8#27",
+            "playgroundId": "#84M2SR#21",
             "referenceImage": "gltfSerializerMorphTargetAnimation.png"
         },
         {
+            "title": "GLTF Serializer Morph Target Animation Group",
+            "playgroundId": "#T087A8#27",
+            "referenceImage": "gltfSerializerMorphTargetAnimationGroup.png"
+        },
+        {
             "title": "GLTF Buggy with Draco Mesh Compression",
             "playgroundId": "#JNW207#1",
             "referenceImage": "gltfBuggyDraco.png"