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(); } } }