Rasagar/Library/PackageCache/com.unity.formats.fbx/Editor/ConvertToNestedPrefab.cs
2024-08-30 17:33:54 +03:00

1188 lines
52 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.IO;
using System.Runtime.Serialization;
#if UNITY_2021_2_OR_NEWER
using UnityEditor.Search;
#endif
namespace UnityEditor.Formats.Fbx.Exporter
{
[System.Serializable]
internal class ConvertToNestedPrefabException : System.Exception
{
public ConvertToNestedPrefabException()
{
}
public ConvertToNestedPrefabException(string message)
: base(message)
{
}
public ConvertToNestedPrefabException(string message, System.Exception inner)
: base(message, inner)
{
}
protected ConvertToNestedPrefabException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
/// <summary>
/// Class for converting an exported FBX to a Prefab Variant.
/// </summary>
public static class ConvertToNestedPrefab
{
const string GameObjectMenuItemName = "GameObject/Convert To FBX Prefab Variant...";
const string AssetsMenuItemName = "Assets/Convert To FBX Prefab Variant...";
const string UndoConversionGroup = "Convert {0} to FBX Prefab Variant";
internal const string UndoConversionCreateObject = "Convert to FBX Prefab Variant";
/// <summary>
/// OnContextItem is called either:
/// * when the user selects the menu item via the top menu (with a null MenuCommand), or
/// * when the user selects the menu item via the context menu (in which case there's a context)
///
/// OnContextItem gets called once per selected object (if the
/// parent and child are selected, then OnContextItem will only be
/// called on the parent)
/// </summary>
[MenuItem(GameObjectMenuItemName, false, 30)]
static void OnGameObjectContextItem(MenuCommand command)
{
OnContextItem(command, SelectionMode.Editable | SelectionMode.TopLevel);
}
[MenuItem(AssetsMenuItemName, false, 30)]
static void OnAssetsContextItem(MenuCommand command)
{
OnContextItem(command, SelectionMode.Assets);
}
static void OnContextItem(MenuCommand command, SelectionMode mode)
{
GameObject[] selection = null;
if (command == null || command.context == null)
{
// We were actually invoked from the top GameObject menu, so use the selection.
selection = Selection.GetFiltered<GameObject>(mode);
}
else
{
// We were invoked from the right-click menu, so use the context of the context menu.
var selected = command.context as GameObject;
if (selected)
{
selection = new GameObject[] { selected };
}
}
if (selection == null || selection.Length == 0)
{
ModelExporter.DisplayNoSelectionDialog();
return;
}
Selection.objects = CreateInstantiatedModelPrefab(selection);
}
internal static void DisplayInvalidSelectionDialog(GameObject toConvert, string message = "")
{
UnityEditor.EditorUtility.DisplayDialog(
string.Format("{0} Warning", "FBX Exporter"),
string.Format("Failed to Convert: {0}\n{1}", toConvert.name, message),
"Ok");
}
/// <summary>
/// Validate the menu items defined above.
/// </summary>
[MenuItem(GameObjectMenuItemName, true, 30)]
[MenuItem(AssetsMenuItemName, true, 30)]
internal static bool OnValidateMenuItem()
{
return true;
}
/// <summary>
/// Gets the export settings.
/// </summary>
internal static ExportSettings ExportSettings
{
get { return ExportSettings.instance; }
}
/// <summary>
/// Return true if the given set contains only prefab assets on disk,
/// and nothing from the scene.
/// </summary>
/// <returns></returns>
internal static bool SetContainsOnlyPrefabAssets(Object[] toConvert)
{
foreach (var obj in toConvert)
{
var go = ModelExporter.GetGameObject(obj);
if (go != null && !PrefabUtility.IsPartOfPrefabAsset(go))
{
// return as soon as we find something that is not part of a prefab asset
// on disk
return false;
}
}
return true;
}
/// <summary>
/// Create instantiated model prefabs from a selection of objects.
///
/// Every hierarchy in the selection will be exported, under the name of the root.
///
/// If an object and one of its descendents are both selected, the descendent is not promoted to be a prefab -- we only export the root.
/// </summary>
/// <returns>list of instanced Model Prefabs</returns>
/// <param name="unityGameObjectsToConvert">Unity game objects to convert to Model Prefab instances</param>
/// <param name="path">Path to save Model Prefab; use FbxExportSettings if null</param>
internal static GameObject[] CreateInstantiatedModelPrefab(
GameObject[] unityGameObjectsToConvert)
{
var toExport = ModelExporter.RemoveRedundantObjects(unityGameObjectsToConvert);
if (ExportSettings.instance.DisplayOptionsWindow)
{
if (toExport.Count == 1)
{
var go = toExport.First();
if (PrefabUtility.IsPartOfNonAssetPrefabInstance(go) && !PrefabUtility.IsOutermostPrefabInstanceRoot(go))
{
DisplayInvalidSelectionDialog(go,
"Children of a Prefab instance cannot be converted.\nYou can open the Prefab in Prefab Mode or unpack the Prefab instance to convert it's children");
return null;
}
if (PrefabUtility.IsPartOfPrefabAsset(go) && go.transform.parent != null)
{
DisplayInvalidSelectionDialog(go,
"Children of a Prefab Asset cannot be converted.\nYou can open the Prefab in Prefab Mode or unpack the Prefab instance to convert it's children");
return null;
}
// can't currently handle converting root of prefab in prefab preview scene
if (SceneManagement.EditorSceneManager.IsPreviewSceneObject(go) && go.transform.parent == null)
{
DisplayInvalidSelectionDialog(go,
"Cannot convert Prefab root in the Prefab Preview Scene.\nYou can convert a Prefab Instance or convert the Prefab Asset directly in the Project view");
return null;
}
}
ConvertToPrefabEditorWindow.Init(toExport);
return toExport.ToArray();
}
bool onlyPrefabAssets = ConvertToNestedPrefab.SetContainsOnlyPrefabAssets(unityGameObjectsToConvert);
int groupIndex = -1;
// If only Prefab Assets on disk are selected (nothing in the scene), then do not
// try to undo as modifications on disk cannot be undone.
if (!onlyPrefabAssets)
{
Undo.IncrementCurrentGroup();
groupIndex = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName(UndoConversionCreateObject);
}
var converted = new List<GameObject>();
var exportOptions = ExportSettings.instance.ConvertToPrefabSettings.info;
foreach (var go in toExport)
{
var convertedGO = Convert(go, exportOptions: exportOptions);
if (convertedGO != null)
{
converted.Add(convertedGO);
}
}
if (!onlyPrefabAssets && groupIndex >= 0)
{
Undo.CollapseUndoOperations(groupIndex);
Undo.IncrementCurrentGroup();
}
return converted.ToArray();
}
/// <summary>
/// For sceneObj, replace references of objects that are keys in fixSceneRefsMap, to the value.
///
/// If the scene object is toConvertRoot or a child of it, then do not fix its references as it
/// will be deleted after conversion.
/// </summary>
/// <param name="sceneObj">scene object with reference to replace</param>
/// <param name="fixSceneRefsMap">mapping from original object to new object</param>
/// <param name="toConvertRoot">root of the hierarchy being converted</param>
internal static void FixSceneReferenceToObject(Object sceneObj, GameObject toConvertRoot, Dictionary<Object, Object> fixSceneRefsMap)
{
// item has reference to origObj that need to be replaced by references to newObj
var go = ModelExporter.GetGameObject(sceneObj);
if (go && go.transform.IsChildOf(toConvertRoot.transform))
{
// if this is a child of what we are converting, don't update its references.
return;
}
var components = go.GetComponents<Component>();
SerializedObject serializedComponent;
SerializedProperty property;
foreach (var component in components)
{
serializedComponent = new SerializedObject(component);
property = serializedComponent.GetIterator();
property.Next(true); // skip generic field
var isSkinnedMesh = component is SkinnedMeshRenderer;
// For SkinnedMeshRenderer, the bones array doesn't have visible children, but may have references that need to be fixed.
// For everything else, filtering by visible children in the while loop and then copying properties that don't have visible children,
// ensures that only the leaf properties are copied over. Copying other properties is not usually necessary and may break references that
// were not meant to be copied.
while (property.Next((isSkinnedMesh) ? property.hasChildren : property.hasVisibleChildren))
{
if (!property.hasVisibleChildren)
{
// with Undo operations, copying m_Father reference causes issues. Also, it is not required as the reference is fixed when
// the transform is parented under the correct hierarchy (which happens before this).
if (property.propertyType != SerializedPropertyType.ObjectReference) continue;
if (property.propertyPath == "m_GameObject") continue;
if (property.propertyPath == "m_Father") continue;
if (!property.objectReferenceValue) continue;
if (fixSceneRefsMap.TryGetValue(property.objectReferenceValue, out Object newVal))
{
property.objectReferenceValue = newVal;
}
}
}
serializedComponent.ApplyModifiedProperties();
}
}
#if UNITY_2021_2_OR_NEWER
internal static List<Object> GetSceneReferences(int instanceId)
{
var query = $"h: ref={instanceId}";
using (var searchContext = UnityEditor.Search.SearchService.CreateContext(query))
{
// Initiate the query and get the first results.
var items = UnityEditor.Search.SearchService.GetItems(searchContext, SearchFlags.Synchronous);
return items.ConvertAll(x => x.ToObject());
}
}
#endif // UNITY_2021_2_OR_NEWER
/// <summary>
/// Helper for getting a property from an instance with reflection.
/// </summary>
/// <param name="instance"></param>
/// <param name="propertyName"></param>
/// <param name="isPublic"></param>
/// <returns></returns>
private static object GetPropertyReflection(object instance, string propertyName, bool isPublic)
{
return instance.GetType().GetProperty(propertyName, (isPublic ? System.Reflection.BindingFlags.Public : System.Reflection.BindingFlags.NonPublic) |
System.Reflection.BindingFlags.Instance).GetValue(instance, null);
}
#if !UNITY_2021_2_OR_NEWER
private static EditorWindow s_sceneHierarchyWindow;
private static EditorWindow SceneHierarchyWindow
{
get
{
if (!s_sceneHierarchyWindow)
{
var sceneHierarchyWindowType = typeof(UnityEditor.SearchableEditorWindow).Assembly.GetType("UnityEditor.SceneHierarchyWindow");
// bug 1242332: don't grab the focus!
// The arguments aren't actually optional so they must all be named.
s_sceneHierarchyWindow = EditorWindow.GetWindow(
t: sceneHierarchyWindowType,
utility: false,
title: null,
focus: false);
}
return s_sceneHierarchyWindow;
}
}
private static System.Reflection.MethodInfo s_setSearchFilterMethod;
private static System.Reflection.MethodInfo SetSearchFilterMethod
{
get
{
if (s_setSearchFilterMethod == null)
{
var sceneHierarchyWindowType = typeof(UnityEditor.SearchableEditorWindow).Assembly.GetType("UnityEditor.SceneHierarchyWindow");
s_setSearchFilterMethod = sceneHierarchyWindowType.GetMethod("SetSearchFilter", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
}
return s_setSearchFilterMethod;
}
}
/// <summary>
/// Returns a list of GameObjects in the scene that contain references to the given object.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
internal static List<GameObject> GetSceneReferencesToObject(Object obj)
{
var instanceID = obj.GetInstanceID();
var idFormat = "ref:{0}:";
var sceneHierarchyWindow = SceneHierarchyWindow;
var setSearchFilterMethod = SetSearchFilterMethod;
var sceneHierarchy = GetPropertyReflection(sceneHierarchyWindow, "sceneHierarchy", isPublic: true);
var previousSearchFilter = sceneHierarchy.GetType().GetField("m_SearchFilter", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(sceneHierarchy);
// Set the search filter to find all references in the scene to the given object
setSearchFilterMethod.Invoke(sceneHierarchyWindow, new object[] { string.Format(idFormat, instanceID), SearchableEditorWindow.SearchMode.All, true, false });
// Get objects from list of instance IDs of currently visible objects
var treeView = GetPropertyReflection(sceneHierarchy, "treeView", isPublic: false);
var data = GetPropertyReflection(treeView, "data", isPublic: true);
var getRows = data.GetType().GetMethod("GetRows");
var rows = getRows.Invoke(data, null) as IEnumerable;
var sceneObjects = new List<GameObject>();
foreach (var row in rows)
{
var id = (int)GetPropertyReflection(row, "id", isPublic: true);
var gameObject = EditorUtility.InstanceIDToObject(id) as GameObject;
if (gameObject)
{
sceneObjects.Add(gameObject);
}
}
// remove the filter when done
setSearchFilterMethod.Invoke(sceneHierarchyWindow, new object[] { previousSearchFilter, SearchableEditorWindow.SearchMode.Name, true, false });
return sceneObjects;
}
#endif // UNITY_2021_2_OR_NEWER
/// <summary>
/// Convert one object (and the hierarchy below it) to a prefab variant of a model prefab.
///
/// Returns the prefab asset that's linked to the fbx.
///
/// If 'toConvert' is:
/// <list type="bullet">
/// <item><description>
/// A GameObject in the Scene, then the method exports the hierarchy to an FBX
/// and creates a new Prefab Variant pointing to the exported FBX.
/// </description></item>
/// <item><description>
/// The root of an FBX asset, or the root of an instance of an
/// FBX asset, then the method creates a new Prefab Variant
/// pointing to the existing FBX.
/// </description></item>
/// <item><description>
/// A Prefab asset,
/// then the method exports a new FBX asset and creates a new Prefab Variant
/// pointing to the FBX.
/// </description></item>
/// </list>
/// </summary>
/// <returns>The prefab variant linked to an fbx file.</returns>
/// <param name="toConvert">Object to convert.</param>
/// <param name="fbxFullPath">Absolute platform-specific path to
/// the fbx file. If the file already exists, it will be overwritten.
/// May be null, in which case we construct a unique filename.
/// Ignored if 'toConvert' is an fbx asset or is an instance of
/// one.</param>
/// <param name="fbxDirectoryFullPath">Absolute platform-specific
/// path to a directory in which to put the fbx file under a unique
/// filename. May be null, in which case we use the export settings.
/// Ignored if 'fbxFullPath' is specified. Ignored if 'toConvert' is
/// an fbx asset or an instance of one.</param>
/// <param name="prefabFullPath">Absolute platform-specific path to
/// the prefab file. If the file already exists, it will be
/// overwritten. May be null, in which case we construct a unique
/// filename. Ignored if 'toConvert' is a prefab asset.</param>
/// <param name="prefabDirectoryFullPath">Absolute
/// platform-specific path to a directory in which to put the prefab
/// file under a unique filename. May be null, in which case we use
/// the export settings. Ignored if 'prefabFullPath' is specified.
/// Ignored if 'toConvert' is a prefab asset.</param>
/// <param name="convertOptions">
/// Export options to use for exporting the model asset
/// to convert to a Prefab.
/// </param>
public static GameObject ConvertToPrefabVariant(
GameObject toConvert,
string fbxDirectoryFullPath = null,
string fbxFullPath = null,
string prefabDirectoryFullPath = null,
string prefabFullPath = null,
ConvertToPrefabVariantOptions convertOptions = null)
{
return Convert(toConvert, fbxDirectoryFullPath, fbxFullPath, prefabDirectoryFullPath, prefabFullPath, convertOptions?.ConvertToModelSettingsSerialize());
}
internal static GameObject Convert(
GameObject toConvert,
string fbxDirectoryFullPath = null,
string fbxFullPath = null,
string prefabDirectoryFullPath = null,
string prefabFullPath = null,
ConvertToPrefabSettingsSerialize exportOptions = null)
{
if (toConvert == null)
{
throw new System.ArgumentNullException("toConvert");
}
if (PrefabUtility.IsPartOfNonAssetPrefabInstance(toConvert) && !PrefabUtility.IsOutermostPrefabInstanceRoot(toConvert))
{
return null; // cannot convert in this scenario
}
// can't currently handle converting root of prefab in prefab preview scene
if (SceneManagement.EditorSceneManager.IsPreviewSceneObject(toConvert) && toConvert.transform.parent == null)
{
return null;
}
// if toConvert is part of a prefab asset and not an instance, make it an instance in a preview scene
// so that we can unpack it and avoid issues with nested prefab references.
bool isPrefabAsset = false;
UnityEngine.SceneManagement.Scene? previewScene = null;
if (PrefabUtility.IsPartOfPrefabAsset(toConvert) && PrefabUtility.GetPrefabInstanceStatus(toConvert) == PrefabInstanceStatus.NotAPrefab)
{
previewScene = SceneManagement.EditorSceneManager.NewPreviewScene();
toConvert = PrefabUtility.InstantiatePrefab(toConvert, previewScene.Value) as GameObject;
isPrefabAsset = true;
}
// don't need to undo if we are converting a prefab asset
if (!isPrefabAsset)
{
Undo.IncrementCurrentGroup();
Undo.SetCurrentGroupName(string.Format(UndoConversionGroup, toConvert.name));
}
// if root is a prefab instance, unpack it. Unpack everything below as well
if (PrefabUtility.GetPrefabInstanceStatus(toConvert) == PrefabInstanceStatus.Connected)
{
Undo.RegisterFullObjectHierarchyUndo(toConvert, "unpack prefab instance");
PrefabUtility.UnpackPrefabInstance(toConvert, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
}
// If we selected the something that's already backed by an
// FBX, don't export.
var mainAsset = GetOrCreateFbxAsset(toConvert, fbxDirectoryFullPath, fbxFullPath, exportOptions);
// Gather all the references to toConvert in the scene before exporting/converting.
// Note: unique names are enforced in GetOrCreateFbxAsset()
GatherSceneHierarchy(toConvert);
// create prefab variant from the fbx
var fbxInstance = PrefabUtility.InstantiatePrefab(mainAsset) as GameObject;
// replace hierarchy in the scene
if (!isPrefabAsset && toConvert != null)
{
// don't worry about keeping the world position in the prefab, as we will fix the transform on the instance root
fbxInstance.transform.SetParent(toConvert.transform.parent, worldPositionStays: false);
fbxInstance.transform.SetSiblingIndex(toConvert.transform.GetSiblingIndex());
}
// copy components over
UpdateFromSourceRecursive(fbxInstance, toConvert);
// make sure we have a path for the prefab
if (string.IsNullOrEmpty(prefabFullPath))
{
// Generate a unique filename.
if (string.IsNullOrEmpty(prefabDirectoryFullPath))
{
prefabDirectoryFullPath = UnityEditor.Formats.Fbx.Exporter.ExportSettings.PrefabAbsoluteSavePath;
}
else
{
prefabDirectoryFullPath = Path.GetFullPath(prefabDirectoryFullPath);
}
var prefabBasename = ModelExporter.ConvertToValidFilename(toConvert.name + ".prefab");
prefabFullPath = Path.Combine(prefabDirectoryFullPath, prefabBasename);
if (File.Exists(prefabFullPath))
{
prefabFullPath = IncrementFileName(prefabDirectoryFullPath, prefabFullPath);
}
}
// make sure the directory structure exists
var dirName = Path.GetDirectoryName(prefabFullPath);
if (!Directory.Exists(dirName))
{
Directory.CreateDirectory(dirName);
}
var prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(fbxInstance, ExportSettings.GetProjectRelativePath(prefabFullPath), InteractionMode.AutomatedAction);
// replace hierarchy in the scene
if (!isPrefabAsset && toConvert != null)
{
Undo.DestroyObjectImmediate(toConvert);
Undo.RegisterCreatedObjectUndo(fbxInstance, UndoConversionCreateObject);
SceneManagement.EditorSceneManager.MarkSceneDirty(fbxInstance.scene);
Undo.IncrementCurrentGroup();
return fbxInstance;
}
else
{
Undo.ClearUndo(toConvert);
Undo.ClearUndo(fbxInstance);
Object.DestroyImmediate(fbxInstance);
Object.DestroyImmediate(toConvert);
}
if (previewScene.HasValue)
{
SceneManagement.EditorSceneManager.ClosePreviewScene(previewScene.Value);
}
return prefab;
}
internal struct SourceObjectInfo
{
/// <summary>
/// The exported GameObject that will replace the source.
/// </summary>
public GameObject destGO;
#if UNITY_2021_2_OR_NEWER
/// <summary>
/// A list of scene objects that reference this
/// object or one of its components. Populated before traversing the
/// components to speed up the search time of finding/replacing references.
///
/// Note: In older versions of Unity (without the Search API), searching
/// for references by GameObject ID will not return objects that reference
/// the components of the same GameObject. Therefore in older versions still need
/// to do a search by component.
/// </summary>
public List<Object> sceneObjectsWithReference;
#endif // UNITY_2021_2_OR_NEWER
public SourceObjectInfo(GameObject dest)
{
destGO = dest;
#if UNITY_2021_2_OR_NEWER
sceneObjectsWithReference = new List<Object>();
#endif // UNITY_2021_2_OR_NEWER
}
}
/// <summary>
/// Dictionary from name of source object (in toConvert hierarchy) to info.
/// </summary>
internal static Dictionary<string, SourceObjectInfo> s_nameToInfo = new Dictionary<string, SourceObjectInfo>();
/// <summary>
/// Populates the s_nameToInfo dictionary by traversing the given hierarchy.
/// In newer versions of Unity, as it traverses, will also perform a search
/// for references to each object or component in the scene.
/// </summary>
/// <param name="hierarchyRoot"></param>
internal static void GatherSceneHierarchy(GameObject hierarchyRoot)
{
s_nameToInfo.Clear();
#if UNITY_2021_2_OR_NEWER
var isPreviewScene = SceneManagement.EditorSceneManager.IsPreviewSceneObject(hierarchyRoot);
#endif // UNITY_2021_2_OR_NEWER
var s = new Stack<Transform>();
s.Push(hierarchyRoot.transform);
while (s.Count > 0)
{
var t = s.Pop();
var info = new SourceObjectInfo();
#if UNITY_2021_2_OR_NEWER
if (!isPreviewScene)
{
var instanceID = t.gameObject.GetInstanceID();
info.sceneObjectsWithReference = GetSceneReferences(instanceID);
}
#endif // UNITY_2021_2_OR_NEWER
s_nameToInfo[t.name] = info;
foreach (Transform child in t)
{
s.Push(child);
}
}
}
/// <summary>
/// Check whether <see>Convert</see> will be exporting an fbx file,
/// or reusing one.
/// </summary>
internal static bool WillExportFbx(GameObject toConvert)
{
return GetFbxAssetOrNull(toConvert) == null;
}
/// <summary>
/// Return an FBX asset that corresponds to 'toConvert'.
///
/// If 'toConvert' is the root of an FBX asset, return it.
///
/// If it's an instance in a scene the points to the root of an FBX
/// asset, return that asset.
///
/// Otherwise, export according to the paths and options, and
/// return the new asset.
/// </summary>
/// <param name="toConvert">GameObject for which we want an fbx asset</param>
/// <param name="fbxDirectoryFullPath">Export will choose an
/// appropriate filename in this directory. Ignored if fbxFullPath is
/// set. Ignored if toConvert is an fbx asset or an instance of an
/// fbx.</param>
/// <param name="fbxDirectoryFullPath">Export will create this
/// file. Overrides fbxDirectoryFullPath. Ignored if toConvert is an
/// fbx asset or an instance of an fbx.</param>
/// <returns>The root of a model prefab asset.</returns>
internal static GameObject GetOrCreateFbxAsset(GameObject toConvert,
string fbxDirectoryFullPath = null,
string fbxFullPath = null,
ConvertToPrefabSettingsSerialize exportOptions = null)
{
if (toConvert == null)
{
throw new System.ArgumentNullException("toConvert");
}
var mainAsset = GetFbxAssetOrNull(toConvert);
if (mainAsset)
{
return mainAsset;
}
if (string.IsNullOrEmpty(fbxFullPath))
{
// Generate a unique filename.
if (string.IsNullOrEmpty(fbxDirectoryFullPath))
{
fbxDirectoryFullPath = UnityEditor.Formats.Fbx.Exporter.ExportSettings.FbxAbsoluteSavePath;
}
else
{
fbxDirectoryFullPath = Path.GetFullPath(fbxDirectoryFullPath);
}
var fbxBasename = ModelExporter.ConvertToValidFilename(toConvert.name + ".fbx");
fbxFullPath = Path.Combine(fbxDirectoryFullPath, fbxBasename);
if (File.Exists(fbxFullPath))
{
fbxFullPath = IncrementFileName(fbxDirectoryFullPath, fbxFullPath);
}
}
var projectRelativePath = ExportSettings.GetProjectRelativePath(fbxFullPath);
// Make sure that the object names in the hierarchy are unique.
// The import back in to Unity would do this automatically but
// we prefer to control it so that the Maya artist can see the
// same names as exist in Unity.
EnforceUniqueNames(new GameObject[] { toConvert });
// Export to FBX. It refreshes the database.
{
var fbxActualPath = ModelExporter.ExportObject(
fbxFullPath, toConvert,
exportOptions != null ? exportOptions : new ConvertToPrefabSettingsSerialize()
);
if (fbxActualPath != fbxFullPath)
{
throw new ConvertToNestedPrefabException("Failed to convert " + toConvert.name);
}
}
// Replace w Model asset. LoadMainAssetAtPath wants a path
// relative to the project, not relative to the assets folder.
var unityMainAsset = AssetDatabase.LoadMainAssetAtPath(projectRelativePath) as GameObject;
if (!unityMainAsset)
{
throw new ConvertToNestedPrefabException("Failed to convert " + toConvert.name);
}
return unityMainAsset;
}
/// <summary>
/// Returns the fbx asset on disk corresponding to the same hierarchy as is selected.
///
/// Returns go if go is the root of a model prefab.
/// Returns the prefab parent of go if it's the root of a model prefab.
/// Returns null in all other circumstances.
/// </summary>
/// <returns>The root of a model prefab asset, or null.</returns>
/// <param name="go">A gameobject either in the scene or in the assets folder.</param>
internal static GameObject GetFbxAssetOrNull(GameObject go)
{
// Children of model prefab instances will also have "model prefab instance"
// as their prefab type, so it is important that it is the root that is selected.
//
// e.g. If I have the following hierarchy:
// Cube
// -- Sphere
//
// Both the Cube and Sphere will have ModelPrefab as their prefab type.
// However, when selecting the Sphere to convert, we don't want to connect it to the
// existing FBX but create a new FBX containing just the sphere.
if (!PrefabUtility.IsPartOfModelPrefab(go))
{
return null;
}
PrefabInstanceStatus prefabStatus = PrefabUtility.GetPrefabInstanceStatus(go);
switch (prefabStatus)
{
case PrefabInstanceStatus.Connected:
// this is a prefab instance, get the object from source
if (PrefabUtility.IsOutermostPrefabInstanceRoot(go))
{
return PrefabUtility.GetCorrespondingObjectFromSource(go) as GameObject;
}
else
{
return null;
}
case PrefabInstanceStatus.NotAPrefab:
// a prefab asset
if (go.transform.root.gameObject == go)
{
return go;
}
else
{
return null;
}
default:
return null;
}
}
/// <summary>
/// Check if the file exists, and if it does, then increment the name.
/// e.g. if filename is Sphere.fbx and it already exists, change it to Sphere 1.fbx.
/// </summary>
/// <returns>new file name.</returns>
/// <param name="filename">Filename.</param>
internal static string IncrementFileName(string path, string filename)
{
string fileWithoutExt = Path.GetFileNameWithoutExtension(filename);
string ext = Path.GetExtension(filename);
// file, space, number, extension.
string format = "{0} {1}{2}";
int index = 1;
// try extracting the current index from the name and incrementing it
var result = System.Text.RegularExpressions.Regex.Match(fileWithoutExt, @"\d+$");
if (result != null)
{
var number = result.Value;
// Parse the number.
int tempIndex;
if (int.TryParse(number, out tempIndex))
{
fileWithoutExt = fileWithoutExt.Remove(fileWithoutExt.LastIndexOf(number));
// Change the format to remove the extra space we'd add
// if there weren't already a number. Also, try to use the
// same width (so Cube001 increments to Cube002, not Cube2).
format = "{0}{1:D" + number.Length + "}{2}"; // file, number with padding, extension
index = tempIndex + 1;
}
}
string file = null;
do
{
file = string.Format(format, fileWithoutExt, index, ext);
file = Path.Combine(path, file);
index++;
}
while (File.Exists(file));
return file;
}
/// <summary>
/// Enforces that all object names be unique before exporting.
/// If an object with a duplicate name is found, then it is incremented.
/// e.g. Sphere becomes Sphere 1
/// </summary>
/// <param name="exportSet">Export set.</param>
internal static void EnforceUniqueNames(IEnumerable<GameObject> exportSet)
{
Dictionary<string, int> NameToIndexMap = new Dictionary<string, int>();
string format = "{0} {1}";
Queue<GameObject> queue = new Queue<GameObject>(exportSet);
while (queue.Count > 0)
{
var go = queue.Dequeue();
var name = go.name;
if (NameToIndexMap.ContainsKey(name))
{
go.name = string.Format(format, name, NameToIndexMap[name]);
NameToIndexMap[name]++;
}
else
{
NameToIndexMap[name] = 1;
}
foreach (Transform child in go.transform)
{
queue.Enqueue(child.gameObject);
}
}
}
/// <summary>
/// Updates the meshes and materials of the exported GameObjects
/// to link to those imported from the FBX.
/// </summary>
/// <param name="dest">GameObject to update.</param>
/// <param name="source">Source to update from.</param>
internal static void UpdateFromSourceRecursive(GameObject dest, GameObject source)
{
// recurse over orig, for each transform finding the corresponding transform in the FBX
// and copying the meshes and materials over from the FBX
MapNameToSourceRecursive(source, dest);
var q = new Queue<Transform>();
q.Enqueue(source.transform);
while (q.Count > 0)
{
var t = q.Dequeue();
if (!s_nameToInfo[t.name].destGO)
{
Debug.LogWarning(string.Format("Warning: Could not find Object {0} in FBX", t.name));
continue;
}
var info = s_nameToInfo[t.name];
var destGO = info.destGO;
var sourceGO = t.gameObject;
if (PrefabUtility.GetPrefabInstanceStatus(sourceGO) == PrefabInstanceStatus.Connected)
{
Undo.RegisterFullObjectHierarchyUndo(sourceGO, "unpack prefab instance");
PrefabUtility.UnpackPrefabInstance(sourceGO, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
}
// Map of objects that may need to have their scene references fixed.
// Maps from original to new object.
var fixSceneRefsMap = new Dictionary<Object, Object>()
{
{ sourceGO, destGO }
};
CopyComponents(destGO, sourceGO, source, fixSceneRefsMap);
// Fix references of other objects in the scene
#if UNITY_2021_2_OR_NEWER
if (info.sceneObjectsWithReference != null)
{
var items = info.sceneObjectsWithReference;
foreach (var item in items)
{
FixSceneReferenceToObject(item, source, fixSceneRefsMap);
}
}
#else // UNITY_2021_2_OR_NEWER
foreach (var obj in fixSceneRefsMap.Keys)
{
// find and fix the references in the scene
var sceneObjs = GetSceneReferencesToObject(obj);
// try to fix references on each component of each scene object, if applicable
foreach (var sceneObj in sceneObjs)
{
FixSceneReferenceToObject(sceneObj, source, fixSceneRefsMap);
}
}
#endif // UNITY_2021_2_OR_NEWER
// also make sure GameObject properties, such as tag and layer
// are copied over as well
destGO.SetActive(sourceGO.activeSelf);
destGO.isStatic = sourceGO.isStatic;
destGO.layer = sourceGO.layer;
destGO.tag = sourceGO.tag;
GameObjectUtility.SetStaticEditorFlags(destGO, GameObjectUtility.GetStaticEditorFlags(sourceGO));
// set icon
#if UNITY_2021_2_OR_NEWER
var sourceIcon = EditorGUIUtility.GetIconForObject(sourceGO);
EditorGUIUtility.SetIconForObject(destGO, sourceIcon);
#else // UNITY_2021_2_OR_NEWER
System.Reflection.BindingFlags bindingFlags = System.Reflection.BindingFlags.InvokeMethod | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public;
var sourceIcon = typeof(EditorGUIUtility).InvokeMember("GetIconForObject", bindingFlags, null, null, new object[] { sourceGO });
object[] args = new object[] { destGO, sourceIcon };
typeof(EditorGUIUtility).InvokeMember("SetIconForObject", bindingFlags, null, null, args);
#endif // UNITY_2021_2_OR_NEWER
foreach (Transform child in t)
{
q.Enqueue(child);
}
}
}
/// <summary>
/// Gets a dictionary linking dest GameObject name to source game object.
///
/// Before export we ensure that the hierarchy has unique names, so that we can map by name afterwards.
/// </summary>
/// <returns>Dictionary containing the name to source game object.</returns>
/// <param name="dest">Destination GameObject.</param>
/// <param name="source">Source GameObject.</param>
internal static void MapNameToSourceRecursive(GameObject dest, GameObject source)
{
var temp = s_nameToInfo[dest.name];
temp.destGO = source;
s_nameToInfo[dest.name] = temp;
var fbxQ = new Queue<Transform>();
foreach (Transform child in source.transform)
{
fbxQ.Enqueue(child);
}
while (fbxQ.Count > 0)
{
var t = fbxQ.Dequeue();
SourceObjectInfo info;
if (!s_nameToInfo.TryGetValue(t.name, out info))
{
Debug.LogWarning(string.Format("Warning: {0} in FBX but not in converted hierarchy", t.name));
continue;
}
info.destGO = t.gameObject;
s_nameToInfo[t.name] = info;
foreach (Transform child in t)
{
fbxQ.Enqueue(child);
}
}
}
/// <summary>
/// Copy the object reference from fromProperty to the matching property on serializedObject.
/// Use nameMap to find the correct object reference to use.
/// </summary>
/// <param name="serializedObject"></param>
/// <param name="fromProperty"></param>
/// <param name="nameMap"></param>
internal static void CopySerializedProperty(SerializedObject serializedObject, SerializedProperty fromProperty)
{
var toProperty = serializedObject.FindProperty(fromProperty.propertyPath);
if (s_nameToInfo.TryGetValue(fromProperty.objectReferenceValue.name, out var info))
{
var value = info.destGO;
if (fromProperty.objectReferenceValue is GameObject)
{
toProperty.objectReferenceValue = value;
}
else
{
toProperty.objectReferenceValue = value.GetComponent(fromProperty.objectReferenceValue.GetType());
}
serializedObject.ApplyModifiedProperties();
}
else
{
// try to make sure any references in the scene are maintained for prefab instances
toProperty.objectReferenceValue = fromProperty.objectReferenceValue;
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Copy components on the 'from' object which is the object being converted,
/// over to the 'to' object which is the FBX.
///
/// Copy over everything except meshes and materials, since these
/// are already in the FBX.
///
/// The 'from' hierarchy is not modified.
///
/// Note: 'root' is the root object that is being converted
///
/// For each component, add to fixSceneRefsMap a mapping from the original component on "from"
/// to the new component on "to". In order to be able to fix scene references that were
/// pointing to the original component.
/// </summary>
internal static void CopyComponents(GameObject to, GameObject from, GameObject root, Dictionary<Object, Object> fixSceneRefsMap)
{
// copy components on "from" to "to". Don't want to copy over meshes and materials that were exported
var originalComponents = new List<Component>(from.GetComponents<Component>());
var destinationComponents = new List<Component>(to.GetComponents<Component>());
foreach (var fromComponent in originalComponents)
{
// ignore missing components
if (fromComponent == null)
{
continue;
}
// ignore MeshFilter and Transform, but still ensure scene references are maintained.
// Don't need to copy regular transform (except for the root object) as the values should already be correct in the FBX.
// Furthermore, copying transform values may result in overrides in the prefab, which is undesired as if
// the transform is updated in the FBX, it won't be in the prefab.
if (fromComponent is MeshFilter || (fromComponent is Transform && from != root))
{
fixSceneRefsMap.Add(fromComponent, to.GetComponent(fromComponent.GetType()));
continue;
}
// ignore FbxPrefab (when converting LinkedPrefabs)
// Also ignore RectTransform, since it is not currently possible to switch transforms
// in a prefab.
if (fromComponent is UnityEngine.Formats.Fbx.Exporter.FbxPrefab ||
fromComponent is RectTransform)
{
continue;
}
var json = EditorJsonUtility.ToJson(fromComponent);
if (string.IsNullOrEmpty(json))
{
// this happens for missing scripts
continue;
}
System.Type expectedType = fromComponent.GetType();
Component toComponent = null;
// Find the component to copy to.
for (int i = 0, n = destinationComponents.Count; i < n; i++)
{
// ignore missing components
if (destinationComponents[i] == null)
{
continue;
}
if (destinationComponents[i].GetType() == expectedType)
{
// We have found the component we are looking for,
// remove it so we don't try to copy to it again
toComponent = destinationComponents[i];
destinationComponents.RemoveAt(i);
break;
}
}
// If it's a particle system renderer, then check to see if it hasn't already
// been added when adding the particle system.
// An object can have only one ParticleSystem so there shouldn't be an issue of the renderer
// belonging to a different ParticleSystem.
if (!toComponent && fromComponent is ParticleSystemRenderer)
{
toComponent = to.GetComponent<ParticleSystemRenderer>();
}
if (!toComponent)
{
toComponent = to.AddComponent(fromComponent.GetType());
}
if (!toComponent)
{
// Failed to add component
Debug.LogWarningFormat("{0}: Failed to add component of type {1} to converted object", ModelExporter.PACKAGE_UI_NAME, fromComponent.GetType().Name);
continue;
}
fixSceneRefsMap.Add(fromComponent, toComponent);
// SkinnedMeshRenderer also stores the mesh.
// Make sure this is not copied over when the SkinnedMeshRenderer is updated,
// as we want to keep the mesh from the FBX not the scene.
if (fromComponent is SkinnedMeshRenderer)
{
var skinnedMesh = toComponent as SkinnedMeshRenderer;
var mesh = skinnedMesh.sharedMesh;
EditorJsonUtility.FromJsonOverwrite(json, toComponent);
skinnedMesh.sharedMesh = mesh;
}
else
{
EditorJsonUtility.FromJsonOverwrite(json, toComponent);
}
if (fromComponent is MeshCollider)
{
// UNI-27534: This fixes the issue where the mesh collider would not update to point to the mesh in the fbx after export
// Point the mesh included in the mesh collider to the mesh in the FBX file, which is the same as the one in mesh filter
var fromMeshCollider = from.GetComponent<MeshCollider>();
var fromMeshFilter = from.GetComponent<MeshFilter>();
// if the mesh collider isn't pointing to the same mesh as in the current mesh filter then don't
// do anything as it's probably pointing to a mesh in a different fbx
if (fromMeshCollider && fromMeshFilter && fromMeshCollider.sharedMesh == fromMeshFilter.sharedMesh)
{
var toFilter = to.GetComponent<MeshFilter>();
if (toFilter)
{
var toMeshCollider = toComponent as MeshCollider;
toMeshCollider.sharedMesh = toFilter.sharedMesh;
}
}
}
var serializedFromComponent = new SerializedObject(fromComponent);
var serializedToComponent = new SerializedObject(toComponent);
var fromProperty = serializedFromComponent.GetIterator();
fromProperty.Next(true); // skip generic field
// For SkinnedMeshRenderer, the bones array doesn't have visible children, but still needs to be copied over.
// For everything else, filtering by visible children in the while loop and then copying properties that don't have visible children,
// ensures that only the leaf properties are copied over. Copying other properties is not usually necessary and may break references that
// were not meant to be copied.
while (fromProperty.Next((fromComponent is SkinnedMeshRenderer) ? fromProperty.hasChildren : fromProperty.hasVisibleChildren))
{
if (!fromProperty.hasVisibleChildren)
{
// with Undo operations, copying m_Father reference causes issues. Also, it is not required as the reference is fixed when
// the transform is parented under the correct hierarchy (which happens before this).
if (fromProperty.propertyType == SerializedPropertyType.ObjectReference && fromProperty.propertyPath != "m_GameObject" &&
fromProperty.propertyPath != "m_Father" && fromProperty.objectReferenceValue &&
(fromProperty.objectReferenceValue is GameObject || fromProperty.objectReferenceValue is Component))
{
CopySerializedProperty(serializedToComponent, fromProperty);
}
}
}
}
}
}
}