// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // #if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditorInternal; using UnityEngine; using Object = UnityEngine.Object; namespace Animancer.Editor.Tools { /// [Editor-Only] [Pro-Only] /// A for packing multiple s into a single image. /// /// /// Documentation: Pack Textures /// /// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/PackTexturesTool /// [Serializable] public class PackTexturesTool : AnimancerToolsWindow.Tool { /************************************************************************************************************************/ [SerializeField] private List _AssetsToPack; [SerializeField] private int _Padding; [SerializeField] private int _MaximumSize = 8192; [NonSerialized] private ReorderableList _TexturesDisplay; /************************************************************************************************************************/ /// public override int DisplayOrder => 0; /// public override string Name => "Pack Textures"; /// public override string HelpURL => Strings.DocsURLs.PackTextures; /// public override string Instructions { get { if (_AssetsToPack.Count == 0) return "Add the texture, sprites, and folders you want to pack to the list."; return "Set the other details then click Pack and it will ask where you want to save the combined texture."; } } /************************************************************************************************************************/ /// public override void OnEnable(int index) { base.OnEnable(index); if (_AssetsToPack == null) _AssetsToPack = new List(); _TexturesDisplay = AnimancerToolsWindow.CreateReorderableObjectList(_AssetsToPack, "Textures", true); } /************************************************************************************************************************/ /// public override void DoBodyGUI() { GUILayout.BeginVertical(); _TexturesDisplay.DoLayoutList(); GUILayout.EndVertical(); HandleDragAndDropIntoList(GUILayoutUtility.GetLastRect(), _AssetsToPack, overwrite: false); RemoveDuplicates(_AssetsToPack); AnimancerToolsWindow.BeginChangeCheck(); var padding = EditorGUILayout.IntField("Padding", _Padding); AnimancerToolsWindow.EndChangeCheck(ref _Padding, padding); AnimancerToolsWindow.BeginChangeCheck(); var maximumSize = EditorGUILayout.IntField("Maximum Size", _MaximumSize); maximumSize = Math.Max(maximumSize, 16); AnimancerToolsWindow.EndChangeCheck(ref _MaximumSize, maximumSize); GUILayout.BeginHorizontal(); { GUILayout.FlexibleSpace(); GUI.enabled = _AssetsToPack.Count > 0; if (GUILayout.Button("Clear")) { AnimancerGUI.Deselect(); AnimancerToolsWindow.RecordUndo(); _AssetsToPack.Clear(); } if (GUILayout.Button("Pack")) { AnimancerGUI.Deselect(); Pack(); } } GUILayout.EndHorizontal(); } /************************************************************************************************************************/ /// Removes any items from the `list` that are the same as earlier items. private static void RemoveDuplicates(IList list) { for (int i = list.Count - 1; i >= 0; i--) { var item = list[i]; if (item == null) continue; for (int j = 0; j < i; j++) { if (item.Equals(list[j])) { list.RemoveAt(i); break; } } } } /************************************************************************************************************************/ /// Combines the into a new texture and saves it. private void Pack() { var textures = GatherTextures(); if (textures.Count == 0 || !MakeTexturesReadable(textures)) return; var path = GetCommonDirectory(_AssetsToPack); path = EditorUtility.SaveFilePanelInProject("Save Packed Texture", "PackedTexture", "png", "Where would you like to save the packed texture?", path); if (string.IsNullOrEmpty(path)) return; try { const string ProgressTitle = "Packing Textures"; EditorUtility.DisplayProgressBar(ProgressTitle, "Gathering", 0); var tightSprites = GatherTightSprites(); EditorUtility.DisplayProgressBar(ProgressTitle, "Packing", 0.1f); var packedTexture = new Texture2D(0, 0, TextureFormat.ARGB32, false); var tightTextures = new Texture2D[tightSprites.Count]; var index = 0; foreach (var sprite in tightSprites) tightTextures[index++] = sprite.texture; var packedUVs = packedTexture.PackTextures(tightTextures, _Padding, _MaximumSize); EditorUtility.DisplayProgressBar(ProgressTitle, "Encoding", 0.4f); var bytes = packedTexture.EncodeToPNG(); if (bytes == null) return; EditorUtility.DisplayProgressBar(ProgressTitle, "Writing", 0.5f); File.WriteAllBytes(path, bytes); AssetDatabase.Refresh(); var importer = GetTextureImporter(path); importer.maxTextureSize = Math.Max(packedTexture.width, packedTexture.height); importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Multiple; var data = new SpriteDataEditor(importer) { SpriteCount = 0 }; CopyCompressionSettings(importer, textures); EditorUtility.SetDirty(importer); importer.SaveAndReimport(); // Use the UV coordinates to set up sprites for the new texture. EditorUtility.DisplayProgressBar(ProgressTitle, "Generating Sprites", 0.7f); data.SpriteCount = tightSprites.Count; index = 0; foreach (var sprite in tightSprites) { var rect = packedUVs[index]; rect.x *= packedTexture.width; rect.y *= packedTexture.height; rect.width *= packedTexture.width; rect.height *= packedTexture.height; var spriteRect = rect; spriteRect.x += spriteRect.width * sprite.rect.x / sprite.texture.width; spriteRect.y += spriteRect.height * sprite.rect.y / sprite.texture.height; spriteRect.width *= sprite.rect.width / sprite.texture.width; spriteRect.height *= sprite.rect.height / sprite.texture.height; var pivot = sprite.pivot; pivot.x /= rect.width; pivot.y /= rect.height; data.SetName(index, sprite.name); data.SetRect(index, spriteRect); data.SetAlignment(index, GetAlignment(sprite.pivot)); data.SetPivot(index, pivot); data.SetBorder(index, sprite.border); index++; } EditorUtility.DisplayProgressBar(ProgressTitle, "Saving", 0.9f); data.Apply(); EditorUtility.SetDirty(importer); importer.SaveAndReimport(); Selection.activeObject = AssetDatabase.LoadAssetAtPath(path); } finally { EditorUtility.ClearProgressBar(); } } /************************************************************************************************************************/ private HashSet GatherTextures() { var textures = new HashSet(); for (int i = 0; i < _AssetsToPack.Count; i++) { var obj = _AssetsToPack[i]; var path = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(path)) continue; if (obj is Texture2D texture) textures.Add(texture); else if (obj is Sprite sprite) textures.Add(sprite.texture); else if (obj is DefaultAsset) ForEachTextureInFolder(path, tex => textures.Add(tex)); } return textures; } /************************************************************************************************************************/ private HashSet GatherTightSprites() { var sprites = new HashSet(); for (int i = 0; i < _AssetsToPack.Count; i++) { var obj = _AssetsToPack[i]; var path = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(path)) continue; if (obj is Texture2D texture) GatherTightSprites(sprites, texture); else if (obj is Sprite sprite) sprites.Add(CreateTightSprite(sprite)); else if (obj is DefaultAsset) ForEachTextureInFolder(path, tex => GatherTightSprites(sprites, tex)); } return sprites; } /************************************************************************************************************************/ private static void GatherTightSprites(ICollection sprites, Texture2D texture) { var path = AssetDatabase.GetAssetPath(texture); var assets = AssetDatabase.LoadAllAssetsAtPath(path); var foundSprite = false; for (int i = 0; i < assets.Length; i++) { if (assets[i] is Sprite sprite) { sprite = CreateTightSprite(sprite); sprites.Add(sprite); foundSprite = true; } } if (!foundSprite) { var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f)); sprite.name = texture.name; sprites.Add(sprite); } } /************************************************************************************************************************/ private static Sprite CreateTightSprite(Sprite sprite) { var rect = sprite.rect; var width = Mathf.CeilToInt(rect.width); var height = Mathf.CeilToInt(rect.height); if (width == sprite.texture.width && height == sprite.texture.height) return sprite; var pixels = sprite.texture.GetPixels( Mathf.FloorToInt(rect.x), Mathf.FloorToInt(rect.y), width, height); var texture = new Texture2D(width, height, sprite.texture.format, false, true); texture.SetPixels(pixels); texture.Apply(); rect.x = 0; rect.y = 0; var pivot = sprite.pivot; pivot.x /= rect.width; pivot.y /= rect.height; var newSprite = Sprite.Create(texture, rect, pivot, sprite.pixelsPerUnit); newSprite.name = sprite.name; return newSprite; } /************************************************************************************************************************/ private static bool MakeTexturesReadable(HashSet textures) { var hasAsked = false; foreach (var texture in textures) { var importer = GetTextureImporter(texture); if (importer == null) continue; if (importer.isReadable && importer.textureCompression == TextureImporterCompression.Uncompressed) continue; if (!hasAsked) { if (!EditorUtility.DisplayDialog("Make Textures Readable and Uncompressed?", "This tool requires the source textures to be marked as readable and uncompressed in their import settings.", "Make Textures Readable and Uncompressed", "Cancel")) return false; hasAsked = true; } importer.isReadable = true; importer.textureCompression = TextureImporterCompression.Uncompressed; importer.SaveAndReimport(); } return true; } /************************************************************************************************************************/ private static void ForEachTextureInFolder(string path, Action action) { var guids = AssetDatabase.FindAssets($"t:{nameof(Texture2D)}", new string[] { path }); for (int i = 0; i < guids.Length; i++) { path = AssetDatabase.GUIDToAssetPath(guids[i]); var texture = AssetDatabase.LoadAssetAtPath(path); if (texture != null) action(texture); } } /************************************************************************************************************************/ private static void CopyCompressionSettings(TextureImporter copyTo, IEnumerable copyFrom) { var first = true; foreach (var texture in copyFrom) { var copyFromImporter = GetTextureImporter(texture); if (copyFromImporter == null) continue; if (first) { first = false; copyTo.textureCompression = copyFromImporter.textureCompression; copyTo.crunchedCompression = copyFromImporter.crunchedCompression; copyTo.compressionQuality = copyFromImporter.compressionQuality; copyTo.filterMode = copyFromImporter.filterMode; } else { if (IsHigherQuality(copyFromImporter.textureCompression, copyTo.textureCompression)) copyTo.textureCompression = copyFromImporter.textureCompression; if (copyFromImporter.crunchedCompression) copyTo.crunchedCompression = true; if (copyTo.compressionQuality < copyFromImporter.compressionQuality) copyTo.compressionQuality = copyFromImporter.compressionQuality; if (copyTo.filterMode > copyFromImporter.filterMode) copyTo.filterMode = copyFromImporter.filterMode; } } } /************************************************************************************************************************/ private static bool IsHigherQuality(TextureImporterCompression higher, TextureImporterCompression lower) { switch (higher) { case TextureImporterCompression.Uncompressed: return lower != TextureImporterCompression.Uncompressed; case TextureImporterCompression.Compressed: return lower == TextureImporterCompression.CompressedLQ; case TextureImporterCompression.CompressedHQ: return lower == TextureImporterCompression.Compressed || lower == TextureImporterCompression.CompressedLQ; case TextureImporterCompression.CompressedLQ: return false; default: throw AnimancerUtilities.CreateUnsupportedArgumentException(higher); } } /************************************************************************************************************************/ private static string GetCommonDirectory(IList objects) where T : Object { if (objects == null) return null; var count = objects.Count; for (int i = count - 1; i >= 0; i--) { if (objects[i] == null) { objects.RemoveAt(i); count--; } } if (count == 0) return null; var path = AssetDatabase.GetAssetPath(objects[0]); path = Path.GetDirectoryName(path); for (int i = 1; i < count; i++) { var otherPath = AssetDatabase.GetAssetPath(objects[i]); otherPath = Path.GetDirectoryName(otherPath); while (string.Compare(path, 0, otherPath, 0, path.Length) != 0) { path = Path.GetDirectoryName(path); } } return path; } /************************************************************************************************************************/ private static SpriteAlignment GetAlignment(Vector2 pivot) { switch (pivot.x) { case 0: switch (pivot.y) { case 0: return SpriteAlignment.BottomLeft; case 0.5f: return SpriteAlignment.BottomCenter; case 1: return SpriteAlignment.BottomRight; } break; case 0.5f: switch (pivot.y) { case 0: return SpriteAlignment.LeftCenter; case 0.5f: return SpriteAlignment.Center; case 1: return SpriteAlignment.RightCenter; } break; case 1: switch (pivot.y) { case 0: return SpriteAlignment.TopLeft; case 0.5f: return SpriteAlignment.TopCenter; case 1: return SpriteAlignment.TopRight; } break; } return SpriteAlignment.Custom; } /************************************************************************************************************************/ private static TextureImporter GetTextureImporter(Object asset) { var path = AssetDatabase.GetAssetPath(asset); if (string.IsNullOrEmpty(path)) return null; return GetTextureImporter(path); } private static TextureImporter GetTextureImporter(string path) => AssetImporter.GetAtPath(path) as TextureImporter; /************************************************************************************************************************/ } } #endif