Skip to content

Commit 63e3e13

Browse files
committed
Merge branch 'master' into Uni-36986-ExportRecordedAnimToFBX
2 parents b902fa1 + f2aec78 commit 63e3e13

38 files changed

+4491
-380
lines changed

Assets/FbxExporters/Editor/FbxExportSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ public class ExportSettings : ScriptableSingleton<ExportSettings>
289289
public const string kMayaOptionName = "Maya ";
290290
public const string kMayaLtOptionName = "Maya LT";
291291

292+
public bool Verbose = false;
293+
292294
private static string DefaultIntegrationSavePath {
293295
get{
294296
return Path.GetDirectoryName(Application.dataPath);

Assets/FbxExporters/Editor/FbxExporter.cs

Lines changed: 59 additions & 215 deletions
Large diffs are not rendered by default.

Assets/FbxExporters/Editor/FbxPrefabAutoUpdater.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using UnityEditor;
44
using System.Linq;
55
using System;
6+
using FbxExporters.Editor;
67

78
namespace FbxExporters
89
{
@@ -231,8 +232,21 @@ public static void UpdateLinkedPrefab(GameObject prefabInstance)
231232

232233
foreach (var fbxPrefabComponent in prefab.GetComponentsInChildren<FbxPrefab>())
233234
{
235+
// Launch the manual update UI to allow the user to fix
236+
// renamed nodes (or auto-update if there's nothing to rename).
234237
var fbxPrefabUtility = new FbxPrefabUtility(fbxPrefabComponent);
235-
fbxPrefabUtility.SyncPrefab();
238+
239+
if (FbxExporters.EditorTools.ExportSettings.instance.autoUpdaterEnabled || runningUnitTest)
240+
{
241+
fbxPrefabUtility.SyncPrefab();
242+
}
243+
else
244+
{
245+
ManualUpdateEditorWindow window = (ManualUpdateEditorWindow)EditorWindow.GetWindow(typeof(ManualUpdateEditorWindow));
246+
window.Init(fbxPrefabUtility, fbxPrefabComponent);
247+
window.Show();
248+
}
249+
236250
}
237251
}
238252

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/ManualUpdateEditorWindow.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ public void Init(FbxPrefabAutoUpdater.FbxPrefabUtility fbxPrefabUtility, FbxPref
3333
m_nodeNameToSuggest.AddRange(m_nodesToCreate);
3434
m_nodeNameToSuggest.AddRange(m_nodesToRename);
3535

36-
// Add extra 1 for the [Delete] option
37-
selectedNodesToDestroy = new int[m_nodeNameToSuggest.Count + 1];
38-
selectedNodesToRename = new int[m_nodeNameToSuggest.Count + 1];
36+
// Keep track of the selected combo option in each type
37+
selectedNodesToDestroy = new int[m_nodesToDestroy.Count];
38+
selectedNodesToRename = new int[m_nodesToRename.Count];
3939

4040
// Default option for nodes to rename. Shows the current name mapping
4141
for (int i = 0; i < m_nodesToRename.Count; i++)

Assets/FbxExporters/Editor/UnitTests/DefaultSelectionTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ public class DefaultSelectionTest : ExporterTestBase
1919
protected bool m_centerObjectsSetting;
2020

2121
[SetUp]
22-
public void Init ()
22+
public override void Init ()
2323
{
24+
base.Init();
2425
m_centerObjectsSetting = FbxExporters.EditorTools.ExportSettings.instance.centerObjects;
2526
}
2627

Assets/FbxExporters/Editor/UnitTests/ExportPerformanceTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ public class ExportPerformanceTest : ExporterTestBase
1818
private GameObject m_toExport;
1919

2020
[SetUp]
21-
public void Init()
21+
public override void Init()
2222
{
23+
base.Init();
2324
m_stopwatch = new Stopwatch ();
2425
m_toExport = CreateGameObjectToExport ();
2526
}

Assets/FbxExporters/Editor/UnitTests/ExporterTestBase.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace FbxExporters.UnitTests
99
{
1010
public abstract class ExporterTestBase
1111
{
12+
bool isAutoUpdaterOn;
1213
/// <summary>
1314
/// Sleep an amount of time (in ms) so we can safely assume that the
1415
/// timestamp on an fbx will change.
@@ -200,8 +201,19 @@ public virtual void Term ()
200201
// Delete the directory on the next editor update. Otherwise,
201202
// prefabs don't get deleted and the directory delete fails.
202203
EditorApplication.update += DeleteOnNextUpdate;
204+
205+
// Put back the initial setting for the auto-updater toggle
206+
FbxExporters.EditorTools.ExportSettings.instance.autoUpdaterEnabled = isAutoUpdaterOn;
207+
}
208+
209+
[SetUp]
210+
public virtual void Init()
211+
{
212+
isAutoUpdaterOn = FbxExporters.EditorTools.ExportSettings.instance.autoUpdaterEnabled;
213+
FbxExporters.EditorTools.ExportSettings.instance.autoUpdaterEnabled = true;
203214
}
204215

216+
205217
/// <summary>
206218
/// Exports the Objects in selected.
207219
/// </summary>
@@ -240,6 +252,33 @@ protected virtual string ExportSelectedObjects(string filename, params Object[]
240252
return fbxFileName;
241253
}
242254

255+
/// <summary>
256+
/// Exports a single hierarchy to a random fbx file.
257+
/// </summary>
258+
/// <returns>The exported fbx file path.</returns>
259+
/// <param name="hierarchy">Hierarchy.</param>
260+
/// <param name="animOnly">If set to <c>true</c> export animation only.</param>
261+
protected string ExportToFbx (GameObject hierarchy, bool animOnly = false){
262+
string filename = GetRandomFbxFilePath ();
263+
var exportedFilePath = FbxExporters.Editor.ModelExporter.ExportObject (filename, hierarchy, animOnly);
264+
Assert.That (exportedFilePath, Is.EqualTo (filename));
265+
return filename;
266+
}
267+
268+
/// <summary>
269+
/// Adds the asset at asset path to the scene.
270+
/// </summary>
271+
/// <returns>The new GameObject in the scene.</returns>
272+
/// <param name="assetPath">Asset path.</param>
273+
protected GameObject AddAssetToScene(string assetPath){
274+
GameObject originalObj = AssetDatabase.LoadMainAssetAtPath ("Assets/" + assetPath) as GameObject;
275+
Assert.IsNotNull (originalObj);
276+
GameObject originalGO = GameObject.Instantiate (originalObj);
277+
Assert.IsTrue (originalGO);
278+
279+
return originalGO;
280+
}
281+
243282
/// <summary>
244283
/// Compares two hierarchies, asserts that they match precisely.
245284
/// The root can be allowed to mismatch. That's normal with

0 commit comments

Comments
 (0)