// Animancer // https://kybernetik.com.au/animancer // Copyright 2021 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 { partial class AnimancerToolsWindow { /// [Editor-Only] [Pro-Only] /// A for packing multiple s into a single image. /// /// /// Documentation: Pack Textures /// /// https://kybernetik.com.au/animancer/api/Animancer.Editor/PackTextures /// [Serializable] public sealed class PackTextures : Panel { /************************************************************************************************************************/ [SerializeField] private List _Textures; [SerializeField] private int _Padding; [SerializeField] private int _MaximumSize = 8192; [NonSerialized] private ReorderableList _TexturesDisplay; /************************************************************************************************************************/ /// public override string Name => "Pack Textures"; /// public override string HelpURL => Strings.DocsURLs.PackTextures; /// public override string Instructions { get { if (_Textures.Count == 0) return "Add the textures 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 (_Textures == null) _Textures = new List(); _TexturesDisplay = CreateReorderableObjectList(_Textures, "Textures", true); } /************************************************************************************************************************/ /// public override void DoBodyGUI() { GUILayout.BeginVertical(); _TexturesDisplay.DoLayoutList(); GUILayout.EndVertical(); HandleDragAndDropIntoList(GUILayoutUtility.GetLastRect(), _Textures, overwrite: false); RemoveDuplicates(_Textures); BeginChangeCheck(); var padding = EditorGUILayout.IntField("Padding", _Padding); EndChangeCheck(ref _Padding, padding); BeginChangeCheck(); var maximumSize = EditorGUILayout.IntField("Maximum Size", _MaximumSize); maximumSize = Math.Max(maximumSize, 16); EndChangeCheck(ref _MaximumSize, maximumSize); GUILayout.BeginHorizontal(); { GUILayout.FlexibleSpace(); GUI.enabled = _Textures.Count > 0; if (GUILayout.Button("Clear")) { AnimancerGUI.Deselect(); RecordUndo(); _Textures.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 one and saves it. private void Pack() { var textures = GatherTextures(); if (!MakeTexturesReadable(textures)) return; var path = GetCommonDirectory(_Textures); 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"; EditorUtility.DisplayProgressBar(ProgressTitle, "Packing", 0); var packedTexture = new Texture2D(0, 0, TextureFormat.ARGB32, false); var uvs = packedTexture.PackTextures(textures, _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 = (TextureImporter)AssetImporter.GetAtPath(path); importer.maxTextureSize = Math.Max(packedTexture.width, packedTexture.height); importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Multiple; importer.spritesheet = new SpriteMetaData[0]; EditorUtility.SetDirty(importer); importer.SaveAndReimport(); // Use the UV coordinates to set up sprites for the new texture. EditorUtility.DisplayProgressBar(ProgressTitle, "Generating Sprites", 0.7f); var sprites = new List(); var spriteSheet = new List(); for (int iTexture = 0; iTexture < textures.Length; iTexture++) { var texture = textures[iTexture]; sprites.Clear(); GatherSprites(sprites, texture); var rect = uvs[iTexture]; rect.x *= packedTexture.width; rect.y *= packedTexture.height; rect.width *= packedTexture.width; rect.height *= packedTexture.height; for (int iSprite = 0; iSprite < sprites.Count; iSprite++) { var sprite = sprites[iSprite]; 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; spriteSheet.Add(new SpriteMetaData { name = sprite.name, rect = spriteRect, alignment = (int)GetAlignment(sprite.pivot), pivot = sprite.pivot, border = sprite.border, }); } } importer.spritesheet = spriteSheet.ToArray(); EditorUtility.SetDirty(importer); importer.SaveAndReimport(); Selection.activeObject = AssetDatabase.LoadAssetAtPath(path); } finally { EditorUtility.ClearProgressBar(); } } /************************************************************************************************************************/ private Texture2D[] GatherTextures() { var textures = new List(); for (int i = 0; i < _Textures.Count; i++) { var obj = _Textures[i]; if (obj is Texture2D texture) textures.Add(texture); if (obj is DefaultAsset) GatherTexturesRecursive(textures, AssetDatabase.GetAssetPath(obj)); } RemoveDuplicates(textures); return textures.ToArray(); } private static void GatherTexturesRecursive(List textures, string path) { 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) textures.Add(texture); } } /************************************************************************************************************************/ private static void GatherSprites(List 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) { 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 bool MakeTexturesReadable(Texture2D[] textures) { var hasAsked = false; for (int i = 0; i < textures.Length; i++) { var texture = textures[i]; var path = AssetDatabase.GetAssetPath(texture); var importer = (TextureImporter)AssetImporter.GetAtPath(path); 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 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; } /************************************************************************************************************************/ } } } #endif