// ***********************************************************************
// Copyright (c) 2017 Unity Technologies. All rights reserved.
//
// Licensed under the ##LICENSENAME##.
// See LICENSE.md file in the project root for full license information.
// ***********************************************************************
using Autodesk.Fbx;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class FbxExporter07 : System.IDisposable
{
const string Title =
"Example 07: exporting a skinned mesh with bones";
const string Subject =
@"Example FbxExporter07 illustrates how to:
1) create and initialize an exporter
2) create a scene
3) create a skeleton
4) exported mesh
5) bind mesh to skeleton
6) create a bind pose
7) export the skinned mesh to a FBX file (FBX201400 compatible)
";
const string Keywords =
"export skeleton mesh skin cluster pose";
const string Comments =
"";
const string MenuItemName = "File/Export FBX/7. Skinned mesh with bones";
const string FileBaseName = "example_skinned_mesh_with_bones";
///
/// Create instance of example
///
public static FbxExporter07 Create() { return new FbxExporter07(); }
///
/// Export GameObject's as a skinned mesh with bones
///
protected void ExportSkinnedMesh(Animator unityAnimator, FbxScene fbxScene, FbxNode fbxParentNode)
{
GameObject unityGo = unityAnimator.gameObject;
SkinnedMeshRenderer unitySkin = unityGo.GetComponentInChildren();
if (unitySkin == null)
{
Log.Error("could not find skinned mesh");
return;
}
var meshInfo = GetSkinnedMeshInfo(unitySkin.gameObject);
if (meshInfo.renderer == null)
{
Log.Error("mesh has no renderer");
return;
}
// create an FbxNode and add it as a child of fbxParentNode
FbxNode fbxNode = FbxNode.Create(fbxScene, meshInfo.unityObject.name);
SetNodeMatrix(fbxNode, meshInfo.unityObject.transform);
Dictionary boneNodes = new Dictionary();
// export skeleton
if (ExportSkeleton(meshInfo, fbxScene, fbxNode, boneNodes, out FbxNode meshNode))
{
// export skin
FbxNode fbxMeshNode = ExportMesh(meshInfo, fbxScene, fbxNode, meshNode);
FbxMesh fbxMesh = fbxMeshNode.GetMesh();
if (fbxMesh == null)
{
Log.Error("Could not find mesh");
return;
}
// bind mesh to skeleton
ExportSkin(meshInfo, fbxScene, fbxMesh, fbxParentNode, boneNodes);
// add bind pose
ExportBindPose(fbxNode, fbxMeshNode, fbxScene, boneNodes);
fbxParentNode.AddChild(fbxNode);
NumNodes++;
if (Verbose)
Log.Out(string.Format("exporting {0} {1}", "Skin", fbxNode.GetName()));
}
else
{
Log.Error("failed to export skeleton");
}
}
///
/// Export bones of skinned mesh
///
protected bool ExportSkeleton(MeshInfo meshInfo, FbxScene fbxScene, FbxNode fbxParentNode, Dictionary boneNodes, out FbxNode meshNode)
{
SkinnedMeshRenderer unitySkinnedMeshRenderer = meshInfo.renderer as SkinnedMeshRenderer;
meshNode = null;
if (unitySkinnedMeshRenderer.bones.Length <= 0)
{
return false;
}
Dictionary boneBindPose = new Dictionary();
for (int boneIndex = 0; boneIndex < unitySkinnedMeshRenderer.bones.Length; boneIndex++)
{
Transform unityBoneTransform = unitySkinnedMeshRenderer.bones[boneIndex];
FbxNode fbxBoneNode = FbxNode.Create(fbxScene, unityBoneTransform.name);
// Create the node's attributes
FbxSkeleton fbxSkeleton = FbxSkeleton.Create(fbxScene, unityBoneTransform.name + "_Skel");
var fbxSkeletonType = FbxSkeleton.EType.eLimbNode;
if (unityBoneTransform == unityBoneTransform.root || fbxParentNode.GetName().Equals(unityBoneTransform.parent.name))
{
fbxSkeletonType = FbxSkeleton.EType.eRoot;
}
fbxSkeleton.SetSkeletonType(fbxSkeletonType);
fbxSkeleton.Size.Set(1.0f);
// Set the node's attribute
fbxBoneNode.SetNodeAttribute(fbxSkeleton);
boneBindPose.Add(unityBoneTransform, meshInfo.BindPoses[boneIndex]);
// save relatation between unity transform and fbx bone node for skinning
boneNodes[unityBoneTransform] = fbxBoneNode;
}
Transform root = meshInfo.unityObject.transform;
Dictionary dict_empty_parents = new Dictionary();
// set the hierarchy for the FbxNodes
foreach (KeyValuePair t in boneNodes)
{
Matrix4x4 pose;
// if this is a root node then don't need to do anything
if (t.Key == t.Key.root)
{
fbxParentNode.AddChild(t.Value);
pose = boneBindPose[t.Key].inverse; // assuming parent is identity matrix
SetBoneMatrix(t.Value, pose);
}
else if (!boneNodes.ContainsKey(t.Key.parent))
{
Transform parent = t.Key.parent, cur = t.Key;
FbxNode parentNode = null, curNode = t.Value;
//pose = GetLocalMatrix(cur); // localToWorld
while (parent != root)
{
if (!boneNodes.TryGetValue(parent, out parentNode))
{
if (dict_empty_parents.TryGetValue(parent, out parentNode))
{
parentNode.AddChild(curNode);
pose = GetLocalMatrix(cur, true);
SetBoneMatrix(curNode, pose);
//if (boneNodes.ContainsKey(cur))
//{
// pose = GetLocalMatrix(cur, true);
// SetBoneMatrix(curNode, pose);
//}
//else
//{
// SetNodeMatrix(curNode, cur);
//}
break;
}
else
{
parentNode = FbxNode.Create(fbxScene, parent.name);
parentNode.AddChild(curNode);
pose = GetLocalMatrix(cur, true);
SetBoneMatrix(curNode, pose);
//if (boneNodes.ContainsKey(cur))
//{
// pose = GetLocalMatrix(cur, true);
// SetBoneMatrix(curNode, pose);
//}
//else
//{
// SetNodeMatrix(curNode, cur);
//}
if (parent.TryGetComponent(out _))
meshNode = parentNode;
dict_empty_parents.Add(parent, parentNode);
cur = parent;
parent = parent.parent;
curNode = parentNode;
parentNode = null;
}
}
else
{
parentNode.AddChild(curNode);
pose = GetLocalMatrix(cur, true);
SetBoneMatrix(curNode, pose);
//if (boneNodes.ContainsKey(cur))
//{
// pose = GetLocalMatrix(cur, true);
// SetBoneMatrix(curNode, pose);
//}
//else
//{
// SetNodeMatrix(curNode, cur);
//}
break;
}
}
if (parent == root)
{
if (parent.TryGetComponent(out _))
meshNode = fbxParentNode;
fbxParentNode.AddChild(curNode);
if (boneNodes.ContainsKey(cur))
{
pose = boneBindPose[cur].inverse;
//pose = boneBindPose[parent] * boneBindPose[cur].inverse;
SetBoneMatrix(curNode, pose);
}
else
{
SetNodeMatrix(curNode, cur);
}
}
}
else
{
boneNodes[t.Key.parent].AddChild(t.Value);
// inverse of my bind pose times parent bind pose
pose = boneBindPose[t.Key.parent] * boneBindPose[t.Key].inverse;
SetBoneMatrix(t.Value, pose);
}
}
return true;
}
private Matrix4x4 GetLocalMatrix(Transform t, bool inverse)
{
//var matrix = Matrix4x4.TRS(t.localPosition, t.localRotation, t.localScale);
var matrix = t.worldToLocalMatrix * t.parent.localToWorldMatrix;
return inverse ? matrix.inverse : matrix;
}
private void SetBoneMatrix(FbxNode node, Matrix4x4 pose)
{
// use FbxMatrix to get translation and rotation relative to parent
FbxMatrix matrix = new FbxMatrix();
matrix.SetColumn(0, new FbxVector4(pose.GetRow(0).x, pose.GetRow(0).y, pose.GetRow(0).z, pose.GetRow(0).w));
matrix.SetColumn(1, new FbxVector4(pose.GetRow(1).x, pose.GetRow(1).y, pose.GetRow(1).z, pose.GetRow(1).w));
matrix.SetColumn(2, new FbxVector4(pose.GetRow(2).x, pose.GetRow(2).y, pose.GetRow(2).z, pose.GetRow(2).w));
matrix.SetColumn(3, new FbxVector4(pose.GetRow(3).x, pose.GetRow(3).y, pose.GetRow(3).z, pose.GetRow(3).w));
FbxVector4 translation, rotation, shear, scale;
double sign;
matrix.GetElements(out translation, out rotation, out shear, out scale, out sign);
// Negating the x value of the translation, and the y and z values of the prerotation
// to convert from Unity to Maya coordinates (left to righthanded)
node.LclTranslation.Set(new FbxDouble3(-translation.X, translation.Y, translation.Z));
node.LclRotation.Set(new FbxDouble3(0, 0, 0));
node.LclScaling.Set(new FbxDouble3(scale.X, scale.Y, scale.Z));
node.SetRotationActive(true);
node.SetPivotState(FbxNode.EPivotSet.eSourcePivot, FbxNode.EPivotState.ePivotReference);
node.SetPreRotation(FbxNode.EPivotSet.eSourcePivot, new FbxVector4(rotation.X, -rotation.Y, -rotation.Z));
}
private void SetNodeMatrix(FbxNode node, Transform unityTransform)
{
// get local position of fbxNode (from Unity)
UnityEngine.Vector3 unityTranslate = unityTransform.localPosition;
UnityEngine.Vector3 unityRotate = unityTransform.localRotation.eulerAngles;
UnityEngine.Vector3 unityScale = unityTransform.localScale;
// transfer transform data from Unity to Fbx
// Negating the x value of the translation, and the y and z values of the rotation
// to convert from Unity to Maya coordinates (left to righthanded)
var fbxTranslate = new FbxDouble3(-unityTranslate.x, unityTranslate.y, unityTranslate.z);
var fbxRotate = new FbxDouble3(unityRotate.x, -unityRotate.y, -unityRotate.z);
var fbxScale = new FbxDouble3(unityScale.x, unityScale.y, unityScale.z);
// set the local position of fbxNode
node.LclTranslation.Set(fbxTranslate);
node.LclRotation.Set(fbxRotate);
node.LclScaling.Set(fbxScale);
}
///
/// Export binding of mesh to skeleton
///
protected void ExportSkin(MeshInfo meshInfo, FbxScene fbxScene, FbxMesh fbxMesh, FbxNode fbxRootNode, Dictionary boneNodes)
{
SkinnedMeshRenderer unitySkinnedMeshRenderer
= meshInfo.renderer as SkinnedMeshRenderer;
FbxSkin fbxSkin = FbxSkin.Create(fbxScene, (meshInfo.unityObject.name + "_Skin"));
FbxAMatrix fbxMeshMatrix = fbxRootNode.EvaluateGlobalTransform();
// keep track of the bone index -> fbx cluster mapping, so that we can add the bone weights afterwards
Dictionary boneCluster = new Dictionary();
for (int i = 0; i < unitySkinnedMeshRenderer.bones.Length; i++)
{
FbxNode fbxBoneNode = boneNodes[unitySkinnedMeshRenderer.bones[i]];
// Create the deforming cluster
FbxCluster fbxCluster = FbxCluster.Create(fbxScene, "BoneWeightCluster");
fbxCluster.SetLink(fbxBoneNode);
fbxCluster.SetLinkMode(FbxCluster.ELinkMode.eTotalOne);
boneCluster.Add(i, fbxCluster);
// set the Transform and TransformLink matrix
fbxCluster.SetTransformMatrix(fbxMeshMatrix);
FbxAMatrix fbxLinkMatrix = fbxBoneNode.EvaluateGlobalTransform();
fbxCluster.SetTransformLinkMatrix(fbxLinkMatrix);
// add the cluster to the skin
fbxSkin.AddCluster(fbxCluster);
}
// set the vertex weights for each bone
SetVertexWeights(meshInfo, boneCluster);
// Add the skin to the mesh after the clusters have been added
fbxMesh.AddDeformer(fbxSkin);
}
///
/// set weight vertices to cluster
///
protected void SetVertexWeights(MeshInfo meshInfo, Dictionary boneCluster)
{
// set the vertex weights for each bone
for (int i = 0; i < meshInfo.BoneWeights.Length; i++)
{
var boneWeights = meshInfo.BoneWeights;
int[] indices = {
boneWeights [i].boneIndex0,
boneWeights [i].boneIndex1,
boneWeights [i].boneIndex2,
boneWeights [i].boneIndex3
};
float[] weights = {
boneWeights [i].weight0,
boneWeights [i].weight1,
boneWeights [i].weight2,
boneWeights [i].weight3
};
for (int j = 0; j < indices.Length; j++)
{
if (weights[j] <= 0)
{
continue;
}
if (!boneCluster.ContainsKey(indices[j]))
{
continue;
}
boneCluster[indices[j]].AddControlPointIndex(i, weights[j]);
}
}
}
///
/// Export bind pose of mesh to skeleton
///
protected void ExportBindPose(FbxNode fbxRootNode, FbxNode meshNode, FbxScene fbxScene, Dictionary boneNodes)
{
FbxPose fbxPose = FbxPose.Create(fbxScene, fbxRootNode.GetName());
// set as bind pose
fbxPose.SetIsBindPose(true);
// assume each bone node has one weighted vertex cluster
foreach (FbxNode fbxNode in boneNodes.Values)
{
// EvaluateGlobalTransform returns an FbxAMatrix (affine matrix)
// which has to be converted to an FbxMatrix so that it can be passed to fbxPose.Add().
// The hierarchy for FbxMatrix and FbxAMatrix is as follows:
//
// FbxDouble4x4
// / \
// FbxMatrix FbxAMatrix
//
// Therefore we can't convert directly from FbxAMatrix to FbxMatrix,
// however FbxMatrix has a constructor that takes an FbxAMatrix.
FbxMatrix fbxBindMatrix = new FbxMatrix(fbxNode.EvaluateGlobalTransform());
fbxPose.Add(fbxNode, fbxBindMatrix);
}
FbxMatrix bindMatrix = new FbxMatrix(meshNode.EvaluateGlobalTransform());
fbxPose.Add(meshNode, bindMatrix);
// add the pose to the scene
fbxScene.AddPose(fbxPose);
}
///
/// Unconditionally export this mesh object to the file.
/// We have decided; this mesh is definitely getting exported.
///
public FbxNode ExportMesh(MeshInfo meshInfo, FbxScene fbxScene, FbxNode fbxNode, FbxNode meshNode)
{
if (!meshInfo.IsValid)
{
Log.Error("Invalid mesh info");
return null;
}
// create a node for the mesh
if (meshNode == null)
{
meshNode = FbxNode.Create(fbxScene, "geo");
fbxNode.AddChild(meshNode);
}
// create the mesh structure.
FbxMesh fbxMesh = FbxMesh.Create(fbxScene, "Mesh");
// Create control points.
int NumControlPoints = meshInfo.VertexCount;
fbxMesh.InitControlPoints(NumControlPoints);
// copy control point data from Unity to FBX
for (int v = 0; v < NumControlPoints; v++)
{
// convert from left to right-handed by negating x (Unity negates x again on import)
fbxMesh.SetControlPointAt(new FbxVector4(-meshInfo.Vertices[v].x, meshInfo.Vertices[v].y, meshInfo.Vertices[v].z), v);
}
/*
* Create polygons
* Triangles have to be added in reverse order,
* or else they will be inverted on import
* (due to the conversion from left to right handed coords)
*/
for (int f = 0; f < meshInfo.Triangles.Length / 3; f++)
{
fbxMesh.BeginPolygon();
fbxMesh.AddPolygon(meshInfo.Triangles[3 * f + 2]);
fbxMesh.AddPolygon(meshInfo.Triangles[3 * f + 1]);
fbxMesh.AddPolygon(meshInfo.Triangles[3 * f]);
fbxMesh.EndPolygon();
}
// set the fbxNode containing the mesh
meshNode.SetNodeAttribute(fbxMesh);
meshNode.SetShadingMode(FbxNode.EShadingMode.eWireFrame);
return meshNode;
}
protected void ExportComponents(GameObject unityGo, FbxScene fbxScene, FbxNode fbxParentNode)
{
Animator unityAnimator = unityGo.GetComponent();
Log.Out($"Exporting Components: {unityGo.name}, animator: {(unityAnimator == null ? null : unityAnimator.ToString())}");
if (unityAnimator == null)
return;
ExportSkinnedMesh(unityAnimator, fbxScene, fbxParentNode);
return;
}
///
/// Export all the objects in the set.
/// Return the number of objects in the set that we exported.
///
public int ExportAll(IEnumerable unityExportSet)
{
// Create the FBX manager
using (var fbxManager = FbxManager.Create())
{
// Configure IO settings.
fbxManager.SetIOSettings(FbxIOSettings.Create(fbxManager, Globals.IOSROOT));
// Create the exporter
var fbxExporter = FbxExporter.Create(fbxManager, "Exporter");
// Initialize the exporter.
int fileFormat = -1;
fileFormat = fbxManager.GetIOPluginRegistry().FindWriterIDByDescription("FBX ascii (*.fbx)");
bool status = fbxExporter.Initialize(LastFilePath, fileFormat, fbxManager.GetIOSettings());
// Check that initialization of the fbxExporter was successful
if (!status)
{
Log.Error("failed to initialize exporter");
return 0;
}
// By default, FBX exports in its most recent version. You might want to specify
// an older version for compatibility with other applications.
fbxExporter.SetFileExportVersion("FBX201400");
// Create a scene
var fbxScene = FbxScene.Create(fbxManager, "Scene");
// create scene info
FbxDocumentInfo fbxSceneInfo = FbxDocumentInfo.Create(fbxManager, "SceneInfo");
// set some scene info values
fbxSceneInfo.mTitle = Title;
fbxSceneInfo.mSubject = Subject;
fbxSceneInfo.mAuthor = "Unity Technologies";
fbxSceneInfo.mRevision = "1.0";
fbxSceneInfo.mKeywords = Keywords;
fbxSceneInfo.mComment = Comments;
fbxScene.SetSceneInfo(fbxSceneInfo);
var fbxSettings = fbxScene.GetGlobalSettings();
fbxSettings.SetSystemUnit(FbxSystemUnit.m); // Unity unit is meters
// The Unity axis system has Y up, Z forward, X to the right (left handed system with odd parity).
// The Maya axis system has Y up, Z forward, X to the left (right handed system with odd parity).
// We need to export right-handed for Maya because ConvertScene can't switch handedness:
// https://forums.autodesk.com/t5/fbx-forum/get-confused-with-fbxaxissystem-convertscene/td-p/4265472
fbxSettings.SetAxisSystem(FbxAxisSystem.MayaYUp);
FbxNode fbxRootNode = fbxScene.GetRootNode();
// export set of objects
foreach (var obj in unityExportSet)
{
var unityGo = GetGameObject(obj);
if (unityGo)
{
this.ExportComponents(unityGo, fbxScene, fbxRootNode);
}
}
// Export the scene to the file.
status = fbxExporter.Export(fbxScene);
// cleanup
fbxScene.Destroy();
fbxExporter.Destroy();
return status == true ? NumNodes : 0;
}
}
///
/// Number of nodes exported including siblings and decendents
///
public int NumNodes { private set; get; }
///
/// Clean up this class on garbage collection
///
public void Dispose() { }
static bool Verbose { get { return true; } }
const string NamePrefix = "";
///
/// manage the selection of a filename
///
static string LastFilePath { get; set; }
const string Extension = "fbx";
///
///Information about the mesh that is important for exporting.
///
public struct MeshInfo
{
///
/// The transform of the mesh.
///
public Matrix4x4 xform;
public Mesh mesh;
public Renderer renderer;
///
/// The gameobject in the scene to which this mesh is attached.
/// This can be null: don't rely on it existing!
///
public GameObject unityObject;
///
/// Return true if there's a valid mesh information
///
/// The vertex count.
public bool IsValid { get { return mesh != null; } }
///
/// Gets the vertex count.
///
/// The vertex count.
public int VertexCount { get { return mesh.vertexCount; } }
///
/// Gets the triangles. Each triangle is represented as 3 indices from the vertices array.
/// Ex: if triangles = [3,4,2], then we have one triangle with vertices vertices[3], vertices[4], and vertices[2]
///
/// The triangles.
public int[] Triangles { get { return mesh.triangles; } }
///
/// Gets the vertices, represented in local coordinates.
///
/// The vertices.
public Vector3[] Vertices { get { return mesh.vertices; } }
///
/// Gets the normals for the vertices.
///
/// The normals.
public Vector3[] Normals { get { return mesh.normals; } }
///
/// Gets the uvs.
///
/// The uv.
public Vector2[] UV { get { return mesh.uv; } }
public BoneWeight[] BoneWeights { get { return mesh.boneWeights; } }
public Matrix4x4[] BindPoses { get { return mesh.bindposes; } }
///
/// Initializes a new instance of the struct.
///
/// The GameObject the mesh is attached to.
/// A mesh we want to export
public MeshInfo(GameObject gameObject, Mesh mesh, Renderer renderer)
{
this.renderer = renderer;
this.mesh = mesh;
this.xform = gameObject.transform.localToWorldMatrix;
this.unityObject = gameObject;
}
}
///
/// Get a mesh renderer's mesh.
///
private MeshInfo GetSkinnedMeshInfo(GameObject gameObject)
{
// Verify that we are rendering. Otherwise, don't export.
var renderer = gameObject.GetComponentInChildren();
if (!renderer)
{
Log.Error("could not find renderer");
return new MeshInfo();
}
var mesh = renderer.sharedMesh;
if (!mesh)
{
Log.Error("Could not find mesh");
return new MeshInfo();
}
return new MeshInfo(gameObject, mesh, renderer);
}
///
/// Get the GameObject
///
private static GameObject GetGameObject(Object obj)
{
if (obj is UnityEngine.Transform)
{
var xform = obj as UnityEngine.Transform;
return xform.gameObject;
}
else if (obj is UnityEngine.GameObject)
{
return obj as UnityEngine.GameObject;
}
else if (obj is Component)
{
var mono = obj as Component;
return mono.gameObject;
}
return null;
}
private static string MakeFileName(string basename = "test", string extension = "fbx")
{
return basename + "." + extension;
}
// use the SaveFile panel to allow user to enter a file name
public static void OnExport(IEnumerable