Skip to content

Commit 6409c2d

Browse files
authored
Merge pull request #183 from Unity-Technologies/UNI-27542-convert-to-prefab-loses-references
UNI-27542 create a prefab from the converted GO instead of FBX (fix references)
2 parents 9d8e308 + 85d6312 commit 6409c2d

File tree

4 files changed

+134
-231
lines changed

4 files changed

+134
-231
lines changed

Assets/FbxExporters/Editor/ConvertToModel.cs

Lines changed: 98 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,6 @@ public static bool OnValidateMenuItem ()
6262
return true;
6363
}
6464

65-
/// <summary>
66-
/// After a convert-to-model, we normally delete the original object.
67-
/// That can be overridden.
68-
/// </summary>
69-
public enum KeepOriginal {
70-
Default, // Use the export settings.
71-
Keep, // Don't delete, just rename it.
72-
Delete, // Delete the original hierarchy.
73-
}
74-
7565
/// <summary>
7666
/// Gets the export settings.
7767
/// </summary>
@@ -89,11 +79,9 @@ public static EditorTools.ExportSettings ExportSettings {
8979
/// <returns>list of instanced Model Prefabs</returns>
9080
/// <param name="unityGameObjectsToConvert">Unity game objects to convert to Model Prefab instances</param>
9181
/// <param name="path">Path to save Model Prefab; use FbxExportSettings if null</param>
92-
/// <param name="keepOriginal">If set to <c>true</c> keep original gameobject hierarchy.</param>
9382
public static GameObject[] CreateInstantiatedModelPrefab (
9483
GameObject [] unityGameObjectsToConvert,
95-
string directoryFullPath = null,
96-
KeepOriginal keepOriginal = KeepOriginal.Default)
84+
string directoryFullPath = null)
9785
{
9886
if (directoryFullPath == null) {
9987
directoryFullPath = FbxExporters.EditorTools.ExportSettings.GetAbsoluteSavePath();
@@ -106,8 +94,7 @@ public static GameObject[] CreateInstantiatedModelPrefab (
10694
foreach(var go in toExport) {
10795
try {
10896
wasExported.Add(Convert(go,
109-
directoryFullPath: directoryFullPath,
110-
keepOriginal: keepOriginal));
97+
directoryFullPath: directoryFullPath));
11198
} catch(System.Exception xcp) {
11299
Debug.LogException(xcp);
113100
}
@@ -121,8 +108,6 @@ public static GameObject[] CreateInstantiatedModelPrefab (
121108
/// This returns a new object; the converted object may be modified or destroyed.
122109
///
123110
/// This refreshes the asset database.
124-
///
125-
/// If "keepOriginal" is set, the converted object is modified but remains in the scene.
126111
/// </summary>
127112
/// <returns>The instance that replaces 'toConvert' in the scene.</returns>
128113
/// <param name="toConvert">GameObject hierarchy to replace with a prefab.</param>
@@ -134,12 +119,10 @@ public static GameObject[] CreateInstantiatedModelPrefab (
134119
/// to a directory in which to put the fbx file. Ignored if
135120
/// fbxFullPath is specified. May be null, in which case we use the
136121
/// export settings.</param>
137-
/// <param name="keepOriginal">If set to <c>true</c>, keep the original in the scene.</param>
138122
public static GameObject Convert (
139123
GameObject toConvert,
140124
string directoryFullPath = null,
141-
string fbxFullPath = null,
142-
KeepOriginal keepOriginal = KeepOriginal.Default)
125+
string fbxFullPath = null)
143126
{
144127
if (string.IsNullOrEmpty(fbxFullPath)) {
145128
// Generate a unique filename.
@@ -182,61 +165,29 @@ public static GameObject Convert (
182165
throw new System.Exception ("Failed to convert " + toConvert.name);;
183166
}
184167

185-
// Instantiate the FBX file.
186-
var unityGO = PrefabUtility.InstantiatePrefab (unityMainAsset, toConvert.scene)
187-
as GameObject;
188-
if (!unityGO) {
189-
throw new System.Exception ("Failed to convert " + toConvert.name);;
190-
}
191-
192-
// Copy the components over to the instance of the FBX.
193-
SetupImportedGameObject (toConvert, unityGO);
168+
// Copy the mesh/materials from the FBX
169+
UpdateFromSourceRecursive (toConvert, unityMainAsset);
194170

195171
// Set up the FbxPrefab component so it will auto-update.
196172
// Make sure to delete whatever FbxPrefab history we had.
197-
var fbxPrefab = unityGO.GetComponent<FbxPrefab>();
173+
var fbxPrefab = toConvert.GetComponent<FbxPrefab>();
198174
if (fbxPrefab) {
199175
Object.DestroyImmediate(fbxPrefab);
200176
}
201-
fbxPrefab = unityGO.AddComponent<FbxPrefab>();
177+
fbxPrefab = toConvert.AddComponent<FbxPrefab>();
202178
var fbxPrefabUtility = new FbxPrefabAutoUpdater.FbxPrefabUtility (fbxPrefab);
203179
fbxPrefabUtility.SetSourceModel(unityMainAsset);
204180

205-
// Disconnect from the FBX file.
206-
PrefabUtility.DisconnectPrefabInstance(unityGO);
207-
208181
// Create a prefab from the instantiated and componentized unityGO.
209182
var prefabFileName = Path.ChangeExtension(projectRelativePath, ".prefab");
210-
var prefab = PrefabUtility.CreatePrefab(prefabFileName, unityGO, ReplacePrefabOptions.ConnectToPrefab);
183+
var prefab = PrefabUtility.CreatePrefab(prefabFileName, toConvert, ReplacePrefabOptions.ConnectToPrefab);
211184
if (!prefab) {
212185
throw new System.Exception(
213186
string.Format("Failed to create prefab asset in [{0}] from fbx [{1}]",
214187
prefabFileName, fbxFullPath));
215188
}
216189

217-
// Remove (now redundant) gameobject
218-
bool actuallyKeepOriginal;
219-
switch(keepOriginal) {
220-
case KeepOriginal.Delete:
221-
actuallyKeepOriginal = false;
222-
break;
223-
case KeepOriginal.Keep:
224-
actuallyKeepOriginal = true;
225-
break;
226-
case KeepOriginal.Default:
227-
default:
228-
actuallyKeepOriginal = ExportSettings.keepOriginalAfterConvert;
229-
break;
230-
}
231-
if (!actuallyKeepOriginal) {
232-
Object.DestroyImmediate (toConvert);
233-
} else {
234-
// rename and put under scene root in case we need to check values
235-
toConvert.name = "_safe_to_delete_" + toConvert.name;
236-
toConvert.SetActive (false);
237-
}
238-
239-
return unityGO;
190+
return toConvert;
240191
}
241192

242193
/// <summary>
@@ -311,91 +262,87 @@ public static void EnforceUniqueNames(IEnumerable<GameObject> exportSet)
311262
}
312263

313264
/// <summary>
314-
/// Sets up the imported GameObject to match the original.
315-
/// - Updates the name to be the same as original (i.e. remove the "(Clone)")
316-
/// - Moves the imported object to the correct position in the hierarchy
317-
/// - Updates the transform of the imported GameObject to match the original
318-
/// - Copy over missing components and component values
265+
/// Updates the meshes and materials of the exported GameObjects
266+
/// to link to those imported from the FBX.
319267
/// </summary>
320-
/// <param name="orig">Original GameObject.</param>
321-
/// <param name="imported">Imported GameObject.</param>
322-
public static void SetupImportedGameObject(GameObject orig, GameObject imported)
268+
/// <param name="dest">GameObject to update.</param>
269+
/// <param name="source">Source to update from.</param>
270+
public static void UpdateFromSourceRecursive(GameObject dest, GameObject source)
323271
{
324-
Transform importedTransform = imported.transform;
325-
Transform origTransform = orig.transform;
326-
327-
// configure transform and maintain local pose
328-
importedTransform.SetParent (origTransform.parent, false);
329-
importedTransform.SetSiblingIndex (origTransform.GetSiblingIndex());
330-
331-
// copy the components over, assuming that the hierarchy order is unchanged
332-
if (origTransform.hierarchyCount != importedTransform.hierarchyCount) {
333-
Debug.LogWarning (string.Format ("Warning: Exported {0} objects, but only imported {1}",
334-
origTransform.hierarchyCount, importedTransform.hierarchyCount));
272+
// recurse over orig, for each transform finding the corresponding transform in the FBX
273+
// and copying the meshes and materials over from the FBX
274+
var goDict = MapNameToSourceRecursive(dest, source);
275+
var q = new Queue<Transform> ();
276+
q.Enqueue (dest.transform);
277+
while (q.Count > 0) {
278+
var t = q.Dequeue ();
279+
280+
if (goDict [t.name] == null) {
281+
Debug.LogWarning (string.Format ("Warning: Could not find Object {0} in FBX", t.name));
282+
continue;
283+
}
284+
CopyComponents (t.gameObject, goDict [t.name]);
285+
foreach (Transform child in t) {
286+
q.Enqueue (child);
287+
}
335288
}
336-
FixSiblingOrder (orig.transform, imported.transform);
337-
338-
// the imported GameObject will have the same name as the file to which it was imported from,
339-
// which might not be the same name as the original GameObject
340-
CopyComponentsRecursive (orig, imported, namesExpectedMatch:false);
341289
}
342290

343291
/// <summary>
344-
/// Given two hierarchies of nodes whose names match up,
345-
/// make the 'imported' hierarchy have its children be in the same
346-
/// order as the 'orig' hierarchy.
347-
///
348-
/// The 'orig' hierarchy is not modified.
292+
/// Gets a dictionary linking dest GameObject name to source game object.
349293
/// </summary>
350-
public static void FixSiblingOrder(Transform orig, Transform imported){
351-
foreach (Transform origChild in orig) {
352-
Transform importedChild = imported.Find (origChild.name);
353-
if (importedChild == null) {
354-
Debug.LogWarning (string.Format(
355-
"Warning: Could not find {0} in parented under {1} in import hierarchy",
356-
origChild.name, imported.name
357-
));
358-
continue;
294+
/// <returns>Dictionary containing the name to source game object.</returns>
295+
/// <param name="dest">Destination GameObject.</param>
296+
/// <param name="source">Source GameObject.</param>
297+
private static Dictionary<string,GameObject> MapNameToSourceRecursive(GameObject dest, GameObject source){
298+
var nameToGO = new Dictionary<string,GameObject> ();
299+
300+
var q = new Queue<Transform> ();
301+
q.Enqueue (dest.transform);
302+
while (q.Count > 0) {
303+
var t = q.Dequeue ();
304+
nameToGO [t.name] = null;
305+
foreach (Transform child in t) {
306+
q.Enqueue (child);
359307
}
360-
importedChild.SetSiblingIndex (origChild.GetSiblingIndex ());
361-
FixSiblingOrder (origChild, importedChild);
362308
}
363-
}
364309

365-
private static void CopyComponentsRecursive(GameObject from, GameObject to, bool namesExpectedMatch = true){
366-
if (namesExpectedMatch && !to.name.StartsWith(from.name) || from.transform.childCount != to.transform.childCount) {
367-
Debug.LogError (string.Format("Error: hierarchies don't match (From: {0}, To: {1})", from.name, to.name));
368-
return;
310+
nameToGO [dest.name] = source;
311+
312+
var fbxQ = new Queue<Transform> ();
313+
foreach (Transform child in source.transform) {
314+
fbxQ.Enqueue (child);
369315
}
370316

371-
CopyComponents (from, to);
372-
for (int i = 0; i < from.transform.childCount; i++) {
373-
CopyComponentsRecursive(from.transform.GetChild(i).gameObject, to.transform.GetChild(i).gameObject);
317+
while (fbxQ.Count > 0) {
318+
var t = fbxQ.Dequeue ();
319+
if (!nameToGO.ContainsKey (t.name)) {
320+
Debug.LogWarning (string.Format("Warning: {0} in FBX but not in converted hierarchy", t.name));
321+
continue;
322+
}
323+
nameToGO [t.name] = t.gameObject;
324+
foreach (Transform child in t) {
325+
fbxQ.Enqueue (child);
326+
}
374327
}
328+
329+
return nameToGO;
375330
}
376331

377332
/// <summary>
378-
/// Copy components on the 'from' object which is the object in the
379-
/// scene we exported, over to the 'to' object which is the object
380-
/// in the scene that we imported from the FBX.
333+
/// Copy components on the 'from' object which is the FBX,
334+
/// over to the 'to' object which is the object in the
335+
/// scene we exported.
381336
///
382-
/// Exception: don't copy the references to assets in the scene that
383-
/// were also exported, in particular the meshes and materials.
337+
/// Only copy over meshes and materials, since that is all the FBX contains
338+
/// that is not already in the scene.
384339
///
385340
/// The 'from' hierarchy is not modified.
386341
/// </summary>
387-
public static void CopyComponents(GameObject from, GameObject to){
342+
public static void CopyComponents(GameObject to, GameObject from){
388343
var originalComponents = new List<Component>(to.GetComponents<Component> ());
389-
foreach(var component in from.GetComponents<Component> ()) {
390-
// UNI-24379: we don't support SkinnedMeshRenderer right
391-
// now: we just bake the mesh into its current pose. So
392-
// don't copy the SMR over. There will already be a
393-
// MeshFilter and MeshRenderer due to the static mesh.
394-
// Remove this code when we support skinning.
395-
if (component is SkinnedMeshRenderer) {
396-
continue;
397-
}
398-
344+
// copy over meshes, materials, and nothing else
345+
foreach (var component in from.GetComponents<Component>()) {
399346
var json = EditorJsonUtility.ToJson(component);
400347
if (string.IsNullOrEmpty (json)) {
401348
// this happens for missing scripts
@@ -417,40 +364,36 @@ public static void CopyComponents(GameObject from, GameObject to){
417364
}
418365

419366
if (!toComponent) {
420-
// It doesn't exist => create and copy.
421-
toComponent = to.AddComponent(component.GetType());
422-
if (toComponent) {
423-
EditorJsonUtility.FromJsonOverwrite (json, toComponent);
424-
}
425-
} else {
426-
// It exists => copy.
427-
// But we want to override that behaviour in a few
428-
// cases, to avoid clobbering references to the new FBX
429-
// TODO: interpret the object or the json more directly
430-
// TODO: be more generic
431-
// TODO: handle references to other objects in the same hierarchy
432-
433-
if (toComponent is MeshFilter) {
434-
// Don't copy the mesh. But there's nothing else to
435-
// copy, so just don't copy anything.
436-
} else if (toComponent is SkinnedMeshRenderer) {
437-
// Don't want to clobber materials or the mesh.
438-
var skinnedMesh = toComponent as SkinnedMeshRenderer;
439-
var sharedMesh = skinnedMesh.sharedMesh;
440-
var sharedMats = skinnedMesh.sharedMaterials;
441-
EditorJsonUtility.FromJsonOverwrite(json, toComponent);
442-
skinnedMesh.sharedMesh = sharedMesh;
443-
skinnedMesh.sharedMaterials = sharedMats;
444-
} else if (toComponent is Renderer) {
445-
// Don't want to clobber materials.
446-
var renderer = toComponent as Renderer;
447-
var sharedMats = renderer.sharedMaterials;
448-
EditorJsonUtility.FromJsonOverwrite(json, toComponent);
449-
renderer.sharedMaterials = sharedMats;
450-
} else {
451-
// Normal case: copy everything.
452-
EditorJsonUtility.FromJsonOverwrite(json, toComponent);
367+
// copy over mesh filter and mesh renderer to replace
368+
// skinned mesh renderer
369+
if (component is MeshFilter) {
370+
var skinnedMesh = to.GetComponent<SkinnedMeshRenderer> ();
371+
if (skinnedMesh) {
372+
toComponent = to.AddComponent(component.GetType());
373+
EditorJsonUtility.FromJsonOverwrite (json, toComponent);
374+
375+
var toRenderer = to.AddComponent <MeshRenderer>();
376+
var fromRenderer = from.GetComponent<MeshRenderer> ();
377+
if (toRenderer && fromRenderer) {
378+
EditorJsonUtility.FromJsonOverwrite (EditorJsonUtility.ToJson(fromRenderer), toRenderer);
379+
}
380+
Object.DestroyImmediate (skinnedMesh);
381+
}
453382
}
383+
continue;
384+
}
385+
386+
if (toComponent is SkinnedMeshRenderer) {
387+
var skinnedMesh = toComponent as SkinnedMeshRenderer;
388+
var fromSkinnedMesh = component as SkinnedMeshRenderer;
389+
skinnedMesh.sharedMesh = fromSkinnedMesh.sharedMesh;
390+
skinnedMesh.sharedMaterials = fromSkinnedMesh.sharedMaterials;
391+
} else if (toComponent is MeshFilter) {
392+
EditorJsonUtility.FromJsonOverwrite (json, toComponent);
393+
} else if (toComponent is Renderer) {
394+
var toRenderer = toComponent as Renderer;
395+
var fromRenderer = component as Renderer;
396+
toRenderer.sharedMaterials = fromRenderer.sharedMaterials;
454397
}
455398
}
456399
}

Assets/FbxExporters/Editor/FbxExportSettings.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,6 @@ public static string kDefaultAdskRoot {
210210
public bool mayaCompatibleNames;
211211
public bool centerObjects;
212212

213-
/// <summary>
214-
/// In Convert-to-model, by default we delete the original object.
215-
/// This option lets you override that.
216-
/// </summary>
217-
[HideInInspector]
218-
public bool keepOriginalAfterConvert;
219-
220213
public int selectedMayaApp = 0;
221214

222215
/// <summary>
@@ -243,7 +236,6 @@ protected override void LoadDefaults()
243236
{
244237
mayaCompatibleNames = true;
245238
centerObjects = true;
246-
keepOriginalAfterConvert = false;
247239
convertToModelSavePath = kDefaultSavePath;
248240
mayaOptionPaths = null;
249241
mayaOptionNames = null;

0 commit comments

Comments
 (0)