Skip to content

Commit d19e64e

Browse files
authored
Merge pull request #305 from Unity-Technologies/UNI-37547-fix-rotation-order-on-anim
Uni 37547 fix rotation order on euler animation
2 parents bd99351 + a0a191b commit d19e64e

19 files changed

+4083
-242
lines changed

Assets/FbxExporters/Editor/FbxExporter.cs

Lines changed: 58 additions & 214 deletions
Large diffs are not rendered by default.
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
using Unity.FbxSdk;
2+
using UnityEngine;
3+
using System.Collections.Generic;
4+
5+
namespace FbxExporters
6+
{
7+
namespace Editor
8+
{
9+
/// <summary>
10+
/// Base class for QuaternionCurve and EulerCurve.
11+
/// Provides implementation for computing keys and generating FbxAnimCurves
12+
/// for euler rotation.
13+
/// </summary>
14+
public abstract class RotationCurve {
15+
public double sampleRate;
16+
public AnimationCurve[] m_curves;
17+
18+
public struct Key {
19+
public FbxTime time;
20+
public FbxVector4 euler;
21+
}
22+
23+
public RotationCurve() { }
24+
25+
public void SetCurve(int i, AnimationCurve curve) {
26+
m_curves [i] = curve;
27+
}
28+
29+
protected abstract FbxQuaternion GetConvertedQuaternionRotation (float seconds, UnityEngine.Quaternion restRotation);
30+
31+
private Key [] ComputeKeys(UnityEngine.Quaternion restRotation, FbxNode node) {
32+
// Get the source pivot pre-rotation if any, so we can
33+
// remove it from the animation we get from Unity.
34+
var fbxPreRotationEuler = node.GetRotationActive()
35+
? node.GetPreRotation(FbxNode.EPivotSet.eSourcePivot)
36+
: new FbxVector4();
37+
38+
// Get the inverse of the prerotation
39+
var fbxPreRotationInverse = ModelExporter.EulerToQuaternion (fbxPreRotationEuler);
40+
fbxPreRotationInverse.Inverse();
41+
42+
// Find when we have keys set.
43+
var keyTimes =
44+
(FbxExporters.Editor.ModelExporter.ExportSettings.BakeAnimation)
45+
? ModelExporter.GetSampleTimes(m_curves, sampleRate)
46+
: ModelExporter.GetKeyTimes(m_curves);
47+
48+
// Convert to the Key type.
49+
var keys = new Key[keyTimes.Count];
50+
int i = 0;
51+
foreach(var seconds in keyTimes) {
52+
var fbxFinalAnimation = GetConvertedQuaternionRotation (seconds, restRotation);
53+
54+
// Cancel out the pre-rotation. Order matters. FBX reads left-to-right.
55+
// When we run animation we will apply:
56+
// pre-rotation
57+
// then pre-rotation inverse
58+
// then animation.
59+
var fbxFinalQuat = fbxPreRotationInverse * fbxFinalAnimation;
60+
61+
// Store the key so we can sort them later.
62+
Key key;
63+
key.time = FbxTime.FromSecondDouble(seconds);
64+
key.euler = ModelExporter.QuaternionToEuler (fbxFinalQuat);
65+
keys[i++] = key;
66+
}
67+
68+
// Sort the keys by time
69+
System.Array.Sort(keys, (Key a, Key b) => a.time.CompareTo(b.time));
70+
71+
return keys;
72+
}
73+
74+
public void Animate(Transform unityTransform, FbxNode fbxNode, FbxAnimLayer fbxAnimLayer, bool Verbose) {
75+
76+
/* Find or create the three curves. */
77+
var fbxAnimCurveX = fbxNode.LclRotation.GetCurve(fbxAnimLayer, Globals.FBXSDK_CURVENODE_COMPONENT_X, true);
78+
var fbxAnimCurveY = fbxNode.LclRotation.GetCurve(fbxAnimLayer, Globals.FBXSDK_CURVENODE_COMPONENT_Y, true);
79+
var fbxAnimCurveZ = fbxNode.LclRotation.GetCurve(fbxAnimLayer, Globals.FBXSDK_CURVENODE_COMPONENT_Z, true);
80+
81+
/* set the keys */
82+
using (new FbxAnimCurveModifyHelper(new List<FbxAnimCurve>{fbxAnimCurveX,fbxAnimCurveY,fbxAnimCurveZ}))
83+
{
84+
foreach (var key in ComputeKeys(unityTransform.localRotation, fbxNode)) {
85+
86+
int i = fbxAnimCurveX.KeyAdd(key.time);
87+
fbxAnimCurveX.KeySet(i, key.time, (float)key.euler.X);
88+
89+
i = fbxAnimCurveY.KeyAdd(key.time);
90+
fbxAnimCurveY.KeySet(i, key.time, (float)key.euler.Y);
91+
92+
i = fbxAnimCurveZ.KeyAdd(key.time);
93+
fbxAnimCurveZ.KeySet(i, key.time, (float)key.euler.Z);
94+
}
95+
}
96+
97+
// Uni-35616 unroll curves to preserve continuous rotations
98+
var fbxCurveNode = fbxNode.LclRotation.GetCurveNode(fbxAnimLayer, false /*should already exist*/);
99+
100+
FbxAnimCurveFilterUnroll fbxAnimUnrollFilter = new FbxAnimCurveFilterUnroll();
101+
fbxAnimUnrollFilter.Apply(fbxCurveNode);
102+
103+
if (Verbose) {
104+
Debug.Log("Exported rotation animation for " + fbxNode.GetName());
105+
}
106+
}
107+
}
108+
109+
/// <summary>
110+
/// Convert from ZXY to XYZ euler, and remove
111+
/// prerotation from animated rotation.
112+
/// </summary>
113+
public class EulerCurve : RotationCurve {
114+
public EulerCurve() { m_curves = new AnimationCurve[3]; }
115+
116+
/// <summary>
117+
/// Gets the index of the euler curve by property name.
118+
/// x = 0, y = 1, z = 2
119+
/// </summary>
120+
/// <returns>The index of the curve, or -1 if property doesn't map to Euler curve.</returns>
121+
/// <param name="uniPropertyName">Unity property name.</param>
122+
public static int GetEulerIndex(string uniPropertyName) {
123+
System.StringComparison ct = System.StringComparison.CurrentCulture;
124+
bool isEulerComponent = uniPropertyName.StartsWith ("localEulerAnglesRaw.", ct);
125+
126+
if (!isEulerComponent) { return -1; }
127+
128+
switch(uniPropertyName[uniPropertyName.Length - 1]) {
129+
case 'x': return 0;
130+
case 'y': return 1;
131+
case 'z': return 2;
132+
default: return -1;
133+
}
134+
}
135+
136+
protected override FbxQuaternion GetConvertedQuaternionRotation (float seconds, Quaternion restRotation)
137+
{
138+
var eulerRest = restRotation.eulerAngles;
139+
AnimationCurve x = m_curves [0], y = m_curves [1], z = m_curves [2];
140+
141+
// The final animation, including the effect of pre-rotation.
142+
// If we have no curve, assume the node has the correct rotation right now.
143+
// We need to evaluate since we might only have keys in one of the axes.
144+
var unityFinalAnimation = Quaternion.Euler (
145+
(x == null) ? eulerRest [0] : x.Evaluate (seconds),
146+
(y == null) ? eulerRest [1] : y.Evaluate (seconds),
147+
(z == null) ? eulerRest [2] : z.Evaluate (seconds)
148+
);
149+
150+
// convert the final animation to righthanded coords
151+
var finalEuler = ModelExporter.ConvertQuaternionToXYZEuler(unityFinalAnimation);
152+
153+
return ModelExporter.EulerToQuaternion (new FbxVector4(finalEuler));
154+
}
155+
}
156+
157+
/// <summary>
158+
/// Exporting rotations is more complicated. We need to convert
159+
/// from quaternion to euler. We use this class to help.
160+
/// </summary>
161+
public class QuaternionCurve : RotationCurve {
162+
163+
public QuaternionCurve() { m_curves = new AnimationCurve[4]; }
164+
165+
/// <summary>
166+
/// Gets the index of the curve by property name.
167+
/// x = 0, y = 1, z = 2, w = 3
168+
/// </summary>
169+
/// <returns>The index of the curve, or -1 if property doesn't map to Quaternion curve.</returns>
170+
/// <param name="uniPropertyName">Unity property name.</param>
171+
public static int GetQuaternionIndex(string uniPropertyName) {
172+
System.StringComparison ct = System.StringComparison.CurrentCulture;
173+
bool isQuaternionComponent = false;
174+
175+
isQuaternionComponent |= uniPropertyName.StartsWith ("m_LocalRotation.", ct);
176+
isQuaternionComponent |= uniPropertyName.EndsWith ("Q.x", ct);
177+
isQuaternionComponent |= uniPropertyName.EndsWith ("Q.y", ct);
178+
isQuaternionComponent |= uniPropertyName.EndsWith ("Q.z", ct);
179+
isQuaternionComponent |= uniPropertyName.EndsWith ("Q.w", ct);
180+
181+
if (!isQuaternionComponent) { return -1; }
182+
183+
switch(uniPropertyName[uniPropertyName.Length - 1]) {
184+
case 'x': return 0;
185+
case 'y': return 1;
186+
case 'z': return 2;
187+
case 'w': return 3;
188+
default: return -1;
189+
}
190+
}
191+
192+
protected override FbxQuaternion GetConvertedQuaternionRotation (float seconds, Quaternion restRotation)
193+
{
194+
AnimationCurve x = m_curves [0], y = m_curves [1], z = m_curves [2], w = m_curves[3];
195+
196+
// The final animation, including the effect of pre-rotation.
197+
// If we have no curve, assume the node has the correct rotation right now.
198+
// We need to evaluate since we might only have keys in one of the axes.
199+
var fbxFinalAnimation = new FbxQuaternion(
200+
(x == null) ? restRotation[0] : x.Evaluate(seconds),
201+
(y == null) ? restRotation[1] : y.Evaluate(seconds),
202+
(z == null) ? restRotation[2] : z.Evaluate(seconds),
203+
(w == null) ? restRotation[3] : w.Evaluate(seconds));
204+
205+
// convert the final animation to righthanded coords
206+
var finalEuler = ModelExporter.ConvertQuaternionToXYZEuler(fbxFinalAnimation);
207+
208+
return ModelExporter.EulerToQuaternion (finalEuler);
209+
}
210+
}
211+
212+
/// <summary>
213+
/// Exporting rotations is more complicated. We need to convert
214+
/// from quaternion to euler. We use this class to help.
215+
/// </summary>
216+
public class FbxAnimCurveModifyHelper : System.IDisposable
217+
{
218+
public List<FbxAnimCurve> Curves { get ; private set; }
219+
220+
public FbxAnimCurveModifyHelper(List<FbxAnimCurve> list)
221+
{
222+
Curves = list;
223+
224+
foreach (var curve in Curves)
225+
curve.KeyModifyBegin();
226+
}
227+
228+
~FbxAnimCurveModifyHelper() {
229+
Dispose();
230+
}
231+
232+
public void Dispose()
233+
{
234+
foreach (var curve in Curves)
235+
curve.KeyModifyEnd();
236+
}
237+
}
238+
}
239+
}

Assets/FbxExporters/Editor/FbxRotationCurve.cs.meta

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/FbxExporters/Editor/UnitTests/ExporterTestBase.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,33 @@ protected virtual string ExportSelectedObjects(string filename, params Object[]
240240
return fbxFileName;
241241
}
242242

243+
/// <summary>
244+
/// Exports a single hierarchy to a random fbx file.
245+
/// </summary>
246+
/// <returns>The exported fbx file path.</returns>
247+
/// <param name="hierarchy">Hierarchy.</param>
248+
/// <param name="animOnly">If set to <c>true</c> export animation only.</param>
249+
protected string ExportToFbx (GameObject hierarchy, bool animOnly = false){
250+
string filename = GetRandomFbxFilePath ();
251+
var exportedFilePath = FbxExporters.Editor.ModelExporter.ExportObject (filename, hierarchy, animOnly);
252+
Assert.That (exportedFilePath, Is.EqualTo (filename));
253+
return filename;
254+
}
255+
256+
/// <summary>
257+
/// Adds the asset at asset path to the scene.
258+
/// </summary>
259+
/// <returns>The new GameObject in the scene.</returns>
260+
/// <param name="assetPath">Asset path.</param>
261+
protected GameObject AddAssetToScene(string assetPath){
262+
GameObject originalObj = AssetDatabase.LoadMainAssetAtPath ("Assets/" + assetPath) as GameObject;
263+
Assert.IsNotNull (originalObj);
264+
GameObject originalGO = GameObject.Instantiate (originalObj);
265+
Assert.IsTrue (originalGO);
266+
267+
return originalGO;
268+
}
269+
243270
/// <summary>
244271
/// Compares two hierarchies, asserts that they match precisely.
245272
/// The root can be allowed to mismatch. That's normal with

0 commit comments

Comments
 (0)