using Autodesk.Fbx; using UnityEngine; using System.Collections.Generic; namespace UnityEditor.Formats.Fbx.Exporter { /// /// Base class for QuaternionCurve and EulerCurve. /// Provides implementation for computing keys and generating FbxAnimCurves /// for euler rotation. /// internal abstract class RotationCurve { private double m_sampleRate; public double SampleRate { get { return m_sampleRate; } set { m_sampleRate = value; } } private AnimationCurve[] m_curves; public AnimationCurve[] GetCurves() { return m_curves; } public void SetCurves(AnimationCurve[] value) { m_curves = value; } protected struct Key { private FbxTime m_time; public FbxTime time { get { return m_time; } set { m_time = value; } } private FbxVector4 m_euler; public FbxVector4 euler { get { return m_euler; } set { m_euler = value; } } } protected RotationCurve() {} public void SetCurve(int i, AnimationCurve curve) { GetCurves()[i] = curve; } protected abstract FbxQuaternion GetConvertedQuaternionRotation(float seconds, UnityEngine.Quaternion restRotation); private Key[] ComputeKeys(UnityEngine.Quaternion restRotation, FbxNode node) { // Get the source pivot pre-rotation if any, so we can // remove it from the animation we get from Unity. var fbxPreRotationEuler = node.GetRotationActive() ? node.GetPreRotation(FbxNode.EPivotSet.eSourcePivot) : new FbxVector4(); // Get the inverse of the prerotation var fbxPreRotationInverse = ModelExporter.EulerToQuaternionXYZ(fbxPreRotationEuler); fbxPreRotationInverse.Inverse(); // Find when we have keys set. var keyTimes = ModelExporter.GetSampleTimes(GetCurves(), SampleRate); // Convert to the Key type. var keys = new Key[keyTimes.Count]; int i = 0; foreach (var seconds in keyTimes) { var fbxFinalAnimation = GetConvertedQuaternionRotation(seconds, restRotation); // Cancel out the pre-rotation. Order matters. FBX reads left-to-right. // When we run animation we will apply: // pre-rotation // then pre-rotation inverse // then animation. var fbxFinalQuat = fbxPreRotationInverse * fbxFinalAnimation; var finalUnityQuat = new Quaternion((float)fbxFinalQuat.X, (float)fbxFinalQuat.Y, (float)fbxFinalQuat.Z, (float)fbxFinalQuat.W); // Store the key so we can sort them later. Key key = new Key(); key.time = FbxTime.FromSecondDouble(seconds); key.euler = ModelExporter.ConvertToFbxVector4(finalUnityQuat.eulerAngles); keys[i++] = key; } // Sort the keys by time System.Array.Sort(keys, (Key a, Key b) => a.time.CompareTo(b.time)); return keys; } public void Animate(Transform unityTransform, FbxNode fbxNode, FbxAnimLayer fbxAnimLayer, bool Verbose) { if (!unityTransform || fbxNode == null) { return; } /* Find or create the three curves. */ var fbxAnimCurveX = fbxNode.LclRotation.GetCurve(fbxAnimLayer, Globals.FBXSDK_CURVENODE_COMPONENT_X, true); var fbxAnimCurveY = fbxNode.LclRotation.GetCurve(fbxAnimLayer, Globals.FBXSDK_CURVENODE_COMPONENT_Y, true); var fbxAnimCurveZ = fbxNode.LclRotation.GetCurve(fbxAnimLayer, Globals.FBXSDK_CURVENODE_COMPONENT_Z, true); /* set the keys */ using (new FbxAnimCurveModifyHelper(new List {fbxAnimCurveX, fbxAnimCurveY, fbxAnimCurveZ})) { foreach (var key in ComputeKeys(unityTransform.localRotation, fbxNode)) { int i = fbxAnimCurveX.KeyAdd(key.time); fbxAnimCurveX.KeySet(i, key.time, (float)key.euler.X); i = fbxAnimCurveY.KeyAdd(key.time); fbxAnimCurveY.KeySet(i, key.time, (float)key.euler.Y); i = fbxAnimCurveZ.KeyAdd(key.time); fbxAnimCurveZ.KeySet(i, key.time, (float)key.euler.Z); } } if (Verbose) { Debug.Log("Exported rotation animation for " + fbxNode.GetName()); } } } /// /// Convert from ZXY to XYZ euler, and remove /// prerotation from animated rotation. /// internal class EulerCurve : RotationCurve { public EulerCurve() { SetCurves(new AnimationCurve[3]); } /// /// Gets the index of the euler curve by property name. /// x = 0, y = 1, z = 2 /// /// The index of the curve, or -1 if property doesn't map to Euler curve. /// Unity property name. public static int GetEulerIndex(string uniPropertyName) { if (string.IsNullOrEmpty(uniPropertyName)) { return -1; } System.StringComparison ct = System.StringComparison.CurrentCulture; bool isEulerComponent = uniPropertyName.StartsWith("localEulerAnglesRaw.", ct); if (!isEulerComponent) { return -1; } switch (uniPropertyName[uniPropertyName.Length - 1]) { case 'x': return 0; case 'y': return 1; case 'z': return 2; default: return -1; } } protected override FbxQuaternion GetConvertedQuaternionRotation(float seconds, Quaternion restRotation) { var eulerRest = restRotation.eulerAngles; AnimationCurve x = GetCurves()[0], y = GetCurves()[1], z = GetCurves()[2]; // The final animation, including the effect of pre-rotation. // If we have no curve, assume the node has the correct rotation right now. // We need to evaluate since we might only have keys in one of the axes. var unityFinalAnimation = new Vector3( (x == null) ? eulerRest[0] : x.Evaluate(seconds), (y == null) ? eulerRest[1] : y.Evaluate(seconds), (z == null) ? eulerRest[2] : z.Evaluate(seconds) ); // convert the final animation to righthanded coords return ModelExporter.EulerToQuaternionZXY(unityFinalAnimation); } } /// /// Exporting rotations is more complicated. We need to convert /// from quaternion to euler. We use this class to help. /// internal class QuaternionCurve : RotationCurve { public QuaternionCurve() { SetCurves(new AnimationCurve[4]); } /// /// Gets the index of the curve by property name. /// x = 0, y = 1, z = 2, w = 3 /// /// The index of the curve, or -1 if property doesn't map to Quaternion curve. /// Unity property name. public static int GetQuaternionIndex(string uniPropertyName) { if (string.IsNullOrEmpty(uniPropertyName)) { return -1; } System.StringComparison ct = System.StringComparison.CurrentCulture; bool isQuaternionComponent = false; isQuaternionComponent |= uniPropertyName.StartsWith("m_LocalRotation.", ct); isQuaternionComponent |= uniPropertyName.EndsWith("Q.x", ct); isQuaternionComponent |= uniPropertyName.EndsWith("Q.y", ct); isQuaternionComponent |= uniPropertyName.EndsWith("Q.z", ct); isQuaternionComponent |= uniPropertyName.EndsWith("Q.w", ct); if (!isQuaternionComponent) { return -1; } switch (uniPropertyName[uniPropertyName.Length - 1]) { case 'x': return 0; case 'y': return 1; case 'z': return 2; case 'w': return 3; default: return -1; } } protected override FbxQuaternion GetConvertedQuaternionRotation(float seconds, Quaternion restRotation) { AnimationCurve x = GetCurves()[0], y = GetCurves()[1], z = GetCurves()[2], w = GetCurves()[3]; // The final animation, including the effect of pre-rotation. // If we have no curve, assume the node has the correct rotation right now. // We need to evaluate since we might only have keys in one of the axes. var fbxFinalAnimation = new FbxQuaternion( (x == null) ? restRotation[0] : x.Evaluate(seconds), (y == null) ? restRotation[1] : y.Evaluate(seconds), (z == null) ? restRotation[2] : z.Evaluate(seconds), (w == null) ? restRotation[3] : w.Evaluate(seconds)); return fbxFinalAnimation; } } /// /// Exporting rotations is more complicated. We need to convert /// from quaternion to euler. We use this class to help. /// internal class FbxAnimCurveModifyHelper : System.IDisposable { public List Curves { get; private set; } public FbxAnimCurveModifyHelper(List list) { Curves = list; foreach (var curve in Curves) curve.KeyModifyBegin(); } ~FbxAnimCurveModifyHelper() { Dispose(false); } public void Dispose() { Dispose(true); System.GC.SuppressFinalize(this); } protected virtual void Dispose(bool cleanUpManaged) { foreach (var curve in Curves) curve.KeyModifyEnd(); } } }