Rasagar/Library/PackageCache/com.unity.formats.fbx/Editor/FbxExporter.cs
2024-08-30 17:33:54 +03:00

5012 lines
201 KiB
C#

using System.IO;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Timeline;
using System.Linq;
using Autodesk.Fbx;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using UnityEditor.Formats.Fbx.Exporter.Visitors;
using UnityEngine.Playables;
[assembly: InternalsVisibleTo("Unity.Formats.Fbx.Editor.Tests")]
[assembly: InternalsVisibleTo("Unity.ProBuilder.AddOns.Editor")]
namespace UnityEditor.Formats.Fbx.Exporter
{
/// <summary>
/// If your MonoBehaviour knows about some custom geometry that
/// isn't in a MeshFilter or SkinnedMeshRenderer, use
/// RegisterMeshCallback to get a callback when the exporter tries
/// to export your component.
///
/// The callback should return true, and output the mesh you want.
///
/// Return false if you don't want to drive this game object.
///
/// Return true and output a null mesh if you don't want the
/// exporter to output anything.
/// </summary>
internal delegate bool GetMeshForComponent<T>(ModelExporter exporter, T component, FbxNode fbxNode) where T : MonoBehaviour;
internal delegate bool GetMeshForComponent(ModelExporter exporter, MonoBehaviour component, FbxNode fbxNode);
/// <summary>
/// Delegate used to convert a GameObject into a mesh.
///
/// This is useful if you want to have broader control over
/// the export process than the GetMeshForComponent callbacks
/// provide. But it's less efficient because you'll get a callback
/// on every single GameObject.
/// </summary>
internal delegate bool GetMeshForObject(ModelExporter exporter, GameObject gameObject, FbxNode fbxNode);
[System.Serializable]
internal class ModelExportException : System.Exception
{
public ModelExportException() {}
public ModelExportException(string message)
: base(message) {}
public ModelExportException(string message, System.Exception inner)
: base(message, inner) {}
protected ModelExportException(SerializationInfo info, StreamingContext context)
: base(info, context) {}
}
/// <summary>
/// Use the ModelExporter class to export Unity GameObjects to an FBX file.
/// <para>
/// Use the ExportObject and ExportObjects methods. The default export
/// options are used when exporting the objects to the FBX file.
/// </para>
/// <para>For information on using the ModelExporter class, see <a href="index.html">the Developer's Guide</a>.</para>
/// </summary>
public sealed class ModelExporter
{
const string Title =
"Created by FBX Exporter from Unity Technologies";
const string Subject =
"";
const string Keywords =
"Nodes Meshes Materials Textures Cameras Lights Skins Animation";
const string Comments =
@"";
/// <summary>
/// Path to the CHANGELOG file in Unity's virtual file system. Used to get the version number.
/// </summary>
const string ChangeLogPath = "Packages/com.unity.formats.fbx/CHANGELOG.md";
// NOTE: The ellipsis at the end of the Menu Item name prevents the context
// from being passed to command, thus resulting in OnContextItem()
// being called only once regardless of what is selected.
const string MenuItemName = "GameObject/Export To FBX...";
const string ProgressBarTitle = "FBX Export";
const char MayaNamespaceSeparator = ':';
// replace invalid chars with this one
const char InvalidCharReplacement = '_';
const string RegexCharStart = "[";
const string RegexCharEnd = "]";
internal const float UnitScaleFactor = 100f;
internal const string PACKAGE_UI_NAME = "FBX Exporter";
/// <summary>
/// name of the scene's default camera
/// </summary>
private static string DefaultCamera = "";
private const string SkeletonPrefix = "_Skel";
private const string SkinPrefix = "_Skin";
/// <summary>
/// name prefix for custom properties
/// </summary>
const string NamePrefix = "Unity_";
private static string MakeName(string basename)
{
return NamePrefix + basename;
}
/// <summary>
/// Create instance of exporter.
/// </summary>
static ModelExporter Create()
{
return new ModelExporter();
}
/// <summary>
/// Which components map from Unity Object to Fbx Object
/// </summary>
internal enum FbxNodeRelationType
{
NodeAttribute,
Property,
Material
}
internal static Dictionary<System.Type, KeyValuePair<System.Type, FbxNodeRelationType>> MapsToFbxObject = new Dictionary<System.Type, KeyValuePair<System.Type, FbxNodeRelationType>>()
{
{ typeof(Transform), new KeyValuePair<System.Type, FbxNodeRelationType>(typeof(FbxProperty), FbxNodeRelationType.Property) },
{ typeof(MeshFilter), new KeyValuePair<System.Type, FbxNodeRelationType>(typeof(FbxMesh), FbxNodeRelationType.NodeAttribute) },
{ typeof(SkinnedMeshRenderer), new KeyValuePair<System.Type, FbxNodeRelationType>(typeof(FbxMesh), FbxNodeRelationType.NodeAttribute) },
{ typeof(Light), new KeyValuePair<System.Type, FbxNodeRelationType>(typeof(FbxLight), FbxNodeRelationType.NodeAttribute) },
{ typeof(Camera), new KeyValuePair<System.Type, FbxNodeRelationType>(typeof(FbxCamera), FbxNodeRelationType.NodeAttribute) },
{ typeof(Material), new KeyValuePair<System.Type, FbxNodeRelationType>(typeof(FbxSurfaceMaterial), FbxNodeRelationType.Material) },
};
/// <summary>
/// keep a map between GameObject and FbxNode for quick lookup when we export
/// animation.
/// </summary>
Dictionary<GameObject, FbxNode> MapUnityObjectToFbxNode = new Dictionary<GameObject, FbxNode>();
/// <summary>
/// keep a map between the constrained FbxNode (in Unity this is the GameObject with constraint component)
/// and its FbxConstraints for quick lookup when exporting constraint animations.
/// </summary>
Dictionary<FbxNode, Dictionary<FbxConstraint, System.Type>> MapConstrainedObjectToConstraints = new Dictionary<FbxNode, Dictionary<FbxConstraint, System.Type>>();
/// <summary>
/// keep a map between the FbxNode and its blendshape channels for quick lookup when exporting blendshapes.
/// </summary>
Dictionary<FbxNode, List<FbxBlendShapeChannel>> MapUnityObjectToBlendShapes = new Dictionary<FbxNode, List<FbxBlendShapeChannel>>();
/// <summary>
/// Map Unity material ID to FBX material object
/// </summary>
Dictionary<int, FbxSurfaceMaterial> MaterialMap = new Dictionary<int, FbxSurfaceMaterial>();
/// <summary>
/// Map texture properties to FBX texture object
/// </summary>
Dictionary<(Texture unityTexture, Vector2 offset, Vector2 scale, TextureWrapMode wrapModeU, TextureWrapMode wrapModeV), FbxFileTexture> TextureMap =
new Dictionary<(Texture unityTexture, Vector2 offset, Vector2 scale, TextureWrapMode wrapModeU, TextureWrapMode wrapModeV), FbxFileTexture>();
/// <summary>
/// Map a Unity mesh to an fbx node (for preserving instances)
/// </summary>
Dictionary<Mesh, FbxNode> SharedMeshes = new Dictionary<Mesh, FbxNode>();
/// <summary>
/// Map for the Name of an Object to number of objects with this name.
/// Used for enforcing unique names on export.
/// </summary>
Dictionary<string, int> NameToIndexMap = new Dictionary<string, int>();
/// <summary>
/// Map for the Material Name to number of materials with this name.
/// Used for enforcing unique names on export.
/// </summary>
Dictionary<string, int> MaterialNameToIndexMap = new Dictionary<string, int>();
/// <summary>
/// Map for the Texture Name to number of textures with this name.
/// Used for enforcing unique names on export.
/// </summary>
Dictionary<string, int> TextureNameToIndexMap = new Dictionary<string, int>();
/// <summary>
/// Format for creating unique names
/// </summary>
const string UniqueNameFormat = "{0}_{1}";
/// <summary>
/// The animation fbx file format.
/// </summary>
const string AnimFbxFileFormat = "{0}/{1}@{2}.fbx";
/// <summary>
/// Gets the export settings.
/// </summary>
internal static ExportSettings ExportSettings
{
get { return ExportSettings.instance; }
}
internal static IExportOptions DefaultOptions
{
get { return new ExportModelSettingsSerialize(); }
}
private IExportOptions m_exportOptions;
private IExportOptions ExportOptions
{
get
{
if (m_exportOptions == null)
{
// get default settings;
m_exportOptions = DefaultOptions;
}
return m_exportOptions;
}
set { m_exportOptions = value; }
}
/// <summary>
/// Gets the Unity default material.
/// </summary>
internal static Material DefaultMaterial
{
get
{
if (!s_defaultMaterial)
{
var obj = GameObject.CreatePrimitive(PrimitiveType.Quad);
s_defaultMaterial = obj.GetComponent<Renderer>().sharedMaterial;
Object.DestroyImmediate(obj);
}
return s_defaultMaterial;
}
}
static Material s_defaultMaterial = null;
static Dictionary<UnityEngine.LightType, FbxLight.EType> MapLightType = new Dictionary<UnityEngine.LightType, FbxLight.EType>()
{
{ UnityEngine.LightType.Directional, FbxLight.EType.eDirectional },
{ UnityEngine.LightType.Spot, FbxLight.EType.eSpot },
{ UnityEngine.LightType.Point, FbxLight.EType.ePoint },
{ UnityEngine.LightType.Rectangle, FbxLight.EType.eArea },
};
/// <summary>
/// Gets the version number of the FbxExporters plugin from the readme.
/// </summary>
internal static string GetVersionFromReadme()
{
if (!File.Exists(ChangeLogPath))
{
Debug.LogWarning(string.Format("Could not find version number, the ChangeLog file is missing from: {0}", ChangeLogPath));
return null;
}
try
{
// The standard format is:
// ## [a.b.c-whatever] - yyyy-mm-dd
// Another format is:
// **Version**: a.b.c-whatever
// we handle either one and read out the version
var lines = File.ReadAllLines(ChangeLogPath);
var regexes = new string[]
{
@"^\s*##\s*\[(.*)\]",
@"^\s*\*\*Version\*\*:\s*(.*)\s*"
};
foreach (var line in lines)
{
foreach (var regex in regexes)
{
var match = System.Text.RegularExpressions.Regex.Match(line, regex);
if (match.Success)
{
var version = match.Groups[1].Value;
return version.Trim();
}
}
}
// If we're here, we didn't find any match.
Debug.LogWarning(string.Format("Could not find most recent version number in {0}", ChangeLogPath));
return null;
}
catch (IOException e)
{
Debug.LogException(e);
Debug.LogWarning(string.Format("Error reading file {0} ({1})", ChangeLogPath, e));
return null;
}
}
/// <summary>
/// Get a layer (to store UVs, normals, etc) on the mesh.
/// If it doesn't exist yet, create it.
/// </summary>
internal static FbxLayer GetOrCreateLayer(FbxMesh fbxMesh, int layer = 0 /* default layer */)
{
int maxLayerIndex = fbxMesh.GetLayerCount() - 1;
while (layer > maxLayerIndex)
{
// We'll have to create the layer (potentially several).
// Make sure to avoid infinite loops even if there's an
// FbxSdk bug.
int newLayerIndex = fbxMesh.CreateLayer();
if (newLayerIndex <= maxLayerIndex)
{
// Error!
throw new ModelExportException(
"Internal error: Unable to create mesh layer "
+ (maxLayerIndex + 1)
+ " on mesh " + fbxMesh.GetName());
}
maxLayerIndex = newLayerIndex;
}
return fbxMesh.GetLayer(layer);
}
/// <summary>
/// Export the mesh's attributes using layer 0.
/// </summary>
private bool ExportComponentAttributes(MeshInfo mesh, FbxMesh fbxMesh, int[] unmergedTriangles)
{
// return true if any attribute was exported
bool exportedAttribute = false;
// Set the normals on Layer 0.
FbxLayer fbxLayer = GetOrCreateLayer(fbxMesh);
if (mesh.HasValidNormals())
{
using (var fbxLayerElement = FbxLayerElementNormal.Create(fbxMesh, "Normals"))
{
fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex);
fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect);
// Add one normal per each vertex face index (3 per triangle)
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray();
for (int n = 0; n < unmergedTriangles.Length; n++)
{
int unityTriangle = unmergedTriangles[n];
fbxElementArray.Add(ConvertToFbxVector4(mesh.Normals[unityTriangle]));
}
fbxLayer.SetNormals(fbxLayerElement);
}
exportedAttribute = true;
}
/// Set the binormals on Layer 0.
if (mesh.HasValidBinormals())
{
using (var fbxLayerElement = FbxLayerElementBinormal.Create(fbxMesh, "Binormals"))
{
fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex);
fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect);
// Add one normal per each vertex face index (3 per triangle)
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray();
for (int n = 0; n < unmergedTriangles.Length; n++)
{
int unityTriangle = unmergedTriangles[n];
fbxElementArray.Add(ConvertToFbxVector4(mesh.Binormals[unityTriangle]));
}
fbxLayer.SetBinormals(fbxLayerElement);
}
exportedAttribute = true;
}
/// Set the tangents on Layer 0.
if (mesh.HasValidTangents())
{
using (var fbxLayerElement = FbxLayerElementTangent.Create(fbxMesh, "Tangents"))
{
fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex);
fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect);
// Add one normal per each vertex face index (3 per triangle)
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray();
for (int n = 0; n < unmergedTriangles.Length; n++)
{
int unityTriangle = unmergedTriangles[n];
fbxElementArray.Add(ConvertToFbxVector4(
new Vector3(
mesh.Tangents[unityTriangle][0],
mesh.Tangents[unityTriangle][1],
mesh.Tangents[unityTriangle][2]
)
));
}
fbxLayer.SetTangents(fbxLayerElement);
}
exportedAttribute = true;
}
exportedAttribute |= ExportUVs(fbxMesh, mesh, unmergedTriangles);
if (mesh.HasValidVertexColors())
{
using (var fbxLayerElement = FbxLayerElementVertexColor.Create(fbxMesh, "VertexColors"))
{
fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex);
fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eIndexToDirect);
// set texture coordinates per vertex
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray();
// (Uni-31596) only copy unique UVs into this array, and index appropriately
for (int n = 0; n < mesh.VertexColors.Length; n++)
{
// Converting to Color from Color32, as Color32 stores the colors
// as ints between 0-255, while FbxColor and Color
// use doubles between 0-1
Color color = mesh.VertexColors[n];
fbxElementArray.Add(new FbxColor(color.r,
color.g,
color.b,
color.a));
}
// For each face index, point to a texture uv
FbxLayerElementArray fbxIndexArray = fbxLayerElement.GetIndexArray();
fbxIndexArray.SetCount(unmergedTriangles.Length);
for (int i = 0; i < unmergedTriangles.Length; i++)
{
fbxIndexArray.SetAt(i, unmergedTriangles[i]);
}
fbxLayer.SetVertexColors(fbxLayerElement);
}
exportedAttribute = true;
}
return exportedAttribute;
}
/// <summary>
/// Unity has up to 4 uv sets per mesh. Export all the ones that exist.
/// </summary>
/// <param name="fbxMesh">Fbx mesh.</param>
/// <param name="mesh">Mesh.</param>
/// <param name="unmergedTriangles">Unmerged triangles.</param>
private static bool ExportUVs(FbxMesh fbxMesh, MeshInfo meshInfo, int[] unmergedTriangles)
{
var mesh = meshInfo.mesh;
List<Vector2> uvs = new List<Vector2>();
int k = 0;
for (int i = 0; i < 8; i++)
{
mesh.GetUVs(i, uvs);
if (uvs == null || uvs.Count == 0)
{
continue; // don't have these UV's, so skip
}
FbxLayer fbxLayer = GetOrCreateLayer(fbxMesh, k);
using (var fbxLayerElement = FbxLayerElementUV.Create(fbxMesh, "UVSet" + i))
{
fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex);
fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eIndexToDirect);
// set texture coordinates per vertex
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray();
// (Uni-31596) only copy unique UVs into this array, and index appropriately
for (int n = 0; n < uvs.Count; n++)
{
fbxElementArray.Add(new FbxVector2(uvs[n][0],
uvs[n][1]));
}
// For each face index, point to a texture uv
FbxLayerElementArray fbxIndexArray = fbxLayerElement.GetIndexArray();
fbxIndexArray.SetCount(unmergedTriangles.Length);
for (int j = 0; j < unmergedTriangles.Length; j++)
{
fbxIndexArray.SetAt(j, unmergedTriangles[j]);
}
fbxLayer.SetUVs(fbxLayerElement, FbxLayerElement.EType.eTextureDiffuse);
}
k++;
uvs.Clear();
}
// if we incremented k, then at least on set of UV's were exported
return k > 0;
}
/// <summary>
/// Export the mesh's blend shapes.
/// </summary>
private FbxBlendShape ExportBlendShapes(MeshInfo mesh, FbxMesh fbxMesh, FbxScene fbxScene, int[] unmergedTriangles)
{
var umesh = mesh.mesh;
if (umesh.blendShapeCount == 0)
return null;
var fbxBlendShape = FbxBlendShape.Create(fbxScene, umesh.name + "_BlendShape");
fbxMesh.AddDeformer(fbxBlendShape);
var numVertices = umesh.vertexCount;
var basePoints = umesh.vertices;
var baseNormals = umesh.normals;
var baseTangents = umesh.tangents;
var deltaPoints = new Vector3[numVertices];
var deltaNormals = new Vector3[numVertices];
var deltaTangents = new Vector3[numVertices];
for (int bi = 0; bi < umesh.blendShapeCount; ++bi)
{
var bsName = umesh.GetBlendShapeName(bi);
var numFrames = umesh.GetBlendShapeFrameCount(bi);
var fbxChannel = FbxBlendShapeChannel.Create(fbxScene, bsName);
fbxBlendShape.AddBlendShapeChannel(fbxChannel);
for (int fi = 0; fi < numFrames; ++fi)
{
var weight = umesh.GetBlendShapeFrameWeight(bi, fi);
umesh.GetBlendShapeFrameVertices(bi, fi, deltaPoints, deltaNormals, deltaTangents);
var fbxShapeName = bsName;
if (numFrames > 1)
{
fbxShapeName += "_" + fi;
}
var fbxShape = FbxShape.Create(fbxScene, fbxShapeName);
fbxChannel.AddTargetShape(fbxShape, weight);
// control points
fbxShape.InitControlPoints(ControlPointToIndex.Count);
for (int vi = 0; vi < numVertices; ++vi)
{
int ni = ControlPointToIndex[basePoints[vi]];
var v = basePoints[vi] + deltaPoints[vi];
fbxShape.SetControlPointAt(ConvertToFbxVector4(v, UnitScaleFactor), ni);
}
// normals
if (mesh.HasValidNormals())
{
var elemNormals = fbxShape.CreateElementNormal();
elemNormals.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex);
elemNormals.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect);
var dstNormals = elemNormals.GetDirectArray();
dstNormals.SetCount(unmergedTriangles.Length);
for (int ii = 0; ii < unmergedTriangles.Length; ++ii)
{
int vi = unmergedTriangles[ii];
var n = baseNormals[vi] + deltaNormals[vi];
dstNormals.SetAt(ii, ConvertToFbxVector4(n));
}
}
// tangents
if (mesh.HasValidTangents())
{
var elemTangents = fbxShape.CreateElementTangent();
elemTangents.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex);
elemTangents.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect);
var dstTangents = elemTangents.GetDirectArray();
dstTangents.SetCount(unmergedTriangles.Length);
for (int ii = 0; ii < unmergedTriangles.Length; ++ii)
{
int vi = unmergedTriangles[ii];
var t = (Vector3)baseTangents[vi] + deltaTangents[vi];
dstTangents.SetAt(ii, ConvertToFbxVector4(t));
}
}
}
}
return fbxBlendShape;
}
/// <summary>
/// Takes in a left-handed UnityEngine.Vector3 denoting a normal,
/// returns a right-handed FbxVector4.
///
/// Unity is left-handed, Maya and Max are right-handed.
/// The FbxSdk conversion routines can't handle changing handedness.
///
/// Remember you also need to flip the winding order on your polygons.
/// </summary>
internal static FbxVector4 ConvertToFbxVector4(Vector3 leftHandedVector, float unitScale = 1f)
{
// negating the x component of the vector converts it from left to right handed coordinates
return unitScale * new FbxVector4(
leftHandedVector[0],
leftHandedVector[1],
leftHandedVector[2]);
}
/// <summary>
/// Exports a texture from Unity to FBX.
/// The texture must be a property on the unityMaterial; it gets
/// linked to the FBX via a property on the fbxMaterial.
///
/// The texture file must be a file on disk; it is not embedded within the FBX.
/// </summary>
/// <param name="unityMaterial">Unity material.</param>
/// <param name="unityPropName">Unity property name, e.g. "_MainTex".</param>
/// <param name="fbxMaterial">Fbx material.</param>
/// <param name="fbxPropName">Fbx property name, e.g. <c>FbxSurfaceMaterial.sDiffuse</c>.</param>
internal bool ExportTexture(Material unityMaterial, string unityPropName,
FbxSurfaceMaterial fbxMaterial, string fbxPropName)
{
if (!unityMaterial)
{
return false;
}
// Get the texture on this property, if any.
if (!unityMaterial.HasProperty(unityPropName))
{
return false;
}
var unityTexture = unityMaterial.GetTexture(unityPropName);
if (!unityTexture)
{
return false;
}
// Find its filename
var textureSourceFullPath = AssetDatabase.GetAssetPath(unityTexture);
if (string.IsNullOrEmpty(textureSourceFullPath))
{
return false;
}
// get absolute filepath to texture
textureSourceFullPath = Path.GetFullPath(textureSourceFullPath);
if (Verbose)
{
Debug.Log(string.Format("{2}.{1} setting texture path {0}", textureSourceFullPath, fbxPropName, fbxMaterial.GetName()));
}
// Find the corresponding property on the fbx material.
var fbxMaterialProperty = fbxMaterial.FindProperty(fbxPropName);
if (fbxMaterialProperty == null || !fbxMaterialProperty.IsValid())
{
Debug.Log("property not found");
return false;
}
var offset = unityMaterial.GetTextureOffset(unityPropName);
var scale = unityMaterial.GetTextureScale(unityPropName);
var wrapModeU = unityTexture.wrapModeU;
var wrapModeV = unityTexture.wrapModeV;
var tuple = (unityTexture, offset, scale, wrapModeU, wrapModeV);
FbxFileTexture fbxTexture;
// Find or create an fbx texture and link it up to the fbx material.
if (!TextureMap.TryGetValue(tuple, out fbxTexture))
{
var textureName = GetUniqueTextureName(fbxPropName + "_Texture");
fbxTexture = FbxFileTexture.Create(fbxMaterial, textureName);
fbxTexture.SetFileName(textureSourceFullPath);
fbxTexture.SetTextureUse(FbxTexture.ETextureUse.eStandard);
fbxTexture.SetMappingType(FbxTexture.EMappingType.eUV);
fbxTexture.SetScale(scale.x, scale.y);
fbxTexture.SetTranslation(offset.x, offset.y);
fbxTexture.SetWrapMode(GetWrapModeFromUnityWrapMode(wrapModeU, unityMaterial.name, unityPropName),
GetWrapModeFromUnityWrapMode(wrapModeV, unityMaterial.name, unityPropName));
TextureMap.Add(tuple, fbxTexture);
}
fbxTexture.ConnectDstProperty(fbxMaterialProperty);
return true;
}
private FbxTexture.EWrapMode GetWrapModeFromUnityWrapMode(TextureWrapMode wrapMode, string materialName, string textureName)
{
switch (wrapMode)
{
case TextureWrapMode.Clamp: return FbxTexture.EWrapMode.eClamp;
case TextureWrapMode.Repeat: return FbxTexture.EWrapMode.eRepeat;
default:
Debug.LogWarning($"Texture {textureName} on material {materialName} uses wrap mode {wrapMode} which is not supported by FBX. Will be exported as \"Repeat\".");
return FbxTexture.EWrapMode.eRepeat;
}
}
/// <summary>
/// Get the color of a material, or grey if we can't find it.
/// </summary>
internal FbxDouble3 GetMaterialColor(Material unityMaterial, string unityPropName, float defaultValue = 1)
{
if (!unityMaterial)
{
return new FbxDouble3(defaultValue);
}
if (!unityMaterial.HasProperty(unityPropName))
{
return new FbxDouble3(defaultValue);
}
var unityColor = unityMaterial.GetColor(unityPropName);
return new FbxDouble3(unityColor.r, unityColor.g, unityColor.b);
}
/// <summary>
/// Export (and map) a Unity PBS material to FBX classic material
/// </summary>
internal bool ExportMaterial(Material unityMaterial, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityMaterial)
{
unityMaterial = DefaultMaterial;
}
var unityID = unityMaterial.GetInstanceID();
FbxSurfaceMaterial mappedMaterial;
if (MaterialMap.TryGetValue(unityID, out mappedMaterial))
{
fbxNode.AddMaterial(mappedMaterial);
return true;
}
var unityName = unityMaterial.name;
var fbxName = ExportOptions.UseMayaCompatibleNames
? ConvertToMayaCompatibleName(unityName) : unityName;
fbxName = GetUniqueMaterialName(fbxName);
if (Verbose)
{
if (unityName != fbxName)
{
Debug.Log(string.Format("exporting material {0} as {1}", unityName, fbxName));
}
else
{
Debug.Log(string.Format("exporting material {0}", unityName));
}
}
// We'll export either Phong or Lambert. Phong if it calls
// itself specular, Lambert otherwise.
System.StringComparison stringComparison = System.StringComparison.OrdinalIgnoreCase;
var shader = unityMaterial.shader;
bool specular = shader.name.IndexOf("specular", stringComparison) >= 0;
bool hdrp = shader.name.IndexOf("hdrp", stringComparison) >= 0;
var fbxMaterial = specular
? FbxSurfacePhong.Create(fbxScene, fbxName)
: FbxSurfaceLambert.Create(fbxScene, fbxName);
// Copy the flat colours over from Unity standard materials to FBX.
fbxMaterial.Diffuse.Set(GetMaterialColor(unityMaterial, "_Color"));
fbxMaterial.Emissive.Set(GetMaterialColor(unityMaterial, "_EmissionColor", 0));
// hdrp materials dont export emission properly, so default to 0
if (hdrp)
{
fbxMaterial.Emissive.Set(new FbxDouble3(0, 0, 0));
}
fbxMaterial.Ambient.Set(new FbxDouble3());
fbxMaterial.BumpFactor.Set(unityMaterial.HasProperty("_BumpScale") ? unityMaterial.GetFloat("_BumpScale") : 0);
if (specular)
{
(fbxMaterial as FbxSurfacePhong).Specular.Set(GetMaterialColor(unityMaterial, "_SpecColor"));
}
// Export the textures from Unity standard materials to FBX.
ExportTexture(unityMaterial, "_MainTex", fbxMaterial, FbxSurfaceMaterial.sDiffuse);
ExportTexture(unityMaterial, "_EmissionMap", fbxMaterial, FbxSurfaceMaterial.sEmissive);
ExportTexture(unityMaterial, "_BumpMap", fbxMaterial, FbxSurfaceMaterial.sNormalMap);
if (specular)
{
ExportTexture(unityMaterial, "_SpecGlossMap", fbxMaterial, FbxSurfaceMaterial.sSpecular);
}
MaterialMap.Add(unityID, fbxMaterial);
fbxNode.AddMaterial(fbxMaterial);
return true;
}
/// <summary>
/// Sets up the material to polygon mapping for fbxMesh.
/// To determine which part of the mesh uses which material, look at the submeshes
/// and which polygons they represent.
/// Assuming equal number of materials as submeshes, and that they are in the same order.
/// (i.e. submesh 1 uses material 1)
/// </summary>
/// <param name="fbxMesh">Fbx mesh.</param>
/// <param name="mesh">Mesh.</param>
/// <param name="materials">Materials.</param>
private void AssignLayerElementMaterial(FbxMesh fbxMesh, Mesh mesh, int materialCount)
{
// Add FbxLayerElementMaterial to layer 0 of the node
FbxLayer fbxLayer = fbxMesh.GetLayer(0 /* default layer */);
if (fbxLayer == null)
{
fbxMesh.CreateLayer();
fbxLayer = fbxMesh.GetLayer(0 /* default layer */);
}
using (var fbxLayerElement = FbxLayerElementMaterial.Create(fbxMesh, "Material"))
{
// if there is only one material then set everything to that material
if (materialCount == 1)
{
fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eAllSame);
fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eIndexToDirect);
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetIndexArray();
fbxElementArray.Add(0);
}
else
{
fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygon);
fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eIndexToDirect);
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetIndexArray();
for (int subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; subMeshIndex++)
{
var topology = mesh.GetTopology(subMeshIndex);
int polySize;
switch (topology)
{
case MeshTopology.Triangles:
polySize = 3;
break;
case MeshTopology.Quads:
polySize = 4;
break;
case MeshTopology.Lines:
throw new System.NotImplementedException();
case MeshTopology.Points:
throw new System.NotImplementedException();
case MeshTopology.LineStrip:
throw new System.NotImplementedException();
default:
throw new System.NotImplementedException();
}
// Specify the material index for each polygon.
// Material index should match subMeshIndex.
var indices = mesh.GetIndices(subMeshIndex);
for (int j = 0, n = indices.Length / polySize; j < n; j++)
{
fbxElementArray.Add(subMeshIndex);
}
}
}
fbxLayer.SetMaterials(fbxLayerElement);
}
}
/// <summary>
/// Exports a unity mesh and attaches it to the node as an FbxMesh.
///
/// Able to export materials per sub-mesh as well (by default, exports with the default material).
///
/// Use fbxNode.GetMesh() to access the exported mesh.
/// </summary>
internal bool ExportMesh(Mesh mesh, FbxNode fbxNode, Material[] materials = null)
{
var meshInfo = new MeshInfo(mesh, materials);
return ExportMesh(meshInfo, fbxNode);
}
/// <summary>
/// Keeps track of the index of each point in the exported vertex array.
/// </summary>
private Dictionary<Vector3, int> ControlPointToIndex = new Dictionary<Vector3, int>();
/// <summary>
/// Exports a unity mesh and attaches it to the node as an FbxMesh.
/// </summary>
bool ExportMesh(MeshInfo meshInfo, FbxNode fbxNode)
{
if (!meshInfo.IsValid)
{
return false;
}
NumMeshes++;
NumTriangles += meshInfo.Triangles.Length / 3;
// create the mesh structure.
var fbxScene = fbxNode.GetScene();
FbxMesh fbxMesh = FbxMesh.Create(fbxScene, "Scene");
// Create control points.
ControlPointToIndex.Clear();
{
var vertices = meshInfo.Vertices;
for (int v = 0, n = meshInfo.VertexCount; v < n; v++)
{
if (ControlPointToIndex.ContainsKey(vertices[v]))
{
continue;
}
ControlPointToIndex[vertices[v]] = ControlPointToIndex.Count;
}
fbxMesh.InitControlPoints(ControlPointToIndex.Count);
foreach (var kvp in ControlPointToIndex)
{
var controlPoint = kvp.Key;
var index = kvp.Value;
fbxMesh.SetControlPointAt(ConvertToFbxVector4(controlPoint, UnitScaleFactor), index);
}
}
var unmergedPolygons = new List<int>();
var mesh = meshInfo.mesh;
for (int s = 0; s < mesh.subMeshCount; s++)
{
var topology = mesh.GetTopology(s);
var indices = mesh.GetIndices(s);
int polySize;
int[] vertOrder;
switch (topology)
{
case MeshTopology.Triangles:
polySize = 3;
vertOrder = new int[] { 0, 1, 2 };
break;
case MeshTopology.Quads:
polySize = 4;
vertOrder = new int[] { 0, 1, 2, 3 };
break;
case MeshTopology.Lines:
throw new System.NotImplementedException();
case MeshTopology.Points:
throw new System.NotImplementedException();
case MeshTopology.LineStrip:
throw new System.NotImplementedException();
default:
throw new System.NotImplementedException();
}
for (int f = 0; f < indices.Length / polySize; f++)
{
fbxMesh.BeginPolygon();
foreach (int val in vertOrder)
{
int polyVert = indices[polySize * f + val];
// Save the polygon order (without merging vertices) so we
// properly export UVs, normals, binormals, etc.
unmergedPolygons.Add(polyVert);
polyVert = ControlPointToIndex[meshInfo.Vertices[polyVert]];
fbxMesh.AddPolygon(polyVert);
}
fbxMesh.EndPolygon();
}
}
// Set up materials per submesh.
foreach (var mat in meshInfo.Materials)
{
ExportMaterial(mat, fbxScene, fbxNode);
}
AssignLayerElementMaterial(fbxMesh, meshInfo.mesh, meshInfo.Materials.Length);
// Set up normals, etc.
ExportComponentAttributes(meshInfo, fbxMesh, unmergedPolygons.ToArray());
// Set up blend shapes.
FbxBlendShape fbxBlendShape = ExportBlendShapes(meshInfo, fbxMesh, fbxScene, unmergedPolygons.ToArray());
if (fbxBlendShape != null && fbxBlendShape.GetBlendShapeChannelCount() > 0)
{
// Populate mapping for faster lookup when exporting blendshape animations
List<FbxBlendShapeChannel> blendshapeChannels;
if (!MapUnityObjectToBlendShapes.TryGetValue(fbxNode, out blendshapeChannels))
{
blendshapeChannels = new List<FbxBlendShapeChannel>();
MapUnityObjectToBlendShapes.Add(fbxNode, blendshapeChannels);
}
for (int i = 0; i < fbxBlendShape.GetBlendShapeChannelCount(); i++)
{
var bsChannel = fbxBlendShape.GetBlendShapeChannel(i);
blendshapeChannels.Add(bsChannel);
}
}
// set the fbxNode containing the mesh
fbxNode.SetNodeAttribute(fbxMesh);
fbxNode.SetShadingMode(FbxNode.EShadingMode.eWireFrame);
return true;
}
/// <summary>
/// Export GameObject as a skinned mesh with material, bones, a skin and, a bind pose.
/// </summary>
private bool ExportSkinnedMesh(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGo || fbxNode == null)
{
return false;
}
if (!unityGo.TryGetComponent<SkinnedMeshRenderer>(out var unitySkin))
{
return false;
}
var mesh = unitySkin.sharedMesh;
if (!mesh)
{
return false;
}
if (Verbose)
Debug.Log(string.Format("exporting {0} {1}", "Skin", fbxNode.GetName()));
var meshInfo = new MeshInfo(unitySkin.sharedMesh, unitySkin.sharedMaterials);
FbxMesh fbxMesh = null;
if (ExportMesh(meshInfo, fbxNode))
{
fbxMesh = fbxNode.GetMesh();
}
if (fbxMesh == null)
{
Debug.LogError("Could not find mesh");
return false;
}
Dictionary<SkinnedMeshRenderer, Transform[]> skinnedMeshToBonesMap;
// export skeleton
if (ExportSkeleton(unitySkin, fbxScene, out skinnedMeshToBonesMap))
{
// bind mesh to skeleton
ExportSkin(unitySkin, meshInfo, fbxScene, fbxMesh, fbxNode);
// add bind pose
ExportBindPose(unitySkin, fbxNode, fbxScene, skinnedMeshToBonesMap);
// now that the skin and bindpose are set, make sure that each of the bones
// is set to its original position
var bones = unitySkin.bones;
foreach (var bone in bones)
{
// ignore null bones
if (bone != null)
{
var fbxBone = MapUnityObjectToFbxNode[bone.gameObject];
ExportTransform(bone, fbxBone, newCenter: Vector3.zero, TransformExportType.Local);
// Cancel out the pre-rotation from the exported rotation
// Get prerotation
var fbxPreRotationEuler = fbxBone.GetPreRotation(FbxNode.EPivotSet.eSourcePivot);
// Convert the prerotation to a Quaternion
var fbxPreRotationQuaternion = EulerToQuaternionXYZ(fbxPreRotationEuler);
// Inverse of the prerotation
fbxPreRotationQuaternion.Inverse();
// Multiply LclRotation by pre-rotation inverse to get the LclRotation without pre-rotation applied
var finalLclRotationQuat = fbxPreRotationQuaternion * EulerToQuaternionZXY(bone.localEulerAngles);
// Convert to Euler with Unity axis system and update LclRotation
var finalUnityQuat = new Quaternion((float)finalLclRotationQuat.X, (float)finalLclRotationQuat.Y, (float)finalLclRotationQuat.Z, (float)finalLclRotationQuat.W);
fbxBone.LclRotation.Set(ToFbxDouble3(finalUnityQuat.eulerAngles));
}
else
{
Debug.Log("Warning: One or more bones are null. Skeleton may not export correctly.");
}
}
}
return true;
}
/// <summary>
/// Gets the bind pose for the Unity bone.
/// </summary>
/// <returns>The bind pose.</returns>
/// <param name="unityBone">Unity bone.</param>
/// <param name="bindPoses">Bind poses.</param>
/// <param name="boneInfo">Contains information about bones and skinned mesh.</param>
private Matrix4x4 GetBindPose(
Transform unityBone, Matrix4x4[] bindPoses,
ref SkinnedMeshBoneInfo boneInfo
)
{
var boneDict = boneInfo.boneDict;
var skinnedMesh = boneInfo.skinnedMesh;
var boneToBindPose = boneInfo.boneToBindPose;
// If we have already retrieved the bindpose for this bone before
// it will be present in the boneToBindPose dictionary,
// simply return this bindpose.
Matrix4x4 bindPose;
if (boneToBindPose.TryGetValue(unityBone, out bindPose))
{
return bindPose;
}
// Check if unityBone is a bone registered in the bone list of the skinned mesh.
// If it is, then simply retrieve the bindpose from the boneDict (maps bone to index in bone/bindpose list).
// Make sure to update the boneToBindPose list in case the bindpose for this bone needs to be retrieved again.
int index;
if (boneDict.TryGetValue(unityBone, out index))
{
bindPose = bindPoses[index];
boneToBindPose.Add(unityBone, bindPose);
return bindPose;
}
// unityBone is not registered as a bone in the skinned mesh, therefore there is no bindpose
// associated with it, we need to calculate one.
// If this is the rootbone of the mesh or an object without a parent, use the global matrix relative to the skinned mesh
// as the bindpose.
if (unityBone == skinnedMesh.rootBone || unityBone.parent == null)
{
// there is no bone above this object with a bindpose, calculate bindpose relative to skinned mesh
bindPose = (unityBone.worldToLocalMatrix * skinnedMesh.transform.localToWorldMatrix);
boneToBindPose.Add(unityBone, bindPose);
return bindPose;
}
// If this object has a parent that could be a bone, then it is not enough to use the worldToLocalMatrix,
// as this will give an incorrect global transform if the parents are not already in the bindpose in the scene.
// Instead calculate what the bindpose would be based on the bindpose of the parent object.
// get the bindpose of the parent
var parentBindPose = GetBindPose(unityBone.parent, bindPoses, ref boneInfo);
// Get the local transformation matrix of the bone, then transform it into
// the global transformation matrix with the parent in the bind pose.
// Formula to get the global transformation matrix:
// (parentBindPose.inverse * boneLocalTRSMatrix)
// The bindpose is then the inverse of this matrix:
// (parentBindPose.inverse * boneLocalTRSMatrix).inverse
// This can be simplified with (AB)^{-1} = B^{-1}A^{-1} rule as follows:
// (parentBindPose.inverse * boneLocalTRSMatrix).inverse
// = boneLocalTRSMatrix.inverse * parentBindPose.inverse.inverse
// = boneLocalTRSMatrix.inverse * parentBindPose
var boneLocalTRSMatrix = Matrix4x4.TRS(unityBone.localPosition, unityBone.localRotation, unityBone.localScale);
bindPose = boneLocalTRSMatrix.inverse * parentBindPose;
boneToBindPose.Add(unityBone, bindPose);
return bindPose;
}
/// <summary>
/// Export bones of skinned mesh, if this is a skinned mesh with
/// bones and bind poses.
/// </summary>
private bool ExportSkeleton(SkinnedMeshRenderer skinnedMesh, FbxScene fbxScene, out Dictionary<SkinnedMeshRenderer, Transform[]> skinnedMeshToBonesMap)
{
skinnedMeshToBonesMap = new Dictionary<SkinnedMeshRenderer, Transform[]>();
if (!skinnedMesh)
{
return false;
}
var bones = skinnedMesh.bones;
if (bones == null || bones.Length == 0)
{
return false;
}
var mesh = skinnedMesh.sharedMesh;
if (!mesh)
{
return false;
}
var bindPoses = mesh.bindposes;
if (bindPoses == null || bindPoses.Length != bones.Length)
{
return false;
}
// Two steps:
// 0. Set up the map from bone to index.
// 1. Set the transforms.
// Step 0: map transform to index so we can look up index by bone.
Dictionary<Transform, int> index = new Dictionary<Transform, int>();
for (int boneIndex = 0; boneIndex < bones.Length; boneIndex++)
{
Transform unityBoneTransform = bones[boneIndex];
// ignore null bones
if (unityBoneTransform != null)
{
index[unityBoneTransform] = boneIndex;
}
}
skinnedMeshToBonesMap.Add(skinnedMesh, bones);
// Step 1: Set transforms
var boneInfo = new SkinnedMeshBoneInfo(skinnedMesh, index);
foreach (var bone in bones)
{
// ignore null bones
if (bone != null)
{
var fbxBone = MapUnityObjectToFbxNode[bone.gameObject];
ExportBoneTransform(fbxBone, fbxScene, bone, boneInfo);
}
}
return true;
}
/// <summary>
/// Export binding of mesh to skeleton
/// </summary>
private bool ExportSkin(SkinnedMeshRenderer skinnedMesh,
MeshInfo meshInfo, FbxScene fbxScene, FbxMesh fbxMesh,
FbxNode fbxRootNode)
{
FbxSkin fbxSkin = FbxSkin.Create(fbxScene, (skinnedMesh.name + SkinPrefix));
FbxAMatrix fbxMeshMatrix = fbxRootNode.EvaluateGlobalTransform();
// keep track of the bone index -> fbx cluster mapping, so that we can add the bone weights afterwards
Dictionary<int, FbxCluster> boneCluster = new Dictionary<int, FbxCluster>();
for (int i = 0; i < skinnedMesh.bones.Length; i++)
{
// ignore null bones
if (skinnedMesh.bones[i] != null)
{
FbxNode fbxBoneNode = MapUnityObjectToFbxNode[skinnedMesh.bones[i].gameObject];
// Create the deforming cluster
FbxCluster fbxCluster = FbxCluster.Create(fbxScene, "BoneWeightCluster");
fbxCluster.SetLink(fbxBoneNode);
fbxCluster.SetLinkMode(FbxCluster.ELinkMode.eNormalize);
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);
return true;
}
/// <summary>
/// set vertex weights in cluster
/// </summary>
private void SetVertexWeights(MeshInfo meshInfo, Dictionary<int, FbxCluster> boneIndexToCluster)
{
var mesh = meshInfo.mesh;
// Get the number of bone weights per vertex
var bonesPerVertex = mesh.GetBonesPerVertex();
if (bonesPerVertex.Length == 0)
{
// no bone weights to set
return;
}
HashSet<int> visitedVertices = new HashSet<int>();
// Get all the bone weights, in vertex index order
// Note: this contains all the bone weights for all vertices.
// Use number of bonesPerVertex to determine where weights end
// for one vertex and begin for another.
var boneWeights1 = mesh.GetAllBoneWeights();
// Keep track of where we are in the array of BoneWeights, as we iterate over the vertices
var boneWeightIndex = 0;
for (var vertIndex = 0; vertIndex < meshInfo.VertexCount; vertIndex++)
{
// Get the index into the list of vertices without duplicates
var actualIndex = ControlPointToIndex[meshInfo.Vertices[vertIndex]];
var numberOfBonesForThisVertex = bonesPerVertex[vertIndex];
if (visitedVertices.Contains(actualIndex))
{
// skip duplicate vertex
boneWeightIndex += numberOfBonesForThisVertex;
continue;
}
visitedVertices.Add(actualIndex);
// For each vertex, iterate over its BoneWeights
for (var i = 0; i < numberOfBonesForThisVertex; i++)
{
var currentBoneWeight = boneWeights1[boneWeightIndex];
// bone index is index into skinnedmesh.bones[]
var boneIndex = currentBoneWeight.boneIndex;
// how much influence does this bone have on vertex at vertIndex
var weight = currentBoneWeight.weight;
if (weight <= 0)
{
continue;
}
// get the right cluster
FbxCluster boneCluster;
if (!boneIndexToCluster.TryGetValue(boneIndex, out boneCluster))
{
continue;
}
// add vertex and weighting on vertex to this bone's cluster
boneCluster.AddControlPointIndex(actualIndex, weight);
boneWeightIndex++;
}
}
}
/// <summary>
/// Export bind pose of mesh to skeleton
/// </summary>
private bool ExportBindPose(SkinnedMeshRenderer skinnedMesh, FbxNode fbxMeshNode,
FbxScene fbxScene, Dictionary<SkinnedMeshRenderer, Transform[]> skinnedMeshToBonesMap)
{
if (fbxMeshNode == null || skinnedMeshToBonesMap == null || fbxScene == null)
{
return false;
}
FbxPose fbxPose = FbxPose.Create(fbxScene, fbxMeshNode.GetName());
// set as bind pose
fbxPose.SetIsBindPose(true);
// assume each bone node has one weighted vertex cluster
Transform[] bones;
if (!skinnedMeshToBonesMap.TryGetValue(skinnedMesh, out bones))
{
return false;
}
for (int i = 0; i < bones.Length; i++)
{
// ignore null bones
if (bones[i] != null)
{
FbxNode fbxBoneNode = MapUnityObjectToFbxNode[bones[i].gameObject];
// 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(fbxBoneNode.EvaluateGlobalTransform());
fbxPose.Add(fbxBoneNode, fbxBindMatrix);
}
}
fbxPose.Add(fbxMeshNode, new FbxMatrix(fbxMeshNode.EvaluateGlobalTransform()));
// add the pose to the scene
fbxScene.AddPose(fbxPose);
return true;
}
internal static FbxDouble3 ToFbxDouble3(Vector3 v)
{
return new FbxDouble3(v.x, v.y, v.z);
}
internal static FbxDouble3 ToFbxDouble3(FbxVector4 v)
{
return new FbxDouble3(v.X, v.Y, v.Z);
}
/// <summary>
/// Euler (roll/pitch/yaw (ZXY rotation order) to quaternion.
/// </summary>
/// <returns>a quaternion.</returns>
/// <param name="euler">ZXY Euler.</param>
internal static FbxQuaternion EulerToQuaternionZXY(Vector3 euler)
{
var unityQuat = Quaternion.Euler(euler);
return new FbxQuaternion(unityQuat.x, unityQuat.y, unityQuat.z, unityQuat.w);
}
/// <summary>
/// Euler X/Y/Z rotation order to quaternion.
/// </summary>
/// <param name="euler">XYZ Euler.</param>
/// <returns>a quaternion</returns>
internal static FbxQuaternion EulerToQuaternionXYZ(FbxVector4 euler)
{
FbxAMatrix m = new FbxAMatrix();
m.SetR(euler);
return m.GetQ();
}
// get a fbxNode's global default position.
internal bool ExportTransform(UnityEngine.Transform unityTransform, FbxNode fbxNode, Vector3 newCenter, TransformExportType exportType)
{
UnityEngine.Vector3 unityTranslate;
FbxDouble3 fbxRotate;
UnityEngine.Vector3 unityScale;
switch (exportType)
{
case TransformExportType.Reset:
unityTranslate = Vector3.zero;
fbxRotate = new FbxDouble3(0);
unityScale = Vector3.one;
break;
case TransformExportType.Global:
unityTranslate = GetRecenteredTranslation(unityTransform, newCenter);
fbxRotate = ToFbxDouble3(unityTransform.eulerAngles);
unityScale = unityTransform.lossyScale;
break;
default: /*case TransformExportType.Local*/
unityTranslate = unityTransform.localPosition;
fbxRotate = ToFbxDouble3(unityTransform.localEulerAngles);
unityScale = unityTransform.localScale;
break;
}
// Transfer transform data from Unity to Fbx
var fbxTranslate = ConvertToFbxVector4(unityTranslate, UnitScaleFactor);
var fbxScale = new FbxDouble3(unityScale.x, unityScale.y, unityScale.z);
// Zero scale causes issues in 3ds Max (child of object with zero scale will end up with a much larger scale, e.g. >9000).
// When exporting 0 scale from Maya, the FBX contains 1e-12 instead of 0,
// which doesn't cause issues in Max. Do the same here.
if (fbxScale.X == 0)
{
fbxScale.X = 1e-12;
}
if (fbxScale.Y == 0)
{
fbxScale.Y = 1e-12;
}
if (fbxScale.Z == 0)
{
fbxScale.Z = 1e-12;
}
// set the local position of fbxNode
fbxNode.LclTranslation.Set(new FbxDouble3(fbxTranslate.X, fbxTranslate.Y, fbxTranslate.Z));
fbxNode.LclRotation.Set(fbxRotate);
fbxNode.LclScaling.Set(fbxScale);
return true;
}
/// <summary>
/// if this game object is a model prefab or the model has already been exported, then export with shared components
/// </summary>
private bool ExportInstance(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGo || fbxNode == null)
{
return false;
}
// where the fbx mesh is stored on a successful export
FbxMesh fbxMesh = null;
// store the shared mesh of the game object
Mesh unityGoMesh = null;
// get the mesh of the game object
if (unityGo.TryGetComponent<MeshFilter>(out MeshFilter meshFilter))
{
unityGoMesh = meshFilter.sharedMesh;
}
if (!unityGoMesh)
{
return false;
}
// export mesh as an instance if it is a duplicate mesh or a prefab
else if (SharedMeshes.TryGetValue(unityGoMesh, out FbxNode node))
{
if (Verbose)
{
Debug.Log(string.Format("exporting instance {0}", unityGo.name));
}
fbxMesh = node.GetMesh();
}
// unique mesh, so save it to find future duplicates
else
{
SharedMeshes.Add(unityGoMesh, fbxNode);
return false;
}
// mesh doesn't exist or wasn't exported successfully
if (fbxMesh == null)
{
return false;
}
// We don't export the mesh because we already have it from the parent, but we still need to assign the material
var renderer = unityGo.GetComponent<Renderer>();
var materials = renderer ? renderer.sharedMaterials : null;
Autodesk.Fbx.FbxSurfaceMaterial newMaterial = null;
if (materials != null)
{
foreach (var mat in materials)
{
if (mat != null && MaterialMap.TryGetValue(mat.GetInstanceID(), out newMaterial))
{
fbxNode.AddMaterial(newMaterial);
}
else
{
// create new material
ExportMaterial(mat, fbxScene, fbxNode);
}
}
}
// set the fbxNode containing the mesh
fbxNode.SetNodeAttribute(fbxMesh);
fbxNode.SetShadingMode(FbxNode.EShadingMode.eWireFrame);
return true;
}
/// <summary>
/// Exports camera component
/// </summary>
private bool ExportCamera(GameObject unityGO, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGO || fbxScene == null || fbxNode == null)
{
return false;
}
if (!unityGO.TryGetComponent<Camera>(out var unityCamera))
{
return false;
}
FbxCamera fbxCamera = FbxCamera.Create(fbxScene.GetFbxManager(), unityCamera.name);
if (fbxCamera == null)
{
return false;
}
CameraVisitor.ConfigureCamera(unityCamera, fbxCamera);
fbxNode.SetNodeAttribute(fbxCamera);
// set +90 post rotation to counteract for FBX camera's facing +X direction by default
fbxNode.SetPostRotation(FbxNode.EPivotSet.eSourcePivot, new FbxVector4(0, 90, 0));
// have to set rotation active to true in order for post rotation to be applied
fbxNode.SetRotationActive(true);
// make the last camera exported the default camera
DefaultCamera = fbxNode.GetName();
return true;
}
/// <summary>
/// Exports light component.
/// Supported types: point, spot and directional
/// Cookie => Gobo
/// </summary>
private bool ExportLight(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGo || fbxScene == null || fbxNode == null)
{
return false;
}
if (!unityGo.TryGetComponent<Light>(out var unityLight))
return false;
FbxLight.EType fbxLightType;
// Is light type supported?
if (!MapLightType.TryGetValue(unityLight.type, out fbxLightType))
return false;
FbxLight fbxLight = FbxLight.Create(fbxScene.GetFbxManager(), unityLight.name);
// Set the type of the light.
fbxLight.LightType.Set(fbxLightType);
switch (unityLight.type)
{
case LightType.Directional:
{
break;
}
case LightType.Spot:
{
// Set the angle of the light's spotlight cone in degrees.
fbxLight.InnerAngle.Set(unityLight.spotAngle);
fbxLight.OuterAngle.Set(unityLight.spotAngle);
break;
}
case LightType.Point:
{
break;
}
case LightType.Rectangle:
{
// TODO: areaSize: The size of the area light by scaling the node XY
break;
}
}
// The color of the light.
var unityLightColor = unityLight.color;
fbxLight.Color.Set(new FbxDouble3(unityLightColor.r, unityLightColor.g, unityLightColor.b));
// Set the Intensity of a light is multiplied with the Light color.
fbxLight.Intensity.Set(unityLight.intensity * UnitScaleFactor /*compensate for Maya scaling by system units*/);
// Set the range of the light.
// applies-to: Point & Spot
// => FarAttenuationStart, FarAttenuationEnd
fbxLight.FarAttenuationStart.Set(0.01f /* none zero start */);
fbxLight.FarAttenuationEnd.Set(unityLight.range * UnitScaleFactor);
// shadows Set how this light casts shadows
// applies-to: Point & Spot
bool unityLightCastShadows = unityLight.shadows != LightShadows.None;
fbxLight.CastShadows.Set(unityLightCastShadows);
fbxNode.SetNodeAttribute(fbxLight);
// set +90 post rotation on x to counteract for FBX light's facing -Y direction by default
fbxNode.SetPostRotation(FbxNode.EPivotSet.eSourcePivot, new FbxVector4(90, 0, 0));
// have to set rotation active to true in order for post rotation to be applied
fbxNode.SetRotationActive(true);
return true;
}
private bool ExportCommonConstraintProperties<TUnityConstraint, TFbxConstraint>(TUnityConstraint uniConstraint, TFbxConstraint fbxConstraint, FbxNode fbxNode)
where TUnityConstraint : IConstraint where TFbxConstraint : FbxConstraint
{
fbxConstraint.Active.Set(uniConstraint.constraintActive);
fbxConstraint.Lock.Set(uniConstraint.locked);
fbxConstraint.Weight.Set(uniConstraint.weight * UnitScaleFactor);
AddFbxNodeToConstraintsMapping(fbxNode, fbxConstraint, typeof(TUnityConstraint));
return true;
}
private struct ExpConstraintSource
{
private FbxNode m_node;
public FbxNode node
{
get { return m_node; }
set { m_node = value; }
}
private float m_weight;
public float weight
{
get { return m_weight; }
set { m_weight = value; }
}
public ExpConstraintSource(FbxNode node, float weight)
{
this.m_node = node;
this.m_weight = weight;
}
}
private List<ExpConstraintSource> GetConstraintSources(IConstraint unityConstraint)
{
if (unityConstraint == null)
{
return null;
}
var fbxSources = new List<ExpConstraintSource>();
var sources = new List<ConstraintSource>();
unityConstraint.GetSources(sources);
foreach (var source in sources)
{
if (!source.sourceTransform)
{
continue;
}
// ignore any sources that are not getting exported
FbxNode sourceNode;
if (!MapUnityObjectToFbxNode.TryGetValue(source.sourceTransform.gameObject, out sourceNode))
{
continue;
}
fbxSources.Add(new ExpConstraintSource(sourceNode, source.weight * UnitScaleFactor));
}
return fbxSources;
}
private void AddFbxNodeToConstraintsMapping<T>(FbxNode fbxNode, T fbxConstraint, System.Type uniConstraintType) where T : FbxConstraint
{
Dictionary<FbxConstraint, System.Type> constraintMapping;
if (!MapConstrainedObjectToConstraints.TryGetValue(fbxNode, out constraintMapping))
{
constraintMapping = new Dictionary<FbxConstraint, System.Type>();
MapConstrainedObjectToConstraints.Add(fbxNode, constraintMapping);
}
constraintMapping.Add(fbxConstraint, uniConstraintType);
}
private bool ExportPositionConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode)
{
if (fbxNode == null)
{
return false;
}
var uniPosConstraint = uniConstraint as PositionConstraint;
Debug.Assert(uniPosConstraint != null);
FbxConstraintPosition fbxPosConstraint = FbxConstraintPosition.Create(fbxScene, fbxNode.GetName() + "_positionConstraint");
fbxPosConstraint.SetConstrainedObject(fbxNode);
var uniSources = GetConstraintSources(uniPosConstraint);
uniSources.ForEach(uniSource => fbxPosConstraint.AddConstraintSource(uniSource.node, uniSource.weight));
ExportCommonConstraintProperties(uniPosConstraint, fbxPosConstraint, fbxNode);
var uniAffectedAxes = uniPosConstraint.translationAxis;
fbxPosConstraint.AffectX.Set((uniAffectedAxes & Axis.X) == Axis.X);
fbxPosConstraint.AffectY.Set((uniAffectedAxes & Axis.Y) == Axis.Y);
fbxPosConstraint.AffectZ.Set((uniAffectedAxes & Axis.Z) == Axis.Z);
var fbxTranslationOffset = ConvertToFbxVector4(uniPosConstraint.translationOffset, UnitScaleFactor);
fbxPosConstraint.Translation.Set(ToFbxDouble3(fbxTranslationOffset));
// rest position is the position of the fbx node
var fbxRestTranslation = ConvertToFbxVector4(uniPosConstraint.translationAtRest, UnitScaleFactor);
// set the local position of fbxNode
fbxNode.LclTranslation.Set(ToFbxDouble3(fbxRestTranslation));
return true;
}
private bool ExportRotationConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode)
{
if (fbxNode == null)
{
return false;
}
var uniRotConstraint = uniConstraint as RotationConstraint;
Debug.Assert(uniRotConstraint != null);
FbxConstraintRotation fbxRotConstraint = FbxConstraintRotation.Create(fbxScene, fbxNode.GetName() + "_rotationConstraint");
fbxRotConstraint.SetConstrainedObject(fbxNode);
var uniSources = GetConstraintSources(uniRotConstraint);
uniSources.ForEach(uniSource => fbxRotConstraint.AddConstraintSource(uniSource.node, uniSource.weight));
ExportCommonConstraintProperties(uniRotConstraint, fbxRotConstraint, fbxNode);
var uniAffectedAxes = uniRotConstraint.rotationAxis;
fbxRotConstraint.AffectX.Set((uniAffectedAxes & Axis.X) == Axis.X);
fbxRotConstraint.AffectY.Set((uniAffectedAxes & Axis.Y) == Axis.Y);
fbxRotConstraint.AffectZ.Set((uniAffectedAxes & Axis.Z) == Axis.Z);
// Not converting rotation offset to XYZ euler as it gives the incorrect result in both Maya and Unity.
var uniRotationOffset = uniRotConstraint.rotationOffset;
var fbxRotationOffset = ToFbxDouble3(uniRotationOffset);
fbxRotConstraint.Rotation.Set(fbxRotationOffset);
// rest rotation is the rotation of the fbx node
var fbxRestRotation = ToFbxDouble3(uniRotConstraint.rotationAtRest);
// set the local rotation of fbxNode
fbxNode.LclRotation.Set(fbxRestRotation);
return true;
}
private bool ExportScaleConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode)
{
if (fbxNode == null)
{
return false;
}
var uniScaleConstraint = uniConstraint as ScaleConstraint;
Debug.Assert(uniScaleConstraint != null);
FbxConstraintScale fbxScaleConstraint = FbxConstraintScale.Create(fbxScene, fbxNode.GetName() + "_scaleConstraint");
fbxScaleConstraint.SetConstrainedObject(fbxNode);
var uniSources = GetConstraintSources(uniScaleConstraint);
uniSources.ForEach(uniSource => fbxScaleConstraint.AddConstraintSource(uniSource.node, uniSource.weight));
ExportCommonConstraintProperties(uniScaleConstraint, fbxScaleConstraint, fbxNode);
var uniAffectedAxes = uniScaleConstraint.scalingAxis;
fbxScaleConstraint.AffectX.Set((uniAffectedAxes & Axis.X) == Axis.X);
fbxScaleConstraint.AffectY.Set((uniAffectedAxes & Axis.Y) == Axis.Y);
fbxScaleConstraint.AffectZ.Set((uniAffectedAxes & Axis.Z) == Axis.Z);
var uniScaleOffset = uniScaleConstraint.scaleOffset;
var fbxScalingOffset = ToFbxDouble3(uniScaleOffset);
fbxScaleConstraint.Scaling.Set(fbxScalingOffset);
// rest rotation is the rotation of the fbx node
var uniRestScale = uniScaleConstraint.scaleAtRest;
var fbxRestScale = ToFbxDouble3(uniRestScale);
// set the local rotation of fbxNode
fbxNode.LclScaling.Set(fbxRestScale);
return true;
}
private bool ExportAimConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode)
{
if (fbxNode == null)
{
return false;
}
var uniAimConstraint = uniConstraint as AimConstraint;
Debug.Assert(uniAimConstraint != null);
FbxConstraintAim fbxAimConstraint = FbxConstraintAim.Create(fbxScene, fbxNode.GetName() + "_aimConstraint");
fbxAimConstraint.SetConstrainedObject(fbxNode);
var uniSources = GetConstraintSources(uniAimConstraint);
uniSources.ForEach(uniSource => fbxAimConstraint.AddConstraintSource(uniSource.node, uniSource.weight));
ExportCommonConstraintProperties(uniAimConstraint, fbxAimConstraint, fbxNode);
var uniAffectedAxes = uniAimConstraint.rotationAxis;
fbxAimConstraint.AffectX.Set((uniAffectedAxes & Axis.X) == Axis.X);
fbxAimConstraint.AffectY.Set((uniAffectedAxes & Axis.Y) == Axis.Y);
fbxAimConstraint.AffectZ.Set((uniAffectedAxes & Axis.Z) == Axis.Z);
var uniRotationOffset = uniAimConstraint.rotationOffset;
var fbxRotationOffset = ToFbxDouble3(uniRotationOffset);
fbxAimConstraint.RotationOffset.Set(fbxRotationOffset);
// rest rotation is the rotation of the fbx node
var fbxRestRotation = ToFbxDouble3(uniAimConstraint.rotationAtRest);
// set the local rotation of fbxNode
fbxNode.LclRotation.Set(fbxRestRotation);
FbxConstraintAim.EWorldUp fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtNone;
switch (uniAimConstraint.worldUpType)
{
case AimConstraint.WorldUpType.None:
fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtNone;
break;
case AimConstraint.WorldUpType.ObjectRotationUp:
fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtObjectRotationUp;
break;
case AimConstraint.WorldUpType.ObjectUp:
fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtObjectUp;
break;
case AimConstraint.WorldUpType.SceneUp:
fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtSceneUp;
break;
case AimConstraint.WorldUpType.Vector:
fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtVector;
break;
default:
throw new System.NotImplementedException();
}
fbxAimConstraint.WorldUpType.Set((int)fbxWorldUpType);
var uniAimVector = ConvertToFbxVector4(uniAimConstraint.aimVector);
fbxAimConstraint.AimVector.Set(ToFbxDouble3(uniAimVector));
fbxAimConstraint.UpVector.Set(ToFbxDouble3(uniAimConstraint.upVector));
fbxAimConstraint.WorldUpVector.Set(ToFbxDouble3(uniAimConstraint.worldUpVector));
if (uniAimConstraint.worldUpObject && MapUnityObjectToFbxNode.ContainsKey(uniAimConstraint.worldUpObject.gameObject))
{
fbxAimConstraint.SetWorldUpObject(MapUnityObjectToFbxNode[uniAimConstraint.worldUpObject.gameObject]);
}
return true;
}
private bool ExportParentConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode)
{
if (fbxNode == null)
{
return false;
}
var uniParentConstraint = uniConstraint as ParentConstraint;
Debug.Assert(uniParentConstraint != null);
FbxConstraintParent fbxParentConstraint = FbxConstraintParent.Create(fbxScene, fbxNode.GetName() + "_parentConstraint");
fbxParentConstraint.SetConstrainedObject(fbxNode);
var uniSources = GetConstraintSources(uniParentConstraint);
var uniTranslationOffsets = uniParentConstraint.translationOffsets;
var uniRotationOffsets = uniParentConstraint.rotationOffsets;
for (int i = 0; i < uniSources.Count; i++)
{
var uniSource = uniSources[i];
var uniTranslationOffset = uniTranslationOffsets[i];
var uniRotationOffset = uniRotationOffsets[i];
fbxParentConstraint.AddConstraintSource(uniSource.node, uniSource.weight);
var fbxTranslationOffset = ConvertToFbxVector4(uniTranslationOffset, UnitScaleFactor);
fbxParentConstraint.SetTranslationOffset(uniSource.node, fbxTranslationOffset);
var fbxRotationOffset = ConvertToFbxVector4(uniRotationOffset);
fbxParentConstraint.SetRotationOffset(uniSource.node, fbxRotationOffset);
}
ExportCommonConstraintProperties(uniParentConstraint, fbxParentConstraint, fbxNode);
var uniTranslationAxes = uniParentConstraint.translationAxis;
fbxParentConstraint.AffectTranslationX.Set((uniTranslationAxes & Axis.X) == Axis.X);
fbxParentConstraint.AffectTranslationY.Set((uniTranslationAxes & Axis.Y) == Axis.Y);
fbxParentConstraint.AffectTranslationZ.Set((uniTranslationAxes & Axis.Z) == Axis.Z);
var uniRotationAxes = uniParentConstraint.rotationAxis;
fbxParentConstraint.AffectRotationX.Set((uniRotationAxes & Axis.X) == Axis.X);
fbxParentConstraint.AffectRotationY.Set((uniRotationAxes & Axis.Y) == Axis.Y);
fbxParentConstraint.AffectRotationZ.Set((uniRotationAxes & Axis.Z) == Axis.Z);
// rest position is the position of the fbx node
var fbxRestTranslation = ConvertToFbxVector4(uniParentConstraint.translationAtRest, UnitScaleFactor);
// set the local position of fbxNode
fbxNode.LclTranslation.Set(ToFbxDouble3(fbxRestTranslation));
// rest rotation is the rotation of the fbx node
var fbxRestRotation = ToFbxDouble3(uniParentConstraint.rotationAtRest);
// set the local rotation of fbxNode
fbxNode.LclRotation.Set(fbxRestRotation);
return true;
}
private delegate bool ExportConstraintDelegate(IConstraint c, FbxScene fs, FbxNode fn);
private bool ExportConstraints(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGo)
{
return false;
}
var mapConstraintTypeToExportFunction = new Dictionary<System.Type, ExportConstraintDelegate>()
{
{ typeof(PositionConstraint), ExportPositionConstraint },
{ typeof(RotationConstraint), ExportRotationConstraint },
{ typeof(ScaleConstraint), ExportScaleConstraint },
{ typeof(AimConstraint), ExportAimConstraint },
{ typeof(ParentConstraint), ExportParentConstraint }
};
// check if GameObject has one of the 5 supported constraints: aim, parent, position, rotation, scale
var uniConstraints = unityGo.GetComponents<IConstraint>();
foreach (var uniConstraint in uniConstraints)
{
var uniConstraintType = uniConstraint.GetType();
ExportConstraintDelegate constraintDelegate;
if (!mapConstraintTypeToExportFunction.TryGetValue(uniConstraintType, out constraintDelegate))
{
Debug.LogWarningFormat("FbxExporter: Missing function to export constraint of type {0}", uniConstraintType.Name);
continue;
}
constraintDelegate(uniConstraint, fbxScene, fbxNode);
}
return true;
}
/// <summary>
/// Return set of sample times to cover all keys on animation curves
/// </summary>
internal static HashSet<float> GetSampleTimes(AnimationCurve[] animCurves, double sampleRate)
{
var keyTimes = new HashSet<float>();
double fs = 1.0 / sampleRate;
double firstTime = double.MaxValue, lastTime = double.MinValue;
foreach (var ac in animCurves)
{
if (ac == null || ac.length <= 0) continue;
firstTime = System.Math.Min(firstTime, ac[0].time);
lastTime = System.Math.Max(lastTime, ac[ac.length - 1].time);
}
// if these values didn't get set there were no valid anim curves,
// so don't return any keys
if (firstTime == double.MaxValue || lastTime == double.MinValue)
{
return keyTimes;
}
int firstframe = (int)System.Math.Floor(firstTime * sampleRate);
int lastframe = (int)System.Math.Ceiling(lastTime * sampleRate);
for (int i = firstframe; i <= lastframe; i++)
{
keyTimes.Add((float)(i * fs));
}
return keyTimes;
}
/// <summary>
/// Return set of all keys times on animation curves
/// </summary>
internal static HashSet<float> GetKeyTimes(AnimationCurve[] animCurves)
{
var keyTimes = new HashSet<float>();
foreach (var ac in animCurves)
{
if (ac != null) foreach (var key in ac.keys) { keyTimes.Add(key.time); }
}
return keyTimes;
}
/// <summary>
/// Export animation curve key frames with key tangents
/// NOTE : This is a work in progress (WIP). We only export the key time and value on
/// a Cubic curve using the default tangents.
/// </summary>
internal static void ExportAnimationKeys(AnimationCurve uniAnimCurve, FbxAnimCurve fbxAnimCurve,
UnityToMayaConvertSceneHelper convertSceneHelper)
{
// Copy Unity AnimCurve to FBX AnimCurve.
// NOTE: only cubic keys are supported by the FbxImporter
using (new FbxAnimCurveModifyHelper(new List<FbxAnimCurve> {fbxAnimCurve}))
{
for (int keyIndex = 0; keyIndex < uniAnimCurve.length; ++keyIndex)
{
var uniKeyFrame = uniAnimCurve[keyIndex];
var fbxTime = FbxTime.FromSecondDouble(uniKeyFrame.time);
int fbxKeyIndex = fbxAnimCurve.KeyAdd(fbxTime);
// configure tangents
var lTangent = AnimationUtility.GetKeyLeftTangentMode(uniAnimCurve, keyIndex);
var rTangent = AnimationUtility.GetKeyRightTangentMode(uniAnimCurve, keyIndex);
// Always set tangent mode to eTangentBreak, as other modes are not handled the same in FBX as in
// Unity, thus leading to discrepancies in animation curves.
FbxAnimCurveDef.ETangentMode tanMode = FbxAnimCurveDef.ETangentMode.eTangentBreak;
// Default to cubic interpolation, which is the default for KeySet
FbxAnimCurveDef.EInterpolationType interpMode = FbxAnimCurveDef.EInterpolationType.eInterpolationCubic;
switch (rTangent)
{
case AnimationUtility.TangentMode.Linear:
interpMode = FbxAnimCurveDef.EInterpolationType.eInterpolationLinear;
break;
case AnimationUtility.TangentMode.Constant:
interpMode = FbxAnimCurveDef.EInterpolationType.eInterpolationConstant;
break;
default:
break;
}
fbxAnimCurve.KeySet(fbxKeyIndex,
fbxTime,
convertSceneHelper.Convert(uniKeyFrame.value),
interpMode,
tanMode,
// value of right slope
convertSceneHelper.Convert(uniKeyFrame.outTangent),
// value of next left slope
keyIndex < uniAnimCurve.length - 1 ? convertSceneHelper.Convert(uniAnimCurve[keyIndex + 1].inTangent) : 0,
FbxAnimCurveDef.EWeightedMode.eWeightedAll,
// weight for right slope
uniKeyFrame.outWeight,
// weight for next left slope
keyIndex < uniAnimCurve.length - 1 ? uniAnimCurve[keyIndex + 1].inWeight : 0
);
}
}
}
/// <summary>
/// Export animation curve key samples
/// </summary>
internal void ExportAnimationSamples(AnimationCurve uniAnimCurve, FbxAnimCurve fbxAnimCurve,
double sampleRate,
UnityToMayaConvertSceneHelper convertSceneHelper)
{
using (new FbxAnimCurveModifyHelper(new List<FbxAnimCurve> {fbxAnimCurve}))
{
foreach (var currSampleTime in GetSampleTimes(new AnimationCurve[] {uniAnimCurve}, sampleRate))
{
float currSampleValue = uniAnimCurve.Evaluate((float)currSampleTime);
var fbxTime = FbxTime.FromSecondDouble(currSampleTime);
int fbxKeyIndex = fbxAnimCurve.KeyAdd(fbxTime);
fbxAnimCurve.KeySet(fbxKeyIndex,
fbxTime,
convertSceneHelper.Convert(currSampleValue)
);
}
}
}
/// <summary>
/// Get the FbxConstraint associated with the constrained node.
/// </summary>
/// <param name="constrainedNode"></param>
/// <param name="uniConstraintType"></param>
/// <returns></returns>
private FbxConstraint GetFbxConstraint(FbxNode constrainedNode, System.Type uniConstraintType)
{
if (uniConstraintType == null || !uniConstraintType.GetInterfaces().Contains(typeof(IConstraint)))
{
// not actually a constraint
return null;
}
Dictionary<FbxConstraint, System.Type> constraints;
if (MapConstrainedObjectToConstraints.TryGetValue(constrainedNode, out constraints))
{
var targetConstraint = constraints.FirstOrDefault(constraint => (constraint.Value == uniConstraintType));
if (!targetConstraint.Equals(default(KeyValuePair<FbxConstraint, System.Type>)))
{
return targetConstraint.Key;
}
}
return null;
}
/// <summary>
/// Get the FbxBlendshape with the given name associated with the FbxNode.
/// </summary>
/// <param name="blendshapeNode"></param>
/// <param name="uniPropertyName"></param>
/// <returns></returns>
private FbxBlendShapeChannel GetFbxBlendShape(FbxNode blendshapeNode, string uniPropertyName)
{
List<FbxBlendShapeChannel> blendshapeChannels;
if (MapUnityObjectToBlendShapes.TryGetValue(blendshapeNode, out blendshapeChannels))
{
var match = System.Text.RegularExpressions.Regex.Match(uniPropertyName, @"blendShape\.(\S+)");
if (match.Success && match.Groups.Count > 0)
{
string blendshapeName = match.Groups[1].Value;
var targetChannel = blendshapeChannels.FirstOrDefault(channel => (channel.GetName() == blendshapeName));
if (targetChannel != null)
{
return targetChannel;
}
}
}
return null;
}
private FbxProperty GetFbxProperty(FbxNode fbxNode, string fbxPropertyName, System.Type uniPropertyType, string uniPropertyName)
{
if (fbxNode == null)
{
return null;
}
// check if property maps to a constraint
// check this first because both constraints and FbxNodes can contain a RotationOffset property,
// but only the constraint one is animatable.
var fbxConstraint = GetFbxConstraint(fbxNode, uniPropertyType);
if (fbxConstraint != null)
{
var prop = fbxConstraint.FindProperty(fbxPropertyName, false);
if (prop.IsValid())
{
return prop;
}
}
// check if the property maps to a blendshape
var fbxBlendShape = GetFbxBlendShape(fbxNode, uniPropertyName);
if (fbxBlendShape != null)
{
var prop = fbxBlendShape.FindProperty(fbxPropertyName, false);
if (prop.IsValid())
{
return prop;
}
}
// map unity property name to fbx property
var fbxProperty = fbxNode.FindProperty(fbxPropertyName, false);
if (fbxProperty.IsValid())
{
return fbxProperty;
}
var fbxNodeAttribute = fbxNode.GetNodeAttribute();
if (fbxNodeAttribute != null)
{
fbxProperty = fbxNodeAttribute.FindProperty(fbxPropertyName, false);
}
return fbxProperty;
}
/// <summary>
/// Export an AnimationCurve.
/// NOTE: This is not used for rotations, because we need to convert from
/// quaternion to euler and various other stuff.
/// </summary>
private void ExportAnimationCurve(FbxNode fbxNode,
AnimationCurve uniAnimCurve,
float frameRate,
string uniPropertyName,
System.Type uniPropertyType,
FbxAnimLayer fbxAnimLayer)
{
if (fbxNode == null)
{
return;
}
if (Verbose)
{
Debug.Log("Exporting animation for " + fbxNode.GetName() + " (" + uniPropertyName + ")");
}
var fbxConstraint = GetFbxConstraint(fbxNode, uniPropertyType);
FbxPropertyChannelPair[] fbxPropertyChannelPairs;
if (!FbxPropertyChannelPair.TryGetValue(uniPropertyName, out fbxPropertyChannelPairs, fbxConstraint))
{
Debug.LogWarning(string.Format("no mapping from Unity '{0}' to fbx property", uniPropertyName));
return;
}
foreach (var fbxPropertyChannelPair in fbxPropertyChannelPairs)
{
// map unity property name to fbx property
var fbxProperty = GetFbxProperty(fbxNode, fbxPropertyChannelPair.Property, uniPropertyType, uniPropertyName);
if (!fbxProperty.IsValid())
{
Debug.LogError(string.Format("no fbx property {0} found on {1} node or nodeAttribute ", fbxPropertyChannelPair.Property, fbxNode.GetName()));
return;
}
if (!fbxProperty.GetFlag(FbxPropertyFlags.EFlags.eAnimatable))
{
Debug.LogErrorFormat("fbx property {0} found on node {1} is not animatable", fbxPropertyChannelPair.Property, fbxNode.GetName());
}
// Create the AnimCurve on the channel
FbxAnimCurve fbxAnimCurve = fbxProperty.GetCurve(fbxAnimLayer, fbxPropertyChannelPair.Channel, true);
if (fbxAnimCurve == null)
{
return;
}
// create a convert scene helper so that we can convert from Unity to Maya
// AxisSystem (LeftHanded to RightHanded) and FBX's default units
// (Meters to Centimetres)
var convertSceneHelper = new UnityToMayaConvertSceneHelper(uniPropertyName, fbxNode);
if (ModelExporter.ExportSettings.BakeAnimationProperty)
{
ExportAnimationSamples(uniAnimCurve, fbxAnimCurve, frameRate, convertSceneHelper);
}
else
{
ExportAnimationKeys(uniAnimCurve, fbxAnimCurve, convertSceneHelper);
}
}
}
internal class UnityToMayaConvertSceneHelper
{
bool convertDistance = false;
bool convertToRadian = false;
bool convertLensShiftX = false;
bool convertLensShiftY = false;
FbxCamera camera = null;
float unitScaleFactor = 1f;
public UnityToMayaConvertSceneHelper(string uniPropertyName, FbxNode fbxNode)
{
System.StringComparison cc = System.StringComparison.CurrentCulture;
bool partT = uniPropertyName.StartsWith("m_LocalPosition.", cc) || uniPropertyName.StartsWith("m_TranslationOffset", cc);
convertDistance |= partT;
convertDistance |= uniPropertyName.StartsWith("m_Intensity", cc);
convertDistance |= uniPropertyName.ToLower().EndsWith("weight", cc);
convertLensShiftX |= uniPropertyName.StartsWith("m_LensShift.x", cc);
convertLensShiftY |= uniPropertyName.StartsWith("m_LensShift.y", cc);
if (convertLensShiftX || convertLensShiftY)
{
camera = fbxNode.GetCamera();
}
// The ParentConstraint's source Rotation Offsets are read in as radians, so make sure they are exported as radians
convertToRadian = uniPropertyName.StartsWith("m_RotationOffsets.Array.data", cc);
if (convertDistance)
unitScaleFactor = ModelExporter.UnitScaleFactor;
if (convertToRadian)
{
unitScaleFactor *= (Mathf.PI / 180);
}
}
public float Convert(float value)
{
float convertedValue = value;
if (convertLensShiftX || convertLensShiftY)
{
convertedValue = Mathf.Clamp(Mathf.Abs(value), 0f, 1f) * Mathf.Sign(value);
}
if (camera != null)
{
if (convertLensShiftX)
{
convertedValue *= (float)camera.GetApertureWidth();
}
else if (convertLensShiftY)
{
convertedValue *= (float)camera.GetApertureHeight();
}
}
// left handed to right handed conversion
// meters to centimetres conversion
return unitScaleFactor * convertedValue;
}
}
/// <summary>
/// Export an AnimationClip as a single take
/// </summary>
private void ExportAnimationClip(AnimationClip uniAnimClip, GameObject uniRoot, FbxScene fbxScene)
{
if (!uniAnimClip || !uniRoot || fbxScene == null) return;
if (Verbose)
Debug.Log(string.Format("Exporting animation clip ({1}) for {0}", uniRoot.name, uniAnimClip.name));
// setup anim stack
FbxAnimStack fbxAnimStack = FbxAnimStack.Create(fbxScene, uniAnimClip.name);
fbxAnimStack.Description.Set("Animation Take: " + uniAnimClip.name);
// add one mandatory animation layer
FbxAnimLayer fbxAnimLayer = FbxAnimLayer.Create(fbxScene, "Animation Base Layer");
fbxAnimStack.AddMember(fbxAnimLayer);
// Set up the FPS so our frame-relative math later works out
// Custom frame rate isn't really supported in FBX SDK (there's
// a bug), so try hard to find the nearest time mode.
FbxTime.EMode timeMode = FbxTime.EMode.eCustom;
double precision = 1e-6;
while (timeMode == FbxTime.EMode.eCustom && precision < 1000)
{
timeMode = FbxTime.ConvertFrameRateToTimeMode(uniAnimClip.frameRate, precision);
precision *= 10;
}
if (timeMode == FbxTime.EMode.eCustom)
{
timeMode = FbxTime.EMode.eFrames30;
}
fbxScene.GetGlobalSettings().SetTimeMode(timeMode);
// set time correctly
var fbxStartTime = FbxTime.FromSecondDouble(0);
var fbxStopTime = FbxTime.FromSecondDouble(uniAnimClip.length);
fbxAnimStack.SetLocalTimeSpan(new FbxTimeSpan(fbxStartTime, fbxStopTime));
var unityCurves = new Dictionary<GameObject, List<UnityCurve>>();
// extract and store all necessary information from the curve bindings, namely the animation curves
// and their corresponding property names for each GameObject.
foreach (EditorCurveBinding uniCurveBinding in AnimationUtility.GetCurveBindings(uniAnimClip))
{
Object uniObj = AnimationUtility.GetAnimatedObject(uniRoot, uniCurveBinding);
if (!uniObj)
{
continue;
}
AnimationCurve uniAnimCurve = AnimationUtility.GetEditorCurve(uniAnimClip, uniCurveBinding);
if (uniAnimCurve == null)
{
continue;
}
var uniGO = GetGameObject(uniObj);
// Check if the GameObject has an FBX node to the animation. It might be null because the LOD selected doesn't match the one on the gameobject.
if (!uniGO || MapUnityObjectToFbxNode.ContainsKey(uniGO) == false)
{
continue;
}
if (unityCurves.ContainsKey(uniGO))
{
unityCurves[uniGO].Add(new UnityCurve(uniCurveBinding.propertyName, uniAnimCurve, uniCurveBinding.type));
continue;
}
unityCurves.Add(uniGO, new List<UnityCurve>(){ new UnityCurve(uniCurveBinding.propertyName, uniAnimCurve, uniCurveBinding.type) });
}
// transfer root motion
var animSource = ExportOptions.AnimationSource;
var animDest = ExportOptions.AnimationDest;
if (animSource && animDest && animSource != animDest)
{
// list of all transforms between source and dest, including source and dest
var transformsFromSourceToDest = new List<Transform>();
var curr = animDest;
while (curr != animSource)
{
transformsFromSourceToDest.Add(curr);
curr = curr.parent;
}
transformsFromSourceToDest.Add(animSource);
transformsFromSourceToDest.Reverse();
// while there are 2 transforms in the list, transfer the animation from the
// first to the next transform.
// Then remove the first transform from the list.
while (transformsFromSourceToDest.Count >= 2)
{
var source = transformsFromSourceToDest[0];
transformsFromSourceToDest.RemoveAt(0);
var dest = transformsFromSourceToDest[0];
TransferMotion(source, dest, uniAnimClip.frameRate, ref unityCurves);
}
}
/* The major difficulty: Unity uses quaternions for rotation
* (which is how it should be) but FBX uses Euler angles. So we
* need to gather up the list of transform curves per object.
*
* For euler angles, Unity uses ZXY rotation order while Maya uses XYZ.
* Maya doesn't import files with ZXY rotation correctly, so have to convert to XYZ.
* Need all 3 curves in order to convert.
*
* Also, in both cases, prerotation has to be removed from the animated rotation if
* there are bones being exported.
*/
var rotations = new Dictionary<GameObject, RotationCurve>();
// export the animation curves for each GameObject that has animation
foreach (var kvp in unityCurves)
{
var uniGO = kvp.Key;
foreach (var uniCurve in kvp.Value)
{
var propertyName = uniCurve.propertyName;
var uniAnimCurve = uniCurve.uniAnimCurve;
// Do not create the curves if the component is a SkinnedMeshRenderer and if the option in FBX Export settings is toggled on.
if (!ExportOptions.AnimateSkinnedMesh && (uniGO.GetComponent<SkinnedMeshRenderer>() != null))
{
continue;
}
FbxNode fbxNode;
if (!MapUnityObjectToFbxNode.TryGetValue(uniGO, out fbxNode))
{
Debug.LogError(string.Format("no FbxNode found for {0}", uniGO.name));
continue;
}
int index = QuaternionCurve.GetQuaternionIndex(propertyName);
if (index >= 0)
{
// Rotation property; save it to convert quaternion -> euler later.
RotationCurve rotCurve = GetRotationCurve<QuaternionCurve>(uniGO, uniAnimClip.frameRate, ref rotations);
rotCurve.SetCurve(index, uniAnimCurve);
continue;
}
// If this is an euler curve with a prerotation, then need to sample animations to remove the prerotation.
// Otherwise can export normally with tangents.
index = EulerCurve.GetEulerIndex(propertyName);
if (index >= 0 &&
// still need to sample euler curves if baking is specified
(ModelExporter.ExportSettings.BakeAnimationProperty ||
// also need to make sure to sample if there is a prerotation, as this is baked into the Unity curves
fbxNode.GetPreRotation(FbxNode.EPivotSet.eSourcePivot).Distance(new FbxVector4()) > 0))
{
RotationCurve rotCurve = GetRotationCurve<EulerCurve>(uniGO, uniAnimClip.frameRate, ref rotations);
rotCurve.SetCurve(index, uniAnimCurve);
continue;
}
// simple property (e.g. intensity), export right away
ExportAnimationCurve(fbxNode, uniAnimCurve, uniAnimClip.frameRate,
propertyName, uniCurve.propertyType,
fbxAnimLayer);
}
}
// now export all the quaternion curves
foreach (var kvp in rotations)
{
var unityGo = kvp.Key;
var rot = kvp.Value;
FbxNode fbxNode;
if (!MapUnityObjectToFbxNode.TryGetValue(unityGo, out fbxNode))
{
Debug.LogError(string.Format("no FbxNode found for {0}", unityGo.name));
continue;
}
rot.Animate(unityGo.transform, fbxNode, fbxAnimLayer, Verbose);
}
}
/// <summary>
/// Transfers transform animation from source to dest. Replaces dest's Unity Animation Curves with updated animations.
/// NOTE: Source must be the parent of dest.
/// </summary>
/// <param name="source">Source animated object.</param>
/// <param name="dest">Destination, child of the source.</param>
/// <param name="sampleRate">Sample rate.</param>
/// <param name="unityCurves">Unity curves.</param>
private void TransferMotion(Transform source, Transform dest, float sampleRate, ref Dictionary<GameObject, List<UnityCurve>> unityCurves)
{
// get sample times for curves in dest + source
// at each sample time, evaluate all 18 transfom anim curves, creating 2 transform matrices
// combine the matrices, get the new values, apply to the 9 new anim curves for dest
if (dest.parent != source)
{
Debug.LogError("dest must be a child of source");
return;
}
List<UnityCurve> sourceUnityCurves;
if (!unityCurves.TryGetValue(source.gameObject, out sourceUnityCurves))
{
return; // nothing to do, source has no animation
}
List<UnityCurve> destUnityCurves;
if (!unityCurves.TryGetValue(dest.gameObject, out destUnityCurves))
{
destUnityCurves = new List<UnityCurve>();
}
List<AnimationCurve> animCurves = new List<AnimationCurve>();
foreach (var curve in sourceUnityCurves)
{
// TODO: check if curve is anim related
animCurves.Add(curve.uniAnimCurve);
}
foreach (var curve in destUnityCurves)
{
animCurves.Add(curve.uniAnimCurve);
}
var sampleTimes = GetSampleTimes(animCurves.ToArray(), sampleRate);
// need to create 9 new UnityCurves, one for each property
var posKeyFrames = new Keyframe[3][];
var rotKeyFrames = new Keyframe[3][];
var scaleKeyFrames = new Keyframe[3][];
for (int k = 0; k < posKeyFrames.Length; k++)
{
posKeyFrames[k] = new Keyframe[sampleTimes.Count];
rotKeyFrames[k] = new Keyframe[sampleTimes.Count];
scaleKeyFrames[k] = new Keyframe[sampleTimes.Count];
}
// If we have a point in local coords represented as a column-vector x, the equation of x in coordinates relative to source's parent is:
// x_grandparent = source * dest * x
// Now we're going to change dest to dest' which has the animation from source. And we're going to change
// source to source' which has no animation. The equation of x will become:
// x_grandparent = source' * dest' * x
// We're not changing x_grandparent and x, so we need that:
// source * dest = source' * dest'
// We know dest and source (both animated) and source' (static). Solve for dest':
// dest' = (source')^-1 * source * dest
int keyIndex = 0;
var sourceStaticMatrixInverse = Matrix4x4.TRS(source.localPosition, source.localRotation, source.localScale).inverse;
foreach (var currSampleTime in sampleTimes)
{
var sourceLocalMatrix = GetTransformMatrix(currSampleTime, source, sourceUnityCurves);
var destLocalMatrix = GetTransformMatrix(currSampleTime, dest, destUnityCurves);
var newLocalMatrix = sourceStaticMatrixInverse * sourceLocalMatrix * destLocalMatrix;
FbxVector4 translation, rotation, scale;
GetTRSFromMatrix(newLocalMatrix, out translation, out rotation, out scale);
// get rotation directly from matrix, as otherwise causes issues
// with negative rotations.
var rot = newLocalMatrix.rotation.eulerAngles;
for (int k = 0; k < 3; k++)
{
posKeyFrames[k][keyIndex] = new Keyframe(currSampleTime, (float)translation[k]);
rotKeyFrames[k][keyIndex] = new Keyframe(currSampleTime, rot[k]);
scaleKeyFrames[k][keyIndex] = new Keyframe(currSampleTime, (float)scale[k]);
}
keyIndex++;
}
// create the new list of unity curves, and add it to dest's curves
var newUnityCurves = new List<UnityCurve>();
string posPropName = "m_LocalPosition.";
string rotPropName = "localEulerAnglesRaw.";
string scalePropName = "m_LocalScale.";
var xyz = "xyz";
for (int k = 0; k < 3; k++)
{
var posUniCurve = new UnityCurve(posPropName + xyz[k], new AnimationCurve(posKeyFrames[k]), typeof(Transform));
newUnityCurves.Add(posUniCurve);
var rotUniCurve = new UnityCurve(rotPropName + xyz[k], new AnimationCurve(rotKeyFrames[k]), typeof(Transform));
newUnityCurves.Add(rotUniCurve);
var scaleUniCurve = new UnityCurve(scalePropName + xyz[k], new AnimationCurve(scaleKeyFrames[k]), typeof(Transform));
newUnityCurves.Add(scaleUniCurve);
}
// remove old transform curves
RemoveTransformCurves(ref sourceUnityCurves);
RemoveTransformCurves(ref destUnityCurves);
unityCurves[source.gameObject] = sourceUnityCurves;
if (!unityCurves.ContainsKey(dest.gameObject))
{
unityCurves.Add(dest.gameObject, newUnityCurves);
return;
}
unityCurves[dest.gameObject].AddRange(newUnityCurves);
}
private void RemoveTransformCurves(ref List<UnityCurve> curves)
{
var transformCurves = new List<UnityCurve>();
var transformPropNames = new string[] {"m_LocalPosition.", "m_LocalRotation", "localEulerAnglesRaw.", "m_LocalScale."};
foreach (var curve in curves)
{
foreach (var prop in transformPropNames)
{
if (curve.propertyName.StartsWith(prop))
{
transformCurves.Add(curve);
break;
}
}
}
foreach (var curve in transformCurves)
{
curves.Remove(curve);
}
}
private Matrix4x4 GetTransformMatrix(float currSampleTime, Transform orig, List<UnityCurve> unityCurves)
{
var sourcePos = orig.localPosition;
var sourceRot = orig.localRotation;
var sourceScale = orig.localScale;
foreach (var uniCurve in unityCurves)
{
float currSampleValue = uniCurve.uniAnimCurve.Evaluate(currSampleTime);
string propName = uniCurve.propertyName;
// try position, scale, quat then euler
int temp = QuaternionCurve.GetQuaternionIndex(propName);
if (temp >= 0)
{
sourceRot[temp] = currSampleValue;
continue;
}
temp = EulerCurve.GetEulerIndex(propName);
if (temp >= 0)
{
var euler = sourceRot.eulerAngles;
euler[temp] = currSampleValue;
sourceRot.eulerAngles = euler;
continue;
}
temp = GetPositionIndex(propName);
if (temp >= 0)
{
sourcePos[temp] = currSampleValue;
continue;
}
temp = GetScaleIndex(propName);
if (temp >= 0)
{
sourceScale[temp] = currSampleValue;
}
}
sourceRot = Quaternion.Euler(sourceRot.eulerAngles.x, sourceRot.eulerAngles.y, sourceRot.eulerAngles.z);
return Matrix4x4.TRS(sourcePos, sourceRot, sourceScale);
}
internal struct UnityCurve
{
public string propertyName;
public AnimationCurve uniAnimCurve;
public System.Type propertyType;
public UnityCurve(string propertyName, AnimationCurve uniAnimCurve, System.Type propertyType)
{
this.propertyName = propertyName;
this.uniAnimCurve = uniAnimCurve;
this.propertyType = propertyType;
}
}
private int GetPositionIndex(string uniPropertyName)
{
System.StringComparison ct = System.StringComparison.CurrentCulture;
bool isPositionComponent = uniPropertyName.StartsWith("m_LocalPosition.", ct);
if (!isPositionComponent) { return -1; }
switch (uniPropertyName[uniPropertyName.Length - 1])
{
case 'x':
return 0;
case 'y':
return 1;
case 'z':
return 2;
default:
return -1;
}
}
private int GetScaleIndex(string uniPropertyName)
{
System.StringComparison ct = System.StringComparison.CurrentCulture;
bool isScaleComponent = uniPropertyName.StartsWith("m_LocalScale.", ct);
if (!isScaleComponent) { return -1; }
switch (uniPropertyName[uniPropertyName.Length - 1])
{
case 'x':
return 0;
case 'y':
return 1;
case 'z':
return 2;
default:
return -1;
}
}
/// <summary>
/// Gets or creates the rotation curve for GameObject uniGO.
/// </summary>
/// <returns>The rotation curve.</returns>
/// <param name="uniGO">Unity GameObject.</param>
/// <param name="frameRate">Frame rate.</param>
/// <param name="rotations">Rotations.</param>
/// <typeparam name="T"> RotationCurve is abstract so specify type of RotationCurve to create.</typeparam>
private RotationCurve GetRotationCurve<T>(
GameObject uniGO, float frameRate,
ref Dictionary<GameObject, RotationCurve> rotations
) where T : RotationCurve, new()
{
RotationCurve rotCurve;
if (!rotations.TryGetValue(uniGO, out rotCurve))
{
rotCurve = new T { SampleRate = frameRate };
rotations.Add(uniGO, rotCurve);
}
return rotCurve;
}
/// <summary>
/// Export the Animator component on this game object
/// </summary>
private void ExportAnimation(GameObject uniRoot, FbxScene fbxScene)
{
if (!uniRoot)
{
return;
}
var exportedClips = new HashSet<AnimationClip>();
var uniAnimator = uniRoot.GetComponent<Animator>();
if (uniAnimator)
{
// Try the animator controller (mecanim)
var controller = uniAnimator.runtimeAnimatorController;
if (controller)
{
// Only export each clip once per game object.
foreach (var clip in controller.animationClips)
{
if (exportedClips.Add(clip))
{
ExportAnimationClip(clip, uniRoot, fbxScene);
}
}
}
}
// Try the playable director
var director = uniRoot.GetComponent<UnityEngine.Playables.PlayableDirector>();
if (director)
{
Debug.LogWarning(string.Format("Exporting animation from PlayableDirector on {0} not supported", uniRoot.name));
// TODO: export animationclips from playabledirector
}
// Try the animation (legacy)
var uniAnimation = uniRoot.GetComponent<Animation>();
if (uniAnimation)
{
// Only export each clip once per game object.
foreach (var uniAnimObj in uniAnimation)
{
AnimationState uniAnimState = uniAnimObj as AnimationState;
if (uniAnimState)
{
AnimationClip uniAnimClip = uniAnimState.clip;
if (exportedClips.Add(uniAnimClip))
{
ExportAnimationClip(uniAnimClip, uniRoot, fbxScene);
}
}
}
}
}
/// <summary>
/// configures default camera for the scene
/// </summary>
private void SetDefaultCamera(FbxScene fbxScene)
{
if (fbxScene == null) { return; }
if (string.IsNullOrEmpty(DefaultCamera))
DefaultCamera = Globals.FBXSDK_CAMERA_PERSPECTIVE;
fbxScene.GetGlobalSettings().SetDefaultCamera(DefaultCamera);
}
/// <summary>
/// Ensures that the inputted name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. Sphere becomes Sphere_1
/// </summary>
/// <returns>Unique name</returns>
/// <param name="name">Name</param>
/// <param name="nameToCountMap">The dictionary to use to map name to # of occurences</param>
private string GetUniqueName(string name, Dictionary<string, int> nameToCountMap)
{
var uniqueName = name;
int count;
if (nameToCountMap.TryGetValue(name, out count))
{
uniqueName = string.Format(UniqueNameFormat, name, count);
}
else
{
count = 0;
}
nameToCountMap[name] = count + 1;
return uniqueName;
}
/// <summary>
/// Ensures that the inputted name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. Sphere becomes Sphere_1
/// </summary>
/// <returns>Unique name</returns>
/// <param name="name">Name</param>
private string GetUniqueFbxNodeName(string name)
{
return GetUniqueName(name, NameToIndexMap);
}
/// <summary>
/// Ensures that the inputted material name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. mat becomes mat_1
/// </summary>
/// <param name="name">Name</param>
/// <returns>Unique material name</returns>
private string GetUniqueMaterialName(string name)
{
return GetUniqueName(name, MaterialNameToIndexMap);
}
/// <summary>
/// Ensures that the inputted texture name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. tex becomes tex_1
/// </summary>
/// <param name="name">Name</param>
/// <returns>Unique texture name</returns>
private string GetUniqueTextureName(string name)
{
return GetUniqueName(name, TextureNameToIndexMap);
}
/// <summary>
/// Create a fbxNode from unityGo.
/// </summary>
/// <param name="unityGo"></param>
/// <param name="fbxScene"></param>
/// <returns>the created FbxNode</returns>
private FbxNode CreateFbxNode(GameObject unityGo, FbxScene fbxScene)
{
string fbxName = unityGo.name;
if (ExportOptions.UseMayaCompatibleNames)
{
fbxName = ConvertToMayaCompatibleName(unityGo.name);
if (ExportOptions.AllowSceneModification)
{
Undo.RecordObject(unityGo, "rename " + fbxName);
unityGo.name = fbxName;
}
}
FbxNode fbxNode = FbxNode.Create(fbxScene, GetUniqueFbxNodeName(fbxName));
// Default inheritance type in FBX is RrSs, which causes scaling issues in Maya as
// both Maya and Unity use RSrs inheritance by default.
// Note: MotionBuilder uses RrSs inheritance by default as well, though it is possible
// to select a different inheritance type in the UI.
// Use RSrs as the scaling inheritance instead.
fbxNode.SetTransformationInheritType(FbxTransform.EInheritType.eInheritRSrs);
// Fbx rotation order is XYZ, but Unity rotation order is ZXY.
// Also, DeepConvert does not convert the rotation order (assumes XYZ), unless RotationActive is true.
fbxNode.SetRotationOrder(FbxNode.EPivotSet.eSourcePivot, FbxEuler.EOrder.eOrderZXY);
fbxNode.SetRotationActive(true);
MapUnityObjectToFbxNode[unityGo] = fbxNode;
return fbxNode;
}
/// <summary>
/// Creates an FbxNode for each GameObject.
/// </summary>
/// <returns>The number of nodes exported.</returns>
internal int ExportTransformHierarchy(
GameObject unityGo, FbxScene fbxScene, FbxNode fbxNodeParent,
int exportProgress, int objectCount, Vector3 newCenter,
TransformExportType exportType = TransformExportType.Local,
LODExportType lodExportType = LODExportType.All
)
{
int numObjectsExported = exportProgress;
FbxNode fbxNode = CreateFbxNode(unityGo, fbxScene);
if (Verbose)
Debug.Log(string.Format("exporting {0}", fbxNode.GetName()));
numObjectsExported++;
if (EditorUtility.DisplayCancelableProgressBar(
ProgressBarTitle,
string.Format("Creating FbxNode {0}/{1}", numObjectsExported, objectCount),
(numObjectsExported / (float)objectCount) * 0.25f))
{
// cancel silently
return -1;
}
ExportTransform(unityGo.transform, fbxNode, newCenter, exportType);
fbxNodeParent.AddChild(fbxNode);
// if this object has an LOD group, then export according to the LOD preference setting
var lodGroup = unityGo.GetComponent<LODGroup>();
if (lodGroup && lodExportType != LODExportType.All)
{
LOD[] lods = lodGroup.GetLODs();
// LODs are ordered from highest to lowest.
// If exporting lowest LOD, reverse the array
if (lodExportType == LODExportType.Lowest)
{
// reverse the array
LOD[] tempLods = new LOD[lods.Length];
System.Array.Copy(lods, tempLods, lods.Length);
System.Array.Reverse(tempLods);
lods = tempLods;
}
for (int i = 0; i < lods.Length; i++)
{
var lod = lods[i];
bool exportedRenderer = false;
foreach (var renderer in lod.renderers)
{
// only export if parented under LOD group
if (renderer.transform.parent == unityGo.transform)
{
numObjectsExported = ExportTransformHierarchy(renderer.gameObject, fbxScene, fbxNode, numObjectsExported, objectCount, newCenter, lodExportType: lodExportType);
exportedRenderer = true;
}
else if (Verbose)
{
Debug.LogFormat("FbxExporter: Not exporting LOD {0}: {1}", i, renderer.name);
}
}
// if at least one renderer for this LOD was exported, then we succeeded
// so stop exporting.
if (exportedRenderer)
{
return numObjectsExported;
}
}
}
// now unityGo through our children and recurse
foreach (Transform childT in unityGo.transform)
{
numObjectsExported = ExportTransformHierarchy(childT.gameObject, fbxScene, fbxNode, numObjectsExported, objectCount, newCenter, lodExportType: lodExportType);
}
return numObjectsExported;
}
/// <summary>
/// Exports all animation clips in the hierarchy along with
/// the minimum required GameObject information.
/// i.e. Animated GameObjects, their ancestors, and their transforms are exported,
/// but components are only exported if explicitly animated. Meshes are not exported.
/// </summary>
/// <returns>The number of nodes exported.</returns>
internal int ExportAnimationOnly(
GameObject unityGO,
FbxScene fbxScene,
int exportProgress,
int objectCount,
Vector3 newCenter,
IExportData data,
TransformExportType exportType = TransformExportType.Local
)
{
AnimationOnlyExportData exportData = (AnimationOnlyExportData)data;
int numObjectsExported = exportProgress;
// make sure anim destination node is exported as well
var exportSet = exportData.Objects;
if (ExportOptions.AnimationDest && ExportOptions.AnimationSource)
{
exportSet.Add(ExportOptions.AnimationDest.gameObject);
}
// first export all the animated bones that are in the export set
// as only a subset of bones are exported, but we still need to make sure the bone transforms are correct
if (!ExportAnimatedBones(unityGO, fbxScene, ref numObjectsExported, objectCount, exportData))
{
// export cancelled
return -1;
}
// export everything else and make sure all nodes are connected
foreach (var go in exportSet)
{
FbxNode node;
if (!ExportGameObjectAndParents(
go, unityGO, fbxScene, out node, newCenter, exportType, ref numObjectsExported, objectCount
))
{
// export cancelled
return -1;
}
ExportConstraints(go, fbxScene, node);
System.Type compType;
if (exportData.exportComponent.TryGetValue(go, out compType))
{
if (compType == typeof(Light))
{
ExportLight(go, fbxScene, node);
}
else if (compType == typeof(Camera))
{
ExportCamera(go, fbxScene, node);
}
else if (compType == typeof(SkinnedMeshRenderer))
{
// export only what is necessary for exporting blendshape animation
var unitySkin = go.GetComponent<SkinnedMeshRenderer>();
var meshInfo = new MeshInfo(unitySkin.sharedMesh, unitySkin.sharedMaterials);
ExportMesh(meshInfo, node);
}
}
}
return numObjectsExported;
}
internal class SkinnedMeshBoneInfo
{
public SkinnedMeshRenderer skinnedMesh;
public Dictionary<Transform, int> boneDict;
public Dictionary<Transform, Matrix4x4> boneToBindPose;
public SkinnedMeshBoneInfo(SkinnedMeshRenderer skinnedMesh, Dictionary<Transform, int> boneDict)
{
this.skinnedMesh = skinnedMesh;
this.boneDict = boneDict;
this.boneToBindPose = new Dictionary<Transform, Matrix4x4>();
}
}
private bool ExportAnimatedBones(
GameObject unityGo,
FbxScene fbxScene,
ref int exportProgress,
int objectCount,
AnimationOnlyExportData exportData
)
{
var skinnedMeshRenderers = unityGo.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var skinnedMesh in skinnedMeshRenderers)
{
var boneArray = skinnedMesh.bones;
var bones = new HashSet<GameObject>();
var boneDict = new Dictionary<Transform, int>();
for (int i = 0; i < boneArray.Length; i++)
{
bones.Add(boneArray[i].gameObject);
boneDict.Add(boneArray[i], i);
}
// get the bones that are also in the export set
bones.IntersectWith(exportData.Objects);
var boneInfo = new SkinnedMeshBoneInfo(skinnedMesh, boneDict);
foreach (var bone in bones)
{
FbxNode fbxNode;
// bone already exported
if (MapUnityObjectToFbxNode.TryGetValue(bone, out fbxNode))
{
continue;
}
fbxNode = CreateFbxNode(bone, fbxScene);
exportProgress++;
if (EditorUtility.DisplayCancelableProgressBar(
ProgressBarTitle,
string.Format("Creating FbxNode {0}/{1}", exportProgress, objectCount),
(exportProgress / (float)objectCount) * 0.5f))
{
// cancel silently
return false;
}
ExportBoneTransform(fbxNode, fbxScene, bone.transform, boneInfo);
}
}
return true;
}
/// <summary>
/// Exports the Gameobject and its ancestors.
/// </summary>
/// <returns><c>true</c>, if game object and parents were exported,
/// <c>false</c> if export cancelled.</returns>
private bool ExportGameObjectAndParents(
GameObject unityGo,
GameObject rootObject,
FbxScene fbxScene,
out FbxNode fbxNode,
Vector3 newCenter,
TransformExportType exportType,
ref int exportProgress,
int objectCount
)
{
// node doesn't exist so create it
if (!MapUnityObjectToFbxNode.TryGetValue(unityGo, out fbxNode))
{
fbxNode = CreateFbxNode(unityGo, fbxScene);
exportProgress++;
if (EditorUtility.DisplayCancelableProgressBar(
ProgressBarTitle,
string.Format("Creating FbxNode {0}/{1}", exportProgress, objectCount),
(exportProgress / (float)objectCount) * 0.5f))
{
// cancel silently
return false;
}
ExportTransform(unityGo.transform, fbxNode, newCenter, exportType);
}
if (unityGo == rootObject || unityGo.transform.parent == null)
{
fbxScene.GetRootNode().AddChild(fbxNode);
return true;
}
// make sure all the nodes are connected and exported
FbxNode fbxNodeParent;
if (!ExportGameObjectAndParents(
unityGo.transform.parent.gameObject,
rootObject,
fbxScene,
out fbxNodeParent,
newCenter,
TransformExportType.Local,
ref exportProgress,
objectCount
))
{
// export cancelled
return false;
}
fbxNodeParent.AddChild(fbxNode);
return true;
}
/// <summary>
/// Exports the bone transform.
/// </summary>
/// <returns><c>true</c>, if bone transform was exported, <c>false</c> otherwise.</returns>
/// <param name="fbxNode">Fbx node.</param>
/// <param name="fbxScene">Fbx scene.</param>
/// <param name="unityBone">Unity bone.</param>
/// <param name="boneInfo">Bone info.</param>
private bool ExportBoneTransform(
FbxNode fbxNode, FbxScene fbxScene, Transform unityBone, SkinnedMeshBoneInfo boneInfo
)
{
if (boneInfo == null || boneInfo.skinnedMesh == null || boneInfo.boneDict == null || unityBone == null)
{
return false;
}
var skinnedMesh = boneInfo.skinnedMesh;
var boneDict = boneInfo.boneDict;
var rootBone = skinnedMesh.rootBone;
// setup the skeleton
var fbxSkeleton = fbxNode.GetSkeleton();
if (fbxSkeleton == null)
{
fbxSkeleton = FbxSkeleton.Create(fbxScene, unityBone.name + SkeletonPrefix);
fbxSkeleton.Size.Set(1.0f * UnitScaleFactor);
fbxNode.SetNodeAttribute(fbxSkeleton);
}
var fbxSkeletonType = FbxSkeleton.EType.eLimbNode;
// Only set the rootbone's skeleton type to FbxSkeleton.EType.eRoot
// if it has at least one child that is also a bone.
// Otherwise if it is marked as Root but has no bones underneath,
// Maya will import it as a Null object instead of a bone.
if (rootBone == unityBone && rootBone.childCount > 0)
{
var hasChildBone = false;
foreach (Transform child in unityBone)
{
if (boneDict.ContainsKey(child))
{
hasChildBone = true;
break;
}
}
if (hasChildBone)
{
fbxSkeletonType = FbxSkeleton.EType.eRoot;
}
}
fbxSkeleton.SetSkeletonType(fbxSkeletonType);
var bindPoses = skinnedMesh.sharedMesh.bindposes;
// get bind pose
Matrix4x4 bindPose = GetBindPose(unityBone, bindPoses, ref boneInfo);
Matrix4x4 pose;
// get parent's bind pose
Matrix4x4 parentBindPose = GetBindPose(unityBone.parent, bindPoses, ref boneInfo);
pose = parentBindPose * bindPose.inverse;
FbxVector4 translation, rotation, scale;
GetTRSFromMatrix(pose, out translation, out rotation, out scale);
// Export bones with zero rotation, using a pivot instead to set the rotation
// so that the bones are easier to animate and the rotation shows up as the "joint orientation" in Maya.
fbxNode.LclTranslation.Set(new FbxDouble3(translation.X * UnitScaleFactor, translation.Y * UnitScaleFactor, translation.Z * UnitScaleFactor));
fbxNode.LclRotation.Set(new FbxDouble3(0, 0, 0));
fbxNode.LclScaling.Set(new FbxDouble3(scale.X, scale.Y, scale.Z));
// TODO (UNI-34294): add detailed comment about why we export rotation as pre-rotation
fbxNode.SetRotationActive(true);
fbxNode.SetPivotState(FbxNode.EPivotSet.eSourcePivot, FbxNode.EPivotState.ePivotReference);
fbxNode.SetPreRotation(FbxNode.EPivotSet.eSourcePivot, new FbxVector4(rotation.X, rotation.Y, rotation.Z));
return true;
}
private void GetTRSFromMatrix(Matrix4x4 unityMatrix, out FbxVector4 translation, out FbxVector4 rotation, out FbxVector4 scale)
{
// FBX is transposed relative to Unity: transpose as we convert.
FbxMatrix matrix = new FbxMatrix();
matrix.SetColumn(0, new FbxVector4(unityMatrix.GetRow(0).x, unityMatrix.GetRow(0).y, unityMatrix.GetRow(0).z, unityMatrix.GetRow(0).w));
matrix.SetColumn(1, new FbxVector4(unityMatrix.GetRow(1).x, unityMatrix.GetRow(1).y, unityMatrix.GetRow(1).z, unityMatrix.GetRow(1).w));
matrix.SetColumn(2, new FbxVector4(unityMatrix.GetRow(2).x, unityMatrix.GetRow(2).y, unityMatrix.GetRow(2).z, unityMatrix.GetRow(2).w));
matrix.SetColumn(3, new FbxVector4(unityMatrix.GetRow(3).x, unityMatrix.GetRow(3).y, unityMatrix.GetRow(3).z, unityMatrix.GetRow(3).w));
// FBX wants translation, rotation (in euler angles) and scale.
// We assume there's no real shear, just rounding error.
FbxVector4 shear;
double sign;
matrix.GetElements(out translation, out rotation, out shear, out scale, out sign);
}
/// <summary>
/// Counts how many objects are between this object and the root (exclusive).
/// </summary>
/// <returns>The object to root count.</returns>
/// <param name="startObject">Start object.</param>
/// <param name="root">Root object.</param>
private static int GetObjectToRootDepth(Transform startObject, Transform root)
{
if (startObject == null)
{
return 0;
}
int count = 0;
var parent = startObject.parent;
while (parent != null && parent != root)
{
count++;
parent = parent.parent;
}
return count;
}
/// <summary>
/// Gets the count of animated objects to be exported.
///
/// In addition, collects the minimum set of what needs to be exported for each GameObject hierarchy.
/// This contains all the animated GameObjects, their ancestors, their transforms, as well as any animated
/// components and the animation clips. Also, the first animation to export, if any.
/// </summary>
/// <returns>The animation only hierarchy count.</returns>
/// <param name="hierarchyToExportData">Map from GameObject hierarchy to animation export data.</param>
internal int GetAnimOnlyHierarchyCount(Dictionary<GameObject, IExportData> hierarchyToExportData)
{
// including any parents of animated objects that are exported
var completeExpSet = new HashSet<GameObject>();
foreach (var data in hierarchyToExportData.Values)
{
if (data == null || data.Objects == null || data.Objects.Count == 0)
{
continue;
}
foreach (var go in data.Objects)
{
completeExpSet.Add(go);
var parent = go.transform.parent;
while (parent != null && completeExpSet.Add(parent.gameObject))
{
parent = parent.parent;
}
}
}
return completeExpSet.Count;
}
internal static Dictionary<GameObject, IExportData> GetExportData(TimelineClip timelineClip, PlayableDirector director = null, IExportOptions exportOptions = null)
{
if (timelineClip == null)
{
return null;
}
if (exportOptions == null)
exportOptions = DefaultOptions;
Debug.Assert(exportOptions != null);
// only support anim only export for timeline clips
if (exportOptions.ModelAnimIncludeOption != Include.Anim)
{
Debug.LogWarning("Timeline clips must be exported with Anim only include option");
return null;
}
Dictionary<GameObject, IExportData> exportData = new Dictionary<GameObject, IExportData>();
KeyValuePair<GameObject, AnimationClip> pair = AnimationOnlyExportData.GetGameObjectAndAnimationClip(timelineClip, director);
var boundGo = pair.Key;
if (boundGo == null)
{
return null;
}
exportData[boundGo] = GetExportData(boundGo, pair.Value, exportOptions);
return exportData;
}
internal static Dictionary<GameObject, IExportData> GetExportData(Object[] objects, IExportOptions exportOptions = null)
{
if (exportOptions == null)
exportOptions = DefaultOptions;
Debug.Assert(exportOptions != null);
if (exportOptions.ModelAnimIncludeOption == Include.Model)
{
return null;
}
Dictionary<GameObject, IExportData> exportData = new Dictionary<GameObject, IExportData>();
foreach (var obj in objects)
{
GameObject go = ModelExporter.GetGameObject(obj);
if (go)
{
exportData[go] = GetExportData(go, exportOptions);
}
}
return exportData.Count == 0 ? null : exportData;
}
internal static IExportData GetExportData(GameObject rootObject, AnimationClip animationClip, IExportOptions exportOptions = null)
{
if (rootObject == null || animationClip == null)
{
return null;
}
if (exportOptions == null)
exportOptions = DefaultOptions;
Debug.Assert(exportOptions != null);
var exportData = new AnimationOnlyExportData();
exportData.CollectDependencies(animationClip, rootObject, exportOptions);
// could not find any dependencies, return null
if (exportData.Objects.Count == 0)
{
return null;
}
return exportData;
}
internal static IExportData GetExportData(GameObject go, IExportOptions exportOptions = null)
{
if (exportOptions == null)
exportOptions = DefaultOptions;
Debug.Assert(exportOptions != null);
// gather all animation clips
var legacyAnim = go.GetComponentsInChildren<Animation>();
var genericAnim = go.GetComponentsInChildren<Animator>();
var exportData = new AnimationOnlyExportData();
int depthFromRootAnimation = int.MaxValue;
Animation rootAnimation = null;
foreach (var anim in legacyAnim)
{
int count = GetObjectToRootDepth(anim.transform, go.transform);
if (count < depthFromRootAnimation)
{
depthFromRootAnimation = count;
rootAnimation = anim;
}
var animClips = AnimationUtility.GetAnimationClips(anim.gameObject);
exportData.CollectDependencies(animClips, anim.gameObject, exportOptions);
}
int depthFromRootAnimator = int.MaxValue;
Animator rootAnimator = null;
foreach (var anim in genericAnim)
{
int count = GetObjectToRootDepth(anim.transform, go.transform);
if (count < depthFromRootAnimator)
{
depthFromRootAnimator = count;
rootAnimator = anim;
}
// Try the animator controller (mecanim)
var controller = anim.runtimeAnimatorController;
if (controller)
{
exportData.CollectDependencies(controller.animationClips, anim.gameObject, exportOptions);
}
}
// set the first clip to export
if (depthFromRootAnimation < depthFromRootAnimator)
{
exportData.defaultClip = rootAnimation.clip;
}
else if (rootAnimator)
{
// Try the animator controller (mecanim)
var controller = rootAnimator.runtimeAnimatorController;
if (controller)
{
var dController = controller as UnityEditor.Animations.AnimatorController;
var controllerLayers = dController != null ? dController.layers : null;
if (controllerLayers != null && controllerLayers.Length > 0)
{
var motion = controllerLayers[0].stateMachine.defaultState.motion;
var defaultClip = motion as AnimationClip;
if (defaultClip)
{
exportData.defaultClip = defaultClip;
}
else
{
if (motion != null)
{
Debug.LogWarningFormat("Couldn't export motion {0}", motion.name);
}
// missing animation
else
{
Debug.LogWarningFormat("Couldn't export motion. Motion is missing.");
}
}
}
}
}
return exportData;
}
/// <summary>
/// Export components on this game object.
/// Transform components have already been exported.
/// This function exports the other components and animation.
/// </summary>
private bool ExportComponents(FbxScene fbxScene)
{
int numObjectsExported = 0;
int objectCount = MapUnityObjectToFbxNode.Count;
foreach (KeyValuePair<GameObject, FbxNode> entry in MapUnityObjectToFbxNode)
{
numObjectsExported++;
if (EditorUtility.DisplayCancelableProgressBar(
ProgressBarTitle,
string.Format("Exporting Components for GameObject {0}/{1}", numObjectsExported, objectCount),
((numObjectsExported / (float)objectCount) * 0.25f) + 0.25f))
{
// cancel silently
return false;
}
var unityGo = entry.Key;
var fbxNode = entry.Value;
// try export mesh
bool exportedMesh = false;
if (ExportOptions.KeepInstances)
{
exportedMesh = ExportInstance(unityGo, fbxScene, fbxNode);
}
if (!exportedMesh)
{
exportedMesh = ExportMesh(unityGo, fbxNode);
}
// export camera, but only if no mesh was exported
bool exportedCamera = false;
if (!exportedMesh)
{
exportedCamera = ExportCamera(unityGo, fbxScene, fbxNode);
}
// export light, but only if no mesh or camera was exported
if (!exportedMesh && !exportedCamera)
{
ExportLight(unityGo, fbxScene, fbxNode);
}
ExportConstraints(unityGo, fbxScene, fbxNode);
}
return true;
}
/// <summary>
/// Checks if the GameObject has animation.
/// </summary>
/// <returns><c>true</c>, if object has animation, <c>false</c> otherwise.</returns>
/// <param name="go">Go.</param>
private bool GameObjectHasAnimation(GameObject go)
{
return go != null &&
(go.GetComponent<Animator>() ||
go.GetComponent<Animation>() ||
go.GetComponent<UnityEngine.Playables.PlayableDirector>());
}
/// <summary>
/// A count of how many GameObjects we are exporting, to have a rough
/// idea of how long creating the scene will take.
/// </summary>
/// <returns>The hierarchy count.</returns>
/// <param name="exportSet">Export set.</param>
internal int GetHierarchyCount(HashSet<GameObject> exportSet)
{
int count = 0;
Queue<GameObject> queue = new Queue<GameObject>(exportSet);
while (queue.Count > 0)
{
var obj = queue.Dequeue();
var objTransform = obj.transform;
foreach (Transform child in objTransform)
{
queue.Enqueue(child.gameObject);
}
count++;
}
return count;
}
/// <summary>
/// Removes objects that will already be exported anyway.
/// E.g. if a parent and its child are both selected, then the child
/// will be removed from the export set.
/// </summary>
/// <returns>The revised export set</returns>
/// <param name="unityExportSet">Unity export set.</param>
internal static HashSet<GameObject> RemoveRedundantObjects(IEnumerable<UnityEngine.Object> unityExportSet)
{
// basically just remove the descendents from the unity export set
HashSet<GameObject> toExport = new HashSet<GameObject>();
HashSet<UnityEngine.Object> hashedExportSet = new HashSet<Object>(unityExportSet);
foreach (var obj in unityExportSet)
{
var unityGo = GetGameObject(obj);
if (unityGo)
{
// if any of this nodes ancestors is already in the export set,
// then ignore it, it will get exported already
bool parentInSet = false;
var parent = unityGo.transform.parent;
while (parent != null)
{
if (hashedExportSet.Contains(parent.gameObject))
{
parentInSet = true;
break;
}
parent = parent.parent;
}
if (!parentInSet)
{
toExport.Add(unityGo);
}
}
}
return toExport;
}
/// <summary>
/// Recursively go through the hierarchy, unioning the bounding box centers
/// of all the children, to find the combined bounds.
/// </summary>
/// <param name="t">Transform.</param>
/// <param name="boundsUnion">The Bounds that is the Union of all the bounds on this transform's hierarchy.</param>
private static void EncapsulateBounds(Transform t, ref Bounds boundsUnion)
{
var bounds = GetBounds(t);
boundsUnion.Encapsulate(bounds);
foreach (Transform child in t)
{
EncapsulateBounds(child, ref boundsUnion);
}
}
/// <summary>
/// Gets the bounds of a transform.
/// Looks first at the Renderer, then Mesh, then Collider.
/// Default to a bounds with center transform.position and size zero.
/// </summary>
/// <returns>The bounds.</returns>
/// <param name="t">Transform.</param>
private static Bounds GetBounds(Transform t)
{
if (t.TryGetComponent<Renderer>(out Renderer renderer))
{
return renderer.bounds;
}
if (t.TryGetComponent<MeshFilter>(out MeshFilter meshFilter))
{
return meshFilter.mesh.bounds;
}
if (t.TryGetComponent<Collider>(out Collider collider))
{
return collider.bounds;
}
return new Bounds(t.position, Vector3.zero);
}
/// <summary>
/// Finds the center of a group of GameObjects.
/// </summary>
/// <returns>Center of gameObjects.</returns>
/// <param name="gameObjects">Game objects.</param>
internal static Vector3 FindCenter(IEnumerable<GameObject> gameObjects)
{
Bounds bounds = new Bounds();
// Assign the initial bounds to first GameObject's bounds
// (if we initialize the bounds to 0, then 0 will be part of the bounds)
foreach (var go in gameObjects)
{
var tempBounds = GetBounds(go.transform);
bounds = new Bounds(tempBounds.center, tempBounds.size);
break;
}
foreach (var go in gameObjects)
{
EncapsulateBounds(go.transform, ref bounds);
}
return bounds.center;
}
/// <summary>
/// Gets the recentered translation.
/// </summary>
/// <returns>The recentered translation.</returns>
/// <param name="t">Transform.</param>
/// <param name="center">Center point.</param>
internal static Vector3 GetRecenteredTranslation(Transform t, Vector3 center)
{
return t.position - center;
}
internal enum TransformExportType { Local, Global, Reset };
/// <summary>
/// Export all the objects in the set.
/// Return the number of objects in the set that we exported.
///
/// This refreshes the asset database.
/// </summary>
internal int ExportAll(
IEnumerable<UnityEngine.Object> unityExportSet,
Dictionary<GameObject, IExportData> exportData)
{
exportCancelled = false;
m_lastFilePath = LastFilePath;
// Export first to a temporary file
// in case the export is cancelled.
// This way we won't overwrite existing files.
try
{
// create a temp file in the same directory where the fbx will be exported
var exportDir = Path.GetDirectoryName(m_lastFilePath);
var lastFileName = Path.GetFileName(m_lastFilePath);
var tempFileName = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) + "_" + lastFileName;
m_tempFilePath = Path.Combine(new string[] { exportDir, tempFileName });
}
catch (IOException)
{
return 0;
}
if (string.IsNullOrEmpty(m_tempFilePath))
{
return 0;
}
try
{
bool animOnly = exportData != null && ExportOptions.ModelAnimIncludeOption == Include.Anim;
bool status = false;
// Create the FBX manager
using (var fbxManager = FbxManager.Create())
{
// Configure fbx IO settings.
var settings = FbxIOSettings.Create(fbxManager, Globals.IOSROOT);
if (ExportOptions.EmbedTextures)
settings.SetBoolProp(Globals.EXP_FBX_EMBEDDED, true);
fbxManager.SetIOSettings(settings);
// Create the exporter
var fbxExporter = FbxExporter.Create(fbxManager, "Exporter");
// Initialize the exporter.
// fileFormat must be binary if we are embedding textures
int fileFormat = -1;
if (ExportOptions.ExportFormat == ExportFormat.ASCII)
{
fileFormat = fbxManager.GetIOPluginRegistry().FindWriterIDByDescription("FBX ascii (*.fbx)");
}
status = fbxExporter.Initialize(m_tempFilePath, fileFormat, fbxManager.GetIOSettings());
// Check that initialization of the fbxExporter was successful
if (!status)
return 0;
// Set the progress callback.
fbxExporter.SetProgressCallback(ExportProgressCallback);
// Create a scene
var fbxScene = FbxScene.Create(fbxManager, "Scene");
// set up the scene info
FbxDocumentInfo fbxSceneInfo = FbxDocumentInfo.Create(fbxManager, "SceneInfo");
fbxSceneInfo.mTitle = Title;
fbxSceneInfo.mSubject = Subject;
fbxSceneInfo.mAuthor = "Unity Technologies";
fbxSceneInfo.mRevision = "1.0";
fbxSceneInfo.mKeywords = Keywords;
fbxSceneInfo.mComment = Comments;
fbxSceneInfo.Original_ApplicationName.Set(string.Format("Unity {0}", PACKAGE_UI_NAME));
// set last saved to be the same as original, as this is a new file.
fbxSceneInfo.LastSaved_ApplicationName.Set(fbxSceneInfo.Original_ApplicationName.Get());
var version = GetVersionFromReadme();
if (version != null)
{
fbxSceneInfo.Original_ApplicationVersion.Set(version);
fbxSceneInfo.LastSaved_ApplicationVersion.Set(fbxSceneInfo.Original_ApplicationVersion.Get());
}
fbxScene.SetSceneInfo(fbxSceneInfo);
// Set up the axes (Y up, Z forward, X to the right) and units (centimeters)
// Exporting in centimeters as this is the default unit for FBX files, and easiest
// to work with when importing into Maya or Max
var fbxSettings = fbxScene.GetGlobalSettings();
fbxSettings.SetSystemUnit(FbxSystemUnit.cm);
// The Unity axis system has Y up, Z forward, X to the right (left handed system with odd parity).
// DirectX has the same axis system, so use this constant.
var unityAxisSystem = FbxAxisSystem.DirectX;
fbxSettings.SetAxisSystem(unityAxisSystem);
// export set of object
FbxNode fbxRootNode = fbxScene.GetRootNode();
// stores how many objects we have exported, -1 if export was cancelled
int exportProgress = 0;
IEnumerable<GameObject> revisedExportSet = null;
// Total # of objects to be exported
// Used by progress bar to show how many objects will be exported in total
// i.e. exporting x/count...
int count = 0;
// number of object hierarchies being exported.
// Used to figure out exported transforms for root objects.
// i.e. if we are exporting a single hierarchy at local position, then it's root is set to zero,
// but if we are exporting multiple hierarchies at local position, then each hierarchy will be recentered according
// to the center of the bounding box.
int rootObjCount = 0;
if (animOnly)
{
count = GetAnimOnlyHierarchyCount(exportData);
revisedExportSet = from entry in exportData select entry.Key;
rootObjCount = exportData.Keys.Count;
}
else
{
var revisedGOSet = RemoveRedundantObjects(unityExportSet);
count = GetHierarchyCount(revisedGOSet);
rootObjCount = revisedGOSet.Count;
revisedExportSet = revisedGOSet;
}
if (count <= 0)
{
// nothing to export
Debug.LogWarning("Nothing to Export");
return 0;
}
Vector3 center = Vector3.zero;
TransformExportType transformExportType = TransformExportType.Global;
switch (ExportOptions.ObjectPosition)
{
case ObjectPosition.LocalCentered:
// one object to export -> move to (0,0,0)
if (rootObjCount == 1)
{
var tempList = new List<GameObject>(revisedExportSet);
center = tempList[0].transform.position;
break;
}
// more than one object to export -> get bounding center
center = FindCenter(revisedExportSet);
break;
case ObjectPosition.Reset:
transformExportType = TransformExportType.Reset;
break;
// absolute center -> don't do anything
default:
center = Vector3.zero;
break;
}
foreach (var unityGo in revisedExportSet)
{
IExportData data;
if (animOnly && exportData.TryGetValue(unityGo, out data))
{
exportProgress = this.ExportAnimationOnly(unityGo, fbxScene, exportProgress, count, center, data, transformExportType);
}
else
{
exportProgress = this.ExportTransformHierarchy(unityGo, fbxScene, fbxRootNode,
exportProgress, count, center, transformExportType, ExportOptions.LODExportType);
}
if (exportCancelled || exportProgress < 0)
{
Debug.LogWarning("Export Cancelled");
return 0;
}
}
if (!animOnly)
{
if (!ExportComponents(fbxScene))
{
Debug.LogWarning("Export Cancelled");
return 0;
}
}
// Export animation if any
if (exportData != null)
{
foreach (var unityGo in revisedExportSet)
{
IExportData iData;
if (!exportData.TryGetValue(unityGo, out iData))
{
continue;
}
var data = iData as AnimationOnlyExportData;
if (data == null)
{
Debug.LogWarningFormat("FBX Exporter: no animation export data found for {0}", unityGo.name);
continue;
}
// export animation
// export default clip first
if (data.defaultClip != null)
{
var defaultClip = data.defaultClip;
ExportAnimationClip(defaultClip, data.animationClips[defaultClip], fbxScene);
data.animationClips.Remove(defaultClip);
}
foreach (var animClip in data.animationClips)
{
ExportAnimationClip(animClip.Key, animClip.Value, fbxScene);
}
}
}
// Set the scene's default camera.
SetDefaultCamera(fbxScene);
// 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 (used by Maya and Max importers) can't switch handedness:
// https://forums.autodesk.com/t5/fbx-forum/get-confused-with-fbxaxissystem-convertscene/td-p/4265472
// This needs to be done last so that everything is converted properly.
FbxAxisSystem.MayaYUp.DeepConvertScene(fbxScene);
// Export the scene to the file.
status = fbxExporter.Export(fbxScene);
// cleanup
fbxScene.Destroy();
fbxExporter.Destroy();
}
if (exportCancelled)
{
Debug.LogWarning("Export Cancelled");
return 0;
}
// make a temporary copy of the original metafile
string originalMetafilePath = "";
if (ExportOptions.PreserveImportSettings && File.Exists(m_lastFilePath))
{
originalMetafilePath = SaveMetafile();
}
// delete old file, move temp file
ReplaceFile();
// refresh the database so Unity knows the file's been deleted
AssetDatabase.Refresh();
// replace with original metafile if specified to
if (ExportOptions.PreserveImportSettings && !string.IsNullOrEmpty(originalMetafilePath))
{
ReplaceMetafile(originalMetafilePath);
}
return status == true ? NumNodes : 0;
}
finally
{
// You must clear the progress bar when you're done,
// otherwise it never goes away and many actions in Unity
// are blocked (e.g. you can't quit).
EditorUtility.ClearProgressBar();
// make sure the temp file is deleted, no matter
// when we return
DeleteTempFile();
}
}
static bool exportCancelled = false;
static bool ExportProgressCallback(float percentage, string status)
{
// Convert from percentage to [0,1].
// Then convert from that to [0.5,1] because the first half of
// the progress bar was for creating the scene.
var progress01 = 0.5f * (1f + (percentage / 100.0f));
bool cancel = EditorUtility.DisplayCancelableProgressBar(ProgressBarTitle, "Exporting Scene...", progress01);
if (cancel)
{
exportCancelled = true;
}
// Unity says "true" for "cancel"; FBX wants "true" for "continue"
return !cancel;
}
/// <summary>
/// Deletes the file that got created while exporting.
/// </summary>
private void DeleteTempFile()
{
if (!File.Exists(m_tempFilePath))
{
return;
}
try
{
File.Delete(m_tempFilePath);
}
catch (IOException)
{
}
if (File.Exists(m_tempFilePath))
{
Debug.LogWarning("Failed to delete file: " + m_tempFilePath);
}
}
/// <summary>
/// Replaces the file we are overwriting with
/// the temp file that was exported to.
/// </summary>
private void ReplaceFile()
{
if (m_tempFilePath.Equals(m_lastFilePath) || !File.Exists(m_tempFilePath))
{
return;
}
// delete old file
try
{
File.Delete(m_lastFilePath);
// delete meta file also
File.Delete(m_lastFilePath + ".meta");
}
catch (IOException)
{
}
if (File.Exists(m_lastFilePath))
{
Debug.LogWarning("Failed to delete file: " + m_lastFilePath);
}
// rename the new file
try
{
File.Move(m_tempFilePath, m_lastFilePath);
}
catch (IOException)
{
Debug.LogWarning(string.Format("Failed to move file {0} to {1}", m_tempFilePath, m_lastFilePath));
}
}
private string SaveMetafile()
{
var tempMetafilePath = Path.GetTempFileName();
// get relative path
var fbxPath = "Assets/" + ExportSettings.ConvertToAssetRelativePath(m_lastFilePath);
if (AssetDatabase.LoadAssetAtPath(fbxPath, typeof(Object)) == null)
{
Debug.LogWarning(string.Format("Failed to find a valid asset at {0}. Import settings will be reset to default values.", m_lastFilePath));
return "";
}
// get metafile for original fbx file
var metafile = fbxPath + ".meta";
#if UNITY_2019_1_OR_NEWER
metafile = VersionControl.Provider.GetAssetByPath(fbxPath).metaPath;
#endif
// save it to a temp file
try
{
File.Copy(metafile, tempMetafilePath, true);
}
catch (IOException)
{
Debug.LogWarning(string.Format("Failed to copy file {0} to {1}. Import settings will be reset to default values.", metafile, tempMetafilePath));
return "";
}
return tempMetafilePath;
}
private void ReplaceMetafile(string metafilePath)
{
// get relative path
var fbxPath = "Assets/" + ExportSettings.ConvertToAssetRelativePath(m_lastFilePath);
if (AssetDatabase.LoadAssetAtPath(fbxPath, typeof(Object)) == null)
{
Debug.LogWarning(string.Format("Failed to find a valid asset at {0}. Import settings will be reset to default values.", m_lastFilePath));
return;
}
// get metafile for new fbx file
var metafile = fbxPath + ".meta";
#if UNITY_2019_1_OR_NEWER
metafile = VersionControl.Provider.GetAssetByPath(fbxPath).metaPath;
#endif
// replace metafile with original one in temp file
try
{
File.Copy(metafilePath, metafile, true);
}
catch (IOException)
{
Debug.LogWarning(string.Format("Failed to copy file {0} to {1}. Import settings will be reset to default values.", metafilePath, m_lastFilePath));
}
}
internal static void ExportSingleTimelineClip(TimelineClip timelineClip, PlayableDirector director = null)
{
string filename = AnimationOnlyExportData.GetFileName(timelineClip);
if (ExportSettings.DisplayOptionsWindow)
{
ExportModelEditorWindow.Init(null, filename, timelineClip, director);
return;
}
var folderPath = ExportSettings.FbxAbsoluteSavePath;
var filePath = System.IO.Path.Combine(folderPath, filename + ".fbx");
if (System.IO.File.Exists(filePath))
{
Debug.LogErrorFormat("{0}: Failed to export to {1}, file already exists", PACKAGE_UI_NAME, filePath);
return;
}
var previousInclude = ExportSettings.instance.ExportModelSettings.info.ModelAnimIncludeOption;
ExportSettings.instance.ExportModelSettings.info.SetModelAnimIncludeOption(Include.Anim);
if (ExportTimelineClip(filePath, timelineClip, director, ExportSettings.instance.ExportModelSettings.info) != null)
{
// refresh the asset database so that the file appears in the
// asset folder view.
AssetDatabase.Refresh();
}
ExportSettings.instance.ExportModelSettings.info.SetModelAnimIncludeOption(previousInclude);
}
/// <summary>
/// Add a menu item "Export Model..." to a GameObject's context menu.
/// </summary>
/// <param name="command">Command.</param>
[MenuItem(MenuItemName, false, 30)]
internal static void OnContextItem(MenuCommand command)
{
if (Selection.objects.Length == 0)
{
DisplayNoSelectionDialog();
return;
}
OnExport();
}
/// <summary>
/// Validate the menu item defined by the function OnContextItem.
/// </summary>
[MenuItem(MenuItemName, true, 30)]
internal static bool OnValidateMenuItem()
{
return true;
}
internal static void DisplayNoSelectionDialog()
{
UnityEditor.EditorUtility.DisplayDialog(
string.Format("{0} Warning", PACKAGE_UI_NAME),
"No GameObjects selected for export.",
"Ok");
}
//
// export mesh info from Unity
//
///<summary>
///Information about the mesh that is important for exporting.
///</summary>
internal class MeshInfo
{
public Mesh mesh;
/// <summary>
/// Return true if there's a valid mesh information
/// </summary>
public bool IsValid { get { return mesh; } }
/// <summary>
/// Gets the vertex count.
/// </summary>
/// <value>The vertex count.</value>
public int VertexCount { get { return Vertices.Length; } }
/// <summary>
/// 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]
/// </summary>
/// <value>The triangles.</value>
private int[] m_triangles;
public int[] Triangles
{
get
{
if (m_triangles == null) { m_triangles = mesh.triangles; }
return m_triangles;
}
}
/// <summary>
/// Gets the vertices, represented in local coordinates.
/// </summary>
/// <value>The vertices.</value>
private Vector3[] m_vertices;
public Vector3[] Vertices
{
get
{
if (m_vertices == null) { m_vertices = mesh.vertices; }
return m_vertices;
}
}
/// <summary>
/// Gets the normals for the vertices.
/// </summary>
/// <value>The normals.</value>
private Vector3[] m_normals;
public Vector3[] Normals
{
get
{
if (m_normals == null)
{
m_normals = mesh.normals;
}
return m_normals;
}
}
/// <summary>
/// Gets the binormals for the vertices.
/// </summary>
/// <value>The normals.</value>
private Vector3[] m_Binormals;
public Vector3[] Binormals
{
get
{
/// NOTE: LINQ
/// return mesh.normals.Zip (mesh.tangents, (first, second)
/// => Math.cross (normal, tangent.xyz) * tangent.w
if (m_Binormals == null || m_Binormals.Length == 0)
{
var normals = Normals;
var tangents = Tangents;
if (HasValidNormals() && HasValidTangents())
{
m_Binormals = new Vector3[normals.Length];
for (int i = 0; i < normals.Length; i++)
m_Binormals[i] = Vector3.Cross(normals[i],
tangents[i])
* tangents[i].w;
}
}
return m_Binormals;
}
}
/// <summary>
/// Gets the tangents for the vertices.
/// </summary>
/// <value>The tangents.</value>
private Vector4[] m_tangents;
public Vector4[] Tangents
{
get
{
if (m_tangents == null)
{
m_tangents = mesh.tangents;
}
return m_tangents;
}
}
/// <summary>
/// Gets the vertex colors for the vertices.
/// </summary>
/// <value>The vertex colors.</value>
private Color32[] m_vertexColors;
public Color32[] VertexColors
{
get
{
if (m_vertexColors == null)
{
m_vertexColors = mesh.colors32;
}
return m_vertexColors;
}
}
/// <summary>
/// Gets the uvs.
/// </summary>
/// <value>The uv.</value>
private Vector2[] m_UVs;
public Vector2[] UV
{
get
{
if (m_UVs == null)
{
m_UVs = mesh.uv;
}
return m_UVs;
}
}
/// <summary>
/// The material(s) used.
/// Always at least one.
/// None are missing materials (we replace missing materials with the default material).
/// </summary>
public Material[] Materials { get; private set; }
/// <summary>
/// Set up the MeshInfo with the given mesh and materials.
/// </summary>
public MeshInfo(Mesh mesh, Material[] materials)
{
this.mesh = mesh;
this.m_Binormals = null;
this.m_vertices = null;
this.m_triangles = null;
this.m_normals = null;
this.m_UVs = null;
this.m_vertexColors = null;
this.m_tangents = null;
if (materials == null)
{
this.Materials = new Material[] { DefaultMaterial };
}
else
{
this.Materials = materials.Select(mat => mat ? mat : DefaultMaterial).ToArray();
if (this.Materials.Length == 0)
{
this.Materials = new Material[] { DefaultMaterial };
}
}
}
public bool HasValidNormals()
{
return Normals != null && Normals.Length > 0;
}
public bool HasValidBinormals()
{
return HasValidNormals() &&
HasValidTangents() &&
Binormals != null;
}
public bool HasValidTangents()
{
return Tangents != null && Tangents.Length > 0;
}
public bool HasValidVertexColors()
{
return VertexColors != null && VertexColors.Length > 0;
}
}
/// <summary>
/// Get the GameObject
/// </summary>
internal static GameObject GetGameObject(Object obj)
{
if (obj is UnityEngine.Transform)
{
var xform = obj as UnityEngine.Transform;
return xform.gameObject;
}
else if (obj is UnityEngine.SkinnedMeshRenderer)
{
var skinnedMeshRenderer = obj as UnityEngine.SkinnedMeshRenderer;
return skinnedMeshRenderer.gameObject;
}
else if (obj is UnityEngine.GameObject)
{
return obj as UnityEngine.GameObject;
}
else if (obj is Behaviour)
{
var behaviour = obj as Behaviour;
return behaviour.gameObject;
}
return null;
}
/// <summary>
/// Map from type (must be a MonoBehaviour) to callback.
/// The type safety is lost; the caller must ensure it at run-time.
/// </summary>
static Dictionary<System.Type, GetMeshForComponent> MeshForComponentCallbacks
= new Dictionary<System.Type, GetMeshForComponent>();
/// <summary>
/// Register a callback to invoke if the object has a component of type T.
///
/// This function is prefered over the other mesh callback
/// registration methods because it's type-safe, efficient, and
/// invocation order between types can be controlled in the UI by
/// reordering the components.
///
/// It's an error to register a callback for a component that
/// already has one, unless 'replace' is set to true.
/// </summary>
internal static void RegisterMeshCallback<T>(GetMeshForComponent<T> callback, bool replace = false)
where T : UnityEngine.MonoBehaviour
{
// Under the hood we lose type safety, but don't let the user notice!
RegisterMeshCallback(typeof(T),
(ModelExporter exporter, MonoBehaviour component, FbxNode fbxNode) =>
callback(exporter, (T)component, fbxNode),
replace);
}
/// <summary>
/// Register a callback to invoke if the object has a component of type T.
///
/// The callback will be invoked with an argument of type T, it's
/// safe to downcast.
///
/// Normally you'll want to use the generic form, but this one is
/// easier to use with reflection.
/// </summary>
internal static void RegisterMeshCallback(System.Type t,
GetMeshForComponent callback,
bool replace = false)
{
if (!t.IsSubclassOf(typeof(MonoBehaviour)))
{
throw new ModelExportException("Registering a callback for a type that isn't derived from MonoBehaviour: " + t);
}
if (!replace && MeshForComponentCallbacks.ContainsKey(t))
{
throw new ModelExportException("Replacing a callback for type " + t);
}
MeshForComponentCallbacks[t] = callback;
}
/// <summary>
/// Forget the callback linked to a component of type T.
/// </summary>
internal static void UnRegisterMeshCallback<T>()
{
MeshForComponentCallbacks.Remove(typeof(T));
}
/// <summary>
/// Forget the callback linked to a component of type T.
/// </summary>
internal static void UnRegisterMeshCallback(System.Type t)
{
MeshForComponentCallbacks.Remove(t);
}
/// <summary>
/// Forget the callbacks linked to components.
/// </summary>
internal static void UnRegisterAllMeshCallbacks()
{
MeshForComponentCallbacks.Clear();
}
static List<GetMeshForObject> MeshForObjectCallbacks = new List<GetMeshForObject>();
/// <summary>
/// Register a callback to invoke on every GameObject we export.
///
/// Avoid doing this if you can use a callback that depends on type.
///
/// The GameObject-based callbacks are checked before the
/// component-based ones.
///
/// Multiple GameObject-based callbacks can be registered; they are
/// checked in order of registration.
/// </summary>
internal static void RegisterMeshObjectCallback(GetMeshForObject callback)
{
MeshForObjectCallbacks.Add(callback);
}
/// <summary>
/// Forget a GameObject-based callback.
/// </summary>
internal static void UnRegisterMeshObjectCallback(GetMeshForObject callback)
{
MeshForObjectCallbacks.Remove(callback);
}
/// <summary>
/// Forget all GameObject-based callbacks.
/// </summary>
internal static void UnRegisterAllMeshObjectCallbacks()
{
MeshForObjectCallbacks.Clear();
}
/// <summary>
/// Exports a mesh for a unity gameObject.
///
/// This goes through the callback system to find the right mesh and
/// allow plugins to substitute their own meshes.
/// </summary>
bool ExportMesh(GameObject gameObject, FbxNode fbxNode)
{
// First allow the object-based callbacks to have a hack at it.
foreach (var callback in MeshForObjectCallbacks)
{
if (callback(this, gameObject, fbxNode))
{
return true;
}
}
// Next iterate over components and allow the component-based
// callbacks to have a hack at it. This is complicated by the
// potential of subclassing. While we're iterating we keep the
// first MeshFilter or SkinnedMeshRenderer we find.
Component defaultComponent = null;
foreach (var component in gameObject.GetComponents<Component>())
{
if (!component)
{
continue;
}
var monoBehaviour = component as MonoBehaviour;
if (!monoBehaviour)
{
// Check for default handling. But don't commit yet.
if (defaultComponent)
{
continue;
}
else if (component is MeshFilter)
{
defaultComponent = component;
}
else if (component is SkinnedMeshRenderer)
{
defaultComponent = component;
}
}
else
{
// Check if we have custom behaviour for this component type, or
// one of its base classes.
if (!monoBehaviour.enabled)
{
continue;
}
var componentType = monoBehaviour.GetType();
do
{
GetMeshForComponent callback;
if (MeshForComponentCallbacks.TryGetValue(componentType, out callback))
{
if (callback(this, monoBehaviour, fbxNode))
{
return true;
}
}
componentType = componentType.BaseType;
}
while (componentType.IsSubclassOf(typeof(MonoBehaviour)));
}
}
// If we're here, custom handling didn't work.
// Revert to default handling.
// if user doesn't want to export mesh colliders, and this gameobject doesn't have a renderer
// then don't export it.
if (!ExportOptions.ExportUnrendered && (!gameObject.GetComponent<Renderer>() || !gameObject.GetComponent<Renderer>().enabled))
{
return false;
}
var meshFilter = defaultComponent as MeshFilter;
if (meshFilter)
{
var renderer = gameObject.GetComponent<Renderer>();
var materials = renderer ? renderer.sharedMaterials : null;
return ExportMesh(new MeshInfo(meshFilter.sharedMesh, materials), fbxNode);
}
else
{
var smr = defaultComponent as SkinnedMeshRenderer;
if (smr)
{
var result = ExportSkinnedMesh(gameObject, fbxNode.GetScene(), fbxNode);
if (!result)
{
// fall back to exporting as a static mesh
var mesh = new Mesh();
smr.BakeMesh(mesh);
var materials = smr.sharedMaterials;
result = ExportMesh(new MeshInfo(mesh, materials), fbxNode);
Object.DestroyImmediate(mesh);
}
return result;
}
}
return false;
}
/// <summary>
/// Number of nodes exported including siblings and decendents
/// </summary>
internal int NumNodes { get { return MapUnityObjectToFbxNode.Count; } }
/// <summary>
/// Number of meshes exported
/// </summary>
internal int NumMeshes { set; get; }
/// <summary>
/// Number of triangles exported
/// </summary>
internal int NumTriangles { set; get; }
internal bool Verbose { get { return ExportSettings.instance.VerboseProperty; } }
/// <summary>
/// manage the selection of a filename
/// </summary>
static string LastFilePath { get; set; }
private string m_tempFilePath { get; set; }
private string m_lastFilePath { get; set; }
const string kFBXFileExtension = "fbx";
private static string MakeFileName(string basename = "test", string extension = kFBXFileExtension)
{
return basename + "." + extension;
}
private static void OnExport()
{
GameObject[] selectedGOs = Selection.GetFiltered<GameObject>(SelectionMode.TopLevel);
var toExport = ModelExporter.RemoveRedundantObjects(selectedGOs);
if (ExportSettings.instance.DisplayOptionsWindow)
{
ExportModelEditorWindow.Init(Enumerable.Cast<UnityEngine.Object>(toExport));
return;
}
string filename;
if (toExport.Count == 1)
{
filename = toExport.ToArray()[0].name;
}
else
{
filename = "Untitled";
}
var folderPath = ExportSettings.FbxAbsoluteSavePath;
var filePath = System.IO.Path.Combine(folderPath, filename + ".fbx");
if (System.IO.File.Exists(filePath))
{
Debug.LogErrorFormat("{0}: Failed to export to {1}, file already exists", PACKAGE_UI_NAME, filePath);
return;
}
if (ExportObjects(filePath, toExport.ToArray(), ExportSettings.instance.ExportModelSettings.info) != null)
{
// refresh the asset database so that the file appears in the
// asset folder view.
AssetDatabase.Refresh();
}
}
/// <summary>
/// Exports a single Unity GameObject to an FBX file,
/// with the specified export settings.
/// </summary>
/// <returns>
/// The FBX file path if successful; otherwise null.
/// </returns>
/// <param name="filePath">Absolute file path to use for the FBX file.</param>
/// <param name="singleObject">The Unity GameObject to export.</param>
/// <param name="exportOptions">The export options to use.</param>
public static string ExportObject(
string filePath,
UnityEngine.Object singleObject,
ExportModelOptions exportOptions = null
)
{
return ExportObjects(filePath, new Object[] { singleObject }, exportOptions?.ConvertToModelSettingsSerialize(), exportData: null);
}
internal static string ExportObject(
string filePath,
UnityEngine.Object singleObject,
IExportOptions exportOptions = null
)
{
return ExportObjects(filePath, new Object[] { singleObject }, exportOptions, exportData: null);
}
/// <summary>
/// Exports an array of Unity GameObjects to an FBX file,
/// with the specified export settings.
/// </summary>
/// <returns>
/// The FBX file path if successful; otherwise null.
/// </returns>
/// <param name="filePath">Absolute file path to use for the FBX file.</param>
/// <param name="objects">Array of Unity GameObjects to export.</param>
/// <param name="exportOptions">The export options to use.</param>
public static string ExportObjects(
string filePath,
UnityEngine.Object[] objects = null,
ExportModelOptions exportOptions = null)
{
return ExportObjects(filePath, objects, exportOptions?.ConvertToModelSettingsSerialize(), exportData: null);
}
/// <summary>
/// Exports an array of Unity GameObjects to an FBX file.
/// </summary>
/// <returns>
/// The FBX file path if successful; otherwise returns null.
/// </returns>
/// <param name="filePath">Absolute file path to use for the FBX file.</param>
/// <param name="objects">Array of Unity GameObjects to export.</param>
public static string ExportObjects(string filePath, UnityEngine.Object[] objects = null)
{
return ExportObjects(filePath, objects, exportOptions: null, exportData: null);
}
/// <summary>
/// Exports a single Unity GameObject to an FBX file.
/// </summary>
/// <returns>
/// The FBX file path if successful; otherwise null.
/// </returns>
/// <param name="filePath">Absolute file path to use for the FBX file.</param>
/// <param name="singleObject">The Unity GameObject to export.</param>
public static string ExportObject(string filePath, UnityEngine.Object singleObject)
{
return ExportObjects(filePath, new Object[] { singleObject }, exportOptions: null);
}
/// <summary>
/// Exports the animation from a single TimelineClip to an FBX file.
/// </summary>
/// <param name="filePath">Absolute file path to use for the FBX file.</param>
/// <param name="timelineClip">The TimelineClip to export.</param>
/// <param name="exportOptions">The export options to use.</param>
/// <returns>The FBX file path if successful; otherwise null.</returns>
internal static string ExportTimelineClip(string filePath, TimelineClip timelineClip, PlayableDirector director = null, IExportOptions exportOptions = null)
{
var exportData = ModelExporter.GetExportData(timelineClip, director, exportOptions);
return ExportObjects(filePath, null, exportOptions: exportOptions, exportData: exportData);
}
/// <summary>
/// Exports a list of GameObjects to an FBX file.
/// <para>
/// Use the SaveFile panel to allow the user to enter a file name.
/// </para>
/// </summary>
internal static string ExportObjects(
string filePath,
UnityEngine.Object[] objects = null,
IExportOptions exportOptions = null,
Dictionary<GameObject, IExportData> exportData = null
)
{
LastFilePath = filePath;
var fbxExporter = Create();
// ensure output directory exists
EnsureDirectory(filePath);
fbxExporter.ExportOptions = exportOptions;
if (objects == null)
{
objects = Selection.objects;
}
if (exportData == null)
{
exportData = GetExportData(objects, exportOptions);
}
if (fbxExporter.ExportAll(objects, exportData) > 0)
{
string message = string.Format("Successfully exported: {0}", filePath);
Debug.Log(message);
return filePath;
}
return null;
}
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);
}
}
/// <summary>
/// Removes the diacritics (i.e. accents) from letters.
/// e.g. é becomes e
/// </summary>
/// <returns>Text with accents removed.</returns>
/// <param name="text">Text.</param>
private static string RemoveDiacritics(string text)
{
var normalizedString = text.Normalize(System.Text.NormalizationForm.FormD);
var stringBuilder = new System.Text.StringBuilder();
foreach (var c in normalizedString)
{
var unicodeCategory = System.Globalization.CharUnicodeInfo.GetUnicodeCategory(c);
if (unicodeCategory != System.Globalization.UnicodeCategory.NonSpacingMark)
{
stringBuilder.Append(c);
}
}
return stringBuilder.ToString().Normalize(System.Text.NormalizationForm.FormC);
}
private static string ConvertToMayaCompatibleName(string name)
{
if (string.IsNullOrEmpty(name))
{
return InvalidCharReplacement.ToString();
}
string newName = RemoveDiacritics(name);
if (char.IsDigit(newName[0]))
{
newName = newName.Insert(0, InvalidCharReplacement.ToString());
}
for (int i = 0; i < newName.Length; i++)
{
if (!char.IsLetterOrDigit(newName, i))
{
if (i < newName.Length - 1 && newName[i] == MayaNamespaceSeparator)
{
continue;
}
newName = newName.Replace(newName[i], InvalidCharReplacement);
}
}
return newName;
}
internal static string ConvertToValidFilename(string filename)
{
return System.Text.RegularExpressions.Regex.Replace(filename,
RegexCharStart + new string(Path.GetInvalidFileNameChars()) + RegexCharEnd,
InvalidCharReplacement.ToString()
);
}
}
}