#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; public class DDSMultiSpriteConverter : EditorWindow { [SerializeField] private List _ddsTextures = new List(); [SerializeField] private bool _autoArrange = true; [SerializeField] private int _paddingPixels = 2; [SerializeField] private float _pixelsPerUnit = 100f; [SerializeField] private string _outputFolder = "Assets/GeneratedSprites"; [SerializeField] private string _atlasName = "DDSAtlas"; [SerializeField] private bool _keepOriginalNames = true; [SerializeField] private bool _createSpriteAtlas = true; private Vector2 _scrollPosition; private string _status = ""; private bool _isProcessing = false; [MenuItem("Tools/Sprites/Convert DDS Files to Multi-Sprite")] private static void Open() { var window = GetWindow("DDS Multi-Sprite Converter"); window.minSize = new Vector2(500, 400); window.Show(); } private void OnGUI() { EditorGUILayout.LabelField("DDS Multi-Sprite Converter", EditorStyles.boldLabel); EditorGUILayout.Space(); // Input Section EditorGUILayout.LabelField("Input DDS Files", EditorStyles.boldLabel); EditorGUILayout.HelpBox("Use 'Add DDS Files' for individual files or 'Add Folder' to process all .dds files in a folder (including subfolders).", MessageType.Info); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Add DDS Files", GUILayout.Height(25))) { AddDDSFiles(); } if (GUILayout.Button("Add Folder", GUILayout.Height(25))) { AddDDSFolder(); } if (GUILayout.Button("Clear All", GUILayout.Height(25))) { _ddsTextures.Clear(); _status = ""; } EditorGUILayout.EndHorizontal(); // Display selected textures if (_ddsTextures.Count > 0) { EditorGUILayout.Space(); EditorGUILayout.LabelField($"Selected Files ({_ddsTextures.Count}):", EditorStyles.miniLabel); _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.Height(150)); for (int i = 0; i < _ddsTextures.Count; i++) { EditorGUILayout.BeginHorizontal(); var texture = _ddsTextures[i]; if (texture != null) { EditorGUILayout.LabelField($"{i + 1}. {texture.name}", GUILayout.Width(200)); EditorGUILayout.LabelField($"{texture.width}x{texture.height}", GUILayout.Width(80)); if (GUILayout.Button("Remove", GUILayout.Width(60))) { _ddsTextures.RemoveAt(i); i--; } } else { EditorGUILayout.LabelField($"{i + 1}. ", GUILayout.Width(200)); if (GUILayout.Button("Remove", GUILayout.Width(60))) { _ddsTextures.RemoveAt(i); i--; } } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); } EditorGUILayout.Space(); // Settings Section EditorGUILayout.LabelField("Settings", EditorStyles.boldLabel); _autoArrange = EditorGUILayout.ToggleLeft("Auto-arrange sprites in atlas", _autoArrange); _paddingPixels = EditorGUILayout.IntField("Padding (pixels)", _paddingPixels); _pixelsPerUnit = EditorGUILayout.FloatField("Pixels Per Unit", _pixelsPerUnit); _keepOriginalNames = EditorGUILayout.ToggleLeft("Keep original DDS names", _keepOriginalNames); _createSpriteAtlas = EditorGUILayout.ToggleLeft("Create Sprite Atlas asset", _createSpriteAtlas); EditorGUILayout.Space(); EditorGUILayout.LabelField("Output", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Output Folder:", GUILayout.Width(100)); EditorGUILayout.SelectableLabel(_outputFolder, GUILayout.Height(16)); if (GUILayout.Button("Browse", GUILayout.Width(60))) { string selectedPath = EditorUtility.OpenFolderPanel("Select Output Folder", _outputFolder, ""); if (!string.IsNullOrEmpty(selectedPath)) { string projectPath = AbsoluteToProjectPath(selectedPath); if (!string.IsNullOrEmpty(projectPath)) { _outputFolder = projectPath; } else { EditorUtility.DisplayDialog("Invalid Path", "Please select a folder inside the Assets directory.", "OK"); } } } EditorGUILayout.EndHorizontal(); _atlasName = EditorGUILayout.TextField("Atlas Name", _atlasName); EditorGUILayout.Space(); // Convert Button EditorGUI.BeginDisabledGroup(_ddsTextures.Count == 0 || _isProcessing); if (GUILayout.Button(_isProcessing ? "Processing..." : "Convert to Multi-Sprite", GUILayout.Height(35))) { ConvertToMultiSprite(); } EditorGUI.EndDisabledGroup(); // Status if (!string.IsNullOrEmpty(_status)) { EditorGUILayout.Space(); EditorGUILayout.HelpBox(_status, MessageType.Info); } } private void AddDDSFiles() { string path = EditorUtility.OpenFilePanelWithFilters( "Select DDS Files", "Assets", new string[] { "DDS files", "dds" } ); if (!string.IsNullOrEmpty(path)) { string projectPath = AbsoluteToProjectPath(path); if (!string.IsNullOrEmpty(projectPath)) { Texture2D texture = AssetDatabase.LoadAssetAtPath(projectPath); if (texture != null && !_ddsTextures.Contains(texture)) { _ddsTextures.Add(texture); _status = $"Added {texture.name}"; } } } } private void AddDDSFolder() { string folderPath = EditorUtility.OpenFolderPanel( "Select Folder Containing DDS Files", "Assets", "" ); if (!string.IsNullOrEmpty(folderPath)) { string projectPath = AbsoluteToProjectPath(folderPath); if (!string.IsNullOrEmpty(projectPath)) { // Find all .dds files in the folder and subfolders string[] ddsFiles = Directory.GetFiles(folderPath, "*.dds", SearchOption.AllDirectories); int addedCount = 0; foreach (string ddsFile in ddsFiles) { string relativePath = AbsoluteToProjectPath(ddsFile); if (!string.IsNullOrEmpty(relativePath)) { Texture2D texture = AssetDatabase.LoadAssetAtPath(relativePath); if (texture != null && !_ddsTextures.Contains(texture)) { _ddsTextures.Add(texture); addedCount++; } } } _status = $"Added {addedCount} DDS files from folder"; } else { EditorUtility.DisplayDialog("Invalid Path", "Please select a folder inside the Assets directory.", "OK"); } } } private void ConvertToMultiSprite() { if (_ddsTextures.Count == 0) { _status = "No DDS files selected"; return; } _isProcessing = true; _status = "Processing..."; try { // Ensure output folder exists EnsureFolderExists(_outputFolder); // Calculate atlas size var atlasSize = CalculateAtlasSize(); // Create atlas texture var atlasTexture = CreateAtlasTexture(atlasSize); // Pack sprites into atlas var spriteData = PackSpritesIntoAtlas(atlasTexture, atlasSize); // Save atlas texture string atlasPath = SaveAtlasTexture(atlasTexture, atlasSize); // Create sprite metadata and import CreateSpriteMetadata(atlasPath, spriteData); // Create Sprite Atlas asset if requested if (_createSpriteAtlas) { CreateSpriteAtlasAsset(atlasPath, spriteData); } _status = $"Successfully converted {_ddsTextures.Count} DDS files to multi-sprite atlas!"; // Clean up DestroyImmediate(atlasTexture); } catch (Exception ex) { Debug.LogError($"[DDSMultiSpriteConverter] Error: {ex}"); _status = $"Error: {ex.Message}"; } finally { _isProcessing = false; Repaint(); } } private Vector2Int CalculateAtlasSize() { if (_autoArrange) { // Calculate optimal atlas size based on sprite count and sizes int totalPixels = 0; int maxWidth = 0; int maxHeight = 0; foreach (var texture in _ddsTextures) { if (texture != null) { totalPixels += texture.width * texture.height; maxWidth = Mathf.Max(maxWidth, texture.width); maxHeight = Mathf.Max(maxHeight, texture.height); } } // Estimate atlas size (square-ish) int atlasSize = Mathf.CeilToInt(Mathf.Sqrt(totalPixels * 1.5f)); // 1.5x for padding // Round up to nearest power of 2 atlasSize = Mathf.NextPowerOfTwo(atlasSize); // Ensure minimum size atlasSize = Mathf.Max(atlasSize, 512); return new Vector2Int(atlasSize, atlasSize); } else { // Use fixed size return new Vector2Int(2048, 2048); } } private Texture2D CreateAtlasTexture(Vector2Int size) { var atlasTexture = new Texture2D(size.x, size.y, TextureFormat.RGBA32, false); // Fill with transparent Color[] clearPixels = new Color[size.x * size.y]; for (int i = 0; i < clearPixels.Length; i++) { clearPixels[i] = Color.clear; } atlasTexture.SetPixels(clearPixels); return atlasTexture; } private List PackSpritesIntoAtlas(Texture2D atlasTexture, Vector2Int atlasSize) { var spriteData = new List(); var currentX = _paddingPixels; var currentY = _paddingPixels; var rowHeight = 0; foreach (var texture in _ddsTextures) { if (texture == null) continue; // Check if sprite fits in current row if (currentX + texture.width + _paddingPixels > atlasSize.x) { // Move to next row currentY += rowHeight + _paddingPixels; currentX = _paddingPixels; rowHeight = 0; } // Check if sprite fits in atlas if (currentY + texture.height + _paddingPixels > atlasSize.y) { Debug.LogWarning($"Sprite {texture.name} doesn't fit in atlas, skipping..."); continue; } // Copy texture to atlas CopyTextureToAtlas(texture, atlasTexture, currentX, currentY); // Store sprite data spriteData.Add(new SpriteData { name = _keepOriginalNames ? texture.name : Path.GetFileNameWithoutExtension(texture.name), rect = new Rect(currentX, currentY, texture.width, texture.height), originalTexture = texture }); // Update position currentX += texture.width + _paddingPixels; rowHeight = Mathf.Max(rowHeight, texture.height); } atlasTexture.Apply(); return spriteData; } private void CopyTextureToAtlas(Texture2D sourceTexture, Texture2D atlasTexture, int x, int y) { // Make source texture readable temporarily var sourcePath = AssetDatabase.GetAssetPath(sourceTexture); var importer = AssetImporter.GetAtPath(sourcePath) as TextureImporter; bool wasReadable = false; bool wasCompressed = false; if (importer != null) { wasReadable = importer.isReadable; wasCompressed = importer.textureCompression != TextureImporterCompression.Uncompressed; if (!wasReadable || wasCompressed) { importer.isReadable = true; importer.textureCompression = TextureImporterCompression.Uncompressed; importer.SaveAndReimport(); // Wait for reimport to complete AssetDatabase.Refresh(); EditorUtility.SetDirty(importer); } } try { // Create a readable copy of the texture Texture2D readableTexture = CreateReadableTexture(sourceTexture); if (readableTexture != null) { // Copy pixels from readable texture var pixels = readableTexture.GetPixels(); atlasTexture.SetPixels(x, y, sourceTexture.width, sourceTexture.height, pixels); // Clean up DestroyImmediate(readableTexture); } else { Debug.LogWarning($"Could not create readable copy of texture: {sourceTexture.name}"); } } catch (Exception ex) { Debug.LogError($"Error copying texture {sourceTexture.name}: {ex.Message}"); } finally { // Restore original settings if (importer != null && (!wasReadable || wasCompressed)) { importer.isReadable = wasReadable; if (wasCompressed) { importer.textureCompression = TextureImporterCompression.Compressed; } importer.SaveAndReimport(); } } } private Texture2D CreateReadableTexture(Texture2D source) { try { // Create a new texture with the same dimensions var readableTexture = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false); // Use RenderTexture to copy the texture data RenderTexture renderTexture = RenderTexture.GetTemporary(source.width, source.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); Graphics.Blit(source, renderTexture); RenderTexture.active = renderTexture; readableTexture.ReadPixels(new Rect(0, 0, source.width, source.height), 0, 0); readableTexture.Apply(); RenderTexture.active = null; RenderTexture.ReleaseTemporary(renderTexture); return readableTexture; } catch (Exception ex) { Debug.LogError($"Error creating readable texture: {ex.Message}"); return null; } } private string SaveAtlasTexture(Texture2D atlasTexture, Vector2Int size) { string fileName = $"{_atlasName}.png"; string filePath = Path.Combine(_outputFolder, fileName); // Ensure unique filename filePath = GetUniqueFilePath(filePath); // Save as PNG byte[] pngData = atlasTexture.EncodeToPNG(); File.WriteAllBytes(filePath, pngData); // Import to Unity AssetDatabase.ImportAsset(filePath); return filePath; } private void CreateSpriteMetadata(string atlasPath, List spriteData) { var importer = AssetImporter.GetAtPath(atlasPath) as TextureImporter; if (importer == null) return; // Configure importer importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Multiple; importer.mipmapEnabled = false; importer.alphaIsTransparency = true; importer.spritePixelsPerUnit = _pixelsPerUnit; // Create sprite metadata using SpriteMetaData var spriteMetas = new List(); foreach (var data in spriteData) { var meta = new SpriteMetaData { name = SafeSpriteName(data.name), rect = data.rect, alignment = (int)SpriteAlignment.Center, border = Vector4.zero, pivot = new Vector2(0.5f, 0.5f) }; spriteMetas.Add(meta); } // Use the legacy spritesheet property (works in most Unity versions) #pragma warning disable CS0618 // Type or member is obsolete importer.spritesheet = spriteMetas.ToArray(); #pragma warning restore CS0618 // Type or member is obsolete importer.SaveAndReimport(); AssetDatabase.Refresh(); } private void CreateSpriteAtlasAsset(string atlasPath, List spriteData) { try { // Create Sprite Atlas asset using new keyword var spriteAtlas = new UnityEngine.U2D.SpriteAtlas(); // Add atlas texture as packable var atlasTexture = AssetDatabase.LoadAssetAtPath(atlasPath); if (atlasTexture != null) { // Use reflection to add objects to sprite atlas (for compatibility) var addMethod = typeof(UnityEngine.U2D.SpriteAtlas).GetMethod("Add", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); if (addMethod != null) { var packableObjects = new UnityEngine.Object[] { atlasTexture }; addMethod.Invoke(spriteAtlas, new object[] { packableObjects }); } } // Save sprite atlas asset string atlasAssetPath = Path.Combine(_outputFolder, $"{_atlasName}_Atlas.spriteatlas"); AssetDatabase.CreateAsset(spriteAtlas, atlasAssetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } catch (Exception ex) { Debug.LogWarning($"[DDSMultiSpriteConverter] Could not create Sprite Atlas asset: {ex.Message}"); _status += " (Sprite Atlas creation failed, but multi-sprite texture was created successfully)"; } } private class SpriteData { public string name; public Rect rect; public Texture2D originalTexture; } // Utility methods private static void EnsureFolderExists(string folderPath) { if (!AssetDatabase.IsValidFolder(folderPath)) { string[] parts = folderPath.Split('/'); string currentPath = parts[0]; // "Assets" for (int i = 1; i < parts.Length; i++) { string nextPath = currentPath + "/" + parts[i]; if (!AssetDatabase.IsValidFolder(nextPath)) { AssetDatabase.CreateFolder(currentPath, parts[i]); } currentPath = nextPath; } } } 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 filePath) { if (!File.Exists(filePath)) return filePath; string directory = Path.GetDirectoryName(filePath); string fileName = Path.GetFileNameWithoutExtension(filePath); string extension = Path.GetExtension(filePath); int counter = 1; string newPath; do { newPath = Path.Combine(directory, $"{fileName}_{counter}{extension}"); counter++; } while (File.Exists(newPath)); return newPath; } private static string SafeSpriteName(string name) { if (string.IsNullOrWhiteSpace(name)) return "Sprite"; // Remove invalid characters char[] invalidChars = Path.GetInvalidFileNameChars(); foreach (char c in invalidChars) { name = name.Replace(c, '_'); } // Remove .dds extension if present if (name.EndsWith(".dds", StringComparison.OrdinalIgnoreCase)) { name = name.Substring(0, name.Length - 4); } return name; } } #endif