Skip to content

Commit eceb57c

Browse files
author
Benoit Hudson
committed
Added callbacks to FbxExporter so we can intercept the export.
We can do it roughly (callback on every object) or finely (callback on particular component types). ProBuilder would likely want to hook into the appropriate component. Added unit tests to make sure it works. Seems to...
1 parent fba9544 commit eceb57c

File tree

3 files changed

+343
-70
lines changed

3 files changed

+343
-70
lines changed

Assets/FbxExporters/Editor/FbxExporter.cs

Lines changed: 181 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,24 +1353,192 @@ private static GameObject GetGameObject (Object obj)
13531353
}
13541354

13551355
/// <summary>
1356-
/// Get a mesh renderer's mesh.
1356+
/// If your MonoBehaviour knows about some custom geometry that
1357+
/// isn't in a MeshFilter or SkinnedMeshRenderer, use
1358+
/// RegisterMeshCallback to get a callback when the exporter tries
1359+
/// to export your component.
1360+
///
1361+
/// The callback should return true, and output the mesh you want.
1362+
///
1363+
/// Return false if you don't want to drive this game object.
1364+
///
1365+
/// Return true and output a null mesh if you don't want the
1366+
/// exporter to output anything.
13571367
/// </summary>
1358-
private MeshInfo GetMeshInfo (GameObject gameObject, bool requireRenderer = true)
1368+
public delegate bool GetMeshForComponent<T>(T component, out Mesh mesh) where T : MonoBehaviour;
1369+
public delegate bool GetMeshForComponent(MonoBehaviour component, out Mesh mesh);
1370+
1371+
/// <summary>
1372+
/// Map from type (must be a MonoBehaviour) to callback.
1373+
/// The type safety is lost; the caller must ensure it at run-time.
1374+
/// </summary>
1375+
static Dictionary<System.Type, GetMeshForComponent> MeshForComponentCallbacks
1376+
= new Dictionary<System.Type, GetMeshForComponent>();
1377+
1378+
/// <summary>
1379+
/// Register a callback to invoke if the object has a component of type T.
1380+
///
1381+
/// This function is prefered over the other mesh callback
1382+
/// registration methods because it's type-safe, efficient, and
1383+
/// invocation order between types can be controlled in the UI by
1384+
/// reordering the components.
1385+
///
1386+
/// It's an error to register a callback for a component that
1387+
/// already has one, unless 'replace' is set to true.
1388+
/// </summary>
1389+
public static void RegisterMeshCallback<T>(GetMeshForComponent<T> callback, bool replace = false)
1390+
where T: UnityEngine.MonoBehaviour
13591391
{
1360-
// Two possibilities: it's a skinned mesh, or we have a mesh filter.
1361-
Mesh mesh;
1362-
var meshFilter = gameObject.GetComponent<MeshFilter> ();
1363-
if (meshFilter) {
1364-
mesh = meshFilter.sharedMesh;
1365-
} else {
1366-
var renderer = gameObject.GetComponent<SkinnedMeshRenderer> ();
1367-
if (!renderer) {
1368-
mesh = null;
1392+
// Under the hood we lose type safety, but don't let the user notice!
1393+
RegisterMeshCallback(typeof(T),
1394+
(MonoBehaviour component, out Mesh mesh) => callback((T)component, out mesh),
1395+
replace);
1396+
}
1397+
1398+
/// <summary>
1399+
/// Register a callback to invoke if the object has a component of type T.
1400+
///
1401+
/// The callback will be invoked with an argument of type T, it's
1402+
/// safe to downcast.
1403+
///
1404+
/// Normally you'll want to use the generic form, but this one is
1405+
/// easier to use with reflection.
1406+
/// </summary>
1407+
public static void RegisterMeshCallback(System.Type t,
1408+
GetMeshForComponent callback,
1409+
bool replace = false)
1410+
{
1411+
if (!t.IsSubclassOf(typeof(MonoBehaviour))) {
1412+
throw new System.Exception("Registering a callback for a type that isn't derived from MonoBehaviour: " + t);
1413+
}
1414+
if (!replace && MeshForComponentCallbacks.ContainsKey(t)) {
1415+
throw new System.Exception("Replacing a callback for type " + t);
1416+
}
1417+
MeshForComponentCallbacks[t] = callback;
1418+
}
1419+
1420+
/// <summary>
1421+
/// Delegate used to convert a GameObject into a mesh.
1422+
///
1423+
/// This is useful if you want to have broader control over
1424+
/// the export process than the GetMeshForComponent callbacks
1425+
/// provide. But it's less efficient because you'll get a callback
1426+
/// on every single GameObject.
1427+
/// </summary>
1428+
public delegate bool GetMeshForObject(GameObject gameObject, out Mesh mesh);
1429+
1430+
static List<GetMeshForObject> MeshForObjectCallbacks = new List<GetMeshForObject>();
1431+
1432+
/// <summary>
1433+
/// Register a callback to invoke on every GameObject we export.
1434+
///
1435+
/// Avoid doing this if you can use a callback that depends on type.
1436+
///
1437+
/// The GameObject-based callbacks are checked before the
1438+
/// component-based ones.
1439+
///
1440+
/// Multiple GameObject-based callbacks can be registered; they are
1441+
/// checked in order of registration.
1442+
/// </summary>
1443+
public static void RegisterMeshObjectCallback(GetMeshForObject callback)
1444+
{
1445+
MeshForObjectCallbacks.Add(callback);
1446+
}
1447+
1448+
/// <summary>
1449+
/// Forget the callback linked to a component of type T.
1450+
/// </summary>
1451+
public static void UnRegisterMeshCallback<T>()
1452+
{
1453+
MeshForComponentCallbacks.Remove(typeof(T));
1454+
}
1455+
1456+
/// <summary>
1457+
/// Forget the callback linked to a component of type T.
1458+
/// </summary>
1459+
public static void UnRegisterMeshCallback(System.Type t)
1460+
{
1461+
MeshForComponentCallbacks.Remove(t);
1462+
}
1463+
1464+
/// <summary>
1465+
/// Forget a GameObject-based callback.
1466+
/// </summary>
1467+
public static void UnRegisterMeshCallback(GetMeshForObject callback)
1468+
{
1469+
MeshForObjectCallbacks.Remove(callback);
1470+
}
1471+
1472+
/// <summary>
1473+
/// Get the mesh we want to use for a GameObject.
1474+
/// May be null.
1475+
/// </summary>
1476+
Mesh ChooseMeshForObject(GameObject gameObject)
1477+
{
1478+
// First allow the object-based callbacks to have a hack at it.
1479+
foreach(var callback in MeshForObjectCallbacks) {
1480+
Mesh mesh;
1481+
if (callback(gameObject, out mesh)) {
1482+
return mesh;
1483+
}
1484+
}
1485+
1486+
// Next iterate over components and allow the component-based
1487+
// callbacks to have a hack at it. This is complicated by the
1488+
// potential of subclassing.
1489+
Mesh defaultMesh = null;
1490+
foreach(var component in gameObject.GetComponents<Component>()) {
1491+
if (!component) {
1492+
continue;
1493+
}
1494+
var monoBehaviour = component as MonoBehaviour;
1495+
if (!monoBehaviour) {
1496+
// Check for default handling. But don't commit yet.
1497+
if (defaultMesh) {
1498+
continue;
1499+
}
1500+
var meshFilter = component as MeshFilter;
1501+
if (meshFilter) {
1502+
defaultMesh = (component as MeshFilter).sharedMesh;
1503+
continue;
1504+
}
1505+
var smr = component as SkinnedMeshRenderer;
1506+
if (smr) {
1507+
defaultMesh = new Mesh();
1508+
smr.BakeMesh(defaultMesh);
1509+
continue;
1510+
}
13691511
} else {
1370-
mesh = new Mesh ();
1371-
renderer.BakeMesh (mesh);
1512+
// Check if we have custom behaviour for this component type, or
1513+
// one of its base classes.
1514+
if (!monoBehaviour.enabled) {
1515+
continue;
1516+
}
1517+
var componentType = monoBehaviour.GetType ();
1518+
do {
1519+
GetMeshForComponent callback;
1520+
if (MeshForComponentCallbacks.TryGetValue (componentType, out callback)) {
1521+
Mesh mesh;
1522+
if (callback (monoBehaviour, out mesh)) {
1523+
return mesh;
1524+
}
1525+
}
1526+
componentType = componentType.BaseType;
1527+
} while(componentType.IsSubclassOf (typeof(MonoBehaviour)));
13721528
}
13731529
}
1530+
1531+
// If we're here, custom handling didn't work.
1532+
// Revert to default handling.
1533+
return defaultMesh;
1534+
}
1535+
1536+
/// <summary>
1537+
/// Get the mesh for an object in an easy-to-use format.
1538+
/// </summary>
1539+
private MeshInfo GetMeshInfo (GameObject gameObject)
1540+
{
1541+
var mesh = ChooseMeshForObject(gameObject);
13741542
if (!mesh) {
13751543
return new MeshInfo(null);
13761544
}

Assets/FbxExporters/Editor/UnitTests/ExporterTestBase.cs

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -121,22 +121,18 @@ protected string GetRandomPrefabAssetPath() {
121121
}
122122

123123
/// <summary>
124-
/// Creates a test hierarchy.
125-
///
126-
/// No two nodes in the hierarchy have the same name.
124+
/// Creates a test hierarchy of cubes.
125+
/// Root
126+
/// -> Parent1
127+
/// ----> Child1
128+
/// ----> Child2
129+
/// -> Parent2
130+
/// ----> Child3
127131
/// </summary>
128132
/// <returns>The hierarchy root.</returns>
129133
public GameObject CreateHierarchy (string rootname = "Root")
130134
{
131-
// Create the following hierarchy:
132-
// Root
133-
// -> Parent1
134-
// ----> Child1
135-
// ----> Child2
136-
// -> Parent2
137-
// ----> Child3
138-
139-
var root = CreateGameObject (rootname);
135+
var root = new GameObject (rootname);
140136
SetTransform (root.transform,
141137
new Vector3 (3, 4, -6),
142138
new Vector3 (45, 10, 34),
@@ -180,22 +176,24 @@ private void SetTransform (Transform t, Vector3 pos, Vector3 rot, Vector3 scale)
180176
/// <summary>
181177
/// Creates a GameObject.
182178
/// </summary>
183-
/// <returns>The created GameObject.</returns>
184-
/// <param name="name">Name.</param>
185-
/// <param name="parent">Parent.</param>
186-
public GameObject CreateGameObject (string name, Transform parent = null)
179+
public GameObject CreateGameObject (string name, Transform parent = null, PrimitiveType type = PrimitiveType.Cube)
187180
{
188-
var go = new GameObject (name);
181+
var go = GameObject.CreatePrimitive (type);
182+
go.name = name;
189183
go.transform.SetParent (parent);
190184
return go;
191185
}
192186

193187
// Helper for the tear-down. This is run from the editor's update loop.
194188
void DeleteOnNextUpdate()
195189
{
196-
Directory.Delete(filePath, recursive: true);
197-
AssetDatabase.Refresh();
198190
EditorApplication.update -= DeleteOnNextUpdate;
191+
try {
192+
Directory.Delete(filePath, recursive: true);
193+
AssetDatabase.Refresh();
194+
} catch(IOException xcp) {
195+
// ignore -- something else must have deleted this.
196+
}
199197
}
200198

201199
[TearDown]

0 commit comments

Comments
 (0)