630 lines
22 KiB
C#
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 |