940 lines
34 KiB
C#
940 lines
34 KiB
C#
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<PsbToUGUIWindow>("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>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
var scaler = canvasGO.GetComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
scaler.referenceResolution = new Vector2(1920, 1080);
|
|
scaler.referencePixelsPerUnit = referencePPU;
|
|
|
|
if (FindObjectOfType<UnityEngine.EventSystems.EventSystem>() == 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<RectTransform>();
|
|
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<Transform, RectTransform>();
|
|
mapping[sourceRoot.transform] = uiRootRT;
|
|
|
|
var all = sourceRoot.GetComponentsInChildren<Transform>(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<RectTransform>();
|
|
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<SpriteRenderer>();
|
|
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<Image>();
|
|
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<RectTransform>();
|
|
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<SpriteRenderer>() : 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).");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将 Sprite 导出为 PNG 文件,并保持原始尺寸和 PPU 设置
|
|
/// </summary>
|
|
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<Sprite>(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<PsbToUGUIWindowOptimized>("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<SpriteExportData>();
|
|
foreach (var kv in mapping)
|
|
{
|
|
var sr = kv.Key.GetComponent<SpriteRenderer>();
|
|
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<Transform, Sprite>();
|
|
|
|
if (exportAsPng && useParallelExport && spriteDataList.Count > 0)
|
|
{
|
|
EditorUtility.DisplayProgressBar("Converting to UGUI (Optimized)", "Exporting PNGs (parallel)...", 0.3f);
|
|
|
|
// 准备并行任务
|
|
var tasks = new List<Task<(Transform src, string pngPath, SpriteExportData data)>>();
|
|
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>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
var scaler = canvasGO.GetComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
scaler.referenceResolution = new Vector2(1920, 1080);
|
|
scaler.referencePixelsPerUnit = referencePPU;
|
|
|
|
if (FindObjectOfType<UnityEngine.EventSystems.EventSystem>() == 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<RectTransform>();
|
|
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<Transform, RectTransform> CreateTransformMapping(GameObject uiRootGO)
|
|
{
|
|
var mapping = new Dictionary<Transform, RectTransform>();
|
|
mapping[sourceRoot.transform] = uiRootGO.GetComponent<RectTransform>();
|
|
|
|
var all = sourceRoot.GetComponentsInChildren<Transform>(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<RectTransform>();
|
|
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<Image>();
|
|
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<Transform, RectTransform> mapping)
|
|
{
|
|
var parents = mapping.Values.Select(rt => rt.parent).Distinct().Where(p => p != null);
|
|
foreach (var p in parents)
|
|
{
|
|
var children = new List<RectTransform>();
|
|
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<SpriteRenderer>() : 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<Sprite>(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<Sprite>(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 |