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
{
///
/// 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.
///
internal delegate bool GetMeshForComponent(ModelExporter exporter, T component, FbxNode fbxNode) where T : MonoBehaviour;
internal delegate bool GetMeshForComponent(ModelExporter exporter, MonoBehaviour component, FbxNode fbxNode);
///
/// 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.
///
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) {}
}
///
/// Use the ModelExporter class to export Unity GameObjects to an FBX file.
///
/// Use the ExportObject and ExportObjects methods. The default export
/// options are used when exporting the objects to the FBX file.
///
/// For information on using the ModelExporter class, see the Developer's Guide.
///
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 =
@"";
///
/// Path to the CHANGELOG file in Unity's virtual file system. Used to get the version number.
///
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";
///
/// name of the scene's default camera
///
private static string DefaultCamera = "";
private const string SkeletonPrefix = "_Skel";
private const string SkinPrefix = "_Skin";
///
/// name prefix for custom properties
///
const string NamePrefix = "Unity_";
private static string MakeName(string basename)
{
return NamePrefix + basename;
}
///
/// Create instance of exporter.
///
static ModelExporter Create()
{
return new ModelExporter();
}
///
/// Which components map from Unity Object to Fbx Object
///
internal enum FbxNodeRelationType
{
NodeAttribute,
Property,
Material
}
internal static Dictionary> MapsToFbxObject = new Dictionary>()
{
{ typeof(Transform), new KeyValuePair(typeof(FbxProperty), FbxNodeRelationType.Property) },
{ typeof(MeshFilter), new KeyValuePair(typeof(FbxMesh), FbxNodeRelationType.NodeAttribute) },
{ typeof(SkinnedMeshRenderer), new KeyValuePair(typeof(FbxMesh), FbxNodeRelationType.NodeAttribute) },
{ typeof(Light), new KeyValuePair(typeof(FbxLight), FbxNodeRelationType.NodeAttribute) },
{ typeof(Camera), new KeyValuePair(typeof(FbxCamera), FbxNodeRelationType.NodeAttribute) },
{ typeof(Material), new KeyValuePair(typeof(FbxSurfaceMaterial), FbxNodeRelationType.Material) },
};
///
/// keep a map between GameObject and FbxNode for quick lookup when we export
/// animation.
///
Dictionary MapUnityObjectToFbxNode = new Dictionary();
///
/// 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.
///
Dictionary> MapConstrainedObjectToConstraints = new Dictionary>();
///
/// keep a map between the FbxNode and its blendshape channels for quick lookup when exporting blendshapes.
///
Dictionary> MapUnityObjectToBlendShapes = new Dictionary>();
///
/// Map Unity material ID to FBX material object
///
Dictionary MaterialMap = new Dictionary();
///
/// Map texture properties to FBX texture object
///
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>();
///
/// Map a Unity mesh to an fbx node (for preserving instances)
///
Dictionary SharedMeshes = new Dictionary();
///
/// Map for the Name of an Object to number of objects with this name.
/// Used for enforcing unique names on export.
///
Dictionary NameToIndexMap = new Dictionary();
///
/// Map for the Material Name to number of materials with this name.
/// Used for enforcing unique names on export.
///
Dictionary MaterialNameToIndexMap = new Dictionary();
///
/// Map for the Texture Name to number of textures with this name.
/// Used for enforcing unique names on export.
///
Dictionary TextureNameToIndexMap = new Dictionary();
///
/// Format for creating unique names
///
const string UniqueNameFormat = "{0}_{1}";
///
/// The animation fbx file format.
///
const string AnimFbxFileFormat = "{0}/{1}@{2}.fbx";
///
/// Gets the export settings.
///
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; }
}
///
/// Gets the Unity default material.
///
internal static Material DefaultMaterial
{
get
{
if (!s_defaultMaterial)
{
var obj = GameObject.CreatePrimitive(PrimitiveType.Quad);
s_defaultMaterial = obj.GetComponent().sharedMaterial;
Object.DestroyImmediate(obj);
}
return s_defaultMaterial;
}
}
static Material s_defaultMaterial = null;
static Dictionary MapLightType = new Dictionary()
{
{ UnityEngine.LightType.Directional, FbxLight.EType.eDirectional },
{ UnityEngine.LightType.Spot, FbxLight.EType.eSpot },
{ UnityEngine.LightType.Point, FbxLight.EType.ePoint },
{ UnityEngine.LightType.Rectangle, FbxLight.EType.eArea },
};
///
/// Gets the version number of the FbxExporters plugin from the readme.
///
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;
}
}
///
/// Get a layer (to store UVs, normals, etc) on the mesh.
/// If it doesn't exist yet, create it.
///
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);
}
///
/// Export the mesh's attributes using layer 0.
///
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;
}
///
/// Unity has up to 4 uv sets per mesh. Export all the ones that exist.
///
/// Fbx mesh.
/// Mesh.
/// Unmerged triangles.
private static bool ExportUVs(FbxMesh fbxMesh, MeshInfo meshInfo, int[] unmergedTriangles)
{
var mesh = meshInfo.mesh;
List uvs = new List();
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;
}
///
/// Export the mesh's blend shapes.
///
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;
}
///
/// 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.
///
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]);
}
///
/// 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.
///
/// Unity material.
/// Unity property name, e.g. "_MainTex".
/// Fbx material.
/// Fbx property name, e.g. FbxSurfaceMaterial.sDiffuse.
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;
}
}
///
/// Get the color of a material, or grey if we can't find it.
///
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);
}
///
/// Export (and map) a Unity PBS material to FBX classic material
///
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;
}
///
/// 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)
///
/// Fbx mesh.
/// Mesh.
/// Materials.
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);
}
}
///
/// 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.
///
internal bool ExportMesh(Mesh mesh, FbxNode fbxNode, Material[] materials = null)
{
var meshInfo = new MeshInfo(mesh, materials);
return ExportMesh(meshInfo, fbxNode);
}
///
/// Keeps track of the index of each point in the exported vertex array.
///
private Dictionary ControlPointToIndex = new Dictionary();
///
/// Exports a unity mesh and attaches it to the node as an FbxMesh.
///
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();
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 blendshapeChannels;
if (!MapUnityObjectToBlendShapes.TryGetValue(fbxNode, out blendshapeChannels))
{
blendshapeChannels = new List();
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;
}
///
/// Export GameObject as a skinned mesh with material, bones, a skin and, a bind pose.
///
private bool ExportSkinnedMesh(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGo || fbxNode == null)
{
return false;
}
if (!unityGo.TryGetComponent(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 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;
}
///
/// Gets the bind pose for the Unity bone.
///
/// The bind pose.
/// Unity bone.
/// Bind poses.
/// Contains information about bones and skinned mesh.
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;
}
///
/// Export bones of skinned mesh, if this is a skinned mesh with
/// bones and bind poses.
///
private bool ExportSkeleton(SkinnedMeshRenderer skinnedMesh, FbxScene fbxScene, out Dictionary skinnedMeshToBonesMap)
{
skinnedMeshToBonesMap = new Dictionary();
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 index = new Dictionary();
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;
}
///
/// Export binding of mesh to skeleton
///
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 boneCluster = new Dictionary();
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;
}
///
/// set vertex weights in cluster
///
private void SetVertexWeights(MeshInfo meshInfo, Dictionary 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 visitedVertices = new HashSet();
// 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++;
}
}
}
///
/// Export bind pose of mesh to skeleton
///
private bool ExportBindPose(SkinnedMeshRenderer skinnedMesh, FbxNode fbxMeshNode,
FbxScene fbxScene, Dictionary 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);
}
///
/// Euler (roll/pitch/yaw (ZXY rotation order) to quaternion.
///
/// a quaternion.
/// ZXY Euler.
internal static FbxQuaternion EulerToQuaternionZXY(Vector3 euler)
{
var unityQuat = Quaternion.Euler(euler);
return new FbxQuaternion(unityQuat.x, unityQuat.y, unityQuat.z, unityQuat.w);
}
///
/// Euler X/Y/Z rotation order to quaternion.
///
/// XYZ Euler.
/// a quaternion
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;
}
///
/// if this game object is a model prefab or the model has already been exported, then export with shared components
///
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(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();
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;
}
///
/// Exports camera component
///
private bool ExportCamera(GameObject unityGO, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGO || fbxScene == null || fbxNode == null)
{
return false;
}
if (!unityGO.TryGetComponent(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;
}
///
/// Exports light component.
/// Supported types: point, spot and directional
/// Cookie => Gobo
///
private bool ExportLight(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGo || fbxScene == null || fbxNode == null)
{
return false;
}
if (!unityGo.TryGetComponent(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 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 GetConstraintSources(IConstraint unityConstraint)
{
if (unityConstraint == null)
{
return null;
}
var fbxSources = new List();
var sources = new List();
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(FbxNode fbxNode, T fbxConstraint, System.Type uniConstraintType) where T : FbxConstraint
{
Dictionary constraintMapping;
if (!MapConstrainedObjectToConstraints.TryGetValue(fbxNode, out constraintMapping))
{
constraintMapping = new Dictionary();
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()
{
{ 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();
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;
}
///
/// Return set of sample times to cover all keys on animation curves
///
internal static HashSet GetSampleTimes(AnimationCurve[] animCurves, double sampleRate)
{
var keyTimes = new HashSet();
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;
}
///
/// Return set of all keys times on animation curves
///
internal static HashSet GetKeyTimes(AnimationCurve[] animCurves)
{
var keyTimes = new HashSet();
foreach (var ac in animCurves)
{
if (ac != null) foreach (var key in ac.keys) { keyTimes.Add(key.time); }
}
return keyTimes;
}
///
/// 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.
///
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}))
{
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
);
}
}
}
///
/// Export animation curve key samples
///
internal void ExportAnimationSamples(AnimationCurve uniAnimCurve, FbxAnimCurve fbxAnimCurve,
double sampleRate,
UnityToMayaConvertSceneHelper convertSceneHelper)
{
using (new FbxAnimCurveModifyHelper(new List {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)
);
}
}
}
///
/// Get the FbxConstraint associated with the constrained node.
///
///
///
///
private FbxConstraint GetFbxConstraint(FbxNode constrainedNode, System.Type uniConstraintType)
{
if (uniConstraintType == null || !uniConstraintType.GetInterfaces().Contains(typeof(IConstraint)))
{
// not actually a constraint
return null;
}
Dictionary constraints;
if (MapConstrainedObjectToConstraints.TryGetValue(constrainedNode, out constraints))
{
var targetConstraint = constraints.FirstOrDefault(constraint => (constraint.Value == uniConstraintType));
if (!targetConstraint.Equals(default(KeyValuePair)))
{
return targetConstraint.Key;
}
}
return null;
}
///
/// Get the FbxBlendshape with the given name associated with the FbxNode.
///
///
///
///
private FbxBlendShapeChannel GetFbxBlendShape(FbxNode blendshapeNode, string uniPropertyName)
{
List 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;
}
///
/// Export an AnimationCurve.
/// NOTE: This is not used for rotations, because we need to convert from
/// quaternion to euler and various other stuff.
///
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;
}
}
///
/// Export an AnimationClip as a single take
///
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>();
// 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(){ 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();
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();
// 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() != 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(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(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);
}
}
///
/// Transfers transform animation from source to dest. Replaces dest's Unity Animation Curves with updated animations.
/// NOTE: Source must be the parent of dest.
///
/// Source animated object.
/// Destination, child of the source.
/// Sample rate.
/// Unity curves.
private void TransferMotion(Transform source, Transform dest, float sampleRate, ref Dictionary> 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 sourceUnityCurves;
if (!unityCurves.TryGetValue(source.gameObject, out sourceUnityCurves))
{
return; // nothing to do, source has no animation
}
List destUnityCurves;
if (!unityCurves.TryGetValue(dest.gameObject, out destUnityCurves))
{
destUnityCurves = new List();
}
List animCurves = new List();
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();
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 curves)
{
var transformCurves = new List();
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 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;
}
}
///
/// Gets or creates the rotation curve for GameObject uniGO.
///
/// The rotation curve.
/// Unity GameObject.
/// Frame rate.
/// Rotations.
/// RotationCurve is abstract so specify type of RotationCurve to create.
private RotationCurve GetRotationCurve(
GameObject uniGO, float frameRate,
ref Dictionary rotations
) where T : RotationCurve, new()
{
RotationCurve rotCurve;
if (!rotations.TryGetValue(uniGO, out rotCurve))
{
rotCurve = new T { SampleRate = frameRate };
rotations.Add(uniGO, rotCurve);
}
return rotCurve;
}
///
/// Export the Animator component on this game object
///
private void ExportAnimation(GameObject uniRoot, FbxScene fbxScene)
{
if (!uniRoot)
{
return;
}
var exportedClips = new HashSet();
var uniAnimator = uniRoot.GetComponent();
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();
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();
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);
}
}
}
}
}
///
/// configures default camera for the scene
///
private void SetDefaultCamera(FbxScene fbxScene)
{
if (fbxScene == null) { return; }
if (string.IsNullOrEmpty(DefaultCamera))
DefaultCamera = Globals.FBXSDK_CAMERA_PERSPECTIVE;
fbxScene.GetGlobalSettings().SetDefaultCamera(DefaultCamera);
}
///
/// Ensures that the inputted name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. Sphere becomes Sphere_1
///
/// Unique name
/// Name
/// The dictionary to use to map name to # of occurences
private string GetUniqueName(string name, Dictionary 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;
}
///
/// Ensures that the inputted name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. Sphere becomes Sphere_1
///
/// Unique name
/// Name
private string GetUniqueFbxNodeName(string name)
{
return GetUniqueName(name, NameToIndexMap);
}
///
/// Ensures that the inputted material name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. mat becomes mat_1
///
/// Name
/// Unique material name
private string GetUniqueMaterialName(string name)
{
return GetUniqueName(name, MaterialNameToIndexMap);
}
///
/// Ensures that the inputted texture name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. tex becomes tex_1
///
/// Name
/// Unique texture name
private string GetUniqueTextureName(string name)
{
return GetUniqueName(name, TextureNameToIndexMap);
}
///
/// Create a fbxNode from unityGo.
///
///
///
/// the created FbxNode
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;
}
///
/// Creates an FbxNode for each GameObject.
///
/// The number of nodes exported.
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();
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;
}
///
/// 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.
///
/// The number of nodes exported.
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();
var meshInfo = new MeshInfo(unitySkin.sharedMesh, unitySkin.sharedMaterials);
ExportMesh(meshInfo, node);
}
}
}
return numObjectsExported;
}
internal class SkinnedMeshBoneInfo
{
public SkinnedMeshRenderer skinnedMesh;
public Dictionary boneDict;
public Dictionary boneToBindPose;
public SkinnedMeshBoneInfo(SkinnedMeshRenderer skinnedMesh, Dictionary boneDict)
{
this.skinnedMesh = skinnedMesh;
this.boneDict = boneDict;
this.boneToBindPose = new Dictionary();
}
}
private bool ExportAnimatedBones(
GameObject unityGo,
FbxScene fbxScene,
ref int exportProgress,
int objectCount,
AnimationOnlyExportData exportData
)
{
var skinnedMeshRenderers = unityGo.GetComponentsInChildren();
foreach (var skinnedMesh in skinnedMeshRenderers)
{
var boneArray = skinnedMesh.bones;
var bones = new HashSet();
var boneDict = new Dictionary();
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;
}
///
/// Exports the Gameobject and its ancestors.
///
/// true, if game object and parents were exported,
/// false if export cancelled.
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;
}
///
/// Exports the bone transform.
///
/// true, if bone transform was exported, false otherwise.
/// Fbx node.
/// Fbx scene.
/// Unity bone.
/// Bone info.
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);
}
///
/// Counts how many objects are between this object and the root (exclusive).
///
/// The object to root count.
/// Start object.
/// Root object.
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;
}
///
/// 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.
///
/// The animation only hierarchy count.
/// Map from GameObject hierarchy to animation export data.
internal int GetAnimOnlyHierarchyCount(Dictionary hierarchyToExportData)
{
// including any parents of animated objects that are exported
var completeExpSet = new HashSet();
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 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 exportData = new Dictionary();
KeyValuePair 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 GetExportData(Object[] objects, IExportOptions exportOptions = null)
{
if (exportOptions == null)
exportOptions = DefaultOptions;
Debug.Assert(exportOptions != null);
if (exportOptions.ModelAnimIncludeOption == Include.Model)
{
return null;
}
Dictionary exportData = new Dictionary();
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();
var genericAnim = go.GetComponentsInChildren();
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;
}
///
/// Export components on this game object.
/// Transform components have already been exported.
/// This function exports the other components and animation.
///
private bool ExportComponents(FbxScene fbxScene)
{
int numObjectsExported = 0;
int objectCount = MapUnityObjectToFbxNode.Count;
foreach (KeyValuePair 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;
}
///
/// Checks if the GameObject has animation.
///
/// true, if object has animation, false otherwise.
/// Go.
private bool GameObjectHasAnimation(GameObject go)
{
return go != null &&
(go.GetComponent() ||
go.GetComponent() ||
go.GetComponent());
}
///
/// A count of how many GameObjects we are exporting, to have a rough
/// idea of how long creating the scene will take.
///
/// The hierarchy count.
/// Export set.
internal int GetHierarchyCount(HashSet exportSet)
{
int count = 0;
Queue queue = new Queue(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;
}
///
/// 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.
///
/// The revised export set
/// Unity export set.
internal static HashSet RemoveRedundantObjects(IEnumerable unityExportSet)
{
// basically just remove the descendents from the unity export set
HashSet toExport = new HashSet();
HashSet hashedExportSet = new HashSet