Files
test/Assets/Editor/DDSMultiSpriteConverter.cs
T
2025-10-16 17:45:39 +07:00

630 lines
22 KiB
C#

#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<Texture2D> _ddsTextures = new List<Texture2D>();
[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<DDSMultiSpriteConverter>("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}. <Missing Texture>", 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<Texture2D>(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<Texture2D>(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<SpriteData> PackSpritesIntoAtlas(Texture2D atlasTexture, Vector2Int atlasSize)
{
var spriteData = new List<SpriteData>();
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> 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<SpriteMetaData>();
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> spriteData)
{
try
{
// Create Sprite Atlas asset using new keyword
var spriteAtlas = new UnityEngine.U2D.SpriteAtlas();
// Add atlas texture as packable
var atlasTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(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