// Assets/Editor/Client_ExportCleanPrefab.cs // Unity 2021.3+ / 2022.3+ // // Menus: // - Tools ? Assets Exporter ? Export Clean Prefab (Ctrl/Cmd + E) // - Tools ? Assets Exporter ? Export From Selected Folders // - Tools ? Assets Exporter ? Export Active Scene (each root as prefab) // - Tools ? Assets Exporter ? Export Active Scene (single prefab) // - Tools ? Assets Exporter ? Open Clean Export Settings // // Outputs (under the Root Folder from CleanExportSettings): // /Prefabs, /MeshesAndColliders, /Materials, /Textures, /Shaders, /Logs // // Features: // - Cancelable progress bar // - Detailed log // - Mesh/MeshCollider: always create new .asset // - Materials/Textures/Shaders: GUID-safe reuse; textures can be baked to PNG (2D only) // - Missing scripts removal + component whitelist // - Scene options: replace in current scene and/or create new scene // - Prefab saving done in a staging scene to avoid any leftover instance (prevents duplicates) // - Cubemap/3D/Array textures handled correctly (no PNG baking for non-2D) using UnityEngine; using UnityEditor; using UnityEngine.SceneManagement; using UnityEditor.SceneManagement; using UnityEngine.Rendering; // TextureDimension using System; using System.Text; using System.Collections.Generic; using System.IO; public static class Client_ExportCleanPrefab { // -------- Destination paths -------- private static string DEST_ROOT, DEST_PREFABS, DEST_MESHES, DEST_MATERIALS, DEST_TEXTURES, DEST_SHADERS, DEST_LOGS; // -------- Single-prefab scene export state -------- private static bool _singlePrefabMode = false; private static List _singlePrefabOriginalRoots = null; // -------- Progress / Cancel -------- private static bool _cancel = false; private static bool Progress(string info, float progress01) { if (EditorUtility.DisplayCancelableProgressBar("Export Clean Prefab", info, Mathf.Clamp01(progress01))) _cancel = true; return _cancel; } // Export result (used for scene actions) private class ExportResult { public GameObject source; public bool wasSceneObject; public Transform parent; public int siblingIndex; public int depth; public Vector3 worldPos; public Quaternion worldRot; public Vector3 worldScale; public Vector3 localPos; public Quaternion localRot; public Vector3 localScale; public string prefabPath; public GameObject prefabAsset; public string log; } // ---------------- MENUS ---------------- [MenuItem("Tools/Assets Exporter/Export Clean Prefab %e")] public static void ExportCleanPrefab_Menu() { var picked = new List(); picked.AddRange(Selection.gameObjects); picked.AddRange(Selection.GetFiltered(SelectionMode.Assets)); // prefab assets _singlePrefabMode = false; _singlePrefabOriginalRoots = null; RunExport(picked, "Selection"); } [MenuItem("Tools/Assets Exporter/Export From Selected Folders")] public static void ExportFromFolders_Menu() { var folderAssets = Selection.GetFiltered(SelectionMode.Assets); var folders = new List(); foreach (var a in folderAssets) { var path = AssetDatabase.GetAssetPath(a); if (AssetDatabase.IsValidFolder(path)) folders.Add(path); } var guids = (folders.Count > 0) ? AssetDatabase.FindAssets("t:Prefab", folders.ToArray()) : AssetDatabase.FindAssets("t:Prefab"); var prefabs = new List(); foreach (var g in guids) { var path = AssetDatabase.GUIDToAssetPath(g); var go = AssetDatabase.LoadAssetAtPath(path); if (go != null) prefabs.Add(go); } _singlePrefabMode = false; _singlePrefabOriginalRoots = null; RunExport(prefabs, folders.Count > 0 ? string.Join(", ", folders) : "(Whole Project)"); } [MenuItem("Tools/Assets Exporter/Export Active Scene (each root as prefab)")] public static void ExportActiveSceneRoots_Menu() { var scene = SceneManager.GetActiveScene(); if (!scene.IsValid() || !scene.isLoaded) { Debug.LogWarning("No active scene loaded."); return; } var roots = new List(); scene.GetRootGameObjects(roots); _singlePrefabMode = false; _singlePrefabOriginalRoots = null; RunExport(roots, $"Active Scene (roots): {scene.name}"); } [MenuItem("Tools/Assets Exporter/Export Active Scene (single prefab)")] public static void ExportActiveSceneAsSinglePrefab_Menu() { var scene = SceneManager.GetActiveScene(); if (!scene.IsValid() || !scene.isLoaded) { Debug.LogWarning("No active scene loaded."); return; } var roots = new List(); scene.GetRootGameObjects(roots); var group = new GameObject($"Scene_{scene.name}"); foreach (var r in roots) { var clone = UnityEngine.Object.Instantiate(r, group.transform); clone.name = r.name; } try { _singlePrefabMode = true; _singlePrefabOriginalRoots = roots; RunExport(new List { group }, $"Active Scene (single prefab): {scene.name}"); } finally { UnityEngine.Object.DestroyImmediate(group); _singlePrefabOriginalRoots = null; _singlePrefabMode = false; } } [MenuItem("Tools/Assets Exporter/Open Clean Export Settings")] public static void OpenSettings_Menu() { var settings = CleanExportSettings.LoadOrCreateAtDefaultPath(); Selection.activeObject = settings; EditorGUIUtility.PingObject(settings); } // ---------------- CORE RUNNER ---------------- private static void RunExport(List sources, string sourceLabel) { sources.RemoveAll(go => go == null); if (sources.Count == 0) { Debug.LogWarning("Select a GameObject in the Hierarchy, Prefab Mode, a Prefab asset, or folders/scenes containing objects."); return; } var settings = CleanExportSettings.LoadOrCreateAtDefaultPath(); ResolveDestPaths(settings); EnsureFolder(DEST_ROOT); EnsureFolder(DEST_PREFABS); EnsureFolder(DEST_MESHES); EnsureFolder(DEST_MATERIALS); EnsureFolder(DEST_TEXTURES); EnsureFolder(DEST_SHADERS); EnsureFolder(DEST_LOGS); EnsureFolder(settings.newSceneFolder); var allowed = BuildAllowedSet(settings); _cancel = false; var sb = new StringBuilder(); var start = DateTime.Now; sb.AppendLine($"Clean Export Report � {start:yyyy-MM-dd HH:mm:ss}"); sb.AppendLine($"Source: {sourceLabel}"); sb.AppendLine($"Items: {sources.Count}"); sb.AppendLine(new string('-', 60)); int done = 0; bool canceled = false; var results = new List(); try { for (int i = 0; i < sources.Count; i++) { var src = sources[i]; float baseP = (float)i / sources.Count; if (Progress($"Queued: {src.name} ({i + 1}/{sources.Count})", baseP)) { canceled = true; break; } try { var result = ExportOne(src, allowed, settings, (p, msg) => Progress($"{src.name} � {msg}", baseP + p / sources.Count)); if (_cancel) { canceled = true; sb.AppendLine($"CANCELED during: {src.name}"); break; } done++; results.Add(result); sb.AppendLine(result.log); } catch (Exception ex) { sb.AppendLine($"ERROR [{src.name}]: {ex.Message}"); } } } finally { EditorUtility.ClearProgressBar(); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); if (!canceled && results.Count > 0) { try { ApplySceneOptions(results, settings); } catch (Exception ex) { sb.AppendLine($"Scene actions ERROR: {ex.Message}"); } } var end = DateTime.Now; sb.AppendLine(new string('-', 60)); sb.AppendLine($"Completed: {done}/{sources.Count} {(canceled ? "(Canceled)" : "(OK)")}"); sb.AppendLine($"Elapsed: {(end - start)}"); string logPath = Path.Combine(DEST_LOGS, $"ExportReport_{DateTime.Now:yyyyMMdd_HHmmss}.txt").Replace("\\", "/"); File.WriteAllText(logPath, sb.ToString()); AssetDatabase.ImportAsset(logPath, ImportAssetOptions.ForceSynchronousImport); Debug.Log($"Clean Export finished: {done}/{sources.Count}. Report: {logPath}"); } private static ExportResult ExportOne( GameObject source, HashSet allowed, CleanExportSettings settings, Action subProgress) { // Snapshot original info (for scene actions) var wasSceneObject = source.scene.IsValid(); var parent = wasSceneObject ? source.transform.parent : null; var siblingIndex = wasSceneObject ? source.transform.GetSiblingIndex() : 0; var depth = wasSceneObject ? GetDepth(source.transform) : 0; var worldPos = wasSceneObject ? source.transform.position : Vector3.zero; var worldRot = wasSceneObject ? source.transform.rotation : Quaternion.identity; var worldScale = wasSceneObject ? source.transform.lossyScale : Vector3.one; var localPos = wasSceneObject ? source.transform.localPosition : Vector3.zero; var localRot = wasSceneObject ? source.transform.localRotation : Quaternion.identity; var localScale = wasSceneObject ? source.transform.localScale : Vector3.one; subProgress?.Invoke(0.02f, "Cloning"); var temp = UnityEngine.Object.Instantiate(source); temp.name = source.name + "_TEMP"; string prefabPath = (DEST_PREFABS + "/" + Sanitize(source.name) + ".prefab").Replace("\\", "/"); GameObject prefabAsset = null; var sb = new StringBuilder(); // Stage & save prefab in an empty additive scene (prevents leftovers in user scene) var staging = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive); try { SceneManager.MoveGameObjectToScene(temp, staging); if (_cancel) return new ExportResult { source = source, wasSceneObject = wasSceneObject, log = $"CANCELED [{source.name}] (before clean)" }; subProgress?.Invoke(0.15f, "Cleaning components"); CleanHierarchy(temp, allowed, settings.removeMissingScripts); if (_cancel) return new ExportResult { source = source, wasSceneObject = wasSceneObject, log = $"CANCELED [{source.name}] (after clean)" }; subProgress?.Invoke(0.55f, "Duplicating meshes/terrain"); SaveMeshesAndTerrain(temp); if (_cancel) return new ExportResult { source = source, wasSceneObject = wasSceneObject, log = $"CANCELED [{source.name}] (after meshes/terrain)" }; subProgress?.Invoke(0.85f, "Duplicating materials/textures/shaders"); SaveMaterialsTexturesShaders(temp, settings); if (_cancel) return new ExportResult { source = source, wasSceneObject = wasSceneObject, log = $"CANCELED [{source.name}] (after materials)" }; subProgress?.Invoke(0.98f, "Saving prefab"); PrefabUtility.SaveAsPrefabAsset(temp, prefabPath); // never Connect prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); sb.AppendLine($"OK [{source.name}] ? {prefabPath}"); } finally { if (temp != null) UnityEngine.Object.DestroyImmediate(temp); EditorSceneManager.CloseScene(staging, true); } return new ExportResult { source = source, wasSceneObject = wasSceneObject, parent = parent, siblingIndex = siblingIndex, depth = depth, worldPos = worldPos, worldRot = worldRot, worldScale = worldScale, localPos = localPos, localRot = localRot, localScale = localScale, prefabPath = prefabPath, prefabAsset = prefabAsset, log = sb.ToString().TrimEnd() }; } // ---------------- CLEANUP ---------------- private static void CleanHierarchy(GameObject root, HashSet allowed, bool removeMissing) { var all = root.GetComponentsInChildren(true); foreach (var t in all) { if (_cancel) return; var go = t.gameObject; if (removeMissing) GameObjectUtility.RemoveMonoBehavioursWithMissingScript(go); var comps = go.GetComponents(); foreach (var c in comps) { if (_cancel) return; if (c == null) continue; if (!allowed.Contains(c.GetType())) UnityEngine.Object.DestroyImmediate(c, true); } } } // ---------------- SAVE: Meshes / Terrain ---------------- private static void SaveMeshesAndTerrain(GameObject root) { if (_cancel) return; var all = root.GetComponentsInChildren(true); foreach (var t in all) { if (_cancel) return; var go = t.gameObject; var mf = go.GetComponent(); if (mf && mf.sharedMesh) mf.sharedMesh = DuplicateMesh_NoReuse(mf.sharedMesh, go.name + "_Mesh"); var smr = go.GetComponent(); if (smr && smr.sharedMesh) smr.sharedMesh = DuplicateMesh_NoReuse(smr.sharedMesh, go.name + "_SkinnedMesh"); var mc = go.GetComponent(); if (mc && mc.sharedMesh) { if (mf && mf.sharedMesh == mc.sharedMesh) mc.sharedMesh = mf.sharedMesh; else if (smr && smr.sharedMesh == mc.sharedMesh) mc.sharedMesh = smr.sharedMesh; else mc.sharedMesh = DuplicateMesh_NoReuse(mc.sharedMesh, go.name + "_Collider"); } var terrain = go.GetComponent(); if (terrain && terrain.terrainData) terrain.terrainData = DuplicateTerrain_WithReuse(terrain.terrainData, go.name + "_TerrainData"); var tcol = go.GetComponent(); if (tcol && tcol.terrainData) { if (terrain && terrain.terrainData) tcol.terrainData = terrain.terrainData; else tcol.terrainData = DuplicateTerrain_WithReuse(tcol.terrainData, go.name + "_TerrainDataCol"); } } } // ---------------- SAVE: Materials / Textures / Shaders ---------------- private static void SaveMaterialsTexturesShaders(GameObject root, CleanExportSettings settings) { if (_cancel) return; var all = root.GetComponentsInChildren(true); foreach (var rend in all) { if (_cancel) return; var mats = rend.sharedMaterials; for (int i = 0; i < mats.Length; i++) { if (_cancel) return; mats[i] = DuplicateMaterial_GuidSafe(mats[i], rend.gameObject.name + "_Mat_" + i, settings); } rend.sharedMaterials = mats; } } // ---------------- SCENE OPTIONS ---------------- private static void ApplySceneOptions(List results, CleanExportSettings settings) { if (!settings.replaceInScene && !settings.createNewScene) return; // Single-prefab mode (whole scene packed into one prefab) if (_singlePrefabMode && results.Count == 1) { var r = results[0]; if (settings.replaceInScene && r.prefabAsset != null && _singlePrefabOriginalRoots != null) { foreach (var root in _singlePrefabOriginalRoots) if (root != null) UnityEngine.Object.DestroyImmediate(root); var instance = PrefabUtility.InstantiatePrefab(r.prefabAsset, SceneManager.GetActiveScene()) as GameObject; instance.name = r.prefabAsset.name; instance.transform.position = Vector3.zero; instance.transform.rotation = Quaternion.identity; instance.transform.localScale = Vector3.one; } if (settings.createNewScene && r.prefabAsset != null) { var newScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive); var inst = PrefabUtility.InstantiatePrefab(r.prefabAsset, newScene) as GameObject; inst.name = r.prefabAsset.name; inst.transform.position = Vector3.zero; inst.transform.rotation = Quaternion.identity; inst.transform.localScale = Vector3.one; string scenePath = AssetDatabase.GenerateUniqueAssetPath( (settings.newSceneFolder.TrimEnd('/') + $"/{settings.newSceneNamePrefix}Scene_{DateTime.Now:yyyyMMdd_HHmmss}.unity").Replace("\\", "/")); EditorSceneManager.SaveScene(newScene, scenePath); Debug.Log($"New scene created: {scenePath}"); } return; } // ---------- Standard mode: many independent objects ---------- // Sort by depth (parents first) to rebuild hierarchy deterministically results.Sort((a, b) => a.depth.CompareTo(b.depth)); if (settings.replaceInScene) { var scene = SceneManager.GetActiveScene(); var map = new Dictionary(); // original -> new // 1) Instantiate all replacements as roots foreach (var r in results) { if (!r.wasSceneObject || r.prefabAsset == null) continue; var inst = PrefabUtility.InstantiatePrefab(r.prefabAsset, scene) as GameObject; inst.name = r.prefabAsset.name; var tr = inst.transform; tr.SetParent(null, true); tr.position = r.worldPos; tr.rotation = r.worldRot; tr.localScale = r.worldScale; map[r.source] = inst; } // 2) Rebuild parenting & local TRS (prefer the new parent if replaced) foreach (var r in results) { if (!r.wasSceneObject || !map.ContainsKey(r.source)) continue; var instTr = map[r.source].transform; Transform newParent = null; if (r.parent != null && map.TryGetValue(r.parent.gameObject, out var parentInst)) newParent = parentInst.transform; else newParent = r.parent; if (newParent != null) instTr.SetParent(newParent, false); // do not keep world; we set locals next instTr.SetSiblingIndex(Mathf.Clamp(r.siblingIndex, 0, (newParent ? newParent.childCount : instTr.root.gameObject.scene.rootCount))); instTr.localPosition = r.localPos; instTr.localRotation = r.localRot; instTr.localScale = r.localScale; } // 3) Destroy originals foreach (var r in results) { if (!r.wasSceneObject || r.source == null) continue; UnityEngine.Object.DestroyImmediate(r.source); } EditorSceneManager.MarkSceneDirty(scene); } if (settings.createNewScene) { var newScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive); var map = new Dictionary(); // original -> new in new scene // 1) Instantiate all as roots foreach (var r in results) { if (r.prefabAsset == null) continue; var inst = PrefabUtility.InstantiatePrefab(r.prefabAsset, newScene) as GameObject; inst.name = r.prefabAsset.name; var tr = inst.transform; tr.SetParent(null, true); tr.position = r.worldPos; tr.rotation = r.worldRot; tr.localScale = r.worldScale; if (r.wasSceneObject && r.source != null) map[r.source] = inst; } // 2) Rebuild parenting & locals foreach (var r in results) { if (!r.wasSceneObject || r.source == null) continue; if (!map.ContainsKey(r.source)) continue; var instTr = map[r.source].transform; if (r.parent != null && map.TryGetValue(r.parent.gameObject, out var parentInst)) { instTr.SetParent(parentInst.transform, false); instTr.localPosition = r.localPos; instTr.localRotation = r.localRot; instTr.localScale = r.localScale; instTr.SetSiblingIndex(r.siblingIndex); } } string scenePath = AssetDatabase.GenerateUniqueAssetPath( (settings.newSceneFolder.TrimEnd('/') + $"/{settings.newSceneNamePrefix}Scene_{DateTime.Now:yyyyMMdd_HHmmss}.unity").Replace("\\", "/")); EditorSceneManager.SaveScene(newScene, scenePath); Debug.Log($"New scene created: {scenePath}"); } } // ---------------- HELPERS ---------------- private static int GetDepth(Transform t) { int d = 0; while (t != null) { d++; t = t.parent; } return d; } private static void ResolveDestPaths(CleanExportSettings s) { string root = s.rootFolder; if (string.IsNullOrWhiteSpace(root)) root = "Assets/_Client_Assets"; root = root.Replace("\\", "/"); if (!root.StartsWith("Assets")) root = "Assets/" + root.TrimStart('/'); DEST_ROOT = root; DEST_PREFABS = root + "/Prefabs"; DEST_MESHES = root + "/MeshesAndColliders"; DEST_MATERIALS = root + "/Materials"; DEST_TEXTURES = root + "/Textures"; DEST_SHADERS = root + "/Shaders"; DEST_LOGS = root + "/Logs"; } private static HashSet BuildAllowedSet(CleanExportSettings s) { var set = new HashSet { typeof(Transform) }; // Core if (s.meshFilter) set.Add(typeof(MeshFilter)); if (s.meshRenderer) set.Add(typeof(MeshRenderer)); if (s.skinnedMesh) set.Add(typeof(SkinnedMeshRenderer)); if (s.meshCollider) set.Add(typeof(MeshCollider)); if (s.boxCollider) set.Add(typeof(BoxCollider)); if (s.sphereCollider) set.Add(typeof(SphereCollider)); if (s.capsuleCollider) set.Add(typeof(CapsuleCollider)); if (s.terrain) set.Add(typeof(Terrain)); if (s.terrainCollider) set.Add(typeof(TerrainCollider)); // Optional if (s.camera) set.Add(typeof(Camera)); if (s.rigidbody) set.Add(typeof(Rigidbody)); if (s.animator) set.Add(typeof(Animator)); if (s.light) set.Add(typeof(Light)); if (s.audioSource) set.Add(typeof(AudioSource)); if (s.lodGroup) set.Add(typeof(LODGroup)); if (s.rectTransform) set.Add(typeof(RectTransform)); if (s.trailRenderer) set.Add(typeof(TrailRenderer)); if (s.lineRenderer) set.Add(typeof(LineRenderer)); if (s.spriteRenderer) set.Add(typeof(SpriteRenderer)); if (s.reflectionProbe) set.Add(typeof(ReflectionProbe)); if (s.characterController) set.Add(typeof(CharacterController)); foreach (var typeName in s.extraAllowedTypes) { if (string.IsNullOrWhiteSpace(typeName)) continue; var t = Type.GetType(typeName.Trim()) ?? Type.GetType("UnityEngine." + typeName.Trim() + ", UnityEngine"); if (t != null && typeof(Component).IsAssignableFrom(t)) set.Add(t); else Debug.LogWarning($"[CleanExport] Unknown extra type: {typeName}"); } return set; } private static bool IsBuiltinPath(string path) { if (string.IsNullOrEmpty(path)) return true; path = path.Replace("\\", "/"); return path.StartsWith("Resources/unity_builtin_extra") || path.StartsWith("Library/unity default resources"); } private static string Sanitize(string name) { foreach (char c in Path.GetInvalidFileNameChars()) name = name.Replace(c.ToString(), "_"); return string.IsNullOrEmpty(name) ? "Asset" : name; } private static void EnsureFolder(string path) { if (AssetDatabase.IsValidFolder(path)) return; var parent = Path.GetDirectoryName(path)?.Replace("\\", "/"); var leaf = Path.GetFileName(path); if (!string.IsNullOrEmpty(parent) && !AssetDatabase.IsValidFolder(parent)) EnsureFolder(parent); AssetDatabase.CreateFolder(parent ?? "Assets", leaf); } private static bool TryGuid(UnityEngine.Object obj, out string guid) { guid = null; if (obj == null) return false; var path = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(path) || IsBuiltinPath(path)) return false; return AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out guid, out long _); } // ---- Mesh / Terrain duplication ---- private static Mesh DuplicateMesh_NoReuse(Mesh src, string baseName) { var copy = UnityEngine.Object.Instantiate(src); copy.name = Sanitize(baseName); string dstPath = AssetDatabase.GenerateUniqueAssetPath((DEST_MESHES + "/" + copy.name + ".asset").Replace("\\", "/")); AssetDatabase.CreateAsset(copy, dstPath); return AssetDatabase.LoadAssetAtPath(dstPath); } private static TerrainData DuplicateTerrain_WithReuse(TerrainData src, string baseName) { string dstPath = (DEST_MESHES + "/" + Sanitize(baseName) + ".asset").Replace("\\", "/"); var existing = AssetDatabase.LoadAssetAtPath(dstPath); if (existing != null) return existing; var copy = UnityEngine.Object.Instantiate(src); copy.name = Sanitize(baseName); AssetDatabase.CreateAsset(copy, dstPath); return AssetDatabase.LoadAssetAtPath(dstPath); } // ---- Shader duplication ---- private static Shader DuplicateShader_GuidSafe(Shader shader) { if (shader == null) return null; if (TryGuid(shader, out var guid)) { string dstPath = (DEST_SHADERS + "/Shader_" + guid + ".shader").Replace("\\", "/"); var existing = AssetDatabase.LoadAssetAtPath(dstPath); if (existing != null) return existing; var srcPath = AssetDatabase.GetAssetPath(shader); AssetDatabase.CopyAsset(srcPath, dstPath); return AssetDatabase.LoadAssetAtPath(dstPath); } else { string listPath = Path.Combine(DEST_SHADERS, "BUILTIN_SHADERS.txt"); File.AppendAllText(Path.GetFullPath(listPath), shader.name + "\n"); AssetDatabase.ImportAsset(listPath, ImportAssetOptions.ForceSynchronousImport); return shader; } } // ---- Material duplication (uses texture dimension-aware duplication) ---- private static Material DuplicateMaterial_GuidSafe(Material src, string baseNameForNew, CleanExportSettings settings) { if (src == null) return null; Material newMat; bool created = false; if (TryGuid(src, out var guid)) { string dstPath = (DEST_MATERIALS + "/MAT_" + guid + ".mat").Replace("\\", "/"); newMat = AssetDatabase.LoadAssetAtPath(dstPath); if (newMat == null) { newMat = new Material(src) { name = "MAT_" + guid }; AssetDatabase.CreateAsset(newMat, dstPath); created = true; } } else { string dstPath = AssetDatabase.GenerateUniqueAssetPath((DEST_MATERIALS + "/" + Sanitize(baseNameForNew) + ".mat").Replace("\\", "/")); newMat = new Material(src) { name = Path.GetFileNameWithoutExtension(dstPath) }; AssetDatabase.CreateAsset(newMat, dstPath); created = true; } if (created) { var newShader = DuplicateShader_GuidSafe(newMat.shader); if (newShader != null) newMat.shader = newShader; int propCount = ShaderUtil.GetPropertyCount(newMat.shader); for (int i = 0; i < propCount; i++) { if (ShaderUtil.GetPropertyType(newMat.shader, i) != ShaderUtil.ShaderPropertyType.TexEnv) continue; string propName = ShaderUtil.GetPropertyName(newMat.shader, i); var tex = newMat.GetTexture(propName); if (tex == null) continue; // Detect shader slot dimension TextureDimension dim = TextureDimension.Tex2D; #if UNITY_2021_3_OR_NEWER dim = ShaderUtil.GetPropertyTextureDimension(newMat.shader, i); #endif Texture dup = null; switch (dim) { case TextureDimension.Cube: dup = DuplicateCubemap_GuidSafe(tex as Cubemap); // only Cubemap makes sense here break; case TextureDimension.Tex2D: dup = DuplicateTexture2D_GuidSafeOrBake(tex, baseNameForNew + "_" + propName, settings); break; default: // 3D / 2DArray / CubeArray / Any other ? try GUID copy if available, else keep original dup = DuplicateByGuidIfPossible(tex); if (dup == null) dup = tex; break; } if (dup != null) newMat.SetTexture(propName, dup); } EditorUtility.SetDirty(newMat); } return newMat; } // ---- Texture helpers ---- // For non-2D textures, try GUID copy, else return null (caller keeps original) private static Texture DuplicateByGuidIfPossible(Texture src) { if (src == null) return null; if (TryGuid(src, out var guid)) { string srcPath = AssetDatabase.GetAssetPath(src); string ext = Path.GetExtension(srcPath); if (string.IsNullOrEmpty(ext)) ext = ".asset"; string dstPath = (DEST_TEXTURES + "/TEX_" + guid + ext).Replace("\\", "/"); var existing = AssetDatabase.LoadAssetAtPath(dstPath); if (existing != null) return existing; AssetDatabase.CopyAsset(srcPath, dstPath); AssetDatabase.ImportAsset(dstPath, ImportAssetOptions.ForceSynchronousImport); return AssetDatabase.LoadAssetAtPath(dstPath); } return null; } private static Cubemap DuplicateCubemap_GuidSafe(Cubemap src) { if (src == null) return null; // Cubemaps cannot be baked to PNG; copy only when there is a real asset, otherwise keep original var copy = DuplicateByGuidIfPossible(src) as Cubemap; return copy != null ? copy : src; } private static Texture DuplicateTexture2D_GuidSafeOrBake(Texture src, string baseNameForBaked, CleanExportSettings s) { if (src == null) return null; // If it's a real Texture2D asset and baking isn't forced, copy by GUID if (!s.forceBakeAllTextures && src is Texture2D && TryGuid(src, out var guid)) { string srcPath = AssetDatabase.GetAssetPath(src); string ext = Path.GetExtension(srcPath); if (string.IsNullOrEmpty(ext)) ext = ".asset"; string dstPath = (DEST_TEXTURES + "/TEX_" + guid + ext).Replace("\\", "/"); var existing = AssetDatabase.LoadAssetAtPath(dstPath); if (existing != null) return existing; AssetDatabase.CopyAsset(srcPath, dstPath); ApplyTextureImporterSettings(dstPath, s); return AssetDatabase.LoadAssetAtPath(dstPath); } // Otherwise bake to PNG (only for Texture2D-like sources) // If not Texture2D, try to blit anyway (RenderTexture works) ? result is a 2D Texture string bakedDst = AssetDatabase.GenerateUniqueAssetPath((DEST_TEXTURES + "/" + Sanitize(baseNameForBaked) + ".png").Replace("\\", "/")); int w = Mathf.Max(1, src.width); int h = Mathf.Max(1, src.height); var rt = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); var prev = RenderTexture.active; try { Graphics.Blit(src, rt); RenderTexture.active = rt; var readable = new Texture2D(w, h, TextureFormat.RGBA32, false, false); readable.ReadPixels(new Rect(0, 0, w, h), 0, 0); readable.Apply(); File.WriteAllBytes(bakedDst, readable.EncodeToPNG()); UnityEngine.Object.DestroyImmediate(readable); AssetDatabase.ImportAsset(bakedDst, ImportAssetOptions.ForceSynchronousImport); ApplyTextureImporterSettings(bakedDst, s); return AssetDatabase.LoadAssetAtPath(bakedDst); } finally { RenderTexture.active = prev; RenderTexture.ReleaseTemporary(rt); } } private static void ApplyTextureImporterSettings(string assetPath, CleanExportSettings s) { var ti = AssetImporter.GetAtPath(assetPath) as TextureImporter; if (ti == null) return; ti.mipmapEnabled = s.pngMipmap; ti.maxTextureSize = s.pngMaxSize; ti.textureCompression = s.pngCompression; ti.crunchedCompression = s.pngUseCrunch; if (s.pngUseCrunch) ti.compressionQuality = Mathf.Clamp(s.pngCrunchQuality, 0, 100); AssetDatabase.WriteImportSettingsIfDirty(assetPath); AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate); } }