// *********************************************************************** // 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 objects, string filePath = null) { if (!File.Exists(filePath)) filePath = Path.Combine(Application.dataPath, MakeFileName(FileBaseName, Extension)); LastFilePath = filePath; using (var fbxExporter = Create()) { // ensure output directory exists EnsureDirectory(filePath); if (fbxExporter.ExportAll(objects) > 0) { string message = string.Format("Successfully exported: {0}", filePath); Log.Out(message); } else { Log.Warning("Nothing exported!"); } } } private static void EnsureDirectory(string path) { //check to make sure the path exists, and if it doesn't then //create all the missing directories. FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists) { Directory.CreateDirectory(fileInfo.Directory.FullName); } } }