PackTexturesTool.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using UnityEditor;
  7. using UnityEditorInternal;
  8. using UnityEngine;
  9. using Object = UnityEngine.Object;
  10. namespace Animancer.Editor.Tools
  11. {
  12. /// <summary>[Editor-Only] [Pro-Only]
  13. /// A <see cref="AnimancerToolsWindow.Tool"/> for packing multiple <see cref="Texture2D"/>s into a single image.
  14. /// </summary>
  15. /// <remarks>
  16. /// <strong>Documentation:</strong>
  17. /// <see href="https://kybernetik.com.au/animancer/docs/manual/tools/pack-textures">
  18. /// Pack Textures</see>
  19. /// </remarks>
  20. /// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/PackTexturesTool
  21. ///
  22. [Serializable]
  23. public class PackTexturesTool : AnimancerToolsWindow.Tool
  24. {
  25. /************************************************************************************************************************/
  26. [SerializeField] private List<Object> _AssetsToPack;
  27. [SerializeField] private int _Padding;
  28. [SerializeField] private int _MaximumSize = 8192;
  29. [NonSerialized] private ReorderableList _TexturesDisplay;
  30. /************************************************************************************************************************/
  31. /// <inheritdoc/>
  32. public override int DisplayOrder => 0;
  33. /// <inheritdoc/>
  34. public override string Name => "Pack Textures";
  35. /// <inheritdoc/>
  36. public override string HelpURL => Strings.DocsURLs.PackTextures;
  37. /// <inheritdoc/>
  38. public override string Instructions
  39. {
  40. get
  41. {
  42. if (_AssetsToPack.Count == 0)
  43. return "Add the texture, sprites, and folders you want to pack to the list.";
  44. return "Set the other details then click Pack and it will ask where you want to save the combined texture.";
  45. }
  46. }
  47. /************************************************************************************************************************/
  48. /// <inheritdoc/>
  49. public override void OnEnable(int index)
  50. {
  51. base.OnEnable(index);
  52. _AssetsToPack ??= new();
  53. _TexturesDisplay = AnimancerToolsWindow.CreateReorderableObjectList(_AssetsToPack, "Textures", true);
  54. }
  55. /************************************************************************************************************************/
  56. /// <inheritdoc/>
  57. public override void DoBodyGUI()
  58. {
  59. GUILayout.BeginVertical();
  60. _TexturesDisplay.DoLayoutList();
  61. GUILayout.EndVertical();
  62. HandleDragAndDropIntoList(GUILayoutUtility.GetLastRect(), _AssetsToPack, overwrite: false);
  63. RemoveDuplicates(_AssetsToPack);
  64. AnimancerToolsWindow.BeginChangeCheck();
  65. var padding = EditorGUILayout.IntField("Padding", _Padding);
  66. AnimancerToolsWindow.EndChangeCheck(ref _Padding, padding);
  67. AnimancerToolsWindow.BeginChangeCheck();
  68. var maximumSize = EditorGUILayout.IntField("Maximum Size", _MaximumSize);
  69. maximumSize = Math.Max(maximumSize, 16);
  70. AnimancerToolsWindow.EndChangeCheck(ref _MaximumSize, maximumSize);
  71. #if !UNITY_IMAGE_CONVERSION
  72. EditorGUILayout.HelpBox(
  73. "This feature requires Unity's Built-in Image Conversion module." +
  74. "\n1. Click here to open the Package Manager." +
  75. "\n2. Open the Packages menu and select 'Built-in'." +
  76. "\n3. Select the 'Image Conversion' module and Enable it.",
  77. MessageType.Error);
  78. if (AnimancerGUI.TryUseClickEventInLastRect())
  79. EditorApplication.ExecuteMenuItem("Window/Package Manager");
  80. #endif
  81. GUILayout.BeginHorizontal();
  82. {
  83. GUILayout.FlexibleSpace();
  84. GUI.enabled = _AssetsToPack.Count > 0;
  85. if (GUILayout.Button("Clear"))
  86. {
  87. AnimancerGUI.Deselect();
  88. AnimancerToolsWindow.RecordUndo();
  89. _AssetsToPack.Clear();
  90. }
  91. #if !UNITY_IMAGE_CONVERSION
  92. var enabled = GUI.enabled;
  93. GUI.enabled = false;
  94. #endif
  95. if (GUILayout.Button("Pack"))
  96. {
  97. #if UNITY_IMAGE_CONVERSION
  98. AnimancerGUI.Deselect();
  99. Pack();
  100. #endif
  101. }
  102. #if !UNITY_IMAGE_CONVERSION
  103. GUI.enabled = enabled;
  104. #endif
  105. }
  106. GUILayout.EndHorizontal();
  107. }
  108. /************************************************************************************************************************/
  109. /// <summary>Removes any items from the `list` that are the same as earlier items.</summary>
  110. private static void RemoveDuplicates<T>(IList<T> list)
  111. {
  112. for (int i = list.Count - 1; i >= 0; i--)
  113. {
  114. var item = list[i];
  115. if (item == null)
  116. continue;
  117. for (int j = 0; j < i; j++)
  118. {
  119. if (item.Equals(list[j]))
  120. {
  121. list.RemoveAt(i);
  122. break;
  123. }
  124. }
  125. }
  126. }
  127. /************************************************************************************************************************/
  128. #if UNITY_IMAGE_CONVERSION
  129. /************************************************************************************************************************/
  130. /// <summary>Combines the <see cref="_AssetsToPack"/> into a new texture and saves it.</summary>
  131. private void Pack()
  132. {
  133. var textures = GatherTextures();
  134. if (textures.Count == 0 ||
  135. !MakeTexturesReadable(textures))
  136. return;
  137. var path = GetCommonDirectory(_AssetsToPack);
  138. path = EditorUtility.SaveFilePanelInProject("Save Packed Texture", "PackedTexture", "png",
  139. "Where would you like to save the packed texture?", path);
  140. if (string.IsNullOrEmpty(path))
  141. return;
  142. try
  143. {
  144. const string ProgressTitle = "Packing Textures";
  145. EditorUtility.DisplayProgressBar(ProgressTitle, "Gathering", 0);
  146. var tightSprites = GatherTightSprites();
  147. EditorUtility.DisplayProgressBar(ProgressTitle, "Packing", 0.1f);
  148. var packedTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false);
  149. var tightTextures = new Texture2D[tightSprites.Count];
  150. var index = 0;
  151. foreach (var sprite in tightSprites)
  152. tightTextures[index++] = sprite.texture;
  153. var packedUVs = packedTexture.PackTextures(tightTextures, _Padding, _MaximumSize);
  154. EditorUtility.DisplayProgressBar(ProgressTitle, "Encoding", 0.4f);
  155. var bytes = packedTexture.EncodeToPNG();
  156. if (bytes == null)
  157. return;
  158. EditorUtility.DisplayProgressBar(ProgressTitle, "Writing", 0.5f);
  159. File.WriteAllBytes(path, bytes);
  160. AssetDatabase.Refresh();
  161. var importer = GetTextureImporter(path);
  162. importer.maxTextureSize = Math.Max(packedTexture.width, packedTexture.height);
  163. importer.textureType = TextureImporterType.Sprite;
  164. importer.spriteImportMode = SpriteImportMode.Multiple;
  165. var data = new SpriteDataEditor(importer)
  166. {
  167. SpriteCount = 0
  168. };
  169. CopyCompressionSettings(importer, textures);
  170. EditorUtility.SetDirty(importer);
  171. importer.SaveAndReimport();
  172. // Use the UV coordinates to set up sprites for the new texture.
  173. EditorUtility.DisplayProgressBar(ProgressTitle, "Generating Sprites", 0.7f);
  174. data.SpriteCount = tightSprites.Count;
  175. index = 0;
  176. foreach (var sprite in tightSprites)
  177. {
  178. var rect = packedUVs[index];
  179. rect.x *= packedTexture.width;
  180. rect.y *= packedTexture.height;
  181. rect.width *= packedTexture.width;
  182. rect.height *= packedTexture.height;
  183. var spriteRect = rect;
  184. spriteRect.x += spriteRect.width * sprite.rect.x / sprite.texture.width;
  185. spriteRect.y += spriteRect.height * sprite.rect.y / sprite.texture.height;
  186. spriteRect.width *= sprite.rect.width / sprite.texture.width;
  187. spriteRect.height *= sprite.rect.height / sprite.texture.height;
  188. var pivot = sprite.pivot;
  189. pivot.x /= rect.width;
  190. pivot.y /= rect.height;
  191. data.SetName(index, sprite.name);
  192. data.SetRect(index, spriteRect);
  193. data.SetPivot(index, pivot);
  194. data.SetBorder(index, sprite.border);
  195. index++;
  196. }
  197. EditorUtility.DisplayProgressBar(ProgressTitle, "Saving", 0.9f);
  198. data.Apply();
  199. EditorUtility.SetDirty(importer);
  200. importer.SaveAndReimport();
  201. Selection.activeObject = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
  202. }
  203. finally
  204. {
  205. EditorUtility.ClearProgressBar();
  206. }
  207. }
  208. /************************************************************************************************************************/
  209. private HashSet<Texture2D> GatherTextures()
  210. {
  211. var textures = new HashSet<Texture2D>();
  212. for (int i = 0; i < _AssetsToPack.Count; i++)
  213. {
  214. var obj = _AssetsToPack[i];
  215. var path = AssetDatabase.GetAssetPath(obj);
  216. if (string.IsNullOrEmpty(path))
  217. continue;
  218. if (obj is Texture2D texture)
  219. textures.Add(texture);
  220. else if (obj is Sprite sprite)
  221. textures.Add(sprite.texture);
  222. else if (obj is DefaultAsset)
  223. ForEachTextureInFolder(path, tex => textures.Add(tex));
  224. }
  225. return textures;
  226. }
  227. /************************************************************************************************************************/
  228. private HashSet<Sprite> GatherTightSprites()
  229. {
  230. var sprites = new HashSet<Sprite>();
  231. for (int i = 0; i < _AssetsToPack.Count; i++)
  232. {
  233. var obj = _AssetsToPack[i];
  234. var path = AssetDatabase.GetAssetPath(obj);
  235. if (string.IsNullOrEmpty(path))
  236. continue;
  237. if (obj is Texture2D texture)
  238. GatherTightSprites(sprites, texture);
  239. else if (obj is Sprite sprite)
  240. sprites.Add(CreateTightSprite(sprite));
  241. else if (obj is DefaultAsset)
  242. ForEachTextureInFolder(path, tex => GatherTightSprites(sprites, tex));
  243. }
  244. return sprites;
  245. }
  246. /************************************************************************************************************************/
  247. private static void GatherTightSprites(ICollection<Sprite> sprites, Texture2D texture)
  248. {
  249. var path = AssetDatabase.GetAssetPath(texture);
  250. var assets = AssetDatabase.LoadAllAssetsAtPath(path);
  251. var foundSprite = false;
  252. for (int i = 0; i < assets.Length; i++)
  253. {
  254. if (assets[i] is Sprite sprite)
  255. {
  256. sprite = CreateTightSprite(sprite);
  257. sprites.Add(sprite);
  258. foundSprite = true;
  259. }
  260. }
  261. if (!foundSprite)
  262. {
  263. var sprite = Sprite.Create(texture,
  264. new(0, 0, texture.width, texture.height),
  265. new(0.5f, 0.5f));
  266. sprite.name = texture.name;
  267. sprites.Add(sprite);
  268. }
  269. }
  270. /************************************************************************************************************************/
  271. private static Sprite CreateTightSprite(Sprite sprite)
  272. {
  273. var rect = sprite.rect;
  274. var width = Mathf.CeilToInt(rect.width);
  275. var height = Mathf.CeilToInt(rect.height);
  276. if (width == sprite.texture.width &&
  277. height == sprite.texture.height)
  278. return sprite;
  279. var pixels = sprite.texture.GetPixels(
  280. Mathf.FloorToInt(rect.x),
  281. Mathf.FloorToInt(rect.y),
  282. width,
  283. height);
  284. var texture = new Texture2D(width, height, sprite.texture.format, false, true);
  285. #pragma warning disable UNT0017 // SetPixels invocation is slow.
  286. texture.SetPixels(pixels);
  287. #pragma warning restore UNT0017 // SetPixels invocation is slow.
  288. texture.Apply();
  289. rect.x = 0;
  290. rect.y = 0;
  291. var pivot = sprite.pivot;
  292. pivot.x /= rect.width;
  293. pivot.y /= rect.height;
  294. var newSprite = Sprite.Create(texture, rect, pivot, sprite.pixelsPerUnit);
  295. newSprite.name = sprite.name;
  296. return newSprite;
  297. }
  298. /************************************************************************************************************************/
  299. private static bool MakeTexturesReadable(HashSet<Texture2D> textures)
  300. {
  301. var hasAsked = false;
  302. foreach (var texture in textures)
  303. {
  304. var importer = GetTextureImporter(texture);
  305. if (importer == null)
  306. continue;
  307. if (importer.isReadable &&
  308. importer.textureCompression == TextureImporterCompression.Uncompressed)
  309. continue;
  310. if (!hasAsked)
  311. {
  312. if (!EditorUtility.DisplayDialog("Make Textures Readable and Uncompressed?",
  313. "This tool requires the source textures to be marked as readable and uncompressed in their import settings.",
  314. "Make Textures Readable and Uncompressed", "Cancel"))
  315. return false;
  316. hasAsked = true;
  317. }
  318. importer.isReadable = true;
  319. importer.textureCompression = TextureImporterCompression.Uncompressed;
  320. importer.SaveAndReimport();
  321. }
  322. return true;
  323. }
  324. /************************************************************************************************************************/
  325. private static void ForEachTextureInFolder(string path, Action<Texture2D> action)
  326. {
  327. var guids = AssetDatabase.FindAssets($"t:{nameof(Texture2D)}", new string[] { path });
  328. for (int i = 0; i < guids.Length; i++)
  329. {
  330. path = AssetDatabase.GUIDToAssetPath(guids[i]);
  331. var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
  332. if (texture != null)
  333. action(texture);
  334. }
  335. }
  336. /************************************************************************************************************************/
  337. private static void CopyCompressionSettings(TextureImporter copyTo, IEnumerable<Texture2D> copyFrom)
  338. {
  339. var first = true;
  340. foreach (var texture in copyFrom)
  341. {
  342. var copyFromImporter = GetTextureImporter(texture);
  343. if (copyFromImporter == null)
  344. continue;
  345. if (first)
  346. {
  347. first = false;
  348. copyTo.textureCompression = copyFromImporter.textureCompression;
  349. copyTo.crunchedCompression = copyFromImporter.crunchedCompression;
  350. copyTo.compressionQuality = copyFromImporter.compressionQuality;
  351. copyTo.filterMode = copyFromImporter.filterMode;
  352. }
  353. else
  354. {
  355. if (IsHigherQuality(copyFromImporter.textureCompression, copyTo.textureCompression))
  356. copyTo.textureCompression = copyFromImporter.textureCompression;
  357. if (copyFromImporter.crunchedCompression)
  358. copyTo.crunchedCompression = true;
  359. if (copyTo.compressionQuality < copyFromImporter.compressionQuality)
  360. copyTo.compressionQuality = copyFromImporter.compressionQuality;
  361. if (copyTo.filterMode > copyFromImporter.filterMode)
  362. copyTo.filterMode = copyFromImporter.filterMode;
  363. }
  364. }
  365. }
  366. /************************************************************************************************************************/
  367. private static bool IsHigherQuality(TextureImporterCompression higher, TextureImporterCompression lower)
  368. {
  369. return higher switch
  370. {
  371. TextureImporterCompression.Uncompressed
  372. => lower != TextureImporterCompression.Uncompressed,
  373. TextureImporterCompression.Compressed
  374. => lower == TextureImporterCompression.CompressedLQ,
  375. TextureImporterCompression.CompressedHQ
  376. => lower == TextureImporterCompression.Compressed
  377. || lower == TextureImporterCompression.CompressedLQ,
  378. TextureImporterCompression.CompressedLQ
  379. => false,
  380. _
  381. => throw AnimancerUtilities.CreateUnsupportedArgumentException(higher),
  382. };
  383. }
  384. /************************************************************************************************************************/
  385. private static string GetCommonDirectory<T>(IList<T> objects) where T : Object
  386. {
  387. if (objects == null)
  388. return null;
  389. var count = objects.Count;
  390. for (int i = count - 1; i >= 0; i--)
  391. {
  392. if (objects[i] == null)
  393. {
  394. objects.RemoveAt(i);
  395. count--;
  396. }
  397. }
  398. if (count == 0)
  399. return null;
  400. var path = AssetDatabase.GetAssetPath(objects[0]);
  401. path = Path.GetDirectoryName(path);
  402. for (int i = 1; i < count; i++)
  403. {
  404. var otherPath = AssetDatabase.GetAssetPath(objects[i]);
  405. otherPath = Path.GetDirectoryName(otherPath);
  406. while (string.Compare(path, 0, otherPath, 0, path.Length) != 0)
  407. {
  408. path = Path.GetDirectoryName(path);
  409. }
  410. }
  411. return path;
  412. }
  413. /************************************************************************************************************************/
  414. private static TextureImporter GetTextureImporter(Object asset)
  415. {
  416. var path = AssetDatabase.GetAssetPath(asset);
  417. if (string.IsNullOrEmpty(path))
  418. return null;
  419. return GetTextureImporter(path);
  420. }
  421. private static TextureImporter GetTextureImporter(string path)
  422. => AssetImporter.GetAtPath(path) as TextureImporter;
  423. /************************************************************************************************************************/
  424. #endif
  425. }
  426. }
  427. #endif