- Home /
How to export an fbx file with assimp that can be used in Unity?
I use Assimpnet to export an fbx file with a skinned mesh, skeleton and animation from 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 fucked 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.
Can anyone help me out?
This is the export code:
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 = "Animation 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;
}
}
Your answer
Follow this Question
Related Questions
Multiple Cars not working 1 Answer
Renaming an animation from FBX format 2 Answers
Importing animation from 3DS Max into Unity problem 3 Answers