using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using UnityEngine.UI; #region 单线程 public class PsbToUGUIWindow : EditorWindow { [Header("Inputs")] private GameObject sourceRoot; private Canvas targetCanvas; [Header("Config")] private float referencePPU = 100f; // Match PSD Importer Pixels Per Unit private bool copyAlphaFromRenderer = true; private bool preserveSortingOrder = true; private bool addRaycastTargets = false; // For UI images; off if purely visual private bool autoCreateCanvasIfMissing = true; private string parentName = "PSB_UI"; // 新增 Toggle private bool exportAsPng = false; [MenuItem("Tools/PSB → UGUI Converter")] public static void ShowWindow() { var win = GetWindow("PSB → UGUI"); win.minSize = new Vector2(420, 360); } private void OnGUI() { EditorGUILayout.LabelField("Source", EditorStyles.boldLabel); sourceRoot = (GameObject)EditorGUILayout.ObjectField( new GUIContent("PSB root (scene GameObject)", "Select the instantiated PSB root that contains SpriteRenderers"), sourceRoot, typeof(GameObject), true); EditorGUILayout.Space(); EditorGUILayout.LabelField("Target", EditorStyles.boldLabel); targetCanvas = (Canvas)EditorGUILayout.ObjectField( new GUIContent("Target Canvas", "Canvas to place generated UI under"), targetCanvas, typeof(Canvas), true); autoCreateCanvasIfMissing = EditorGUILayout.Toggle( new GUIContent("Auto-create canvas if missing", "Create an Overlay Canvas+EventSystem when none provided"), autoCreateCanvasIfMissing ); EditorGUILayout.Space(); EditorGUILayout.LabelField("Conversion settings", EditorStyles.boldLabel); referencePPU = EditorGUILayout.FloatField( new GUIContent("Reference Pixels Per Unit", "Use the PSD Importer PPU (often 100). Controls world→UI pixel mapping"), referencePPU ); copyAlphaFromRenderer = EditorGUILayout.Toggle( new GUIContent("Copy alpha from SpriteRenderer color", "Image color.a = SpriteRenderer.color.a"), copyAlphaFromRenderer ); addRaycastTargets = EditorGUILayout.Toggle( new GUIContent("Enable raycast target on Images", "Turn off for purely decorative layers"), addRaycastTargets ); preserveSortingOrder = EditorGUILayout.Toggle( new GUIContent("Preserve sorting order", "Use sortingOrder to set sibling order"), preserveSortingOrder ); parentName = EditorGUILayout.TextField( new GUIContent("Parent name", "Name of the generated UI root under the Canvas"), parentName ); // 新增 Toggle exportAsPng = EditorGUILayout.Toggle( new GUIContent("Export sprites as PNG", "If checked, each sprite will be exported as a PNG file"), exportAsPng ); EditorGUILayout.Space(); using (new EditorGUI.DisabledScope(sourceRoot == null)) { if (GUILayout.Button("Convert to UGUI")) { Convert(); } } } private void Convert() { if (sourceRoot == null) { ShowNotification(new GUIContent("Select a source root.")); return; } Canvas canvas = targetCanvas; if (canvas == null && autoCreateCanvasIfMissing) { var canvasGO = new GameObject("Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); canvas = canvasGO.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; var scaler = canvasGO.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.referencePixelsPerUnit = referencePPU; if (FindObjectOfType() == null) { var es = new GameObject("EventSystem", typeof(UnityEngine.EventSystems.EventSystem), typeof(UnityEngine.EventSystems.StandaloneInputModule)); Undo.RegisterCreatedObjectUndo(es, "Create EventSystem"); } Undo.RegisterCreatedObjectUndo(canvasGO, "Create Canvas"); } if (canvas == null) { ShowNotification(new GUIContent("Provide a Canvas or enable auto-create.")); return; } GameObject uiRootGO = new GameObject(string.IsNullOrEmpty(parentName) ? "PSB_UI" : parentName, typeof(RectTransform)); Undo.RegisterCreatedObjectUndo(uiRootGO, "Create UI Root"); uiRootGO.transform.SetParent(canvas.transform, false); var uiRootRT = uiRootGO.GetComponent(); uiRootRT.anchorMin = new Vector2(0.5f, 0.5f); uiRootRT.anchorMax = new Vector2(0.5f, 0.5f); uiRootRT.pivot = new Vector2(0.5f, 0.5f); uiRootRT.anchoredPosition = Vector2.zero; uiRootRT.localScale = Vector3.one; var mapping = new Dictionary(); mapping[sourceRoot.transform] = uiRootRT; var all = sourceRoot.GetComponentsInChildren(true) .OrderBy(t => t == sourceRoot.transform ? -1 : 0) .ThenBy(t => t.GetSiblingIndex()) .ToList(); foreach (var t in all) { if (t == sourceRoot.transform) continue; var parent = t.parent; if (!mapping.TryGetValue(parent, out var parentRT)) { Debug.LogWarning($"Parent mapping missing for {t.name}. Skipping."); continue; } var containerGO = new GameObject(t.name, typeof(RectTransform)); Undo.RegisterCreatedObjectUndo(containerGO, "Create UI Container"); var rt = containerGO.GetComponent(); containerGO.transform.SetParent(parentRT, false); rt.localPosition = t.localPosition * referencePPU; rt.localRotation = t.localRotation; rt.localScale = t.localScale; rt.anchorMin = new Vector2(0.5f, 0.5f); rt.anchorMax = new Vector2(0.5f, 0.5f); rt.pivot = new Vector2(0.5f, 0.5f); mapping[t] = rt; } // Create UI Images for SpriteRenderers foreach (var kv in mapping.ToList()) { var src = kv.Key; var dstRT = kv.Value; var sr = src.GetComponent(); if (sr == null || sr.sprite == null) continue; Sprite copiedSprite = null; if (exportAsPng) { // ===== 导出 PNG 文件(修复尺寸问题)===== copiedSprite = ExportSpriteAsPNG(sr.sprite, sourceRoot); if (copiedSprite == null) { Debug.LogWarning($"导出 PNG 失败,使用原始 Sprite。源对象: {src.name}"); copiedSprite = sr.sprite; } } else { copiedSprite = sr.sprite; if (copiedSprite == null) { Debug.LogWarning($"原始 Sprite 为空,跳过。源对象: {src.name}"); continue; } } // Add Image var img = dstRT.gameObject.AddComponent(); img.sprite = copiedSprite; img.preserveAspect = false; img.raycastTarget = addRaycastTargets; if (copyAlphaFromRenderer) { var c = img.color; c.a = sr.color.a; img.color = c; } img.SetNativeSize(); dstRT.pivot = copiedSprite.pivot / new Vector2(copiedSprite.rect.width, copiedSprite.rect.height); } if (preserveSortingOrder) { var parents = mapping.Values.Select(rt => rt.parent).Distinct().Where(p => p != null); foreach (var p in parents) { var children = new List(); for (int i = 0; i < p.childCount; i++) { children.Add(p.GetChild(i) as RectTransform); } children = children .OrderBy(rt => { var correspondingSrc = mapping.FirstOrDefault(x => x.Value == rt).Key; var sr = correspondingSrc != null ? correspondingSrc.GetComponent() : null; int order = sr != null ? sr.sortingOrder : 0; return order; }) .ThenBy(rt => rt.name) .ToList(); for (int i = 0; i < children.Count; i++) { children[i].SetSiblingIndex(i); } } } Selection.activeObject = uiRootGO; EditorGUIUtility.PingObject(uiRootGO); Debug.Log(exportAsPng ? "PSB → UGUI conversion completed (sprites exported as PNG and assigned)." : "PSB → UGUI conversion completed (using original psb sprites)."); } /// /// 将 Sprite 导出为 PNG 文件,并保持原始尺寸和 PPU 设置 /// private Sprite ExportSpriteAsPNG(Sprite originalSprite, GameObject sourceRoot) { try { // 获取原始 Sprite 的信息 Texture2D srcTex = originalSprite.texture; Rect rect = originalSprite.rect; int w = Mathf.RoundToInt(rect.width); int h = Mathf.RoundToInt(rect.height); // 获取原始 PPU 和 pivot float originalPPU = originalSprite.pixelsPerUnit; Vector2 pivotNormalized = new Vector2( originalSprite.pivot.x / rect.width, originalSprite.pivot.y / rect.height ); // 确定保存路径 Object prefab = PrefabUtility.GetCorrespondingObjectFromSource(sourceRoot); string psbPath = AssetDatabase.GetAssetPath(prefab); if (string.IsNullOrEmpty(psbPath)) { psbPath = AssetDatabase.GetAssetPath(originalSprite); } string psbFolder = Path.GetDirectoryName(psbPath); if (string.IsNullOrEmpty(psbFolder)) { Debug.LogError($"无法获取有效的 psb 文件夹路径,源对象: {sourceRoot.name}"); return null; } string psbName = Path.GetFileNameWithoutExtension(psbPath); string targetFolder = Path.Combine(psbFolder, psbName); if (!AssetDatabase.IsValidFolder(targetFolder)) { AssetDatabase.CreateFolder(psbFolder, psbName); } // 使用 RenderTexture 导出 PNG RenderTexture rt = RenderTexture.GetTemporary(srcTex.width, srcTex.height, 0, RenderTextureFormat.ARGB32); Graphics.Blit(srcTex, rt); RenderTexture.active = rt; Texture2D outTex = new Texture2D(w, h, TextureFormat.RGBA32, false); outTex.ReadPixels(new Rect(rect.x, rect.y, rect.width, rect.height), 0, 0); outTex.Apply(); RenderTexture.active = null; RenderTexture.ReleaseTemporary(rt); // 保存 PNG 文件 byte[] pngData = outTex.EncodeToPNG(); string pngPath = Path.Combine(targetFolder, originalSprite.name + ".png"); pngPath = AssetDatabase.GenerateUniqueAssetPath(pngPath); if (string.IsNullOrEmpty(pngPath)) { Debug.LogWarning($"PNG 路径为空,跳过保存。源对象: {sourceRoot.name}"); return null; } File.WriteAllBytes(pngPath, pngData); DestroyImmediate(outTex); // 导入并配置纹理 AssetDatabase.ImportAsset(pngPath); var texImp = AssetImporter.GetAtPath(pngPath) as TextureImporter; if (texImp != null) { texImp.textureType = TextureImporterType.Sprite; texImp.spritePixelsPerUnit = originalPPU; // 关键:使用原始 PPU texImp.alphaIsTransparency = true; texImp.spritePivot = pivotNormalized; texImp.mipmapEnabled = false; texImp.filterMode = FilterMode.Bilinear; texImp.textureCompression = TextureImporterCompression.Uncompressed; texImp.SaveAndReimport(); } Sprite result = AssetDatabase.LoadAssetAtPath(pngPath); if (result == null) { Debug.LogError($"无法加载 Sprite: {pngPath}"); return null; } return result; } catch (System.Exception e) { Debug.LogError($"导出 PNG 时发生错误: {e.Message}\n{e.StackTrace}"); return null; } } } #endregion #region 多线程Jobs public class PsbToUGUIWindowOptimized : EditorWindow { [Header("Inputs")] private GameObject sourceRoot; private Canvas targetCanvas; [Header("Config")] private float referencePPU = 100f; private bool copyAlphaFromRenderer = true; private bool preserveSortingOrder = true; private bool addRaycastTargets = false; private bool autoCreateCanvasIfMissing = true; private string parentName = "PSB_UI"; private bool exportAsPng = false; // 性能优化选项 private bool useParallelExport = true; private int maxParallelTasks = 4; [MenuItem("Tools/PSB → UGUI Converter (Jobs)")] public static void ShowWindow() { var win = GetWindow("PSB → UGUI (Jobs)"); win.minSize = new Vector2(450, 420); } private void OnGUI() { EditorGUILayout.LabelField("Source", EditorStyles.boldLabel); sourceRoot = (GameObject)EditorGUILayout.ObjectField( new GUIContent("PSB root (scene GameObject)", "Select the instantiated PSB root that contains SpriteRenderers"), sourceRoot, typeof(GameObject), true); EditorGUILayout.Space(); EditorGUILayout.LabelField("Target", EditorStyles.boldLabel); targetCanvas = (Canvas)EditorGUILayout.ObjectField( new GUIContent("Target Canvas", "Canvas to place generated UI under"), targetCanvas, typeof(Canvas), true); autoCreateCanvasIfMissing = EditorGUILayout.Toggle( new GUIContent("Auto-create canvas if missing", "Create an Overlay Canvas+EventSystem when none provided"), autoCreateCanvasIfMissing ); EditorGUILayout.Space(); EditorGUILayout.LabelField("Conversion settings", EditorStyles.boldLabel); referencePPU = EditorGUILayout.FloatField( new GUIContent("Reference Pixels Per Unit", "Use the PSD Importer PPU (often 100). Controls world→UI pixel mapping"), referencePPU ); copyAlphaFromRenderer = EditorGUILayout.Toggle( new GUIContent("Copy alpha from SpriteRenderer color", "Image color.a = SpriteRenderer.color.a"), copyAlphaFromRenderer ); addRaycastTargets = EditorGUILayout.Toggle( new GUIContent("Enable raycast target on Images", "Turn off for purely decorative layers"), addRaycastTargets ); preserveSortingOrder = EditorGUILayout.Toggle( new GUIContent("Preserve sorting order", "Use sortingOrder to set sibling order"), preserveSortingOrder ); parentName = EditorGUILayout.TextField( new GUIContent("Parent name", "Name of the generated UI root under the Canvas"), parentName ); EditorGUILayout.Space(); EditorGUILayout.LabelField("Performance", EditorStyles.boldLabel); exportAsPng = EditorGUILayout.Toggle( new GUIContent("Export sprites as PNG", "If checked, each sprite will be exported as a PNG file"), exportAsPng ); if (exportAsPng) { useParallelExport = EditorGUILayout.Toggle( new GUIContent("Parallel export (faster)", "Use multiple threads to export PNGs simultaneously"), useParallelExport ); if (useParallelExport) { maxParallelTasks = EditorGUILayout.IntSlider( new GUIContent("Max parallel tasks", "Number of concurrent export threads"), maxParallelTasks, 1, 8 ); } } EditorGUILayout.Space(); using (new EditorGUI.DisabledScope(sourceRoot == null)) { if (GUILayout.Button("Convert to UGUI (Optimized)")) { Convert(); } } } private void Convert() { if (sourceRoot == null) { ShowNotification(new GUIContent("Select a source root.")); return; } EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Setting up canvas...", 0f); try { Canvas canvas = SetupCanvas(); if (canvas == null) return; GameObject uiRootGO = CreateUIRoot(canvas); var mapping = CreateTransformMapping(uiRootGO); EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Collecting sprite data...", 0.2f); // 收集所有需要处理的SpriteRenderer(在主线程) var spriteDataList = new List(); foreach (var kv in mapping) { var sr = kv.Key.GetComponent(); if (sr != null && sr.sprite != null) { spriteDataList.Add(new SpriteExportData { src = kv.Key, dstRT = kv.Value, sr = sr, originalSprite = sr.sprite, spriteName = sr.sprite.name, // 在主线程获取所有需要的数据 texture = sr.sprite.texture, rect = sr.sprite.rect, originalPPU = sr.sprite.pixelsPerUnit, pivot = sr.sprite.pivot }); } } var spriteMapping = new ConcurrentDictionary(); if (exportAsPng && useParallelExport && spriteDataList.Count > 0) { EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Exporting PNGs (parallel)...", 0.3f); // 准备并行任务 var tasks = new List>(); var semaphore = new SemaphoreSlim(maxParallelTasks); foreach (var data in spriteDataList) { tasks.Add(Task.Run(() => ExportSpritePNGAsync(data, semaphore))); } // 等待所有导出任务完成 Task.WaitAll(tasks.ToArray()); // 在主线程导入所有PNG EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Importing PNGs...", 0.6f); foreach (var task in tasks) { var result = task.Result; if (!string.IsNullOrEmpty(result.pngPath)) { var importedSprite = ImportPNGAsSprite(result.pngPath, result.data); if (importedSprite != null) { spriteMapping[result.src] = importedSprite; } else { spriteMapping[result.src] = result.data.originalSprite; } } else { spriteMapping[result.src] = result.data.originalSprite; } } } else { // 顺序处理 for (int i = 0; i < spriteDataList.Count; i++) { var data = spriteDataList[i]; float progress = 0.3f + (0.5f * i / spriteDataList.Count); EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", $"Processing {data.spriteName}...", progress); Sprite targetSprite = data.originalSprite; if (exportAsPng) { targetSprite = ExportSpriteAsPNGSequential(data) ?? targetSprite; } spriteMapping[data.src] = targetSprite; } } EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Creating UI components...", 0.8f); // 创建UI组件(必须在主线程) foreach (var data in spriteDataList) { if (spriteMapping.TryGetValue(data.src, out var targetSprite)) { CreateUIImage(data.dstRT, targetSprite, data.sr); } } if (preserveSortingOrder) { ApplySortingOrder(mapping); } Selection.activeObject = uiRootGO; EditorGUIUtility.PingObject(uiRootGO); Debug.Log(exportAsPng ? $"PSB → UGUI conversion completed (sprites exported as PNG using {(useParallelExport ? "parallel" : "sequential")} mode)." : "PSB → UGUI conversion completed (using original psb sprites)."); } finally { EditorUtility.ClearProgressBar(); } } private Canvas SetupCanvas() { Canvas canvas = targetCanvas; if (canvas == null && autoCreateCanvasIfMissing) { var canvasGO = new GameObject("Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); canvas = canvasGO.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; var scaler = canvasGO.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.referencePixelsPerUnit = referencePPU; if (FindObjectOfType() == null) { var es = new GameObject("EventSystem", typeof(UnityEngine.EventSystems.EventSystem), typeof(UnityEngine.EventSystems.StandaloneInputModule)); Undo.RegisterCreatedObjectUndo(es, "Create EventSystem"); } Undo.RegisterCreatedObjectUndo(canvasGO, "Create Canvas"); } if (canvas == null) { ShowNotification(new GUIContent("Provide a Canvas or enable auto-create.")); return null; } return canvas; } private GameObject CreateUIRoot(Canvas canvas) { GameObject uiRootGO = new GameObject(string.IsNullOrEmpty(parentName) ? "PSB_UI" : parentName, typeof(RectTransform)); Undo.RegisterCreatedObjectUndo(uiRootGO, "Create UI Root"); uiRootGO.transform.SetParent(canvas.transform, false); var uiRootRT = uiRootGO.GetComponent(); uiRootRT.anchorMin = new Vector2(0.5f, 0.5f); uiRootRT.anchorMax = new Vector2(0.5f, 0.5f); uiRootRT.pivot = new Vector2(0.5f, 0.5f); uiRootRT.anchoredPosition = Vector2.zero; uiRootRT.localScale = Vector3.one; return uiRootGO; } private Dictionary CreateTransformMapping(GameObject uiRootGO) { var mapping = new Dictionary(); mapping[sourceRoot.transform] = uiRootGO.GetComponent(); var all = sourceRoot.GetComponentsInChildren(true) .OrderBy(t => t == sourceRoot.transform ? -1 : 0) .ThenBy(t => t.GetSiblingIndex()) .ToList(); foreach (var t in all) { if (t == sourceRoot.transform) continue; if (!mapping.TryGetValue(t.parent, out var parentRT)) { Debug.LogWarning($"Parent mapping missing for {t.name}. Skipping."); continue; } var containerGO = new GameObject(t.name, typeof(RectTransform)); Undo.RegisterCreatedObjectUndo(containerGO, "Create UI Container"); var rt = containerGO.GetComponent(); containerGO.transform.SetParent(parentRT, false); rt.localPosition = t.localPosition * referencePPU; rt.localRotation = t.localRotation; rt.localScale = t.localScale; rt.anchorMin = new Vector2(0.5f, 0.5f); rt.anchorMax = new Vector2(0.5f, 0.5f); rt.pivot = new Vector2(0.5f, 0.5f); mapping[t] = rt; } return mapping; } private void CreateUIImage(RectTransform dstRT, Sprite sprite, SpriteRenderer sr) { var img = dstRT.gameObject.AddComponent(); img.sprite = sprite; img.preserveAspect = false; img.raycastTarget = addRaycastTargets; if (copyAlphaFromRenderer) { var c = img.color; c.a = sr.color.a; img.color = c; } img.SetNativeSize(); dstRT.pivot = sprite.pivot / new Vector2(sprite.rect.width, sprite.rect.height); } private void ApplySortingOrder(Dictionary mapping) { var parents = mapping.Values.Select(rt => rt.parent).Distinct().Where(p => p != null); foreach (var p in parents) { var children = new List(); for (int i = 0; i < p.childCount; i++) { children.Add(p.GetChild(i) as RectTransform); } children = children .OrderBy(rt => { var correspondingSrc = mapping.FirstOrDefault(x => x.Value == rt).Key; var sr = correspondingSrc != null ? correspondingSrc.GetComponent() : null; return sr != null ? sr.sortingOrder : 0; }) .ThenBy(rt => rt.name) .ToList(); for (int i = 0; i < children.Count; i++) { children[i].SetSiblingIndex(i); } } } // 异步导出PNG(在工作线程执行编码) private (Transform src, string pngPath, SpriteExportData data) ExportSpritePNGAsync(SpriteExportData data, SemaphoreSlim semaphore) { semaphore.Wait(); try { string pngPath = ExportSpritePNGFromData(data); return (data.src, pngPath, data); } finally { semaphore.Release(); } } // 从已收集的数据导出PNG(可以在工作线程执行) private string ExportSpritePNGFromData(SpriteExportData data) { try { int w = Mathf.RoundToInt(data.rect.width); int h = Mathf.RoundToInt(data.rect.height); // 读取纹理数据 Color[] pixels = data.texture.GetPixels((int)data.rect.x, (int)data.rect.y, w, h); // 创建纹理并编码 Texture2D outTex = new Texture2D(w, h, TextureFormat.RGBA32, false); outTex.SetPixels(pixels); outTex.Apply(); byte[] pngData = outTex.EncodeToPNG(); // 确定保存路径 string targetFolder = GetOrCreateTargetFolder(data); if (string.IsNullOrEmpty(targetFolder)) { UnityEngine.Object.DestroyImmediate(outTex); return null; } string pngPath = Path.Combine(targetFolder, data.spriteName + ".png"); // 确保文件名唯一 int counter = 1; string originalPath = pngPath; while (File.Exists(pngPath)) { pngPath = Path.Combine(targetFolder, $"{data.spriteName}_{counter}.png"); counter++; } File.WriteAllBytes(pngPath, pngData); UnityEngine.Object.DestroyImmediate(outTex); return pngPath; } catch (System.Exception e) { Debug.LogError($"导出 PNG 时发生错误 ({data.spriteName}): {e.Message}"); return null; } } // 在主线程导入PNG为Sprite private Sprite ImportPNGAsSprite(string pngPath, SpriteExportData data) { try { AssetDatabase.ImportAsset(pngPath); var texImp = AssetImporter.GetAtPath(pngPath) as TextureImporter; if (texImp != null) { texImp.textureType = TextureImporterType.Sprite; texImp.spritePixelsPerUnit = data.originalPPU; texImp.alphaIsTransparency = true; Vector2 pivotNormalized = new Vector2( data.pivot.x / data.rect.width, data.pivot.y / data.rect.height ); texImp.spritePivot = pivotNormalized; texImp.mipmapEnabled = false; texImp.filterMode = FilterMode.Bilinear; texImp.textureCompression = TextureImporterCompression.Uncompressed; texImp.SaveAndReimport(); } return AssetDatabase.LoadAssetAtPath(pngPath); } catch (System.Exception e) { Debug.LogError($"导入 PNG 时发生错误 ({pngPath}): {e.Message}"); return null; } } // 顺序版本 - 完整功能 private Sprite ExportSpriteAsPNGSequential(SpriteExportData data) { try { int w = Mathf.RoundToInt(data.rect.width); int h = Mathf.RoundToInt(data.rect.height); RenderTexture rt = RenderTexture.GetTemporary(data.texture.width, data.texture.height, 0, RenderTextureFormat.ARGB32); Graphics.Blit(data.texture, rt); RenderTexture.active = rt; Texture2D outTex = new Texture2D(w, h, TextureFormat.RGBA32, false); outTex.ReadPixels(new Rect(data.rect.x, data.rect.y, data.rect.width, data.rect.height), 0, 0); outTex.Apply(); RenderTexture.active = null; RenderTexture.ReleaseTemporary(rt); byte[] pngData = outTex.EncodeToPNG(); string targetFolder = GetOrCreateTargetFolder(data); if (string.IsNullOrEmpty(targetFolder)) { UnityEngine.Object.DestroyImmediate(outTex); return null; } string pngPath = Path.Combine(targetFolder, data.spriteName + ".png"); pngPath = AssetDatabase.GenerateUniqueAssetPath(pngPath); if (string.IsNullOrEmpty(pngPath)) { UnityEngine.Object.DestroyImmediate(outTex); return null; } File.WriteAllBytes(pngPath, pngData); UnityEngine.Object.DestroyImmediate(outTex); AssetDatabase.ImportAsset(pngPath); var texImp = AssetImporter.GetAtPath(pngPath) as TextureImporter; if (texImp != null) { texImp.textureType = TextureImporterType.Sprite; texImp.spritePixelsPerUnit = data.originalPPU; texImp.alphaIsTransparency = true; Vector2 pivotNormalized = new Vector2( data.pivot.x / data.rect.width, data.pivot.y / data.rect.height ); texImp.spritePivot = pivotNormalized; texImp.mipmapEnabled = false; texImp.filterMode = FilterMode.Bilinear; texImp.textureCompression = TextureImporterCompression.Uncompressed; texImp.SaveAndReimport(); } return AssetDatabase.LoadAssetAtPath(pngPath); } catch (System.Exception e) { Debug.LogError($"导出 PNG 时发生错误 ({data.spriteName}): {e.Message}"); return null; } } private string GetOrCreateTargetFolder(SpriteExportData data) { Object prefab = PrefabUtility.GetCorrespondingObjectFromSource(sourceRoot); string psbPath = AssetDatabase.GetAssetPath(prefab); if (string.IsNullOrEmpty(psbPath)) { psbPath = AssetDatabase.GetAssetPath(data.originalSprite); } string psbFolder = Path.GetDirectoryName(psbPath); if (string.IsNullOrEmpty(psbFolder)) { Debug.LogError($"无法获取有效的 psb 文件夹路径,源对象: {sourceRoot.name}"); return null; } string psbName = Path.GetFileNameWithoutExtension(psbPath); string targetFolder = Path.Combine(psbFolder, psbName); // 确保在主线程创建文件夹 if (!AssetDatabase.IsValidFolder(targetFolder)) { // 这个操作必须在主线程,所以如果当前在子线程就返回null if (System.Threading.Thread.CurrentThread.ManagedThreadId != 1) { return null; } AssetDatabase.CreateFolder(psbFolder, psbName); } return targetFolder; } // 数据容器,用于传递Sprite信息 private class SpriteExportData { public Transform src; public RectTransform dstRT; public SpriteRenderer sr; public Sprite originalSprite; public string spriteName; public Texture2D texture; public Rect rect; public float originalPPU; public Vector2 pivot; } } #endregion