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) { } } /// /// Class for converting an exported FBX to a Prefab Variant. /// 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"; /// /// 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) /// [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(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"); } /// /// Validate the menu items defined above. /// [MenuItem(GameObjectMenuItemName, true, 30)] [MenuItem(AssetsMenuItemName, true, 30)] internal static bool OnValidateMenuItem() { return true; } /// /// Gets the export settings. /// internal static ExportSettings ExportSettings { get { return ExportSettings.instance; } } /// /// Return true if the given set contains only prefab assets on disk, /// and nothing from the scene. /// /// 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; } /// /// 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. /// /// list of instanced Model Prefabs /// Unity game objects to convert to Model Prefab instances /// Path to save Model Prefab; use FbxExportSettings if null 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(); 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(); } /// /// 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. /// /// scene object with reference to replace /// mapping from original object to new object /// root of the hierarchy being converted internal static void FixSceneReferenceToObject(Object sceneObj, GameObject toConvertRoot, Dictionary 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(); 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 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 /// /// Helper for getting a property from an instance with reflection. /// /// /// /// /// 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; } } /// /// Returns a list of GameObjects in the scene that contain references to the given object. /// /// /// internal static List 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(); 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 /// /// 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: /// /// /// 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. /// /// /// 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. /// /// /// A Prefab asset, /// then the method exports a new FBX asset and creates a new Prefab Variant /// pointing to the FBX. /// /// /// /// The prefab variant linked to an fbx file. /// Object to convert. /// 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. /// 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. /// 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. /// 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. /// /// Export options to use for exporting the model asset /// to convert to a Prefab. /// 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 { /// /// The exported GameObject that will replace the source. /// public GameObject destGO; #if UNITY_2021_2_OR_NEWER /// /// 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. /// public List sceneObjectsWithReference; #endif // UNITY_2021_2_OR_NEWER public SourceObjectInfo(GameObject dest) { destGO = dest; #if UNITY_2021_2_OR_NEWER sceneObjectsWithReference = new List(); #endif // UNITY_2021_2_OR_NEWER } } /// /// Dictionary from name of source object (in toConvert hierarchy) to info. /// internal static Dictionary s_nameToInfo = new Dictionary(); /// /// 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. /// /// 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(); 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); } } } /// /// Check whether Convert will be exporting an fbx file, /// or reusing one. /// internal static bool WillExportFbx(GameObject toConvert) { return GetFbxAssetOrNull(toConvert) == null; } /// /// 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. /// /// GameObject for which we want an fbx asset /// 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. /// Export will create this /// file. Overrides fbxDirectoryFullPath. Ignored if toConvert is an /// fbx asset or an instance of an fbx. /// The root of a model prefab asset. 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; } /// /// 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. /// /// The root of a model prefab asset, or null. /// A gameobject either in the scene or in the assets folder. 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; } } /// /// 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. /// /// new file name. /// Filename. 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; } /// /// 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 /// /// Export set. internal static void EnforceUniqueNames(IEnumerable exportSet) { Dictionary NameToIndexMap = new Dictionary(); string format = "{0} {1}"; Queue queue = new Queue(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); } } } /// /// Updates the meshes and materials of the exported GameObjects /// to link to those imported from the FBX. /// /// GameObject to update. /// Source to update from. 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(); 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() { { 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); } } } /// /// 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. /// /// Dictionary containing the name to source game object. /// Destination GameObject. /// Source GameObject. 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(); 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); } } } /// /// Copy the object reference from fromProperty to the matching property on serializedObject. /// Use nameMap to find the correct object reference to use. /// /// /// /// 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(); } } /// /// 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. /// internal static void CopyComponents(GameObject to, GameObject from, GameObject root, Dictionary fixSceneRefsMap) { // copy components on "from" to "to". Don't want to copy over meshes and materials that were exported var originalComponents = new List(from.GetComponents()); var destinationComponents = new List(to.GetComponents()); 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(); } 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(); var fromMeshFilter = from.GetComponent(); // 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(); 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); } } } } } } }