2

I use Assimpnet to export an fbx file with a skinned mesh, skeleton and animation from Unity at runtime. The fbx file works perfectly well when imported into Blender or Maya, but when in open Unity editor to import the file the positional values on the rig when playing the animation are super messed up. It even crashes the animation preview panel in the inspector.

Unity gives some AABB errors when playing the animation which could indicate that it cannot calculate the bounds due to NaN values, but I do not know why I have these NaN values in the first place. They do not exist in Blender or Maya.

 public class ExportController : Singleton<ExportController>
    {
        public Assimp.ExportFormatDescription[] formatIds;
        public enum ExportFormat // Indices corresponding to Assimp formats
        {
            None = -1,
            FBX = 17,
            GLTF = 12,
            GLTF2 = 10,
            DAE = 0,
            Count
        }
        
        public int ticksPerSecond = 60;
        public float interval => 1.0f / ticksPerSecond;

        private readonly AssimpContext assimp = new AssimpContext();
        private Scene scene;
        private Assimp.Animation exportedAnimation;
        private int currentExportFrame;
        private bool bindPoseSet;
        
        public bool exportRequested;
        public bool exporting;
        public ExportFormat exportFormat;
        public new Animation animation;
        public float progress;

        private int fbxCallback;
        private int gltfCallback;
        private int gltf2Callback;
        private int daeCallback;

        public string outputFolder;

        private void Start()
        {
            formatIds = assimp.GetSupportedExportFormats();

            fbxCallback = EventHandler.Instance.AddListener(EventHandler.UIEvent.RequestFBXExport, () => OnRequestExport(ExportFormat.FBX));
            gltfCallback = EventHandler.Instance.AddListener(EventHandler.UIEvent.RequestGLTFExport, () => OnRequestExport(ExportFormat.GLTF));
            gltf2Callback = EventHandler.Instance.AddListener(EventHandler.UIEvent.RequestGLTF2Export, () => OnRequestExport(ExportFormat.GLTF2));
            daeCallback = EventHandler.Instance.AddListener(EventHandler.UIEvent.RequestCOLLADAExport, () => OnRequestExport(ExportFormat.DAE));

            outputFolder = UserSettings.Instance.modelExportPath.GetValue();
        }

        private void OnDisable()
        {
            if (EventHandler.Instance)
            {
                EventHandler.Instance.RemoveListener(EventHandler.UIEvent.RequestFBXExport, fbxCallback);
                EventHandler.Instance.RemoveListener(EventHandler.UIEvent.RequestGLTFExport, gltfCallback);
                EventHandler.Instance.RemoveListener(EventHandler.UIEvent.RequestGLTF2Export, gltf2Callback);
                EventHandler.Instance.RemoveListener(EventHandler.UIEvent.RequestCOLLADAExport, daeCallback);
            }
        }

        private void Update()
        {
            if (exportRequested)
            {
                animation = RigReferences.Instance.effectorParent.GetComponent<Animation>();
                InitializeExport();
                exportRequested = false;
                exporting = true;
            }
            else if (exporting)
            {
                progress = animation.currentTime * ticksPerSecond / (float) exportedAnimation.DurationInTicks;
                SampleNextFrameOfAnimation();
                ExportFrame();
                IncrementTick();
                CheckIfExportFinished();
            }
        }

        private void OnRequestExport(ExportFormat exportFormat)
        {
            this.exportFormat = exportFormat;
            exportRequested = true;
        }

        private void InitializeExport()
        { 
            EventHandler.Instance.InvokeEvent(EventHandler.UIEvent.RequestExitMotionCapture);
            outputFolder = UserSettings.Instance.modelExportPath.GetValue();
            animation.playing = false;
            animation.currentTime = 0;
            currentExportFrame = 0;
            progress = 0;

            bindPoseSet = false;
            
            ValidateFileExists(RigReferences.Instance.importPath);
            scene = assimp.ImportFile(RigReferences.Instance.importPath, PostProcessSteps.OptimizeGraph 
                                                                         | PostProcessSteps.OptimizeMeshes 
                                                                         | PostProcessSteps.GlobalScale 
                                                                         | PostProcessSteps.LimitBoneWeights 
                                                                         | PostProcessSteps.JoinIdenticalVertices
                                                                         | PostProcessSteps.ValidateDataStructure);
            
            /* METADATA */
            scene.RootNode.Metadata["FrameRate"] = new Metadata.Entry(MetaDataType.Int32, ticksPerSecond);
            scene.RootNode.Metadata["FrontAxisSign"] = new Metadata.Entry(MetaDataType.Int32, -1);
            scene.RootNode.Metadata["OriginalUnitScaleFactor"] = new Metadata.Entry(MetaDataType.Int32, 100);
            scene.RootNode.Metadata["UnitScaleFactor"] = new Metadata.Entry(MetaDataType.Int32, 1);
            /* METADATA */
            exportedAnimation = new Assimp.Animation
            {
                Name = "SwiftAnimation clip",
                TicksPerSecond = ticksPerSecond,
                DurationInTicks = animation.duration * ticksPerSecond,
            };
            scene.Animations.Clear();
            scene.Animations.Add(exportedAnimation);
            
            animation.requestTPose.Invoke();
            
            EventHandler.Instance.InvokeEvent(EventHandler.SystemEvent.ExportInitialized);
        }

        private static void ValidateFileExists(string path)
        {
            if (!File.Exists(path))
            {
                throw new AssimpException("File for imported animation must exist!");
            }
        }

        private void SampleNextFrameOfAnimation()
        {
            foreach (AnimationChannel animationChannel in animation.animationChannels)
            {
                animationChannel.clip.SampleAnimation(animationChannel.joint.gameObject, animation.currentTime);
            }
        }

        private void IncrementTick()
        {
            animation.currentTime += interval;
        }

        private void CheckIfExportFinished()
        {
            if (animation.currentTime >= animation.duration)
            {
                ValidateScene(scene);
                WriteToFile();
                EventHandler.Instance.InvokeEvent(EventHandler.SystemEvent.ExportEnded);
            }
        }

        private void ExportFrame()
        {
            ExportJoint(RigReferences.Instance.rootJoint);
            currentExportFrame++;
        }

        private void ExportJoint(Transform joint)
        {
            /*
             * For the root node (hips), set scale, world position and world rotations
             * Ignore leaves
             * For other joints set local rotation
             * Start by setting default position and scale keys
             */
            foreach (Transform child in joint.transform)
            {
                ExportJoint(child);
            }

            if (joint.childCount == 0) return; // Don't animate leaves

            bool isRoot = joint == RigReferences.Instance.rootJoint;

            NodeAnimationChannel animationChannel = exportedAnimation.NodeAnimationChannels.Find(channel =>
            channel.NodeName == joint.name);

            if (animationChannel == null)
            {
                Debug.Log("Setting up animation channel for: " + joint.name, joint);
                // If we add an animation channel for a non existent node assimp crashes
                bool existsInAssimpAnimation = false;
                Stack<Node> unprocessedNodes = new Stack<Node>();
                unprocessedNodes.Push(scene.RootNode);
                while (unprocessedNodes.Count > 0 && !existsInAssimpAnimation)
                {
                    Node node = unprocessedNodes.Pop();

                    if (node.Name == joint.name) existsInAssimpAnimation = true;

                    foreach (Node child in node.Children)
                    {
                        unprocessedNodes.Push(child);
                    }
                }

                if (!existsInAssimpAnimation)
                {
                    Debug.LogError("Failed to find animation channel in Assimp model for " + joint.name);
                    return;
                }
                
                animationChannel = new NodeAnimationChannel
                {
                    NodeName = joint.name
                };
                
                exportedAnimation.NodeAnimationChannels.Add(animationChannel);
            }
            
            Quaternion r = joint.transform.localRotation;
            Vector3 rEul = new Vector3();
            
            if (isRoot)
            {
                // World rotation
                r = joint.transform.rotation;
                rEul = r.eulerAngles;
                rEul.y *= -1;
                rEul.z *= -1;
            }
            else
            {
                // Local rotation
                rEul = r.eulerAngles;
                // LEFTHAND/RIGHTHAND FIX
                rEul.y *= -1;
                rEul.x *= -1;
            }
            
            r = Quaternion.Euler(rEul);

            QuaternionKey quaternionKey =
            new QuaternionKey(animation.currentTime, new Assimp.Quaternion(r.w, r.x, r.y, r.z));
            animationChannel.RotationKeys.Add(quaternionKey);
            
            Vector3 p = joint.transform.localPosition;
            VectorKey positionKey;
  
            if (isRoot)
            {
                p = joint.position * IOConstants.UnityScaleFactor;
                p.x *= -1;
                
                positionKey = new VectorKey(animation.currentTime, new Vector3D(p.x, p.y, p.z));
                animationChannel.PositionKeys.Add(positionKey);
            }
            if (currentExportFrame != 0) return;

            if (!isRoot) // Only one position for all joints, except root
            {
                p.z *= -1;
                
                positionKey = new VectorKey(animation.currentTime, new Vector3D(p.x, p.y, p.z));
                animationChannel.PositionKeys.Add(positionKey);
            }
            
            // Only one scale key for all joints
            Vector3 s = Vector3.one;
            VectorKey scaleKey = new VectorKey(animation.currentTime, new Vector3D(s.x, s.y, s.z));
            animationChannel.ScalingKeys.Add(scaleKey);
        }

        private static void ValidateScene(Scene scene)
        {
            if (scene.HasAnimations || scene.AnimationCount > 0 || scene.Animations.Count > 0)
            {
                foreach (Assimp.Animation animation in scene.Animations)
                {
                    if (!animation.HasMeshAnimations && !animation.HasNodeAnimations)
                    {
                        throw new AssimpException("Exported Assimp animations cannot be empty!");
                    }
                }
            }
        }

        private void WriteToFile()
        {
            string outputPath = RenameFilenameIfNecessary(Path.Combine(outputFolder, Path.GetFileName(RigReferences.Instance.importPath)));
            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
            foreach (KeyValuePair<string, Metadata.Entry> keyValuePair in scene.Metadata)
            {
                Debug.Log(keyValuePair.Key + ": " + keyValuePair.Value);
            }
            foreach (KeyValuePair<string, Metadata.Entry> keyValuePair in scene.RootNode.Metadata)
            {
                Debug.Log(keyValuePair.Key + ": " + keyValuePair.Value);
            }
            
            assimp.ExportFile(scene, outputPath, formatIds[(int)exportFormat].FormatId);
            exporting = false;

            Debug.Log("Exported animation to: " + outputPath);
        }

        private static string RenameFilenameIfNecessary(string path)
        {
            int count = 1;

            string filenameWithoutExtension = Path.GetFileNameWithoutExtension(path);
            string extension = Path.GetExtension(path);
            string directory = Path.GetDirectoryName(path);
            string renamedPath = path;

            while (File.Exists(renamedPath) && count < int.MaxValue)
            {
                string renamedFilename = $"{filenameWithoutExtension} ({count++}){extension}";
                renamedPath = directory == null ? renamedFilename : Path.Combine(directory, renamedFilename);
            }

            return renamedPath;
        }
    }
Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
Miss_Jones
  • 21
  • 3

0 Answers0