From da3a43a2a647bf7c28cb247024f84ae0a28b54ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Fri, 5 Dec 2025 19:03:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=B4=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Editor/TexturePacker.meta | 8 + .../TexturePacker/UnityTexturePackEditor.cs | 334 ++++++++++++++++++ .../UnityTexturePackEditor.cs.meta | 11 + 3 files changed, 353 insertions(+) create mode 100644 Editor/TexturePacker.meta create mode 100644 Editor/TexturePacker/UnityTexturePackEditor.cs create mode 100644 Editor/TexturePacker/UnityTexturePackEditor.cs.meta diff --git a/Editor/TexturePacker.meta b/Editor/TexturePacker.meta new file mode 100644 index 0000000..45a4c6f --- /dev/null +++ b/Editor/TexturePacker.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 958ac3975f1661b4fbd42bbae89ec8ab +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/TexturePacker/UnityTexturePackEditor.cs b/Editor/TexturePacker/UnityTexturePackEditor.cs new file mode 100644 index 0000000..e782d33 --- /dev/null +++ b/Editor/TexturePacker/UnityTexturePackEditor.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using UnityEditor; +using UnityEngine; + +public class UnityTexturePackerEditor : EditorWindow +{ + private List textures = new List(); + private Vector2 scrollPos; + private string outputFolder = "Assets/TexturePackerOutput"; + private string atlasFileName = "atlas.png"; + private string jsonFileName = "atlas.json"; + private int maxAtlasSize = 4096; + private int padding = 2; + private bool overwriteExisting = true; + private string lastJson = ""; + private string lastAtlasPath = ""; + + [MenuItem("Tools/Extension/Texture Packer")] + public static void ShowWindow() + { + var w = GetWindow("Texture Packer"); + w.minSize = new Vector2(450, 300); + } + + private void OnGUI() + { + GUILayout.Label("Texture Packer (for TMP-compatible atlases)", EditorStyles.boldLabel); + DrawDragDropArea(); + GUILayout.Space(6); + GUILayout.Label($"Textures ({textures.Count})", EditorStyles.label); + scrollPos = GUILayout.BeginScrollView(scrollPos, GUILayout.Height(160)); + for (int i = 0; i < textures.Count; i++) + { + GUILayout.BeginHorizontal("box"); + Texture2D tex = textures[i]; + Texture preview = AssetPreview.GetAssetPreview(tex) ?? tex; + GUILayout.Label(preview, GUILayout.Width(64), GUILayout.Height(64)); + GUILayout.BeginVertical(); + GUILayout.Label(tex.name, EditorStyles.boldLabel); + string path = AssetDatabase.GetAssetPath(tex); + GUILayout.Label($"{tex.width}x{tex.height} — {path}"); + GUILayout.EndVertical(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("▲", GUILayout.Width(24))) { if (i > 0) { var t = textures[i - 1]; textures[i - 1] = textures[i]; textures[i] = t; } } + if (GUILayout.Button("▼", GUILayout.Width(24))) { if (i < textures.Count - 1) { var t = textures[i + 1]; textures[i + 1] = textures[i]; textures[i] = t; } } + if (GUILayout.Button("Remove", GUILayout.Width(64))) { textures.RemoveAt(i); } + GUILayout.EndHorizontal(); + } + GUILayout.EndScrollView(); + GUILayout.Space(8); + GUILayout.Label("Output Settings", EditorStyles.boldLabel); + outputFolder = EditorGUILayout.TextField("Output Folder", outputFolder); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Choose...")) + { + string folder = EditorUtility.OpenFolderPanel("Select output folder", "", ""); + if (!string.IsNullOrEmpty(folder)) + { + if (folder.StartsWith(Application.dataPath)) + { + outputFolder = "Assets" + folder.Substring(Application.dataPath.Length); + } + else + { + EditorUtility.DisplayDialog("Invalid Folder", "Please select a folder inside this Unity project (under Assets).", "OK"); + } + } + } + GUILayout.EndHorizontal(); + atlasFileName = EditorGUILayout.TextField("Atlas Filename", atlasFileName); + jsonFileName = EditorGUILayout.TextField("JSON Filename", jsonFileName); + maxAtlasSize = EditorGUILayout.IntPopup("Max Atlas Size", maxAtlasSize, new string[] { "1024", "2048", "4096" }, new int[] { 1024, 2048, 4096 }); + padding = EditorGUILayout.IntField("Padding (px)", padding); + overwriteExisting = EditorGUILayout.Toggle("Overwrite Existing", overwriteExisting); + GUILayout.Space(6); + if (GUILayout.Button("Build Atlas", GUILayout.Height(30))) + { + if (textures.Count == 0) + { + EditorUtility.DisplayDialog("No textures", "Please drag textures into the list before building.", "OK"); + } + else + { + BuildAtlas(); + } + } + GUILayout.Space(10); + GUILayout.Label("Last result", EditorStyles.boldLabel); + EditorGUILayout.SelectableLabel(lastAtlasPath, GUILayout.Height(16)); + EditorGUILayout.TextArea(lastJson, GUILayout.Height(120)); + GUILayout.FlexibleSpace(); + GUILayout.Label("Notes: Output atlas is created as an uncompressed PNG and imported as multiple sprites so TextMeshPro can use it.", EditorStyles.wordWrappedLabel); + } + + private void DrawDragDropArea() + { + var evt = Event.current; + Rect dropArea = GUILayoutUtility.GetRect(0.0f, 80.0f, GUILayout.ExpandWidth(true)); + GUI.Box(dropArea, "Drag textures here (Project or Explorer)", EditorStyles.helpBox); + switch (evt.type) + { + case EventType.DragUpdated: + case EventType.DragPerform: + if (!dropArea.Contains(evt.mousePosition)) return; + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + if (evt.type == EventType.DragPerform) + { + DragAndDrop.AcceptDrag(); + foreach (var obj in DragAndDrop.objectReferences) + { + if (obj is Texture2D t) + { + AddTexture(t); + } + else if (obj is DefaultAsset) + { + string path = AssetDatabase.GetAssetPath(obj); + string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { path }); + foreach (var g in guids) + { + string p = AssetDatabase.GUIDToAssetPath(g); + var tex = AssetDatabase.LoadAssetAtPath(p); + AddTexture(tex); + } + } + else + { + string p = AssetDatabase.GetAssetPath(obj); + if (!string.IsNullOrEmpty(p)) + { + var tex = AssetDatabase.LoadAssetAtPath(p); + if (tex != null) AddTexture(tex); + } + } + } + } + Event.current.Use(); + break; + } + } + + private void AddTexture(Texture2D tex) + { + if (tex == null) return; + if (!textures.Contains(tex)) textures.Add(tex); + } + + [Serializable] + private class AtlasEntry + { + public string name; + public string sourcePath; + public int x; + public int y; + public int w; + public int h; + public int sourceW; + public int sourceH; + } + + private class ImporterBackup + { + public string path; + public bool isReadable; + public TextureImporterType type; + public TextureImporterCompression compression; + public int maxSize; + } + + private void BuildAtlas() + { + if (!AssetDatabase.IsValidFolder(outputFolder)) + { + if (!outputFolder.StartsWith("Assets")) + { + EditorUtility.DisplayDialog("Invalid output folder", "Output folder must be inside the project's Assets folder.", "OK"); + return; + } + string[] parts = outputFolder.Split('/'); + string parent = "Assets"; + for (int i = 1; i < parts.Length; i++) + { + string sub = string.Join("/", parts, 0, i + 1); + if (!AssetDatabase.IsValidFolder(sub)) + { + AssetDatabase.CreateFolder(parent, parts[i]); + } + parent = sub; + } + } + var backups = new List(); + var readableTextures = new List(); + foreach (var t in textures) + { + string p = AssetDatabase.GetAssetPath(t); + var ti = TextureImporter.GetAtPath(p) as TextureImporter; + if (ti == null) continue; + var b = new ImporterBackup(); + b.path = p; + b.isReadable = ti.isReadable; + b.type = ti.textureType; + b.compression = ti.textureCompression; + b.maxSize = ti.maxTextureSize; + backups.Add(b); + ti.isReadable = true; + ti.textureType = TextureImporterType.Default; + ti.textureCompression = TextureImporterCompression.Uncompressed; + ti.maxTextureSize = Math.Max(t.width, t.height); + EditorUtility.SetDirty(ti); + ti.SaveAndReimport(); + var loaded = AssetDatabase.LoadAssetAtPath(p); + if (loaded != null) readableTextures.Add(loaded); + } + try + { + Texture2D atlas = new Texture2D(2, 2, TextureFormat.RGBA32, false); + Rect[] rects = atlas.PackTextures(readableTextures.ToArray(), padding, maxAtlasSize); + if (rects == null || rects.Length == 0) + { + EditorUtility.DisplayDialog("Pack failed", "Could not pack textures. Try increasing max atlas size or reduce number of textures.", "OK"); + RestoreImporters(backups); + return; + } + int usedW = atlas.width; + int usedH = atlas.height; + byte[] png = atlas.EncodeToPNG(); + string atlasPath = Path.Combine(outputFolder, atlasFileName).Replace("\\", "/"); + if (File.Exists(atlasPath) && !overwriteExisting) + { + if (!EditorUtility.DisplayDialog("File exists", $"{atlasPath} already exists. Overwrite?", "Yes", "No")) + { + RestoreImporters(backups); + return; + } + } + File.WriteAllBytes(atlasPath, png); + AssetDatabase.ImportAsset(atlasPath); + var atlasImporter = TextureImporter.GetAtPath(atlasPath) as TextureImporter; + atlasImporter.textureType = TextureImporterType.Sprite; + atlasImporter.spriteImportMode = SpriteImportMode.Multiple; + atlasImporter.spritePixelsPerUnit = 100; + List metas = new List(); + List mapping = new List(); + for (int i = 0; i < readableTextures.Count; i++) + { + var src = readableTextures[i]; + Rect r = rects[i]; + int px = Mathf.RoundToInt(r.x * atlas.width); + int py = Mathf.RoundToInt(r.y * atlas.height); + int pw = Mathf.RoundToInt(r.width * atlas.width); + int ph = Mathf.RoundToInt(r.height * atlas.height); + SpriteMetaData md = new SpriteMetaData(); + md.name = src.name; + md.pivot = new Vector2(0.5f, 0.5f); + md.rect = new Rect(px, py, pw, ph); + md.alignment = (int)SpriteAlignment.Center; + metas.Add(md); + AtlasEntry e = new AtlasEntry(); + e.name = src.name; + e.sourcePath = AssetDatabase.GetAssetPath(src); + e.x = px; e.y = py; e.w = pw; e.h = ph; + e.sourceW = src.width; e.sourceH = src.height; + mapping.Add(e); + } + atlasImporter.spritesheet = metas.ToArray(); + EditorUtility.SetDirty(atlasImporter); + atlasImporter.SaveAndReimport(); + string jsonPath = Path.Combine(outputFolder, jsonFileName).Replace("\\", "/"); + WriteTexturePackerJson(jsonPath, mapping, atlas.width, atlas.height, atlasFileName); + AssetDatabase.ImportAsset(jsonPath); + lastAtlasPath = atlasPath; + lastJson = File.ReadAllText(jsonPath); + EditorUtility.DisplayDialog("Success", $"Atlas created: {atlasPath}\nJSON: {jsonPath}", "OK"); + } + catch (Exception ex) + { + Debug.LogError("Error while building atlas: " + ex.Message + "\n" + ex.StackTrace); + EditorUtility.DisplayDialog("Error", ex.Message, "OK"); + } + finally + { + RestoreImporters(backups); + AssetDatabase.Refresh(); + } + } + + private void WriteTexturePackerJson(string jsonPath, List entries, int atlasW, int atlasH, string atlasFileName) + { + var sb = new StringBuilder(); + sb.Append('{'); + sb.Append("\"frames\":["); + for (int i = 0; i < entries.Count; i++) + { + var e = entries[i]; + int jsonY = atlasH - (e.y + e.h); + string filename = e.name + ".png"; + sb.Append('{'); + sb.AppendFormat("\"filename\":\"{0}\",", filename); + sb.AppendFormat("\"frame\":{{\"x\":{0},\"y\":{1},\"w\":{2},\"h\":{3}}},", e.x, jsonY, e.w, e.h); + sb.Append("\"rotated\":false,"); + sb.Append("\"trimmed\":false,"); + sb.AppendFormat("\"spriteSourceSize\":{{\"x\":0,\"y\":0,\"w\":{0},\"h\":{1}}},", e.sourceW, e.sourceH); + sb.AppendFormat("\"sourceSize\":{{\"w\":{0},\"h\":{1}}},", e.sourceW, e.sourceH); + sb.Append("\"pivot\":{\"x\":0.5,\"y\":0.5}"); + sb.Append('}'); + if (i < entries.Count - 1) sb.Append(','); + } + sb.Append(']'); + sb.Append(','); + sb.Append("\"meta\":{"); + sb.AppendFormat("\"app\":\"Unity TexturePacker Export\",\"version\":\"1.0\",\"image\":\"{0}\",\"format\":\"RGBA8888\",\"size\":{{\"w\":{1},\"h\":{2}}},\"scale\":\"1\"", atlasFileName, atlasW, atlasH); + sb.Append('}'); + sb.Append('}'); + File.WriteAllText(jsonPath, sb.ToString()); + } + + private void RestoreImporters(List backups) + { + foreach (var b in backups) + { + var ti = TextureImporter.GetAtPath(b.path) as TextureImporter; + if (ti == null) continue; + ti.isReadable = b.isReadable; + ti.textureType = b.type; + ti.textureCompression = b.compression; + ti.maxTextureSize = b.maxSize; + EditorUtility.SetDirty(ti); + ti.SaveAndReimport(); + } + } +} diff --git a/Editor/TexturePacker/UnityTexturePackEditor.cs.meta b/Editor/TexturePacker/UnityTexturePackEditor.cs.meta new file mode 100644 index 0000000..6f44acc --- /dev/null +++ b/Editor/TexturePacker/UnityTexturePackEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c64fbafe8359fb242adff677b54b4697 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: