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