From 5f56c117d75b9be2fd60f8b3e52eb4b0566a342e Mon Sep 17 00:00:00 2001 From: HungDK <> Date: Thu, 16 Oct 2025 17:45:39 +0700 Subject: [PATCH] Add tool convert multi dds file --- Assets/Editor/DDSMultiSpriteConverter.cs | 630 ++++++++++++++++++ Assets/Editor/DDSMultiSpriteConverter.cs.meta | 2 + 2 files changed, 632 insertions(+) create mode 100644 Assets/Editor/DDSMultiSpriteConverter.cs create mode 100644 Assets/Editor/DDSMultiSpriteConverter.cs.meta diff --git a/Assets/Editor/DDSMultiSpriteConverter.cs b/Assets/Editor/DDSMultiSpriteConverter.cs new file mode 100644 index 0000000000..7325306202 --- /dev/null +++ b/Assets/Editor/DDSMultiSpriteConverter.cs @@ -0,0 +1,630 @@ +#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 \ No newline at end of file diff --git a/Assets/Editor/DDSMultiSpriteConverter.cs.meta b/Assets/Editor/DDSMultiSpriteConverter.cs.meta new file mode 100644 index 0000000000..3ce02443e9 --- /dev/null +++ b/Assets/Editor/DDSMultiSpriteConverter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a0338316fbff9784586126d8da119e57 \ No newline at end of file