Browse Source

Export morph targets and their animations to glTF format

noalak 8 years ago
parent
commit
7925a9875e

+ 11 - 2
Exporters/3ds Max/BabylonExport.Entities/BabylonAnimationKey.cs

@@ -1,14 +1,23 @@
-using System.Runtime.Serialization;
+using System;
+using System.Runtime.Serialization;
 
 namespace BabylonExport.Entities
 {
     [DataContract]
-    public class BabylonAnimationKey
+    public class BabylonAnimationKey : IComparable<BabylonAnimationKey>
     {
         [DataMember]
         public int frame { get; set; }
 
         [DataMember]
         public float[] values { get; set; }
+        
+        public int CompareTo(BabylonAnimationKey other)
+        {
+            if (other == null)
+                return 1;
+            else
+                return this.frame.CompareTo(other.frame);
+        }
     }
 }

+ 1 - 0
Exporters/3ds Max/GltfExport.Entities/GLTFExport.Entities.csproj

@@ -51,6 +51,7 @@
     <Compile Include="GLTFCameraPerspective.cs" />
     <Compile Include="GLTFCameraOrthographic.cs" />
     <Compile Include="GLTFCamera.cs" />
+    <Compile Include="GLTFMorphTarget.cs" />
     <Compile Include="GLTFSampler.cs" />
     <Compile Include="GLTFIndexedChildRootProperty.cs" />
     <Compile Include="GLTFImage.cs" />

+ 2 - 2
Exporters/3ds Max/GltfExport.Entities/GLTFMeshPrimitive.cs

@@ -41,7 +41,7 @@ namespace GLTFExport.Entities
         [DataMember(EmitDefaultValue = false)]
         public int? material { get; set; }
 
-        //[DataMember]
-        //public Dictionary<string, int>[] targets { get; set; }
+        [DataMember(EmitDefaultValue = false)]
+        public GLTFMorphTarget[] targets { get; set; }
     }
 }

+ 16 - 0
Exporters/3ds Max/GltfExport.Entities/GLTFMorphTarget.cs

@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace GLTFExport.Entities
+{
+    [DataContract]
+    public class GLTFMorphTarget : Dictionary<string, int>
+    {
+        public enum Attribute
+        {
+            POSITION,
+            NORMAL,
+            TANGENT
+        }
+    }
+}

+ 2 - 2
Exporters/3ds Max/Max2Babylon/Exporter/BabylonExporter.GLTFExporter.AbstractMesh.cs

@@ -5,7 +5,7 @@ namespace Max2Babylon
 {
     partial class BabylonExporter
     {
-        private GLTFNode ExportAbstractMesh(BabylonAbstractMesh babylonAbstractMesh, GLTF gltf, GLTFNode gltfParentNode)
+        private GLTFNode ExportAbstractMesh(BabylonAbstractMesh babylonAbstractMesh, GLTF gltf, GLTFNode gltfParentNode, BabylonScene babylonScene)
         {
             RaiseMessage("GLTFExporter.AbstractMesh | Export abstract mesh named: " + babylonAbstractMesh.name, 1);
 
@@ -56,7 +56,7 @@ namespace Max2Babylon
             }
 
             // Animations
-            ExportNodeAnimation(babylonAbstractMesh, gltf, gltfNode);
+            ExportNodeAnimation(babylonAbstractMesh, gltf, gltfNode, babylonScene);
 
             return gltfNode;
         }

+ 279 - 22
Exporters/3ds Max/Max2Babylon/Exporter/BabylonExporter.GLTFExporter.Animation.cs

@@ -7,16 +7,18 @@ namespace Max2Babylon
 {
     partial class BabylonExporter
     {
-        private GLTFAnimation ExportNodeAnimation(BabylonAbstractMesh babylonAbstractMesh, GLTF gltf, GLTFNode gltfNode)
+        private static float FPS_FACTOR = 60.0f; // TODO - Which FPS factor ?
+
+        private GLTFAnimation ExportNodeAnimation(BabylonNode babylonNode, GLTF gltf, GLTFNode gltfNode, BabylonScene babylonScene = null)
         {
-            if (babylonAbstractMesh.animations != null && babylonAbstractMesh.animations.Length > 0)
-            {
-                RaiseMessage("GLTFExporter.Animation | Export animation of mesh named: " + babylonAbstractMesh.name, 2);
+            var channelList = new List<GLTFChannel>();
+            var samplerList = new List<GLTFAnimationSampler>();
 
-                var channelList = new List<GLTFChannel>();
-                var samplerList = new List<GLTFAnimationSampler>();
+            if (babylonNode.animations != null && babylonNode.animations.Length > 0)
+            {
+                RaiseMessage("GLTFExporter.Animation | Export animation of node named: " + babylonNode.name, 2);
 
-                foreach (BabylonAnimation babylonAnimation in babylonAbstractMesh.animations)
+                foreach (BabylonAnimation babylonAnimation in babylonNode.animations)
                 {
                     // Target
                     var gltfTarget = new GLTFChannelTarget
@@ -27,7 +29,7 @@ namespace Max2Babylon
                     if (gltfTarget.path == null)
                     {
                         // Unkown babylon animation property
-                        RaiseWarning("GLTFExporter.AbstractMesh | Unkown animation property '" + babylonAnimation.property + "'", 3);
+                        RaiseWarning("GLTFExporter.Animation | Unkown animation property '" + babylonAnimation.property + "'", 3);
                         // Ignore this babylon animation
                         continue;
                     }
@@ -48,7 +50,7 @@ namespace Max2Babylon
                     accessorInput.max = new float[] { float.MinValue };
                     foreach (var babylonAnimationKey in babylonAnimation.keys)
                     {
-                        var inputValue = babylonAnimationKey.frame / 60.0f; // TODO - Which FPS factor ?
+                        var inputValue = babylonAnimationKey.frame / FPS_FACTOR;
                         // Store values as bytes
                         accessorInput.bytesList.AddRange(BitConverter.GetBytes(inputValue));
                         // Update min and max values
@@ -117,23 +119,31 @@ namespace Max2Babylon
                     };
                     channelList.Add(gltfChannel);
                 }
+            }
+
+            if (babylonNode.GetType() == typeof(BabylonMesh))
+            {
+                var babylonMesh = babylonNode as BabylonMesh;
 
-                // Do not export empty arrays
-                if (channelList.Count > 0)
+                // Morph targets
+                var babylonMorphTargetManager = GetBabylonMorphTargetManager(babylonScene, babylonMesh);
+                if (babylonMorphTargetManager != null)
                 {
-                    // Animation
-                    var gltfAnimation = new GLTFAnimation
-                    {
-                        channels = channelList.ToArray(),
-                        samplers = samplerList.ToArray()
-                    };
-                    gltf.AnimationsList.Add(gltfAnimation);
-                    return gltfAnimation;
+                    ExportMorphTargetWeightAnimation(babylonMorphTargetManager, gltf, gltfNode, channelList, samplerList);
                 }
-                else
+            }
+
+            // Do not export empty arrays
+            if (channelList.Count > 0)
+            {
+                // Animation
+                var gltfAnimation = new GLTFAnimation
                 {
-                    return null;
-                }
+                    channels = channelList.ToArray(),
+                    samplers = samplerList.ToArray()
+                };
+                gltf.AnimationsList.Add(gltfAnimation);
+                return gltfAnimation;
             }
             else
             {
@@ -141,6 +151,253 @@ namespace Max2Babylon
             }
         }
 
+        private bool ExportMorphTargetWeightAnimation(BabylonMorphTargetManager babylonMorphTargetManager, GLTF gltf, GLTFNode gltfNode, List<GLTFChannel> channelList, List<GLTFAnimationSampler> samplerList)
+        {
+            if (!_isBabylonMorphTargetManagerAnimationValid(babylonMorphTargetManager))
+            {
+                return false;
+            }
+
+            RaiseMessage("GLTFExporter.Animation | Export animation of morph target manager with id: " + babylonMorphTargetManager.id, 2);
+
+            var influencesPerFrame = _getTargetManagerAnimationsData(babylonMorphTargetManager);
+            var frames = new List<int>(influencesPerFrame.Keys);
+            frames.Sort(); // Mandatory otherwise gltf loader of babylon doesn't understand
+
+            // Target
+            var gltfTarget = new GLTFChannelTarget
+            {
+                node = gltfNode.index
+            };
+            gltfTarget.path = "weights";
+
+            // Buffer
+            var buffer = GLTFBufferService.Instance.GetBuffer(gltf);
+
+            // --- Input ---
+            var accessorInput = GLTFBufferService.Instance.CreateAccessor(
+                gltf,
+                GLTFBufferService.Instance.GetBufferViewAnimationFloatScalar(gltf, buffer),
+                "accessorAnimationInput",
+                GLTFAccessor.ComponentType.FLOAT,
+                GLTFAccessor.TypeEnum.SCALAR
+            );
+            // Populate accessor
+            accessorInput.min = new float[] { float.MaxValue };
+            accessorInput.max = new float[] { float.MinValue };
+
+            foreach (var frame in frames)
+            {
+                var inputValue = frame / FPS_FACTOR;
+                // Store values as bytes
+                accessorInput.bytesList.AddRange(BitConverter.GetBytes(inputValue));
+                // Update min and max values
+                GLTFBufferService.UpdateMinMaxAccessor(accessorInput, inputValue);
+            }
+            accessorInput.count = influencesPerFrame.Count;
+
+            // --- Output ---
+            GLTFAccessor accessorOutput = GLTFBufferService.Instance.CreateAccessor(
+                gltf,
+                GLTFBufferService.Instance.GetBufferViewAnimationFloatScalar(gltf, buffer),
+                "accessorAnimationWeights",
+                GLTFAccessor.ComponentType.FLOAT,
+                GLTFAccessor.TypeEnum.SCALAR
+            );
+            // Populate accessor
+            foreach (var frame in frames)
+            {
+                var outputValues = influencesPerFrame[frame];
+                // Store values as bytes
+                foreach (var outputValue in outputValues)
+                {
+                    accessorOutput.count++;
+                    accessorOutput.bytesList.AddRange(BitConverter.GetBytes(outputValue));
+                }
+            }
+
+            // Animation sampler
+            var gltfAnimationSampler = new GLTFAnimationSampler
+            {
+                input = accessorInput.index,
+                output = accessorOutput.index
+            };
+            gltfAnimationSampler.index = samplerList.Count;
+            samplerList.Add(gltfAnimationSampler);
+
+            // Channel
+            var gltfChannel = new GLTFChannel
+            {
+                sampler = gltfAnimationSampler.index,
+                target = gltfTarget
+            };
+            channelList.Add(gltfChannel);
+
+            return true;
+        }
+
+        private bool _isBabylonMorphTargetManagerAnimationValid(BabylonMorphTargetManager babylonMorphTargetManager)
+        {
+            bool hasAnimation = false;
+            bool areAnimationsValid = true;
+            foreach (var babylonMorphTarget in babylonMorphTargetManager.targets)
+            {
+                if (babylonMorphTarget.animations != null && babylonMorphTarget.animations.Length > 0)
+                {
+                    hasAnimation = true;
+
+                    // Ensure target has only one animation
+                    if (babylonMorphTarget.animations.Length > 1)
+                    {
+                        areAnimationsValid = false;
+                        RaiseWarning("GLTFExporter.Animation | Only one animation is supported for morph targets", 3);
+                        continue;
+                    }
+
+                    // Ensure the target animation property is 'influence'
+                    bool targetHasInfluence = false;
+                    foreach (BabylonAnimation babylonAnimation in babylonMorphTarget.animations)
+                    {
+                        if (babylonAnimation.property == "influence")
+                        {
+                            targetHasInfluence = true;
+                        }
+                    }
+                    if (targetHasInfluence == false)
+                    {
+                        areAnimationsValid = false;
+                        RaiseWarning("GLTFExporter.Animation | Only 'influence' animation is supported for morph targets", 3);
+                        continue;
+                    }
+                }
+            }
+
+            return hasAnimation && areAnimationsValid;
+        }
+
+        /// <summary>
+        /// The keys of each BabylonMorphTarget animation ARE NOT assumed to be identical.
+        /// This function merges together all keys and binds to each an influence value for all targets.
+        /// A target influence value is automatically computed when necessary.
+        /// Computation rules are:
+        /// - linear interpolation between target key range
+        /// - constant value outside target key range
+        /// </summary>
+        /// <example>
+        /// When:
+        /// animation1.keys = {0, 25, 50, 100}
+        /// animation2.keys = {50, 75, 100}
+        /// 
+        /// Gives:
+        /// mergedKeys = {0, 25, 50, 100, 75}
+        /// range1=[0, 100]
+        /// range2=[50, 100]
+        /// for animation1, the value associated to key=75 is the interpolation of its values between 50 and 100
+        /// for animation2, the value associated to key=0 is equal to the one at key=50 since 0 is out of range [50, 100] (same for key=25)</example>
+        /// <param name="babylonMorphTargetManager"></param>
+        /// <returns>A map which for each frame, gives the influence value of all targets</returns>
+        private Dictionary<int, List<float>> _getTargetManagerAnimationsData(BabylonMorphTargetManager babylonMorphTargetManager)
+        {
+            // Merge all keys into a single set (no duplicated frame)
+            var mergedFrames = new HashSet<int>();
+            foreach (var babylonMorphTarget in babylonMorphTargetManager.targets)
+            {
+                if (babylonMorphTarget.animations != null)
+                {
+                    var animation = babylonMorphTarget.animations[0];
+                    foreach (BabylonAnimationKey animationKey in animation.keys)
+                    {
+                        mergedFrames.Add(animationKey.frame);
+                    }
+                }
+            }
+
+            // For each frame, gives the influence value of all targets (gltf structure)
+            var influencesPerFrame = new Dictionary<int, List<float>>();
+            foreach (var frame in mergedFrames)
+            {
+                influencesPerFrame.Add(frame, new List<float>());
+            }
+            foreach (var babylonMorphTarget in babylonMorphTargetManager.targets)
+            {
+                // For a given target, for each frame, gives the influence value of the target (babylon structure)
+                var influencePerFrameForTarget = new Dictionary<int, float>();
+
+                if (babylonMorphTarget.animations != null && babylonMorphTarget.animations.Length > 0)
+                {
+                    var animation = babylonMorphTarget.animations[0];
+
+                    if (animation.keys.Length == 1)
+                    {
+                        // Same influence for all frames
+                        var influence = animation.keys[0].values[0];
+                        foreach (var frame in mergedFrames)
+                        {
+                            influencePerFrameForTarget.Add(frame, influence);
+                        }
+                    }
+                    else
+                    {
+                        // Retreive target animation key range [min, max]
+                        var babylonAnimationKeys = new List<BabylonAnimationKey>(animation.keys);
+                        babylonAnimationKeys.Sort();
+                        var minAnimationKey = babylonAnimationKeys[0];
+                        var maxAnimationKey = babylonAnimationKeys[babylonAnimationKeys.Count - 1];
+                        
+                        foreach (var frame in mergedFrames)
+                        {
+                            // Surround the current frame with closest keys available for the target
+                            BabylonAnimationKey lowerAnimationKey = minAnimationKey;
+                            BabylonAnimationKey upperAnimationKey = maxAnimationKey;
+                            foreach (BabylonAnimationKey animationKey in animation.keys)
+                            {
+                                if (lowerAnimationKey.frame < animationKey.frame && animationKey.frame <= frame)
+                                {
+                                    lowerAnimationKey = animationKey;
+                                }
+                                if (frame <= animationKey.frame && animationKey.frame < upperAnimationKey.frame)
+                                {
+                                    upperAnimationKey = animationKey;
+                                }
+                            }
+
+                            // In case the target has a key for this frame
+                            // or the current frame is out of target animation key range
+                            if (lowerAnimationKey.frame == upperAnimationKey.frame)
+                            {
+                                influencePerFrameForTarget.Add(frame, lowerAnimationKey.values[0]);
+                            }
+                            else
+                            {
+                                // Interpolate influence values
+                                var t = 1.0f * (frame - lowerAnimationKey.frame) / (upperAnimationKey.frame - lowerAnimationKey.frame);
+                                var influence = Tools.Lerp(lowerAnimationKey.values[0], upperAnimationKey.values[0], t);
+                                influencePerFrameForTarget.Add(frame, influence);
+                            }
+                        }
+                    }
+                }
+                else
+                {
+                    // Target is not animated
+                    // Fill all frames with 0
+                    foreach (var frame in mergedFrames)
+                    {
+                        influencePerFrameForTarget.Add(frame, 0);
+                    }
+                }
+
+                // Switch from babylon to gltf storage representation
+                foreach (var frame in mergedFrames)
+                {
+                    List<float> influences = influencesPerFrame[frame];
+                    influences.Add(influencePerFrameForTarget[frame]);
+                }
+            }
+
+            return influencesPerFrame;
+        }
+
         private string _getTargetPath(string babylonProperty)
         {
             switch (babylonProperty)

+ 130 - 0
Exporters/3ds Max/Max2Babylon/Exporter/BabylonExporter.GLTFExporter.Mesh.cs

@@ -259,12 +259,142 @@ namespace Max2Babylon
                     uvs2.ForEach(n => accessorUV2s.bytesList.AddRange(BitConverter.GetBytes(n)));
                     accessorUV2s.count = globalVerticesSubMesh.Count;
                 }
+
+                // Morph targets
+                var babylonMorphTargetManager = GetBabylonMorphTargetManager(babylonScene, babylonMesh);
+                if (babylonMorphTargetManager != null)
+                {
+                    _exportMorphTargets(babylonMesh, babylonMorphTargetManager, gltf, buffer, meshPrimitive, weights);
+                }
             }
             gltfMesh.primitives = meshPrimitives.ToArray();
+            if (weights.Count > 0)
+            {
+                gltfMesh.weights = weights.ToArray();
+            }
 
             return gltfMesh;
         }
 
+        private BabylonMorphTargetManager GetBabylonMorphTargetManager(BabylonScene babylonScene, BabylonMesh babylonMesh)
+        {
+            if (babylonMesh.morphTargetManagerId.HasValue)
+            {
+                if (babylonScene.morphTargetManagers == null)
+                {
+                    RaiseWarning("GLTFExporter.Mesh | morphTargetManagers is not defined", 3);
+                }
+                else
+                {
+                    var babylonMorphTargetManager = babylonScene.morphTargetManagers.ElementAtOrDefault(babylonMesh.morphTargetManagerId.Value);
+
+                    if (babylonMorphTargetManager == null)
+                    {
+                        RaiseWarning($"GLTFExporter.Mesh | morphTargetManager with index {babylonMesh.morphTargetManagerId.Value} not found", 3);
+                    }
+                    return babylonMorphTargetManager;
+                }
+            }
+            return null;
+        }
+
+        private void _exportMorphTargets(BabylonMesh babylonMesh, BabylonMorphTargetManager babylonMorphTargetManager, GLTF gltf, GLTFBuffer buffer, GLTFMeshPrimitive meshPrimitive, List<float> weights)
+        {
+            var gltfMorphTargets = new List<GLTFMorphTarget>();
+            foreach (var babylonMorphTarget in babylonMorphTargetManager.targets)
+            {
+                var gltfMorphTarget = new GLTFMorphTarget();
+
+                // Positions
+                if (babylonMorphTarget.positions != null)
+                {
+                    var accessorTargetPositions = GLTFBufferService.Instance.CreateAccessor(
+                        gltf,
+                        GLTFBufferService.Instance.GetBufferViewFloatVec3(gltf, buffer),
+                        "accessorTargetPositions",
+                        GLTFAccessor.ComponentType.FLOAT,
+                        GLTFAccessor.TypeEnum.VEC3
+                    );
+                    gltfMorphTarget.Add(GLTFMorphTarget.Attribute.POSITION.ToString(), accessorTargetPositions.index);
+                    // Populate accessor
+                    accessorTargetPositions.min = new float[] { float.MaxValue, float.MaxValue, float.MaxValue };
+                    accessorTargetPositions.max = new float[] { float.MinValue, float.MinValue, float.MinValue };
+                    for (int indexPosition = 0; indexPosition < babylonMorphTarget.positions.Length; indexPosition += 3)
+                    {
+                        var positionTarget = _subArray(babylonMorphTarget.positions, indexPosition, 3);
+
+                        // Babylon stores morph target information as final data while glTF expects deltas from mesh primitive
+                        var positionMesh = _subArray(babylonMesh.positions, indexPosition, 3);
+                        for (int indexCoordinate = 0; indexCoordinate < positionTarget.Length; indexCoordinate++)
+                        {
+                            positionTarget[indexCoordinate] = positionTarget[indexCoordinate] - positionMesh[indexCoordinate];
+                        }
+
+                        // Store values as bytes
+                        foreach (var coordinate in positionTarget)
+                        {
+                            accessorTargetPositions.bytesList.AddRange(BitConverter.GetBytes(coordinate));
+                        }
+                        // Update min and max values
+                        GLTFBufferService.UpdateMinMaxAccessor(accessorTargetPositions, positionTarget);
+                    }
+                    accessorTargetPositions.count = babylonMorphTarget.positions.Length / 3;
+                }
+
+                // Normals
+                if (babylonMorphTarget.normals != null)
+                {
+                    var accessorTargetNormals = GLTFBufferService.Instance.CreateAccessor(
+                        gltf,
+                        GLTFBufferService.Instance.GetBufferViewFloatVec3(gltf, buffer),
+                        "accessorTargetNormals",
+                        GLTFAccessor.ComponentType.FLOAT,
+                        GLTFAccessor.TypeEnum.VEC3
+                    );
+                    gltfMorphTarget.Add(GLTFMorphTarget.Attribute.NORMAL.ToString(), accessorTargetNormals.index);
+                    // Populate accessor
+                    for (int indexNormal = 0; indexNormal < babylonMorphTarget.positions.Length; indexNormal += 3)
+                    {
+                        var normalTarget = _subArray(babylonMorphTarget.normals, indexNormal, 3);
+
+                        // Babylon stores morph target information as final data while glTF expects deltas from mesh primitive
+                        var normalMesh = _subArray(babylonMesh.normals, indexNormal, 3);
+                        for (int indexCoordinate = 0; indexCoordinate < normalTarget.Length; indexCoordinate++)
+                        {
+                            normalTarget[indexCoordinate] = normalTarget[indexCoordinate] - normalMesh[indexCoordinate];
+                        }
+
+                        // Store values as bytes
+                        foreach (var coordinate in normalTarget)
+                        {
+                            accessorTargetNormals.bytesList.AddRange(BitConverter.GetBytes(coordinate));
+                        }
+                    }
+                    accessorTargetNormals.count = babylonMorphTarget.normals.Length / 3;
+                }
+
+                if (gltfMorphTarget.Count > 0)
+                {
+                    gltfMorphTargets.Add(gltfMorphTarget);
+                    weights.Add(babylonMorphTarget.influence);
+                }
+            }
+            if (gltfMorphTargets.Count > 0)
+            {
+                meshPrimitive.targets = gltfMorphTargets.ToArray();
+            }
+        }
+
+        private float[] _subArray(float[] array, int startIndex, int count)
+        {
+            var result = new float[count];
+            for (int i = 0; i < count; i++)
+            {
+                result[i] = array[startIndex + i];
+            }
+            return result;
+        }
+
         private IPoint2 createIPoint2(float[] array, int index)
         {
             var startIndex = index * 2;

+ 2 - 2
Exporters/3ds Max/Max2Babylon/Exporter/BabylonExporter.GLTFExporter.cs

@@ -84,7 +84,7 @@ namespace Max2Babylon
                 idGroupInstance = -1
             };
             scene.NodesList.Clear(); // Only root node is listed in node list
-            GLTFNode gltfRootNode = ExportAbstractMesh(rootNode, gltf, null);
+            GLTFNode gltfRootNode = ExportAbstractMesh(rootNode, gltf, null, null);
             gltfRootNode.ChildrenList.AddRange(tmpNodesList);
 
             // Materials
@@ -254,7 +254,7 @@ namespace Max2Babylon
             if (type == typeof(BabylonAbstractMesh) ||
                 type.IsSubclassOf(typeof(BabylonAbstractMesh)))
             {
-                gltfNode = ExportAbstractMesh(babylonNode as BabylonAbstractMesh, gltf, gltfParentNode);
+                gltfNode = ExportAbstractMesh(babylonNode as BabylonAbstractMesh, gltf, gltfParentNode, babylonScene);
             }
             else if (type == typeof(BabylonCamera))
             {

+ 3 - 0
Exporters/3ds Max/Max2Babylon/Exporter/BabylonExporter.Mesh.cs

@@ -43,6 +43,9 @@ namespace Max2Babylon
             // Position / rotation / scaling / hierarchy
             exportNode(babylonMesh, meshNode, scene, babylonScene);
 
+            // Animations
+            exportAnimation(babylonMesh, meshNode);
+
             babylonScene.MeshesList.Add(babylonMesh);
 
             return babylonMesh;

+ 2 - 0
Exporters/3ds Max/Max2Babylon/Exporter/BabylonExporter.cs

@@ -167,6 +167,8 @@ namespace Max2Babylon
             var progression = 10.0f;
             ReportProgressChanged((int)progression);
             referencedMaterials.Clear();
+            // Reseting is optionnal. It makes each morph target manager export starts from id = 0.
+            BabylonMorphTargetManager.Reset();
             foreach (var maxRootNode in maxRootNodes)
             {
                 exportNodeRec(maxRootNode, babylonScene, gameScene);

+ 5 - 0
Exporters/3ds Max/Max2Babylon/Tools/Tools.cs

@@ -13,6 +13,11 @@ namespace Max2Babylon
 {
     public static class Tools
     {
+        public static float Lerp(float min, float max, float t)
+        {
+            return min + (max - min) * t;
+        }
+
         // -------------------------
         // -- IIPropertyContainer --
         // -------------------------