#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using UnityEditor; using UnityEngine; public class DDSAtlasSlicerWindow : EditorWindow { [SerializeField] private Texture2D _ddsAtlas; [SerializeField] private TextAsset _txtAsset; // Reference to the .txt in Project [SerializeField] private bool _txtIsGB2312 = true; [SerializeField] private bool _trimEmptyNames = true; [SerializeField] private bool _padIndexIfMissing = true; [SerializeField] private int _paddingPixels = 0; [SerializeField] private float _pixelsPerUnit = 100f; [SerializeField] private bool _exportAsIndividualPngs = false; [SerializeField] private string _exportFolder = string.Empty; // Project relative (starts with Assets/) private string _status; [MenuItem("Tools/Sprites/Slice DDS Atlas From TXT")] private static void Open() { var win = GetWindow(true, "DDS Atlas Slicer", true); win.minSize = new Vector2(520, 280); win.Show(); } private void OnGUI() { EditorGUILayout.LabelField("Input", EditorStyles.boldLabel); _ddsAtlas = (Texture2D)EditorGUILayout.ObjectField("DDS Atlas", _ddsAtlas, typeof(Texture2D), false); _txtAsset = (TextAsset)EditorGUILayout.ObjectField("TXT (name list)", _txtAsset, typeof(TextAsset), false); _txtIsGB2312 = EditorGUILayout.ToggleLeft("Decode TXT with GB2312 (Chinese)", _txtIsGB2312); _trimEmptyNames = EditorGUILayout.ToggleLeft("Trim empty or whitespace-only names", _trimEmptyNames); _padIndexIfMissing = EditorGUILayout.ToggleLeft("If name missing, use index as fallback", _padIndexIfMissing); _paddingPixels = EditorGUILayout.IntField("Sprite Padding (px)", _paddingPixels); _pixelsPerUnit = EditorGUILayout.FloatField("Pixels Per Unit", _pixelsPerUnit); GUILayout.Space(6); EditorGUILayout.LabelField("Output", EditorStyles.boldLabel); _exportAsIndividualPngs = EditorGUILayout.ToggleLeft("Export each sprite as an individual PNG file", _exportAsIndividualPngs); if (_exportAsIndividualPngs) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.PrefixLabel("Export Folder (Project)"); EditorGUILayout.SelectableLabel(string.IsNullOrEmpty(_exportFolder) ? "(auto: /_slices)" : _exportFolder, GUILayout.Height(16)); if (GUILayout.Button("Change...", GUILayout.Width(90))) { string start = string.IsNullOrEmpty(_exportFolder) ? Application.dataPath : ProjectPathToAbsolute(_exportFolder); string chosen = EditorUtility.OpenFolderPanel("Choose export folder (inside Assets)", start, ""); if (!string.IsNullOrEmpty(chosen)) { string projRel = AbsoluteToProjectPath(chosen); if (!string.IsNullOrEmpty(projRel)) { _exportFolder = projRel; } else { EditorUtility.DisplayDialog("Export Folder", "Please choose a folder inside the project's Assets/ directory.", "OK"); } } } EditorGUILayout.EndHorizontal(); } GUILayout.Space(8); if (GUILayout.Button("Slice Atlas From TXT", GUILayout.Height(32))) { TrySlice(); } if (!string.IsNullOrEmpty(_status)) { EditorGUILayout.HelpBox(_status, MessageType.Info); } } private void TrySlice() { _status = string.Empty; if (_ddsAtlas == null) { _status = "Please assign a DDS atlas Texture2D asset."; Repaint(); return; } if (_txtAsset == null) { _status = "Please assign the TXT definition asset."; Repaint(); return; } string txtPath = AssetDatabase.GetAssetPath(_txtAsset); string atlasPath = AssetDatabase.GetAssetPath(_ddsAtlas); if (string.IsNullOrEmpty(txtPath) || string.IsNullOrEmpty(atlasPath)) { _status = "Invalid asset references."; Repaint(); return; } try { var parsed = ParseTxt(txtPath, _txtIsGB2312); if (parsed == null) { _status = "Failed to parse TXT (null)."; Repaint(); return; } if (_exportAsIndividualPngs) { ExportSpritesAsPNGs(atlasPath, parsed.SpriteWidth, parsed.SpriteHeight, parsed.Columns, parsed.Rows, parsed.Names); _status = $"Exported {parsed.Columns * parsed.Rows} PNG sprites."; } else { ApplySlicing(atlasPath, parsed.SpriteWidth, parsed.SpriteHeight, parsed.Columns, parsed.Rows, parsed.Names); _status = $"Sliced {_ddsAtlas.name} into {parsed.Columns * parsed.Rows} sprites."; } } catch (Exception ex) { Debug.LogError($"[DDSAtlasSlicer] Error: {ex}"); _status = "Error. See Console."; } finally { Repaint(); } } private class TxtDefinition { public int SpriteWidth; public int SpriteHeight; public int Columns; public int Rows; public List Names; } private TxtDefinition ParseTxt(string path, bool gb2312) { Encoding GetEncoding() { #if NET_STANDARD_2_1 || NET_6_0_OR_GREATER Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); #endif return Encoding.GetEncoding("GB2312"); } string[] lines; if (gb2312) { var enc = GetEncoding(); lines = File.ReadAllLines(path, enc); } else { lines = File.ReadAllLines(path, Encoding.UTF8); } if (lines.Length < 4) throw new InvalidDataException("TXT must have at least 4 lines: width, height, columns, rows."); int ReadInt(string s) { if (int.TryParse(s.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int v)) return v; if (int.TryParse(s.Trim(), out v)) return v; throw new InvalidDataException($"Cannot parse integer: '{s}'"); } int spriteWidth = ReadInt(lines[0]); int spriteHeight = ReadInt(lines[1]); int columns = ReadInt(lines[3]); int rows = ReadInt(lines[2]); var names = new List(); for (int i = 4; i < lines.Length; i++) { string raw = lines[i]; string name = _trimEmptyNames ? raw.Trim() : raw; if (string.IsNullOrEmpty(name)) { if (_padIndexIfMissing) { names.Add((i - 4).ToString()); } else { names.Add(string.Empty); } } else { // Usually the list contains ".dds" suffix; strip it if (name.EndsWith(".dds", StringComparison.OrdinalIgnoreCase)) { name = name.Substring(0, name.Length - 4); } names.Add(name); } } return new TxtDefinition { SpriteWidth = spriteWidth, SpriteHeight = spriteHeight, Columns = columns, Rows = rows, Names = names }; } private void ApplySlicing(string atlasAssetPath, int spriteWidth, int spriteHeight, int columns, int rows, List names) { var importer = AssetImporter.GetAtPath(atlasAssetPath) as TextureImporter; if (importer == null) { // Fallback path for formats like DDS that don't use TextureImporter for sprite slicing CreateSpritesAsSubAssets(atlasAssetPath, spriteWidth, spriteHeight, columns, rows, names); return; } // Ensure Readable settings and Multiple sprite mode importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Multiple; importer.isReadable = true; importer.mipmapEnabled = false; importer.alphaIsTransparency = true; Texture2D tex = AssetDatabase.LoadAssetAtPath(atlasAssetPath); if (tex == null) throw new InvalidOperationException("Failed to load Texture2D from path."); int atlasWidth = tex.width; int atlasHeight = tex.height; if (spriteWidth <= 0 || spriteHeight <= 0) throw new InvalidDataException("Sprite width/height must be > 0."); if (columns <= 0 || rows <= 0) throw new InvalidDataException("Columns/Rows must be > 0."); int expected = columns * rows; // Align names count if (names.Count < expected) { for (int i = names.Count; i < expected; i++) { names.Add(_padIndexIfMissing ? i.ToString() : string.Empty); } } else if (names.Count > expected) { names = names.Take(expected).ToList(); } var metas = new List(expected); for (int r = 0; r < rows; r++) { for (int c = 0; c < columns; c++) { int index = r * columns + c; int x = c * spriteWidth; int yFromTop = r * spriteHeight; // DDS starts from top-left int yFromBottom = atlasHeight - yFromTop - spriteHeight; // Convert to Unity bottom-left origin var rect = new Rect(x, yFromBottom, spriteWidth, spriteHeight); var meta = new SpriteMetaData { name = SafeSpriteName(names[index], index), rect = rect, alignment = (int)SpriteAlignment.Center, border = Vector4.zero, pivot = new Vector2(0.5f, 0.5f) }; metas.Add(meta); } } if (_paddingPixels > 0) { for (int i = 0; i < metas.Count; i++) metas[i] = ApplyPadding(metas[i], _paddingPixels, atlasWidth, atlasHeight); } importer.spritesheet = metas.ToArray(); EditorUtility.SetDirty(importer); importer.SaveAndReimport(); AssetDatabase.Refresh(); } private void CreateSpritesAsSubAssets(string atlasAssetPath, int spriteWidth, int spriteHeight, int columns, int rows, List names) { Texture2D tex = AssetDatabase.LoadAssetAtPath(atlasAssetPath); if (tex == null) throw new InvalidOperationException($"Failed to load Texture2D from path: {atlasAssetPath}"); int atlasWidth = tex.width; int atlasHeight = tex.height; if (spriteWidth <= 0 || spriteHeight <= 0) throw new InvalidDataException("Sprite width/height must be > 0."); if (columns <= 0 || rows <= 0) throw new InvalidDataException("Columns/Rows must be > 0."); if (columns * spriteWidth > atlasWidth || rows * spriteHeight > atlasHeight) Debug.LogWarning($"[DDSAtlasSlicer] Grid exceeds atlas bounds: grid={columns}x{rows} cell={spriteWidth}x{spriteHeight} atlas={atlasWidth}x{atlasHeight}"); int expected = columns * rows; if (names.Count < expected) { for (int i = names.Count; i < expected; i++) names.Add(_padIndexIfMissing ? i.ToString() : string.Empty); } else if (names.Count > expected) { names = names.Take(expected).ToList(); } // Clear previous Sprite sub-assets under this texture var all = AssetDatabase.LoadAllAssetsAtPath(atlasAssetPath); foreach (var obj in all) { if (obj is Sprite) { UnityEngine.Object.DestroyImmediate(obj, true); } } for (int r = 0; r < rows; r++) { for (int c = 0; c < columns; c++) { int index = r * columns + c; int x = c * spriteWidth; int yFromTop = r * spriteHeight; // DDS starts from top-left int yFromBottom = atlasHeight - yFromTop - spriteHeight; // Convert to Unity bottom-left origin var rect = new Rect(x, yFromBottom, spriteWidth, spriteHeight); if (_paddingPixels > 0) { float nx = Mathf.Max(0, rect.x - _paddingPixels); float ny = Mathf.Max(0, rect.y - _paddingPixels); float nw = Mathf.Min(atlasWidth - nx, rect.width + 2 * _paddingPixels); float nh = Mathf.Min(atlasHeight - ny, rect.height + 2 * _paddingPixels); rect = new Rect(nx, ny, nw, nh); } string sprName = SafeSpriteName(names[index], index); var sprite = Sprite.Create(tex, rect, new Vector2(0.5f, 0.5f), Mathf.Max(1f, _pixelsPerUnit)); sprite.name = sprName; AssetDatabase.AddObjectToAsset(sprite, tex); } } EditorUtility.SetDirty(tex); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private void ExportSpritesAsPNGs(string atlasAssetPath, int spriteWidth, int spriteHeight, int columns, int rows, List names) { Texture2D atlasTex = AssetDatabase.LoadAssetAtPath(atlasAssetPath); if (atlasTex == null) throw new InvalidOperationException($"Failed to load Texture2D from path: {atlasAssetPath}"); int atlasWidth = atlasTex.width; int atlasHeight = atlasTex.height; if (spriteWidth <= 0 || spriteHeight <= 0) throw new InvalidDataException("Sprite width/height must be > 0."); if (columns <= 0 || rows <= 0) throw new InvalidDataException("Columns/Rows must be > 0."); int expected = columns * rows; if (names.Count < expected) { for (int i = names.Count; i < expected; i++) names.Add(_padIndexIfMissing ? i.ToString() : string.Empty); } else if (names.Count > expected) { names = names.Take(expected).ToList(); } // Resolve export path string targetFolderProject = _exportFolder; if (string.IsNullOrEmpty(targetFolderProject)) { string atlasDir = Path.GetDirectoryName(atlasAssetPath).Replace('\\', '/'); string atlasName = Path.GetFileNameWithoutExtension(atlasAssetPath); targetFolderProject = $"{atlasDir}/{atlasName}_multisprite.png"; } else { string atlasName = Path.GetFileNameWithoutExtension(atlasAssetPath); targetFolderProject = $"{targetFolderProject}/{atlasName}_multisprite.png"; } string targetFolderAbs = ProjectPathToAbsolute(Path.GetDirectoryName(targetFolderProject)); string fileName = Path.GetFileName(targetFolderProject); string fileAbs = Path.Combine(targetFolderAbs, fileName); fileAbs = GetUniqueFilePath(fileAbs); // Create a readable copy of the texture Texture2D readableTex = new Texture2D(atlasWidth, atlasHeight, TextureFormat.RGBA32, false); RenderTexture rt = RenderTexture.GetTemporary(atlasWidth, atlasHeight, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); Graphics.Blit(atlasTex, rt); RenderTexture.active = rt; readableTex.ReadPixels(new Rect(0, 0, atlasWidth, atlasHeight), 0, 0); readableTex.Apply(); RenderTexture.active = null; RenderTexture.ReleaseTemporary(rt); // Create PNG from readable copy var png = ImageConversion.EncodeToPNG(readableTex); File.WriteAllBytes(fileAbs, png); // Clean up UnityEngine.Object.DestroyImmediate(readableTex); string fileProject = AbsoluteToProjectPath(fileAbs).Replace('\\', '/'); if (!string.IsNullOrEmpty(fileProject)) { AssetDatabase.ImportAsset(fileProject); var pngImporter = AssetImporter.GetAtPath(fileProject) as TextureImporter; if (pngImporter != null) { pngImporter.textureType = TextureImporterType.Sprite; pngImporter.spriteImportMode = SpriteImportMode.Multiple; pngImporter.mipmapEnabled = false; pngImporter.alphaIsTransparency = true; pngImporter.spritePixelsPerUnit = Mathf.Max(1f, _pixelsPerUnit); // Create sprite metadata var metas = new List(expected); for (int r = 0; r < rows; r++) { for (int c = 0; c < columns; c++) { int index = r * columns + c; int x = c * spriteWidth; int yFromTop = r * spriteHeight; // DDS starts from top-left int yFromBottom = atlasHeight - yFromTop - spriteHeight; // Convert to Unity bottom-left origin var rect = new Rect(x, yFromBottom, spriteWidth, spriteHeight); if (_paddingPixels > 0) { float nx = Mathf.Max(0, rect.x - _paddingPixels); float ny = Mathf.Max(0, rect.y - _paddingPixels); float nw = Mathf.Min(atlasWidth - nx, rect.width + 2 * _paddingPixels); float nh = Mathf.Min(atlasHeight - ny, rect.height + 2 * _paddingPixels); rect = new Rect(nx, ny, nw, nh); } var meta = new SpriteMetaData { name = SafeSpriteName(names[index], index), rect = rect, alignment = (int)SpriteAlignment.Center, border = Vector4.zero, pivot = new Vector2(0.5f, 0.5f) }; metas.Add(meta); } } pngImporter.spritesheet = metas.ToArray(); pngImporter.SaveAndReimport(); } } AssetDatabase.Refresh(); } private static void EnsureFolder(string projectFolderPath) { projectFolderPath = projectFolderPath.Replace('\\', '/'); if (string.IsNullOrEmpty(projectFolderPath)) return; if (!projectFolderPath.StartsWith("Assets")) throw new InvalidOperationException("Export folder must be inside Assets/."); if (AssetDatabase.IsValidFolder(projectFolderPath)) return; string[] parts = projectFolderPath.Split('/'); string current = parts[0]; // "Assets" for (int i = 1; i < parts.Length; i++) { string next = current + "/" + parts[i]; if (!AssetDatabase.IsValidFolder(next)) { AssetDatabase.CreateFolder(current, parts[i]); } current = next; } } private static string ProjectPathToAbsolute(string projectPath) { if (string.IsNullOrEmpty(projectPath)) return string.Empty; string pp = projectPath.Replace('\\', '/'); if (!pp.StartsWith("Assets")) return string.Empty; string data = Application.dataPath.Replace('\\', '/'); return data + pp.Substring("Assets".Length); } private static string AbsoluteToProjectPath(string absolutePath) { if (string.IsNullOrEmpty(absolutePath)) return string.Empty; string abs = absolutePath.Replace('\\', '/'); string data = Application.dataPath.Replace('\\', '/'); if (abs.StartsWith(data, StringComparison.OrdinalIgnoreCase)) { return "Assets" + abs.Substring(data.Length); } return string.Empty; } private static string GetUniqueFilePath(string absolutePath) { string dir = Path.GetDirectoryName(absolutePath); string name = Path.GetFileNameWithoutExtension(absolutePath); string ext = Path.GetExtension(absolutePath); string candidate = absolutePath; int suffix = 1; while (File.Exists(candidate)) { candidate = Path.Combine(dir, $"{name}_{suffix}{ext}"); suffix++; } return candidate; } private static SpriteMetaData ApplyPadding(SpriteMetaData meta, int pad, int atlasWidth, int atlasHeight) { var r = meta.rect; float nx = Mathf.Max(0, r.x - pad); float ny = Mathf.Max(0, r.y - pad); float nw = Mathf.Min(atlasWidth - nx, r.width + 2 * pad); float nh = Mathf.Min(atlasHeight - ny, r.height + 2 * pad); meta.rect = new Rect(nx, ny, nw, nh); return meta; } private static string SafeSpriteName(string raw, int index) { string name = raw; if (string.IsNullOrWhiteSpace(name)) name = index.ToString(); // Unity dislikes slashes and some symbols in asset names char[] invalid = Path.GetInvalidFileNameChars(); var sb = new StringBuilder(name.Length); foreach (char ch in name) { if (invalid.Contains(ch) || ch == '/' || ch == '\\' || ch == ':' || ch == '*') { sb.Append('_'); } else { sb.Append(ch); } } return sb.ToString(); } } #endif