using System.IO;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEditor;
using System.Linq;
using Autodesk.Fbx;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using UnityEditor.Formats.Fbx.Exporter.Visitors;
using UnityEditor.Formats.Fbx.Exporter.CustomExtensions;
using System.Security.Permissions;
[assembly: InternalsVisibleTo("Unity.Formats.Fbx.Editor.Tests")]
[assembly: InternalsVisibleTo("Unity.ProBuilder.AddOns.Editor")]
namespace UnityEditor.Formats.Fbx.Exporter
{
///
/// If your MonoBehaviour knows about some custom geometry that
/// isn't in a MeshFilter or SkinnedMeshRenderer, use
/// RegisterMeshCallback to get a callback when the exporter tries
/// to export your component.
///
/// The callback should return true, and output the mesh you want.
///
/// Return false if you don't want to drive this game object.
///
/// Return true and output a null mesh if you don't want the
/// exporter to output anything.
///
internal delegate bool GetMeshForComponent(ModelExporter exporter, T component, FbxNode fbxNode) where T : MonoBehaviour;
internal delegate bool GetMeshForComponent(ModelExporter exporter, MonoBehaviour component, FbxNode fbxNode);
///
/// Delegate used to convert a GameObject into a mesh.
///
/// This is useful if you want to have broader control over
/// the export process than the GetMeshForComponent callbacks
/// provide. But it's less efficient because you'll get a callback
/// on every single GameObject.
///
internal delegate bool GetMeshForObject(ModelExporter exporter, GameObject gameObject, FbxNode fbxNode);
[System.Serializable]
internal class ModelExportException : System.Exception
{
public ModelExportException(){}
public ModelExportException(string message)
: base(message){}
public ModelExportException(string message, System.Exception inner)
: base(message, inner){}
protected ModelExportException(SerializationInfo info, StreamingContext context)
: base(info, context){}
}
///
/// Use the ModelExporter class to export Unity GameObjects to an FBX file.
///
/// Use the ExportObject and ExportObjects methods. The default export
/// options are used when exporting the objects to the FBX file.
///
/// For information on using the ModelExporter class, see the Developer's Guide.
///
public sealed class ModelExporter : 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 =
@"";
///
/// Path to the CHANGELOG file in Unity's virtual file system. Used to get the version number.
///
const string ChangeLogPath = "Packages/com.unity.formats.fbx/CHANGELOG.md";
// NOTE: The ellipsis at the end of the Menu Item name prevents the context
// from being passed to command, thus resulting in OnContextItem()
// being called only once regardless of what is selected.
const string MenuItemName = "GameObject/Export To FBX...";
const string 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";
///
/// name of the scene's default camera
///
private static string DefaultCamera = "";
private const string SkeletonPrefix = "_Skel";
private const string SkinPrefix = "_Skin";
///
/// name prefix for custom properties
///
const string NamePrefix = "Unity_";
private static string MakeName (string basename)
{
return NamePrefix + basename;
}
///
/// Create instance of exporter.
///
static ModelExporter Create ()
{
return new ModelExporter ();
}
///
/// Which components map from Unity Object to Fbx Object
///
internal enum FbxNodeRelationType
{
NodeAttribute,
Property,
Material
}
internal static Dictionary> MapsToFbxObject = new Dictionary> ()
{
{ typeof(Transform), new KeyValuePair(typeof(FbxProperty), FbxNodeRelationType.Property) },
{ typeof(MeshFilter), new KeyValuePair(typeof(FbxMesh), FbxNodeRelationType.NodeAttribute) },
{ typeof(SkinnedMeshRenderer), new KeyValuePair(typeof(FbxMesh), FbxNodeRelationType.NodeAttribute) },
{ typeof(Light), new KeyValuePair(typeof(FbxLight), FbxNodeRelationType.NodeAttribute) },
{ typeof(Camera), new KeyValuePair(typeof(FbxCamera), FbxNodeRelationType.NodeAttribute) },
{ typeof(Material), new KeyValuePair(typeof(FbxSurfaceMaterial), FbxNodeRelationType.Material) },
};
///
/// keep a map between GameObject and FbxNode for quick lookup when we export
/// animation.
///
Dictionary MapUnityObjectToFbxNode = new Dictionary ();
///
/// keep a map between the constrained FbxNode (in Unity this is the GameObject with constraint component)
/// and its FbxConstraints for quick lookup when exporting constraint animations.
///
Dictionary> MapConstrainedObjectToConstraints = new Dictionary>();
///
/// Map Unity material ID to FBX material object
///
Dictionary MaterialMap = new Dictionary ();
///
/// Map texture filename name to FBX texture object
///
Dictionary TextureMap = new Dictionary ();
///
/// Map the ID of a prefab to an FbxMesh (for preserving instances)
///
Dictionary SharedMeshes = new Dictionary ();
///
/// Map for the Name of an Object to number of objects with this name.
/// Used for enforcing unique names on export.
///
Dictionary NameToIndexMap = new Dictionary ();
///
/// Map for the Material Name to number of materials with this name.
/// Used for enforcing unique names on export.
///
Dictionary MaterialNameToIndexMap = new Dictionary();
///
/// Map for the Texture Name to number of textures with this name.
/// Used for enforcing unique names on export.
///
Dictionary TextureNameToIndexMap = new Dictionary();
Dictionary MeshToFbxNodeMap = new Dictionary();
///
/// Format for creating unique names
///
const string UniqueNameFormat = "{0}_{1}";
///
/// The animation fbx file format.
///
const string AnimFbxFileFormat = "{0}/{1}@{2}.fbx";
///
/// Gets the export settings.
///
internal static ExportSettings ExportSettings {
get { return ExportSettings.instance; }
}
internal static IExportOptions DefaultOptions {
get { return new ExportModelSettingsSerialize(); }
}
private IExportOptions m_exportOptions;
private IExportOptions ExportOptions {
get {
if (m_exportOptions == null) {
// get default settings;
m_exportOptions = DefaultOptions;
}
return m_exportOptions;
}
set { m_exportOptions = value; }
}
///
/// Gets the Unity default material.
///
internal static Material DefaultMaterial {
get {
if (!s_defaultMaterial) {
var obj = GameObject.CreatePrimitive (PrimitiveType.Quad);
s_defaultMaterial = obj.GetComponent ().sharedMaterial;
Object.DestroyImmediate (obj);
}
return s_defaultMaterial;
}
}
static Material s_defaultMaterial = null;
static Dictionary MapLightType = new Dictionary () {
{ UnityEngine.LightType.Directional, FbxLight.EType.eDirectional },
{ UnityEngine.LightType.Spot, FbxLight.EType.eSpot },
{ UnityEngine.LightType.Point, FbxLight.EType.ePoint },
{ UnityEngine.LightType.Area, FbxLight.EType.eArea },
};
///
/// Gets the version number of the FbxExporters plugin from the readme.
///
internal static string GetVersionFromReadme()
{
if (!File.Exists (ChangeLogPath)) {
Debug.LogWarning (string.Format("Could not find version number, the ChangeLog file is missing from: {0}", ChangeLogPath));
return null;
}
try {
// The standard format is:
// ## [a.b.c-whatever] - yyyy-mm-dd
// Another format is:
// **Version**: a.b.c-whatever
// we handle either one and read out the version
var lines = File.ReadAllLines (ChangeLogPath);
var regexes = new string [] {
@"^\s*##\s*\[(.*)\]",
@"^\s*\*\*Version\*\*:\s*(.*)\s*"
};
foreach (var line in lines) {
foreach (var regex in regexes) {
var match = System.Text.RegularExpressions.Regex.Match(line, regex);
if (match.Success) {
var version = match.Groups[1].Value;
return version.Trim ();
}
}
}
// If we're here, we didn't find any match.
Debug.LogWarning (string.Format("Could not find most recent version number in {0}", ChangeLogPath));
return null;
}
catch(IOException e){
Debug.LogException (e);
Debug.LogWarning (string.Format("Error reading file {0} ({1})", ChangeLogPath, e));
return null;
}
}
///
/// Get a layer (to store UVs, normals, etc) on the mesh.
/// If it doesn't exist yet, create it.
///
internal static FbxLayer GetOrCreateLayer(FbxMesh fbxMesh, int layer = 0 /* default layer */)
{
int maxLayerIndex = fbxMesh.GetLayerCount() - 1;
while (layer > maxLayerIndex) {
// We'll have to create the layer (potentially several).
// Make sure to avoid infinite loops even if there's an
// FbxSdk bug.
int newLayerIndex = fbxMesh.CreateLayer();
if (newLayerIndex <= maxLayerIndex) {
// Error!
throw new ModelExportException(
"Internal error: Unable to create mesh layer "
+ (maxLayerIndex + 1)
+ " on mesh " + fbxMesh.GetName ());
}
maxLayerIndex = newLayerIndex;
}
return fbxMesh.GetLayer (layer);
}
///
/// Export the mesh's attributes using layer 0.
///
private bool ExportComponentAttributes (MeshInfo mesh, FbxMesh fbxMesh, int[] unmergedTriangles)
{
// return true if any attribute was exported
bool exportedAttribute = false;
// Set the normals on Layer 0.
FbxLayer fbxLayer = GetOrCreateLayer(fbxMesh);
if (mesh.HasValidNormals()) {
using (var fbxLayerElement = FbxLayerElementNormal.Create (fbxMesh, "Normals")) {
fbxLayerElement.SetMappingMode (FbxLayerElement.EMappingMode.eByPolygonVertex);
fbxLayerElement.SetReferenceMode (FbxLayerElement.EReferenceMode.eDirect);
// Add one normal per each vertex face index (3 per triangle)
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray ();
for (int n = 0; n < unmergedTriangles.Length; n++) {
int unityTriangle = unmergedTriangles [n];
fbxElementArray.Add (ConvertToRightHanded (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 (ConvertToRightHanded (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 (ConvertToRightHanded (
new Vector3 (
mesh.Tangents [unityTriangle] [0],
mesh.Tangents [unityTriangle] [1],
mesh.Tangents [unityTriangle] [2]
)));
}
fbxLayer.SetTangents (fbxLayerElement);
}
exportedAttribute = true;
}
exportedAttribute |= ExportUVs (fbxMesh, mesh, unmergedTriangles);
if (mesh.HasValidVertexColors()) {
using (var fbxLayerElement = FbxLayerElementVertexColor.Create (fbxMesh, "VertexColors")) {
fbxLayerElement.SetMappingMode (FbxLayerElement.EMappingMode.eByPolygonVertex);
fbxLayerElement.SetReferenceMode (FbxLayerElement.EReferenceMode.eIndexToDirect);
// set texture coordinates per vertex
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray ();
// (Uni-31596) only copy unique UVs into this array, and index appropriately
for (int n = 0; n < mesh.VertexColors.Length; n++) {
// Converting to Color from Color32, as Color32 stores the colors
// as ints between 0-255, while FbxColor and Color
// use doubles between 0-1
Color color = mesh.VertexColors [n];
fbxElementArray.Add (new FbxColor (color.r,
color.g,
color.b,
color.a));
}
// For each face index, point to a texture uv
FbxLayerElementArray fbxIndexArray = fbxLayerElement.GetIndexArray ();
fbxIndexArray.SetCount (unmergedTriangles.Length);
for (int i = 0; i < unmergedTriangles.Length; i++) {
fbxIndexArray.SetAt (i, unmergedTriangles [i]);
}
fbxLayer.SetVertexColors (fbxLayerElement);
}
exportedAttribute = true;
}
return exportedAttribute;
}
///
/// Unity has up to 4 uv sets per mesh. Export all the ones that exist.
///
/// Fbx mesh.
/// Mesh.
/// Unmerged triangles.
private static bool ExportUVs(FbxMesh fbxMesh, MeshInfo 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 [i] == null || uvs [i].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[i].Length; n++) {
fbxElementArray.Add (new FbxVector2 (uvs[i] [n] [0],
uvs[i] [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;
}
///
/// Export the mesh's blend shapes.
///
private bool ExportBlendShapes(MeshInfo mesh, FbxMesh fbxMesh, FbxScene fbxScene, int[] unmergedTriangles)
{
var umesh = mesh.mesh;
if (umesh.blendShapeCount == 0)
return false;
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(ConvertToRightHanded(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, ConvertToRightHanded(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, ConvertToRightHanded(t));
}
}
}
}
return true;
}
///
/// Takes in a left-handed UnityEngine.Vector3 denoting a normal,
/// returns a right-handed FbxVector4.
///
/// Unity is left-handed, Maya and Max are right-handed.
/// The FbxSdk conversion routines can't handle changing handedness.
///
/// Remember you also need to flip the winding order on your polygons.
///
internal static FbxVector4 ConvertToRightHanded(Vector3 leftHandedVector, float unitScale = 1f)
{
// negating the x component of the vector converts it from left to right handed coordinates
return unitScale * new FbxVector4 (
-leftHandedVector[0],
leftHandedVector[1],
leftHandedVector[2]);
}
///
/// Exports a texture from Unity to FBX.
/// The texture must be a property on the unityMaterial; it gets
/// linked to the FBX via a property on the fbxMaterial.
///
/// The texture file must be a file on disk; it is not embedded within the FBX.
///
/// Unity material.
/// Unity property name, e.g. "_MainTex".
/// Fbx material.
/// Fbx property name, e.g. FbxSurfaceMaterial.sDiffuse.
internal bool ExportTexture (Material unityMaterial, string unityPropName,
FbxSurfaceMaterial fbxMaterial, string fbxPropName)
{
if (!unityMaterial) {
return false;
}
// Get the texture on this property, if any.
if (!unityMaterial.HasProperty (unityPropName)) {
return false;
}
var unityTexture = unityMaterial.GetTexture (unityPropName);
if (!unityTexture) {
return false;
}
// Find its filename
var textureSourceFullPath = AssetDatabase.GetAssetPath(unityTexture);
if (string.IsNullOrEmpty(textureSourceFullPath)) {
return false;
}
// get absolute filepath to texture
textureSourceFullPath = Path.GetFullPath (textureSourceFullPath);
if (Verbose) {
Debug.Log (string.Format ("{2}.{1} setting texture path {0}", textureSourceFullPath, fbxPropName, fbxMaterial.GetName ()));
}
// Find the corresponding property on the fbx material.
var fbxMaterialProperty = fbxMaterial.FindProperty (fbxPropName);
if (fbxMaterialProperty == null || !fbxMaterialProperty.IsValid ()) {
Debug.Log ("property not found");
return false;
}
// 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;
}
///
/// Get the color of a material, or grey if we can't find it.
///
internal FbxDouble3 GetMaterialColor (Material unityMaterial, string unityPropName, float defaultValue = 1)
{
if (!unityMaterial) {
return new FbxDouble3(defaultValue);
}
if (!unityMaterial.HasProperty (unityPropName)) {
return new FbxDouble3(defaultValue);
}
var unityColor = unityMaterial.GetColor (unityPropName);
return new FbxDouble3 (unityColor.r, unityColor.g, unityColor.b);
}
///
/// Export (and map) a Unity PBS material to FBX classic material
///
internal bool ExportMaterial (Material unityMaterial, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityMaterial) {
unityMaterial = DefaultMaterial;
}
var unityID = unityMaterial.GetInstanceID();
FbxSurfaceMaterial mappedMaterial;
if (MaterialMap.TryGetValue (unityID, out mappedMaterial)) {
fbxNode.AddMaterial (mappedMaterial);
return true;
}
var unityName = unityMaterial.name;
var fbxName = ExportOptions.UseMayaCompatibleNames
? ConvertToMayaCompatibleName(unityName) : unityName;
fbxName = GetUniqueMaterialName(fbxName);
if (Verbose) {
if (unityName != fbxName) {
Debug.Log (string.Format ("exporting material {0} as {1}", unityName, fbxName));
} else {
Debug.Log(string.Format("exporting material {0}", unityName));
}
}
// We'll export either Phong or Lambert. Phong if it calls
// itself specular, Lambert otherwise.
var shader = unityMaterial.shader;
bool specular = shader.name.ToLower ().Contains ("specular");
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));
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, "_SpecGlosMap", fbxMaterial, FbxSurfaceMaterial.sSpecular);
}
MaterialMap.Add (unityID, fbxMaterial);
fbxNode.AddMaterial (fbxMaterial);
return true;
}
///
/// Sets up the material to polygon mapping for fbxMesh.
/// To determine which part of the mesh uses which material, look at the submeshes
/// and which polygons they represent.
/// Assuming equal number of materials as submeshes, and that they are in the same order.
/// (i.e. submesh 1 uses material 1)
///
/// Fbx mesh.
/// Mesh.
/// Materials.
private void AssignLayerElementMaterial(FbxMesh fbxMesh, Mesh mesh, int materialCount)
{
// Add FbxLayerElementMaterial to layer 0 of the node
FbxLayer fbxLayer = fbxMesh.GetLayer (0 /* default layer */);
if (fbxLayer == null) {
fbxMesh.CreateLayer ();
fbxLayer = fbxMesh.GetLayer (0 /* default layer */);
}
using (var fbxLayerElement = FbxLayerElementMaterial.Create (fbxMesh, "Material")) {
// if there is only one material then set everything to that material
if (materialCount == 1) {
fbxLayerElement.SetMappingMode (FbxLayerElement.EMappingMode.eAllSame);
fbxLayerElement.SetReferenceMode (FbxLayerElement.EReferenceMode.eIndexToDirect);
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetIndexArray ();
fbxElementArray.Add (0);
} else {
fbxLayerElement.SetMappingMode (FbxLayerElement.EMappingMode.eByPolygon);
fbxLayerElement.SetReferenceMode (FbxLayerElement.EReferenceMode.eIndexToDirect);
FbxLayerElementArray fbxElementArray = fbxLayerElement.GetIndexArray ();
for (int subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; subMeshIndex++) {
var topology = mesh.GetTopology (subMeshIndex);
int polySize;
switch (topology) {
case MeshTopology.Triangles:
polySize = 3;
break;
case MeshTopology.Quads:
polySize = 4;
break;
case MeshTopology.Lines:
throw new System.NotImplementedException();
case MeshTopology.Points:
throw new System.NotImplementedException();
case MeshTopology.LineStrip:
throw new System.NotImplementedException();
default:
throw new System.NotImplementedException();
}
// Specify the material index for each polygon.
// Material index should match subMeshIndex.
var indices = mesh.GetIndices(subMeshIndex);
for (int j = 0, n = indices.Length / polySize; j < n; j++) {
fbxElementArray.Add(subMeshIndex);
}
}
}
fbxLayer.SetMaterials (fbxLayerElement);
}
}
///
/// Exports a unity mesh and attaches it to the node as an FbxMesh.
///
/// Able to export materials per sub-mesh as well (by default, exports with the default material).
///
/// Use fbxNode.GetMesh() to access the exported mesh.
///
internal bool ExportMesh (Mesh mesh, FbxNode fbxNode, Material[] materials = null)
{
var meshInfo = new MeshInfo(mesh, materials);
return ExportMesh(meshInfo, fbxNode);
}
///
/// Keeps track of the index of each point in the exported vertex array.
///
private Dictionary ControlPointToIndex = new Dictionary ();
///
/// Exports a unity mesh and attaches it to the node as an FbxMesh.
///
bool ExportMesh (MeshInfo meshInfo, FbxNode fbxNode)
{
if (!meshInfo.IsValid) {
return false;
}
NumMeshes++;
NumTriangles += meshInfo.Triangles.Length / 3;
// create the mesh structure.
var fbxScene = fbxNode.GetScene();
FbxMesh fbxMesh = FbxMesh.Create (fbxScene, "Scene");
// Create control points.
ControlPointToIndex.Clear();
{
var vertices = meshInfo.Vertices;
for (int v = 0, n = meshInfo.VertexCount; v < n; v++) {
if (ControlPointToIndex.ContainsKey (vertices [v])) {
continue;
}
ControlPointToIndex [vertices [v]] = ControlPointToIndex.Count();
}
fbxMesh.InitControlPoints (ControlPointToIndex.Count());
foreach (var kvp in ControlPointToIndex) {
var controlPoint = kvp.Key;
var index = kvp.Value;
fbxMesh.SetControlPointAt (ConvertToRightHanded(controlPoint, UnitScaleFactor), index);
}
}
var unmergedPolygons = new List ();
var mesh = meshInfo.mesh;
for (int s = 0; s < mesh.subMeshCount; s++) {
var topology = mesh.GetTopology (s);
var indices = mesh.GetIndices (s);
int polySize;
int[] vertOrder;
switch (topology) {
case MeshTopology.Triangles:
polySize = 3;
// flip winding order so that Maya and Unity import it properly
vertOrder = new int[] { 0, 2, 1 };
break;
case MeshTopology.Quads:
polySize = 4;
// flip winding order so that Maya and Unity import it properly
vertOrder = new int[] { 0, 3, 2, 1 };
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.
ExportBlendShapes(meshInfo, fbxMesh, fbxScene, unmergedPolygons.ToArray());
// set the fbxNode containing the mesh
fbxNode.SetNodeAttribute (fbxMesh);
fbxNode.SetShadingMode (FbxNode.EShadingMode.eWireFrame);
return true;
}
///
/// Export GameObject as a skinned mesh with material, bones, a skin and, a bind pose.
///
[SecurityPermission(SecurityAction.LinkDemand)]
private bool ExportSkinnedMesh (GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if(!unityGo || fbxNode == null)
{
return false;
}
SkinnedMeshRenderer unitySkin
= unityGo.GetComponent ();
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 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 = EulerToQuaternion(fbxPreRotationEuler);
// Inverse of the prerotation
fbxPreRotationQuaternion.Inverse();
// Multiply LclRotation by pre-rotation inverse to get the LclRotation without pre-rotation applied
var finalLclRotationQuat = fbxPreRotationQuaternion * EulerToQuaternion(new FbxVector4(fbxBone.LclRotation.Get()));
// Convert to Euler without axis conversion (Pre-rotation and LclRotation were already in Maya axis)
// and update LclRotation
fbxBone.LclRotation.Set(ToFbxDouble3(QuaternionToEuler(finalLclRotationQuat)));
}
else
{
Debug.Log("Warning: One or more bones are null. Skeleton may not export correctly.");
}
}
}
return true;
}
///
/// Gets the bind pose for the Unity bone.
///
/// The bind pose.
/// Unity bone.
/// Bind poses.
/// Dictionary of bone to index.
/// Skinned mesh.
private Matrix4x4 GetBindPose(
Transform unityBone, Matrix4x4[] bindPoses,
Dictionary boneDict, SkinnedMeshRenderer skinnedMesh
){
Matrix4x4 bindPose;
int index;
if (boneDict.TryGetValue (unityBone, out index)) {
bindPose = bindPoses [index];
} else {
bindPose = unityBone.worldToLocalMatrix * skinnedMesh.transform.localToWorldMatrix;
}
return bindPose;
}
///
/// Export bones of skinned mesh, if this is a skinned mesh with
/// bones and bind poses.
///
[SecurityPermission(SecurityAction.LinkDemand)]
private bool ExportSkeleton (SkinnedMeshRenderer skinnedMesh, FbxScene fbxScene, out Dictionary skinnedMeshToBonesMap)
{
skinnedMeshToBonesMap = new Dictionary ();
if (!skinnedMesh) {
return false;
}
var bones = skinnedMesh.bones;
if (bones == null || bones.Length == 0) {
return false;
}
var mesh = skinnedMesh.sharedMesh;
if (!mesh) {
return false;
}
var bindPoses = mesh.bindposes;
if (bindPoses == null || bindPoses.Length != bones.Length) {
return false;
}
// Two steps:
// 0. Set up the map from bone to index.
// 1. Set the transforms.
// Step 0: map transform to index so we can look up index by bone.
Dictionary index = new Dictionary();
for (int boneIndex = 0; boneIndex < bones.Length; boneIndex++) {
Transform unityBoneTransform = bones [boneIndex];
// ignore null bones
if (unityBoneTransform != null)
{
index[unityBoneTransform] = boneIndex;
}
}
skinnedMeshToBonesMap.Add (skinnedMesh, bones);
// Step 1: Set transforms
var boneInfo = new SkinnedMeshBoneInfo (skinnedMesh, index);
foreach (var bone in bones) {
// ignore null bones
if (bone != null)
{
var fbxBone = MapUnityObjectToFbxNode[bone.gameObject];
ExportBoneTransform(fbxBone, fbxScene, bone, boneInfo);
}
}
return true;
}
///
/// Export binding of mesh to skeleton
///
private bool ExportSkin (SkinnedMeshRenderer skinnedMesh,
MeshInfo meshInfo, FbxScene fbxScene, FbxMesh fbxMesh,
FbxNode fbxRootNode)
{
FbxSkin fbxSkin = FbxSkin.Create (fbxScene, (skinnedMesh.name + SkinPrefix));
FbxAMatrix fbxMeshMatrix = fbxRootNode.EvaluateGlobalTransform ();
// keep track of the bone index -> fbx cluster mapping, so that we can add the bone weights afterwards
Dictionary boneCluster = new Dictionary ();
for(int i = 0; i < skinnedMesh.bones.Length; i++) {
// ignore null bones
if (skinnedMesh.bones[i] != null)
{
FbxNode fbxBoneNode = MapUnityObjectToFbxNode[skinnedMesh.bones[i].gameObject];
// Create the deforming cluster
FbxCluster fbxCluster = FbxCluster.Create(fbxScene, "BoneWeightCluster");
fbxCluster.SetLink(fbxBoneNode);
fbxCluster.SetLinkMode(FbxCluster.ELinkMode.eNormalize);
boneCluster.Add(i, fbxCluster);
// set the Transform and TransformLink matrix
fbxCluster.SetTransformMatrix(fbxMeshMatrix);
FbxAMatrix fbxLinkMatrix = fbxBoneNode.EvaluateGlobalTransform();
fbxCluster.SetTransformLinkMatrix(fbxLinkMatrix);
// add the cluster to the skin
fbxSkin.AddCluster(fbxCluster);
}
}
// set the vertex weights for each bone
SetVertexWeights(meshInfo, boneCluster);
// Add the skin to the mesh after the clusters have been added
fbxMesh.AddDeformer (fbxSkin);
return true;
}
///
/// set vertex weights in cluster
///
private void SetVertexWeights (MeshInfo meshInfo, Dictionary boneCluster)
{
HashSet visitedVertices = new HashSet ();
// set the vertex weights for each bone
for (int i = 0; i < meshInfo.BoneWeights.Length; i++) {
var actualIndex = ControlPointToIndex [meshInfo.Vertices [i]];
if (visitedVertices.Contains (actualIndex)) {
continue;
}
visitedVertices.Add (actualIndex);
var boneWeights = meshInfo.BoneWeights;
int[] indices = {
boneWeights [i].boneIndex0,
boneWeights [i].boneIndex1,
boneWeights [i].boneIndex2,
boneWeights [i].boneIndex3
};
float[] weights = {
boneWeights [i].weight0,
boneWeights [i].weight1,
boneWeights [i].weight2,
boneWeights [i].weight3
};
for (int j = 0; j < indices.Length; j++) {
if (weights [j] <= 0) {
continue;
}
if (!boneCluster.ContainsKey (indices [j])) {
continue;
}
// add vertex and weighting on vertex to this bone's cluster
boneCluster [indices [j]].AddControlPointIndex (actualIndex, weights [j]);
}
}
}
///
/// Export bind pose of mesh to skeleton
///
private bool ExportBindPose (SkinnedMeshRenderer skinnedMesh, FbxNode fbxMeshNode,
FbxScene fbxScene, Dictionary skinnedMeshToBonesMap)
{
if (fbxMeshNode == null || skinnedMeshToBonesMap == null || fbxScene == null)
{
return false;
}
FbxPose fbxPose = FbxPose.Create(fbxScene, fbxMeshNode.GetName());
// set as bind pose
fbxPose.SetIsBindPose (true);
// assume each bone node has one weighted vertex cluster
Transform[] bones;
if (!skinnedMeshToBonesMap.TryGetValue (skinnedMesh, out bones)) {
return false;
}
for (int i = 0; i < bones.Length; i++) {
// ignore null bones
if (bones[i] != null)
{
FbxNode fbxBoneNode = MapUnityObjectToFbxNode[bones[i].gameObject];
// EvaluateGlobalTransform returns an FbxAMatrix (affine matrix)
// which has to be converted to an FbxMatrix so that it can be passed to fbxPose.Add().
// The hierarchy for FbxMatrix and FbxAMatrix is as follows:
//
// FbxDouble4x4
// / \
// FbxMatrix FbxAMatrix
//
// Therefore we can't convert directly from FbxAMatrix to FbxMatrix,
// however FbxMatrix has a constructor that takes an FbxAMatrix.
FbxMatrix fbxBindMatrix = new FbxMatrix(fbxBoneNode.EvaluateGlobalTransform());
fbxPose.Add(fbxBoneNode, fbxBindMatrix);
}
}
fbxPose.Add (fbxMeshNode, new FbxMatrix (fbxMeshNode.EvaluateGlobalTransform ()));
// add the pose to the scene
fbxScene.AddPose (fbxPose);
return true;
}
///
/// Takes a Quaternion and returns a Euler with XYZ rotation order.
/// Also converts from left (Unity) to righthanded (Maya) coordinates.
///
/// Note: Cannot simply use the FbxQuaternion.DecomposeSphericalXYZ()
/// function as this returns the angle in spherical coordinates
/// instead of Euler angles, which Maya does not import properly.
///
/// Euler with XYZ rotation order.
internal static FbxDouble3 ConvertQuaternionToXYZEuler(Quaternion q)
{
FbxQuaternion quat = new FbxQuaternion (q.x, q.y, q.z, q.w);
FbxAMatrix m = new FbxAMatrix ();
m.SetQ (quat);
var vector4 = m.GetR ();
// Negate the y and z values of the rotation to convert
// from Unity to Maya coordinates (left to righthanded).
return new FbxDouble3 (vector4.X, -vector4.Y, -vector4.Z);
}
internal static FbxVector4 ConvertQuaternionToXYZEuler (FbxQuaternion quat)
{
FbxAMatrix m = new FbxAMatrix ();
m.SetQ (quat);
var vector4 = m.GetR ();
// Negate the y and z values of the rotation to convert
// from Unity to Maya coordinates (left to righthanded).
return new FbxVector4 (vector4.X, -vector4.Y, -vector4.Z, vector4.W);
}
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);
}
internal static FbxVector4 ToFbxVector4(FbxDouble3 v)
{
return new FbxVector4(v.X, v.Y, v.Z);
}
internal static FbxDouble3 ConvertToRightHandedEuler(Vector3 rot)
{
rot.y *= -1;
rot.z *= -1;
return ToFbxDouble3(rot);
}
///
/// Euler to quaternion without axis conversion.
///
/// a quaternion.
/// Euler.
internal static FbxQuaternion EulerToQuaternion(FbxVector4 euler)
{
FbxAMatrix m = new FbxAMatrix ();
m.SetR (euler);
return m.GetQ ();
}
///
/// Quaternion to euler without axis conversion.
///
/// a euler.
/// Quaternion.
internal static FbxVector4 QuaternionToEuler(FbxQuaternion quat)
{
FbxAMatrix m = new FbxAMatrix ();
m.SetQ (quat);
return m.GetR ();
}
// get a fbxNode's global default position.
internal bool ExportTransform (UnityEngine.Transform unityTransform, FbxNode fbxNode, Vector3 newCenter, TransformExportType exportType)
{
// Fbx rotation order is XYZ, but Unity rotation order is ZXY.
// This causes issues when converting euler to quaternion, causing the final
// rotation to be slighlty off.
// Fixed by exporting the rotations as eulers with XYZ rotation order.
// Can't just set the rotation order to ZXY on export as Maya incorrectly imports the
// rotation. Appears to first convert to XYZ rotation then set rotation order to ZXY.
fbxNode.SetRotationOrder (FbxNode.EPivotSet.eSourcePivot, FbxEuler.EOrder.eOrderXYZ);
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 = ConvertQuaternionToXYZEuler(unityTransform.rotation);
unityScale = unityTransform.lossyScale;
break;
default: /*case TransformExportType.Local*/
unityTranslate = unityTransform.localPosition;
fbxRotate = ConvertQuaternionToXYZEuler(unityTransform.localRotation);
unityScale = unityTransform.localScale;
break;
}
// Transfer transform data from Unity to Fbx
var fbxTranslate = ConvertToRightHanded(unityTranslate, UnitScaleFactor);
var fbxScale = new FbxDouble3 (unityScale.x, unityScale.y, unityScale.z);
// set the local position of fbxNode
fbxNode.LclTranslation.Set (new FbxDouble3(fbxTranslate.X, fbxTranslate.Y, fbxTranslate.Z));
fbxNode.LclRotation.Set (fbxRotate);
fbxNode.LclScaling.Set (fbxScale);
return true;
}
///
/// if this game object is a model prefab or the model has already been exported, then export with shared components
///
[SecurityPermission(SecurityAction.LinkDemand)]
private bool ExportInstance(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGo || fbxNode == null)
{
return false;
}
Object unityPrefabParent = PrefabUtility.GetCorrespondingObjectFromSource(unityGo);
FbxMesh fbxMesh = null;
if (unityPrefabParent != null && !SharedMeshes.TryGetValue (unityPrefabParent.GetInstanceID(), out fbxMesh))
{
if (Verbose)
Debug.Log (string.Format ("exporting instance {0}({1})", unityGo.name, unityPrefabParent.name));
if (ExportMesh (unityGo, fbxNode) && fbxNode.GetMesh() != null) {
SharedMeshes [unityPrefabParent.GetInstanceID()] = fbxNode.GetMesh ();
return true;
}
return false;
}
// check if mesh is shared between 2 objects that are not prefabs
else if (unityPrefabParent == null)
{
// check if same mesh has already been exported
MeshFilter unityGoMesh = unityGo.GetComponent();
if (unityGoMesh != null && MeshToFbxNodeMap.ContainsKey(unityGoMesh.sharedMesh))
{
fbxMesh = MeshToFbxNodeMap[unityGoMesh.sharedMesh].GetMesh();
}
// export mesh as normal and add it to list
else
{
if (unityGoMesh != null)
{
MeshToFbxNodeMap.Add(unityGoMesh.sharedMesh, fbxNode);
}
return false;
}
}
if (fbxMesh == null)
{
return false;
}
// We don't export the mesh because we already have it from the parent, but we still need to assign the material
var renderer = unityGo.GetComponent();
var materials = renderer ? renderer.sharedMaterials : null;
Autodesk.Fbx.FbxSurfaceMaterial newMaterial = null;
if (materials != null)
{
foreach (var mat in materials) {
if (MaterialMap.TryGetValue(mat.GetInstanceID(), out newMaterial))
{
fbxNode.AddMaterial(newMaterial);
}
else
{
// create new material
ExportMaterial(mat, fbxScene, fbxNode);
}
}
}
// set the fbxNode containing the mesh
fbxNode.SetNodeAttribute (fbxMesh);
fbxNode.SetShadingMode (FbxNode.EShadingMode.eWireFrame);
return true;
}
///
/// Exports camera component
///
private bool ExportCamera (GameObject unityGO, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGO || fbxScene == null || fbxNode == null)
{
return false;
}
Camera unityCamera = unityGO.GetComponent ();
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;
}
///
/// Exports light component.
/// Supported types: point, spot and directional
/// Cookie => Gobo
///
private bool ExportLight (GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if(!unityGo || fbxScene == null || fbxNode == null)
{
return false;
}
Light unityLight = unityGo.GetComponent ();
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 uniConstraint, TFbxConstraint fbxConstraint, FbxNode fbxNode)
where TUnityConstraint : IConstraint where TFbxConstraint : FbxConstraint
{
fbxConstraint.Active.Set(uniConstraint.constraintActive);
fbxConstraint.Lock.Set(uniConstraint.locked);
fbxConstraint.Weight.Set(uniConstraint.weight * UnitScaleFactor);
AddFbxNodeToConstraintsMapping(fbxNode, fbxConstraint, typeof(TUnityConstraint));
return true;
}
private struct ExpConstraintSource
{
private FbxNode m_node;
public FbxNode node
{
get { return m_node; }
set { m_node = value; }
}
private float m_weight;
public float weight
{
get { return m_weight; }
set { m_weight = value; }
}
public ExpConstraintSource(FbxNode node, float weight)
{
this.m_node = node;
this.m_weight = weight;
}
}
private List GetConstraintSources(IConstraint unityConstraint)
{
if(unityConstraint == null)
{
return null;
}
var fbxSources = new List();
var sources = new List();
unityConstraint.GetSources(sources);
foreach (var source in sources)
{
// ignore any sources that are not getting exported
FbxNode sourceNode;
if (!MapUnityObjectToFbxNode.TryGetValue(source.sourceTransform.gameObject, out sourceNode))
{
continue;
}
fbxSources.Add(new ExpConstraintSource(sourceNode, source.weight * UnitScaleFactor));
}
return fbxSources;
}
private void AddFbxNodeToConstraintsMapping(FbxNode fbxNode, T fbxConstraint, System.Type uniConstraintType) where T : FbxConstraint
{
Dictionary constraintMapping;
if (!MapConstrainedObjectToConstraints.TryGetValue(fbxNode, out constraintMapping))
{
constraintMapping = new Dictionary();
MapConstrainedObjectToConstraints.Add(fbxNode, constraintMapping);
}
constraintMapping.Add(fbxConstraint, uniConstraintType);
}
private bool ExportPositionConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode)
{
if(fbxNode == null)
{
return false;
}
var uniPosConstraint = uniConstraint as PositionConstraint;
Debug.Assert (uniPosConstraint != null);
FbxConstraintPosition fbxPosConstraint = FbxConstraintPosition.Create(fbxScene, fbxNode.GetName() + "_positionConstraint");
fbxPosConstraint.SetConstrainedObject(fbxNode);
var uniSources = GetConstraintSources(uniPosConstraint);
uniSources.ForEach(uniSource => fbxPosConstraint.AddConstraintSource(uniSource.node, uniSource.weight));
ExportCommonConstraintProperties(uniPosConstraint, fbxPosConstraint, fbxNode);
var uniAffectedAxes = uniPosConstraint.translationAxis;
fbxPosConstraint.AffectX.Set((uniAffectedAxes & Axis.X) == Axis.X);
fbxPosConstraint.AffectY.Set((uniAffectedAxes & Axis.Y) == Axis.Y);
fbxPosConstraint.AffectZ.Set((uniAffectedAxes & Axis.Z) == Axis.Z);
var fbxTranslationOffset = ConvertToRightHanded(uniPosConstraint.translationOffset, UnitScaleFactor);
fbxPosConstraint.Translation.Set(ToFbxDouble3(fbxTranslationOffset));
// rest position is the position of the fbx node
var fbxRestTranslation = ConvertToRightHanded(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 = ConvertToRightHandedEuler(uniRotationOffset);
fbxRotConstraint.Rotation.Set(fbxRotationOffset);
// rest rotation is the rotation of the fbx node
var uniRestRotationQuat = Quaternion.Euler(uniRotConstraint.rotationAtRest);
var fbxRestRotation = ConvertQuaternionToXYZEuler(uniRestRotationQuat);
// 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 = ConvertToRightHandedEuler(uniRotationOffset);
fbxAimConstraint.RotationOffset.Set(fbxRotationOffset);
// rest rotation is the rotation of the fbx node
var uniRestRotationQuat = Quaternion.Euler(uniAimConstraint.rotationAtRest);
var fbxRestRotation = ConvertQuaternionToXYZEuler(uniRestRotationQuat);
// 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 = ConvertToRightHanded(uniAimConstraint.aimVector);
fbxAimConstraint.AimVector.Set(ToFbxDouble3(uniAimVector));
fbxAimConstraint.UpVector.Set(ToFbxDouble3(uniAimConstraint.upVector));
fbxAimConstraint.WorldUpVector.Set(ToFbxDouble3(uniAimConstraint.worldUpVector));
if (uniAimConstraint.worldUpObject && MapUnityObjectToFbxNode.ContainsKey(uniAimConstraint.worldUpObject.gameObject))
{
fbxAimConstraint.SetWorldUpObject(MapUnityObjectToFbxNode[uniAimConstraint.worldUpObject.gameObject]);
}
return true;
}
private bool ExportParentConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode)
{
if(fbxNode == null)
{
return false;
}
var uniParentConstraint = uniConstraint as ParentConstraint;
Debug.Assert(uniParentConstraint != null);
FbxConstraintParent fbxParentConstraint = FbxConstraintParent.Create(fbxScene, fbxNode.GetName() + "_parentConstraint");
fbxParentConstraint.SetConstrainedObject(fbxNode);
var uniSources = GetConstraintSources(uniParentConstraint);
var uniTranslationOffsets = uniParentConstraint.translationOffsets;
var uniRotationOffsets = uniParentConstraint.rotationOffsets;
for(int i = 0; i < uniSources.Count; i++)
{
var uniSource = uniSources[i];
var uniTranslationOffset = uniTranslationOffsets[i];
var uniRotationOffset = uniRotationOffsets[i];
fbxParentConstraint.AddConstraintSource(uniSource.node, uniSource.weight);
var fbxTranslationOffset = ConvertToRightHanded(uniTranslationOffset, UnitScaleFactor);
fbxParentConstraint.SetTranslationOffset(uniSource.node, fbxTranslationOffset);
var fbxRotationOffset = ToFbxVector4(ConvertToRightHandedEuler(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 = ConvertToRightHanded(uniParentConstraint.translationAtRest, UnitScaleFactor);
// set the local position of fbxNode
fbxNode.LclTranslation.Set(ToFbxDouble3(fbxRestTranslation));
// rest rotation is the rotation of the fbx node
var uniRestRotationQuat = Quaternion.Euler(uniParentConstraint.rotationAtRest);
var fbxRestRotation = ConvertQuaternionToXYZEuler(uniRestRotationQuat);
// set the local rotation of fbxNode
fbxNode.LclRotation.Set(fbxRestRotation);
return true;
}
private delegate bool ExportConstraintDelegate(IConstraint c , FbxScene fs, FbxNode fn);
private bool ExportConstraints (GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode)
{
if (!unityGo)
{
return false;
}
var mapConstraintTypeToExportFunction = new Dictionary()
{
{ typeof(PositionConstraint), ExportPositionConstraint },
{ typeof(RotationConstraint), ExportRotationConstraint },
{ typeof(ScaleConstraint), ExportScaleConstraint },
{ typeof(AimConstraint), ExportAimConstraint },
{ typeof(ParentConstraint), ExportParentConstraint }
};
// check if GameObject has one of the 5 supported constraints: aim, parent, position, rotation, scale
var uniConstraints = unityGo.GetComponents();
foreach(var uniConstraint in uniConstraints)
{
var uniConstraintType = uniConstraint.GetType();
ExportConstraintDelegate constraintDelegate;
if (!mapConstraintTypeToExportFunction.TryGetValue(uniConstraintType, out constraintDelegate))
{
Debug.LogWarningFormat("FbxExporter: Missing function to export constraint of type {0}", uniConstraintType.Name);
continue;
}
constraintDelegate(uniConstraint, fbxScene, fbxNode);
}
return true;
}
///
/// Return set of sample times to cover all keys on animation curves
///
[SecurityPermission(SecurityAction.LinkDemand)]
internal static HashSet GetSampleTimes(AnimationCurve[] animCurves, double sampleRate)
{
var keyTimes = new HashSet();
double fs = 1.0/sampleRate;
double firstTime = double.MaxValue, lastTime = double.MinValue;
foreach (var ac in animCurves)
{
if (ac==null || ac.length<=0) continue;
firstTime = System.Math.Min(firstTime, ac[0].time);
lastTime = System.Math.Max(lastTime, ac[ac.length-1].time);
}
int firstframe = (int)System.Math.Floor(firstTime * sampleRate);
int lastframe = (int)System.Math.Ceiling(lastTime * sampleRate);
for (int i = firstframe; i <= lastframe; i++) {
keyTimes.Add ((float)(i * fs));
}
return keyTimes;
}
///
/// Return set of all keys times on animation curves
///
[SecurityPermission(SecurityAction.LinkDemand)]
internal static HashSet GetKeyTimes(AnimationCurve[] animCurves)
{
var keyTimes = new HashSet();
foreach (var ac in animCurves)
{
if (ac!=null) foreach(var key in ac.keys) { keyTimes.Add(key.time); }
}
return keyTimes;
}
///
/// Export animation curve key frames with key tangents
/// NOTE : This is a work in progress (WIP). We only export the key time and value on
/// a Cubic curve using the default tangents.
///
internal void ExportAnimationKeys (AnimationCurve uniAnimCurve, FbxAnimCurve fbxAnimCurve,
UnityToMayaConvertSceneHelper convertSceneHelper)
{
// TODO: complete the mapping between key tangents modes Unity and FBX
Dictionary> MapUnityKeyTangentModeToFBX =
new Dictionary>
{
//TangeantAuto|GenericTimeIndependent|GenericClampProgressive
{AnimationUtility.TangentMode.Free, new List{FbxAnimCurveDef.ETangentMode.eTangentAuto,FbxAnimCurveDef.ETangentMode.eTangentGenericClampProgressive}},
//TangeantAuto|GenericTimeIndependent
{AnimationUtility.TangentMode.Auto, new List{FbxAnimCurveDef.ETangentMode.eTangentAuto,FbxAnimCurveDef.ETangentMode.eTangentGenericTimeIndependent}},
//TangeantAuto|GenericTimeIndependent|GenericClampProgressive
{AnimationUtility.TangentMode.ClampedAuto, new List{FbxAnimCurveDef.ETangentMode.eTangentAuto,FbxAnimCurveDef.ETangentMode.eTangentGenericClampProgressive}},
};
// Copy Unity AnimCurve to FBX AnimCurve.
// NOTE: only cubic keys are supported by the FbxImporter
using (new FbxAnimCurveModifyHelper(new List{fbxAnimCurve}))
{
for (int keyIndex = 0; keyIndex < uniAnimCurve.length; ++keyIndex)
{
var uniKeyFrame = uniAnimCurve [keyIndex];
var fbxTime = FbxTime.FromSecondDouble (uniKeyFrame.time);
int fbxKeyIndex = fbxAnimCurve.KeyAdd (fbxTime);
fbxAnimCurve.KeySet (fbxKeyIndex,
fbxTime,
convertSceneHelper.Convert(uniKeyFrame.value)
);
// configure tangents
var lTangent = AnimationUtility.GetKeyLeftTangentMode(uniAnimCurve, keyIndex);
var rTangent = AnimationUtility.GetKeyRightTangentMode(uniAnimCurve, keyIndex);
if (!(MapUnityKeyTangentModeToFBX.ContainsKey(lTangent) && MapUnityKeyTangentModeToFBX.ContainsKey(rTangent)))
{
Debug.LogWarning(string.Format("key[{0}] missing tangent mapping ({1},{2})", keyIndex, lTangent.ToString(), rTangent.ToString()));
continue;
}
// TODO : handle broken tangents
// TODO : set key tangents
}
}
}
///
/// Export animation curve key samples
///
[SecurityPermission(SecurityAction.LinkDemand)]
internal void ExportAnimationSamples (AnimationCurve uniAnimCurve, FbxAnimCurve fbxAnimCurve,
double sampleRate,
UnityToMayaConvertSceneHelper convertSceneHelper)
{
using (new FbxAnimCurveModifyHelper(new List{fbxAnimCurve}))
{
foreach (var currSampleTime in GetSampleTimes(new AnimationCurve[]{uniAnimCurve}, sampleRate))
{
float currSampleValue = uniAnimCurve.Evaluate((float)currSampleTime);
var fbxTime = FbxTime.FromSecondDouble (currSampleTime);
int fbxKeyIndex = fbxAnimCurve.KeyAdd (fbxTime);
fbxAnimCurve.KeySet (fbxKeyIndex,
fbxTime,
convertSceneHelper.Convert(currSampleValue)
);
}
}
}
///
/// Get the FbxConstraint associated with the constrained node.
///
///
///
///
private FbxConstraint GetFbxConstraint(FbxNode constrainedNode, System.Type uniConstraintType)
{
if (uniConstraintType == null || !uniConstraintType.GetInterfaces().Contains(typeof(IConstraint)))
{
// not actually a constraint
return null;
}
Dictionary constraints;
if (!MapConstrainedObjectToConstraints.TryGetValue(constrainedNode, out constraints))
{
return null;
}
foreach (var constraint in constraints)
{
if (uniConstraintType != constraint.Value)
{
continue;
}
return constraint.Key;
}
return null;
}
private FbxProperty GetFbxProperty(FbxNode fbxNode, string fbxPropertyName, System.Type uniPropertyType)
{
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;
}
}
// map unity property name to fbx property
var fbxProperty = fbxNode.FindProperty(fbxPropertyName, false);
if (fbxProperty.IsValid())
{
return fbxProperty;
}
var fbxNodeAttribute = fbxNode.GetNodeAttribute();
if (fbxNodeAttribute != null)
{
fbxProperty = fbxNodeAttribute.FindProperty(fbxPropertyName, false);
}
return fbxProperty;
}
///
/// Export an AnimationCurve.
/// NOTE: This is not used for rotations, because we need to convert from
/// quaternion to euler and various other stuff.
///
[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);
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);
// TODO: we'll resample the curve so we don't have to
// configure tangents
if (ModelExporter.ExportSettings.BakeAnimationProperty) {
ExportAnimationSamples (uniAnimCurve, fbxAnimCurve, frameRate, convertSceneHelper);
} else {
ExportAnimationKeys (uniAnimCurve, fbxAnimCurve, convertSceneHelper);
}
}
}
internal class UnityToMayaConvertSceneHelper
{
bool convertDistance = false;
bool convertLtoR = 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);
bool partTx = uniPropertyName.EndsWith("Position.x", cc) || uniPropertyName.EndsWith("T.x", cc) || (uniPropertyName.StartsWith("m_TranslationOffset") && uniPropertyName.EndsWith(".x", cc));
bool partRyz = uniPropertyName.StartsWith("m_RotationOffset", cc) && (uniPropertyName.EndsWith(".y") || uniPropertyName.EndsWith(".z"));
convertLtoR |= partTx;
convertLtoR |= partRyz;
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 (convertLtoR)
unitScaleFactor = -unitScaleFactor;
if (convertToRadian)
{
unitScaleFactor *= (Mathf.PI / 180);
}
}
public float Convert(float value)
{
float convertedValue = value;
if (convertLensShiftX || convertLensShiftY)
{
convertedValue = Mathf.Clamp(Mathf.Abs(value), 0f, 1f)*Mathf.Sign(value);
}
if (camera != null)
{
if (convertLensShiftX)
{
convertedValue *= (float)camera.GetApertureWidth();
}
else if (convertLensShiftY)
{
convertedValue *= (float)camera.GetApertureHeight();
}
}
// left handed to right handed conversion
// meters to centimetres conversion
return unitScaleFactor * convertedValue;
}
}
///
/// Export an AnimationClip as a single take
///
[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> ();
// extract and store all necessary information from the curve bindings, namely the animation curves
// and their corresponding property names for each GameObject.
foreach (EditorCurveBinding uniCurveBinding in AnimationUtility.GetCurveBindings (uniAnimClip)) {
Object uniObj = AnimationUtility.GetAnimatedObject (uniRoot, uniCurveBinding);
if (!uniObj) {
continue;
}
AnimationCurve uniAnimCurve = AnimationUtility.GetEditorCurve (uniAnimClip, uniCurveBinding);
if (uniAnimCurve == null) {
continue;
}
var uniGO = GetGameObject (uniObj);
// Check if the GameObject has an FBX node to the animation. It might be null because the LOD selected doesn't match the one on the gameobject.
if (!uniGO || MapUnityObjectToFbxNode.ContainsKey(uniGO) == false) {
continue;
}
if (unityCurves.ContainsKey (uniGO)) {
unityCurves [uniGO].Add (new UnityCurve(uniCurveBinding.propertyName, uniAnimCurve, uniCurveBinding.type));
continue;
}
unityCurves.Add (uniGO, new List (){ new UnityCurve(uniCurveBinding.propertyName, uniAnimCurve, uniCurveBinding.type) });
}
// transfer root motion
var animSource = ExportOptions.AnimationSource;
var animDest = ExportOptions.AnimationDest;
if (animSource && animDest && animSource != animDest) {
// list of all transforms between source and dest, including source and dest
var transformsFromSourceToDest = new List ();
var curr = animDest;
while (curr != animSource) {
transformsFromSourceToDest.Add (curr);
curr = curr.parent;
}
transformsFromSourceToDest.Add (animSource);
transformsFromSourceToDest.Reverse ();
// while there are 2 transforms in the list, transfer the animation from the
// first to the next transform.
// Then remove the first transform from the list.
while (transformsFromSourceToDest.Count >= 2) {
var source = transformsFromSourceToDest [0];
transformsFromSourceToDest.RemoveAt (0);
var dest = transformsFromSourceToDest [0];
TransferMotion (source, dest, uniAnimClip.frameRate, ref unityCurves);
}
}
/* The major difficulty: Unity uses quaternions for rotation
* (which is how it should be) but FBX uses Euler angles. So we
* need to gather up the list of transform curves per object.
*
* For euler angles, Unity uses ZXY rotation order while Maya uses XYZ.
* Maya doesn't import files with ZXY rotation correctly, so have to convert to XYZ.
* Need all 3 curves in order to convert.
*
* Also, in both cases, prerotation has to be removed from the animated rotation if
* there are bones being exported.
*/
var rotations = new Dictionary();
// export the animation curves for each GameObject that has animation
foreach (var kvp in unityCurves) {
var uniGO = kvp.Key;
foreach (var uniCurve in kvp.Value) {
var propertyName = uniCurve.propertyName;
var uniAnimCurve = uniCurve.uniAnimCurve;
// Do not create the curves if the component is a SkinnedMeshRenderer and if the option in FBX Export settings is toggled on.
if (!ExportOptions.AnimateSkinnedMesh && (uniGO.GetComponent () != null)) {
continue;
}
FbxNode fbxNode;
if (!MapUnityObjectToFbxNode.TryGetValue(uniGO, out fbxNode))
{
Debug.LogError(string.Format("no FbxNode found for {0}", uniGO.name));
continue;
}
int index = QuaternionCurve.GetQuaternionIndex (propertyName);
if (index >= 0) {
// Rotation property; save it to convert quaternion -> euler later.
RotationCurve rotCurve = GetRotationCurve (uniGO, uniAnimClip.frameRate, ref rotations);
rotCurve.SetCurve (index, uniAnimCurve);
continue;
}
index = EulerCurve.GetEulerIndex (propertyName);
if (index >= 0) {
RotationCurve rotCurve = GetRotationCurve (uniGO, uniAnimClip.frameRate, ref rotations);
rotCurve.SetCurve (index, uniAnimCurve);
continue;
}
// simple property (e.g. intensity), export right away
ExportAnimationCurve (fbxNode, uniAnimCurve, uniAnimClip.frameRate,
propertyName, uniCurve.propertyType,
fbxAnimLayer);
}
}
// now export all the quaternion curves
foreach (var kvp in rotations) {
var unityGo = kvp.Key;
var rot = kvp.Value;
FbxNode fbxNode;
if (!MapUnityObjectToFbxNode.TryGetValue (unityGo, out fbxNode)) {
Debug.LogError (string.Format ("no FbxNode found for {0}", unityGo.name));
continue;
}
rot.Animate (unityGo.transform, fbxNode, fbxAnimLayer, Verbose);
}
}
///
/// Transfers transform animation from source to dest. Replaces dest's Unity Animation Curves with updated animations.
/// NOTE: Source must be the parent of dest.
///
/// Source animated object.
/// Destination, child of the source.
/// Sample rate.
/// Unity curves.
[SecurityPermission(SecurityAction.LinkDemand)]
private void TransferMotion(Transform source, Transform dest, float sampleRate, ref Dictionary> unityCurves){
// get sample times for curves in dest + source
// at each sample time, evaluate all 18 transfom anim curves, creating 2 transform matrices
// combine the matrices, get the new values, apply to the 9 new anim curves for dest
if (dest.parent != source) {
Debug.LogError ("dest must be a child of source");
return;
}
List sourceUnityCurves;
if (!unityCurves.TryGetValue (source.gameObject, out sourceUnityCurves)) {
return; // nothing to do, source has no animation
}
List destUnityCurves;
if (!unityCurves.TryGetValue (dest.gameObject, out destUnityCurves)) {
destUnityCurves = new List ();
}
List animCurves = new List ();
foreach (var curve in sourceUnityCurves) {
// TODO: check if curve is anim related
animCurves.Add(curve.uniAnimCurve);
}
foreach (var curve in destUnityCurves) {
animCurves.Add (curve.uniAnimCurve);
}
var sampleTimes = GetSampleTimes (animCurves.ToArray (), sampleRate);
// need to create 9 new UnityCurves, one for each property
var posKeyFrames = new Keyframe[3][];
var rotKeyFrames = new Keyframe[3][];
var scaleKeyFrames = new Keyframe[3][];
for (int k = 0; k < posKeyFrames.Length; k++) {
posKeyFrames [k] = new Keyframe[sampleTimes.Count];
rotKeyFrames[k] = new Keyframe[sampleTimes.Count];
scaleKeyFrames[k] = new Keyframe[sampleTimes.Count];
}
// If we have a point in local coords represented as a column-vector x, the equation of x in coordinates relative to source's parent is:
// x_grandparent = source * dest * x
// Now we're going to change dest to dest' which has the animation from source. And we're going to change
// source to source' which has no animation. The equation of x will become:
// x_grandparent = source' * dest' * x
// We're not changing x_grandparent and x, so we need that:
// source * dest = source' * dest'
// We know dest and source (both animated) and source' (static). Solve for dest':
// dest' = (source')^-1 * source * dest
int keyIndex = 0;
var sourceStaticMatrixInverse = Matrix4x4.TRS(source.localPosition, source.localRotation, source.localScale).inverse;
foreach (var currSampleTime in sampleTimes)
{
var sourceLocalMatrix = GetTransformMatrix (currSampleTime, source, sourceUnityCurves);
var destLocalMatrix = GetTransformMatrix (currSampleTime, dest, destUnityCurves);
var newLocalMatrix = sourceStaticMatrixInverse * sourceLocalMatrix * destLocalMatrix;
FbxVector4 translation, rotation, scale;
GetTRSFromMatrix (newLocalMatrix, out translation, out rotation, out scale);
// get rotation directly from matrix, as otherwise causes issues
// with negative rotations.
var rot = newLocalMatrix.rotation.eulerAngles;
for (int k = 0; k < 3; k++) {
posKeyFrames [k][keyIndex] = new Keyframe(currSampleTime, (float)translation [k]);
rotKeyFrames [k][keyIndex] = new Keyframe(currSampleTime, rot [k]);
scaleKeyFrames [k][keyIndex] = new Keyframe(currSampleTime, (float)scale [k]);
}
keyIndex++;
}
// create the new list of unity curves, and add it to dest's curves
var newUnityCurves = new List();
string posPropName = "m_LocalPosition.";
string rotPropName = "localEulerAnglesRaw.";
string scalePropName = "m_LocalScale.";
var xyz = "xyz";
for (int k = 0; k < 3; k++) {
var posUniCurve = new UnityCurve ( posPropName + xyz[k], new AnimationCurve(posKeyFrames[k]), typeof(Transform));
newUnityCurves.Add (posUniCurve);
var rotUniCurve = new UnityCurve ( rotPropName + xyz[k], new AnimationCurve(rotKeyFrames[k]), typeof(Transform));
newUnityCurves.Add (rotUniCurve);
var scaleUniCurve = new UnityCurve ( scalePropName + xyz[k], new AnimationCurve(scaleKeyFrames[k]), typeof(Transform));
newUnityCurves.Add (scaleUniCurve);
}
// remove old transform curves
RemoveTransformCurves (ref sourceUnityCurves);
RemoveTransformCurves (ref destUnityCurves);
unityCurves [source.gameObject] = sourceUnityCurves;
if (!unityCurves.ContainsKey(dest.gameObject)) {
unityCurves.Add (dest.gameObject, newUnityCurves);
return;
}
unityCurves [dest.gameObject].AddRange(newUnityCurves);
}
private void RemoveTransformCurves(ref List curves){
var transformCurves = new List ();
var transformPropNames = new string[]{"m_LocalPosition.", "m_LocalRotation", "localEulerAnglesRaw.", "m_LocalScale."};
foreach (var curve in curves) {
foreach (var prop in transformPropNames) {
if (curve.propertyName.StartsWith (prop)) {
transformCurves.Add (curve);
break;
}
}
}
foreach (var curve in transformCurves) {
curves.Remove (curve);
}
}
private Matrix4x4 GetTransformMatrix(float currSampleTime, Transform orig, List unityCurves){
var sourcePos = orig.localPosition;
var sourceRot = orig.localRotation;
var sourceScale = orig.localScale;
foreach (var uniCurve in unityCurves) {
float currSampleValue = uniCurve.uniAnimCurve.Evaluate(currSampleTime);
string propName = uniCurve.propertyName;
// try position, scale, quat then euler
int temp = QuaternionCurve.GetQuaternionIndex(propName);
if (temp >= 0) {
sourceRot [temp] = currSampleValue;
continue;
}
temp = EulerCurve.GetEulerIndex (propName);
if (temp >= 0) {
var euler = sourceRot.eulerAngles;
euler [temp] = currSampleValue;
sourceRot.eulerAngles = euler;
continue;
}
temp = GetPositionIndex (propName);
if (temp >= 0) {
sourcePos [temp] = currSampleValue;
continue;
}
temp = GetScaleIndex (propName);
if (temp >= 0) {
sourceScale [temp] = currSampleValue;
}
}
sourceRot = Quaternion.Euler(sourceRot.eulerAngles.x, sourceRot.eulerAngles.y, sourceRot.eulerAngles.z);
return Matrix4x4.TRS(sourcePos, sourceRot, sourceScale);
}
internal struct UnityCurve {
public string propertyName;
public AnimationCurve uniAnimCurve;
public System.Type propertyType;
public UnityCurve(string propertyName, AnimationCurve uniAnimCurve, System.Type propertyType){
this.propertyName = propertyName;
this.uniAnimCurve = uniAnimCurve;
this.propertyType = propertyType;
}
}
private int GetPositionIndex(string uniPropertyName){
System.StringComparison ct = System.StringComparison.CurrentCulture;
bool isPositionComponent = uniPropertyName.StartsWith ("m_LocalPosition.", ct);
if (!isPositionComponent) { return -1; }
switch (uniPropertyName [uniPropertyName.Length - 1]) {
case 'x':
return 0;
case 'y':
return 1;
case 'z':
return 2;
default:
return -1;
}
}
private int GetScaleIndex(string uniPropertyName){
System.StringComparison ct = System.StringComparison.CurrentCulture;
bool isScaleComponent = uniPropertyName.StartsWith ("m_LocalScale.", ct);
if (!isScaleComponent) { return -1; }
switch (uniPropertyName [uniPropertyName.Length - 1]) {
case 'x':
return 0;
case 'y':
return 1;
case 'z':
return 2;
default:
return -1;
}
}
///
/// Gets or creates the rotation curve for GameObject uniGO.
///
/// The rotation curve.
/// Unity GameObject.
/// Frame rate.
/// Rotations.
/// RotationCurve is abstract so specify type of RotationCurve to create.
private RotationCurve GetRotationCurve(
GameObject uniGO, float frameRate,
ref Dictionary rotations
) where T : RotationCurve, new()
{
RotationCurve rotCurve;
if (!rotations.TryGetValue (uniGO, out rotCurve)) {
rotCurve = new T { SampleRate = frameRate };
rotations.Add (uniGO, rotCurve);
}
return rotCurve;
}
///
/// Export the Animator component on this game object
///
[SecurityPermission(SecurityAction.LinkDemand)]
private void ExportAnimation (GameObject uniRoot, FbxScene fbxScene)
{
if (!uniRoot)
{
return;
}
var exportedClips = new HashSet ();
var uniAnimator = uniRoot.GetComponent ();
if (uniAnimator)
{
// Try the animator controller (mecanim)
var controller = uniAnimator.runtimeAnimatorController;
if (controller)
{
// Only export each clip once per game object.
foreach (var clip in controller.animationClips) {
if (exportedClips.Add (clip)) {
ExportAnimationClip (clip, uniRoot, fbxScene);
}
}
}
}
// Try the playable director
var director = uniRoot.GetComponent ();
if (director)
{
Debug.LogWarning(string.Format("Exporting animation from PlayableDirector on {0} not supported", uniRoot.name));
// TODO: export animationclips from playabledirector
}
// Try the animation (legacy)
var uniAnimation = uniRoot.GetComponent ();
if (uniAnimation)
{
// Only export each clip once per game object.
foreach (var uniAnimObj in uniAnimation) {
AnimationState uniAnimState = uniAnimObj as AnimationState;
if (uniAnimState)
{
AnimationClip uniAnimClip = uniAnimState.clip;
if (exportedClips.Add (uniAnimClip)) {
ExportAnimationClip (uniAnimClip, uniRoot, fbxScene);
}
}
}
}
}
///
/// configures default camera for the scene
///
private void SetDefaultCamera (FbxScene fbxScene)
{
if(fbxScene == null) { return; }
if (string.IsNullOrEmpty(DefaultCamera))
DefaultCamera = Globals.FBXSDK_CAMERA_PERSPECTIVE;
fbxScene.GetGlobalSettings ().SetDefaultCamera (DefaultCamera);
}
///
/// Ensures that the inputted name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. Sphere becomes Sphere_1
///
/// Unique name
/// Name
/// The dictionary to use to map name to # of occurences
private string GetUniqueName(string name, Dictionary nameToCountMap)
{
var uniqueName = name;
int count;
if (nameToCountMap.TryGetValue(name, out count))
{
uniqueName = string.Format(UniqueNameFormat, name, count);
}
else
{
count = 0;
}
nameToCountMap[name] = count + 1;
return uniqueName;
}
///
/// Ensures that the inputted name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. Sphere becomes Sphere_1
///
/// Unique name
/// Name
private string GetUniqueFbxNodeName(string name)
{
return GetUniqueName(name, NameToIndexMap);
}
///
/// Ensures that the inputted material name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. mat becomes mat_1
///
/// Name
/// Unique material name
private string GetUniqueMaterialName(string name)
{
return GetUniqueName(name, MaterialNameToIndexMap);
}
///
/// Ensures that the inputted texture name is unique.
/// If a duplicate name is found, then it is incremented.
/// e.g. tex becomes tex_1
///
/// Name
/// Unique texture name
private string GetUniqueTextureName(string name)
{
return GetUniqueName(name, TextureNameToIndexMap);
}
///
/// Create a fbxNode from unityGo.
///
///
///
/// the created FbxNode
private FbxNode CreateFbxNode(GameObject unityGo, FbxScene fbxScene)
{
string fbxName = unityGo.name;
if (ExportOptions.UseMayaCompatibleNames)
{
fbxName = ConvertToMayaCompatibleName(unityGo.name);
if (ExportOptions.AllowSceneModification)
{
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);
MapUnityObjectToFbxNode[unityGo] = fbxNode;
return fbxNode;
}
///
/// Creates an FbxNode for each GameObject.
///
/// The number of nodes exported.
internal int ExportTransformHierarchy(
GameObject unityGo, FbxScene fbxScene, FbxNode fbxNodeParent,
int exportProgress, int objectCount, Vector3 newCenter,
TransformExportType exportType = TransformExportType.Local,
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();
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 [i];
bool exportedRenderer = false;
foreach (var renderer in lod.renderers) {
// only export if parented under LOD group
if (renderer.transform.parent == unityGo.transform) {
numObjectsExported = ExportTransformHierarchy (renderer.gameObject, fbxScene, fbxNode, numObjectsExported, objectCount, newCenter, lodExportType: lodExportType);
exportedRenderer = true;
} else if(Verbose) {
Debug.LogFormat ("FbxExporter: Not exporting LOD {0}: {1}", i, renderer.name);
}
}
// if at least one renderer for this LOD was exported, then we succeeded
// so stop exporting.
if (exportedRenderer) {
return numObjectsExported;
}
}
}
// now unityGo through our children and recurse
foreach (Transform childT in unityGo.transform) {
numObjectsExported = ExportTransformHierarchy (childT.gameObject, fbxScene, fbxNode, numObjectsExported, objectCount, newCenter, lodExportType: lodExportType);
}
return numObjectsExported;
}
///
/// Exports all animation clips in the hierarchy along with
/// the minimum required GameObject information.
/// i.e. Animated GameObjects, their ancestors, and their transforms are exported,
/// but components are only exported if explicitly animated. Meshes are not exported.
///
/// The number of nodes exported.
[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);
}
}
}
return numObjectsExported;
}
internal class SkinnedMeshBoneInfo {
public SkinnedMeshRenderer skinnedMesh;
public Dictionary boneDict;
public Dictionary boneToBindPose;
public SkinnedMeshBoneInfo(SkinnedMeshRenderer skinnedMesh, Dictionary boneDict){
this.skinnedMesh = skinnedMesh;
this.boneDict = boneDict;
this.boneToBindPose = new Dictionary();
}
}
private bool ExportAnimatedBones (
GameObject unityGo,
FbxScene fbxScene,
ref int exportProgress,
int objectCount,
AnimationOnlyExportData exportData
)
{
var skinnedMeshRenderers = unityGo.GetComponentsInChildren();
foreach (var skinnedMesh in skinnedMeshRenderers)
{
var boneArray = skinnedMesh.bones;
var bones = new HashSet();
var boneDict = new Dictionary();
for (int i = 0; i < boneArray.Length; i++)
{
bones.Add(boneArray[i].gameObject);
boneDict.Add(boneArray[i], i);
}
// get the bones that are also in the export set
bones.IntersectWith(exportData.Objects);
var boneInfo = new SkinnedMeshBoneInfo(skinnedMesh, boneDict);
foreach (var bone in bones)
{
FbxNode fbxNode;
// bone already exported
if (MapUnityObjectToFbxNode.TryGetValue(bone, out fbxNode))
{
continue;
}
fbxNode = CreateFbxNode(bone, fbxScene);
exportProgress++;
if (EditorUtility.DisplayCancelableProgressBar(
ProgressBarTitle,
string.Format("Creating FbxNode {0}/{1}", exportProgress, objectCount),
(exportProgress / (float)objectCount) * 0.5f))
{
// cancel silently
return false;
}
ExportBoneTransform(fbxNode, fbxScene, bone.transform, boneInfo);
}
}
return true;
}
///
/// Exports the Gameobject and its ancestors.
///
/// true, if game object and parents were exported,
/// false if export cancelled.
private bool ExportGameObjectAndParents(
GameObject unityGo,
GameObject rootObject,
FbxScene fbxScene,
out FbxNode fbxNode,
Vector3 newCenter,
TransformExportType exportType,
ref int exportProgress,
int objectCount
)
{
// node doesn't exist so create it
if (!MapUnityObjectToFbxNode.TryGetValue(unityGo, out fbxNode))
{
fbxNode = CreateFbxNode(unityGo, fbxScene);
exportProgress++;
if (EditorUtility.DisplayCancelableProgressBar(
ProgressBarTitle,
string.Format("Creating FbxNode {0}/{1}", exportProgress, objectCount),
(exportProgress / (float)objectCount) * 0.5f))
{
// cancel silently
return false;
}
ExportTransform(unityGo.transform, fbxNode, newCenter, exportType);
}
if (unityGo == rootObject || unityGo.transform.parent == null)
{
fbxScene.GetRootNode().AddChild(fbxNode);
return true;
}
// make sure all the nodes are connected and exported
FbxNode fbxNodeParent;
if (!ExportGameObjectAndParents (
unityGo.transform.parent.gameObject,
rootObject,
fbxScene,
out fbxNodeParent,
newCenter,
TransformExportType.Local,
ref exportProgress,
objectCount
)) {
// export cancelled
return false;
}
fbxNodeParent.AddChild (fbxNode);
return true;
}
///
/// Exports the bone transform.
///
/// true, if bone transform was exported, false otherwise.
/// Fbx node.
/// Fbx scene.
/// Unity bone.
/// Bone info.
private bool ExportBoneTransform(
FbxNode fbxNode, FbxScene fbxScene, Transform unityBone, SkinnedMeshBoneInfo boneInfo
){
if (boneInfo == null || boneInfo.skinnedMesh == null || boneInfo.boneDict == null || unityBone == null) {
return false;
}
var skinnedMesh = boneInfo.skinnedMesh;
var boneDict = boneInfo.boneDict;
var rootBone = skinnedMesh.rootBone;
// setup the skeleton
var fbxSkeleton = fbxNode.GetSkeleton ();
if (fbxSkeleton == null) {
fbxSkeleton = FbxSkeleton.Create (fbxScene, unityBone.name + SkeletonPrefix);
fbxSkeleton.Size.Set (1.0f * UnitScaleFactor);
fbxNode.SetNodeAttribute (fbxSkeleton);
}
var fbxSkeletonType = FbxSkeleton.EType.eLimbNode;
// Only set the rootbone's skeleton type to FbxSkeleton.EType.eRoot
// if it has at least one child that is also a bone.
// Otherwise if it is marked as Root but has no bones underneath,
// Maya will import it as a Null object instead of a bone.
if (rootBone == unityBone && rootBone.childCount > 0)
{
var hasChildBone = false;
foreach (Transform child in unityBone)
{
if (boneDict.ContainsKey(child))
{
hasChildBone = true;
break;
}
}
if (hasChildBone)
{
fbxSkeletonType = FbxSkeleton.EType.eRoot;
}
}
fbxSkeleton.SetSkeletonType (fbxSkeletonType);
var bindPoses = skinnedMesh.sharedMesh.bindposes;
// get bind pose
Matrix4x4 bindPose;
if (!boneInfo.boneToBindPose.TryGetValue (unityBone, out bindPose)) {
bindPose = GetBindPose (unityBone, bindPoses, boneDict, skinnedMesh);
boneInfo.boneToBindPose.Add (unityBone, bindPose);
}
Matrix4x4 pose;
// get parent's bind pose
Matrix4x4 parentBindPose;
if (!boneInfo.boneToBindPose.TryGetValue (unityBone.parent, out parentBindPose)) {
parentBindPose = GetBindPose (unityBone.parent, bindPoses, boneDict, skinnedMesh);
boneInfo.boneToBindPose.Add (unityBone.parent, parentBindPose);
}
pose = parentBindPose * bindPose.inverse;
FbxVector4 translation, rotation, scale;
GetTRSFromMatrix (pose, out translation, out rotation, out scale);
// Export bones with zero rotation, using a pivot instead to set the rotation
// so that the bones are easier to animate and the rotation shows up as the "joint orientation" in Maya.
fbxNode.LclTranslation.Set (new FbxDouble3(-translation.X*UnitScaleFactor, translation.Y*UnitScaleFactor, translation.Z*UnitScaleFactor));
fbxNode.LclRotation.Set (new FbxDouble3(0,0,0));
fbxNode.LclScaling.Set (new FbxDouble3 (scale.X, scale.Y, scale.Z));
// TODO (UNI-34294): add detailed comment about why we export rotation as pre-rotation
fbxNode.SetRotationActive (true);
fbxNode.SetPivotState (FbxNode.EPivotSet.eSourcePivot, FbxNode.EPivotState.ePivotReference);
fbxNode.SetPreRotation (FbxNode.EPivotSet.eSourcePivot, new FbxVector4 (rotation.X, -rotation.Y, -rotation.Z));
return true;
}
private void GetTRSFromMatrix(Matrix4x4 unityMatrix, out FbxVector4 translation, out FbxVector4 rotation, out FbxVector4 scale){
// FBX is transposed relative to Unity: transpose as we convert.
FbxMatrix matrix = new FbxMatrix ();
matrix.SetColumn (0, new FbxVector4 (unityMatrix.GetRow (0).x, unityMatrix.GetRow (0).y, unityMatrix.GetRow (0).z, unityMatrix.GetRow (0).w));
matrix.SetColumn (1, new FbxVector4 (unityMatrix.GetRow (1).x, unityMatrix.GetRow (1).y, unityMatrix.GetRow (1).z, unityMatrix.GetRow (1).w));
matrix.SetColumn (2, new FbxVector4 (unityMatrix.GetRow (2).x, unityMatrix.GetRow (2).y, unityMatrix.GetRow (2).z, unityMatrix.GetRow (2).w));
matrix.SetColumn (3, new FbxVector4 (unityMatrix.GetRow (3).x, unityMatrix.GetRow (3).y, unityMatrix.GetRow (3).z, unityMatrix.GetRow (3).w));
// FBX wants translation, rotation (in euler angles) and scale.
// We assume there's no real shear, just rounding error.
FbxVector4 shear;
double sign;
matrix.GetElements (out translation, out rotation, out shear, out scale, out sign);
}
///
/// Counts how many objects are between this object and the root (exclusive).
///
/// The object to root count.
/// Start object.
/// Root object.
private static int GetObjectToRootDepth(Transform startObject, Transform root){
if (startObject == null) {
return 0;
}
int count = 0;
var parent = startObject.parent;
while (parent != null && parent != root) {
count++;
parent = parent.parent;
}
return count;
}
///
/// Gets the count of animated objects to be exported.
///
/// In addition, collects the minimum set of what needs to be exported for each GameObject hierarchy.
/// This contains all the animated GameObjects, their ancestors, their transforms, as well as any animated
/// components and the animation clips. Also, the first animation to export, if any.
///
/// The animation only hierarchy count.
/// GameObject hierarchies selected for export.
/// Map from GameObject hierarchy to animation export data.
[SecurityPermission(SecurityAction.LinkDemand)]
internal int GetAnimOnlyHierarchyCount(Dictionary hierarchyToExportData)
{
// including any parents of animated objects that are exported
var completeExpSet = new HashSet();
foreach (var data in hierarchyToExportData.Values) {
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 GetExportData(Object[] objects, IExportOptions exportOptions = null)
{
if (exportOptions==null)
exportOptions = DefaultOptions;
Debug.Assert(exportOptions!=null);
Dictionary exportData = new Dictionary();
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 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();
var genericAnim = go.GetComponentsInChildren();
var exportData = new AnimationOnlyExportData();
int depthFromRootAnimation = int.MaxValue;
Animation rootAnimation = null;
foreach (var anim in legacyAnim)
{
int count = GetObjectToRootDepth(anim.transform, go.transform);
if (count < depthFromRootAnimation)
{
depthFromRootAnimation = count;
rootAnimation = anim;
}
var animClips = AnimationUtility.GetAnimationClips(anim.gameObject);
exportData.CollectDependencies(animClips, anim.gameObject, exportOptions);
}
int depthFromRootAnimator = int.MaxValue;
Animator rootAnimator = null;
foreach (var anim in genericAnim)
{
int count = GetObjectToRootDepth(anim.transform, go.transform);
if (count < depthFromRootAnimator)
{
depthFromRootAnimator = count;
rootAnimator = anim;
}
// Try the animator controller (mecanim)
var controller = anim.runtimeAnimatorController;
if (controller)
{
exportData.CollectDependencies(controller.animationClips, anim.gameObject, exportOptions);
}
}
// set the first clip to export
if (depthFromRootAnimation < depthFromRootAnimator)
{
exportData.defaultClip = rootAnimation.clip;
}
else if(rootAnimator)
{
// Try the animator controller (mecanim)
var controller = rootAnimator.runtimeAnimatorController;
if (controller)
{
var dController = controller as UnityEditor.Animations.AnimatorController;
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
{
Debug.LogWarningFormat("Couldn't export motion {0}", motion.name);
}
}
}
}
return exportData;
}
///
/// Export components on this game object.
/// Transform components have already been exported.
/// This function exports the other components and animation.
///
[SecurityPermission(SecurityAction.LinkDemand)]
private bool ExportComponents(FbxScene fbxScene)
{
var animationNodes = new HashSet ();
int numObjectsExported = 0;
int objectCount = MapUnityObjectToFbxNode.Count;
foreach (KeyValuePair entry in MapUnityObjectToFbxNode) {
numObjectsExported++;
if (EditorUtility.DisplayCancelableProgressBar (
ProgressBarTitle,
string.Format ("Exporting Components for GameObject {0}/{1}", numObjectsExported, objectCount),
((numObjectsExported / (float)objectCount) * 0.25f) + 0.25f)) {
// cancel silently
return false;
}
var unityGo = entry.Key;
var fbxNode = entry.Value;
// try export mesh
bool exportedMesh = ExportInstance (unityGo, fbxScene, fbxNode);
if (!exportedMesh) {
exportedMesh = ExportMesh (unityGo, fbxNode);
}
// export camera, but only if no mesh was exported
bool exportedCamera = false;
if (!exportedMesh) {
exportedCamera = ExportCamera (unityGo, fbxScene, fbxNode);
}
// export light, but only if no mesh or camera was exported
if (!exportedMesh && !exportedCamera) {
ExportLight (unityGo, fbxScene, fbxNode);
}
ExportConstraints(unityGo, fbxScene, fbxNode);
}
return true;
}
///
/// Checks if the GameObject has animation.
///
/// true, if object has animation, false otherwise.
/// Go.
private bool GameObjectHasAnimation(GameObject go){
return go != null &&
(go.GetComponent () ||
go.GetComponent () ||
go.GetComponent ());
}
///
/// A count of how many GameObjects we are exporting, to have a rough
/// idea of how long creating the scene will take.
///
/// The hierarchy count.
/// Export set.
internal int GetHierarchyCount (HashSet exportSet)
{
int count = 0;
Queue queue = new Queue (exportSet);
while (queue.Count > 0) {
var obj = queue.Dequeue ();
var objTransform = obj.transform;
foreach (Transform child in objTransform) {
queue.Enqueue (child.gameObject);
}
count++;
}
return count;
}
///
/// Removes objects that will already be exported anyway.
/// E.g. if a parent and its child are both selected, then the child
/// will be removed from the export set.
///
/// The revised export set
/// Unity export set.
[SecurityPermission(SecurityAction.LinkDemand)]
internal static HashSet RemoveRedundantObjects(IEnumerable unityExportSet)
{
// basically just remove the descendents from the unity export set
HashSet toExport = new HashSet ();
HashSet hashedExportSet = new HashSet