AtlasUtilities.cs 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852
  1. /******************************************************************************
  2. * Spine Runtimes License Agreement
  3. * Last updated July 28, 2023. Replaces all prior versions.
  4. *
  5. * Copyright (c) 2013-2023, Esoteric Software LLC
  6. *
  7. * Integration of the Spine Runtimes into software or otherwise creating
  8. * derivative works of the Spine Runtimes is permitted under the terms and
  9. * conditions of Section 2 of the Spine Editor License Agreement:
  10. * http://esotericsoftware.com/spine-editor-license
  11. *
  12. * Otherwise, it is permitted to integrate the Spine Runtimes into software or
  13. * otherwise create derivative works of the Spine Runtimes (collectively,
  14. * "Products"), provided that each user of the Products must obtain their own
  15. * Spine Editor license and redistribution of the Products in any form must
  16. * include this license and copyright notice.
  17. *
  18. * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
  19. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
  24. * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
  27. * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *****************************************************************************/
  29. #if UNITY_2019_3_OR_NEWER
  30. #define CONFIGURABLE_ENTER_PLAY_MODE
  31. #endif
  32. using System;
  33. using System.Collections.Generic;
  34. using UnityEngine;
  35. namespace Spine.Unity.AttachmentTools {
  36. public static class AtlasUtilities {
  37. internal const TextureFormat SpineTextureFormat = TextureFormat.RGBA32;
  38. internal const float DefaultMipmapBias = -0.5f;
  39. internal const bool UseMipMaps = false;
  40. internal const float DefaultScale = 0.01f;
  41. const int NonrenderingRegion = -1;
  42. #if CONFIGURABLE_ENTER_PLAY_MODE
  43. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
  44. static void Init () {
  45. // handle disabled domain reload
  46. AtlasUtilities.ClearCache();
  47. }
  48. #endif
  49. public static AtlasRegion ToAtlasRegion (this Texture2D t, Material materialPropertySource, float scale = DefaultScale) {
  50. return t.ToAtlasRegion(materialPropertySource.shader, scale, materialPropertySource);
  51. }
  52. public static AtlasRegion ToAtlasRegion (this Texture2D t, Shader shader, float scale = DefaultScale, Material materialPropertySource = null) {
  53. Material material = new Material(shader);
  54. if (materialPropertySource != null) {
  55. material.CopyPropertiesFromMaterial(materialPropertySource);
  56. material.shaderKeywords = materialPropertySource.shaderKeywords;
  57. }
  58. material.mainTexture = t;
  59. AtlasPage page = material.ToSpineAtlasPage();
  60. float width = t.width;
  61. float height = t.height;
  62. AtlasRegion region = new AtlasRegion();
  63. region.name = t.name;
  64. // World space units
  65. Vector2 boundsMin = Vector2.zero, boundsMax = new Vector2(width, height) * scale;
  66. // Texture space/pixel units
  67. region.width = (int)width;
  68. region.originalWidth = (int)width;
  69. region.height = (int)height;
  70. region.originalHeight = (int)height;
  71. region.offsetX = width * (0.5f - InverseLerp(boundsMin.x, boundsMax.x, 0));
  72. region.offsetY = height * (0.5f - InverseLerp(boundsMin.y, boundsMax.y, 0));
  73. // Use the full area of the texture.
  74. region.u = 0;
  75. region.v = 1;
  76. region.u2 = 1;
  77. region.v2 = 0;
  78. region.x = 0;
  79. region.y = 0;
  80. region.page = page;
  81. return region;
  82. }
  83. /// <summary>
  84. /// Creates a Spine.AtlasRegion that uses a premultiplied alpha duplicate of the Sprite's texture data.</summary>
  85. public static AtlasRegion ToAtlasRegionPMAClone (this Texture2D t, Material materialPropertySource, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps) {
  86. return t.ToAtlasRegionPMAClone(materialPropertySource.shader, textureFormat, mipmaps, materialPropertySource);
  87. }
  88. /// <summary>
  89. /// Creates a Spine.AtlasRegion that uses a premultiplied alpha duplicate of the Sprite's texture data.</summary>
  90. public static AtlasRegion ToAtlasRegionPMAClone (this Texture2D t, Shader shader, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps, Material materialPropertySource = null) {
  91. Material material = new Material(shader);
  92. if (materialPropertySource != null) {
  93. material.CopyPropertiesFromMaterial(materialPropertySource);
  94. material.shaderKeywords = materialPropertySource.shaderKeywords;
  95. }
  96. Texture2D newTexture = t.GetClone(textureFormat, mipmaps, applyPMA: true);
  97. newTexture.name = t.name + "-pma-";
  98. material.name = t.name + shader.name;
  99. material.mainTexture = newTexture;
  100. AtlasPage page = material.ToSpineAtlasPage();
  101. AtlasRegion region = newTexture.ToAtlasRegion(shader);
  102. region.page = page;
  103. return region;
  104. }
  105. /// <summary>
  106. /// Creates a new Spine.AtlasPage from a UnityEngine.Material. If the material has a preassigned texture, the page width and height will be set.</summary>
  107. public static AtlasPage ToSpineAtlasPage (this Material m) {
  108. AtlasPage newPage = new AtlasPage {
  109. rendererObject = m,
  110. name = m.name
  111. };
  112. Texture t = m.mainTexture;
  113. if (t != null) {
  114. newPage.width = t.width;
  115. newPage.height = t.height;
  116. }
  117. return newPage;
  118. }
  119. /// <summary>
  120. /// Creates a Spine.AtlasRegion from a UnityEngine.Sprite.</summary>
  121. public static AtlasRegion ToAtlasRegion (this Sprite s, AtlasPage page) {
  122. if (page == null) throw new System.ArgumentNullException("page", "page cannot be null. AtlasPage determines which texture region belongs and how it should be rendered. You can use material.ToSpineAtlasPage() to get a shareable AtlasPage from a Material, or use the sprite.ToAtlasRegion(material) overload.");
  123. AtlasRegion region = s.ToAtlasRegion();
  124. region.page = page;
  125. return region;
  126. }
  127. /// <summary>
  128. /// Creates a Spine.AtlasRegion from a UnityEngine.Sprite. This creates a new AtlasPage object for every AtlasRegion you create. You can centralize Material control by creating a shared atlas page using Material.ToSpineAtlasPage and using the sprite.ToAtlasRegion(AtlasPage) overload.</summary>
  129. public static AtlasRegion ToAtlasRegion (this Sprite s, Material material) {
  130. AtlasRegion region = s.ToAtlasRegion();
  131. region.page = material.ToSpineAtlasPage();
  132. return region;
  133. }
  134. public static AtlasRegion ToAtlasRegionPMAClone (this Sprite s, Material materialPropertySource, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps) {
  135. return s.ToAtlasRegionPMAClone(materialPropertySource.shader, textureFormat, mipmaps, materialPropertySource);
  136. }
  137. /// <summary>
  138. /// Creates a Spine.AtlasRegion that uses a premultiplied alpha duplicate of the Sprite's texture data.</summary>
  139. public static AtlasRegion ToAtlasRegionPMAClone (this Sprite s, Shader shader, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps, Material materialPropertySource = null) {
  140. Material material = new Material(shader);
  141. if (materialPropertySource != null) {
  142. material.CopyPropertiesFromMaterial(materialPropertySource);
  143. material.shaderKeywords = materialPropertySource.shaderKeywords;
  144. }
  145. Texture2D tex = s.ToTexture(textureFormat, mipmaps, applyPMA: true);
  146. tex.name = s.name + "-pma-";
  147. material.name = tex.name + shader.name;
  148. material.mainTexture = tex;
  149. AtlasPage page = material.ToSpineAtlasPage();
  150. AtlasRegion region = s.ToAtlasRegion(true);
  151. region.page = page;
  152. return region;
  153. }
  154. internal static AtlasRegion ToAtlasRegion (this Sprite s, bool isolatedTexture = false) {
  155. AtlasRegion region = new AtlasRegion();
  156. region.name = s.name;
  157. region.index = -1;
  158. region.degrees = s.packed && s.packingRotation != SpritePackingRotation.None ? 90 : 0;
  159. // World space units
  160. Bounds bounds = s.bounds;
  161. Vector2 boundsMin = bounds.min, boundsMax = bounds.max;
  162. // Texture space/pixel units
  163. Rect spineRect = s.textureRect.SpineUnityFlipRect(s.texture.height);
  164. Rect originalRect = s.rect;
  165. region.width = (int)spineRect.width;
  166. region.originalWidth = (int)originalRect.width;
  167. region.height = (int)spineRect.height;
  168. region.originalHeight = (int)originalRect.height;
  169. region.offsetX = s.textureRectOffset.x + spineRect.width * (0.5f - InverseLerp(boundsMin.x, boundsMax.x, 0));
  170. region.offsetY = s.textureRectOffset.y + spineRect.height * (0.5f - InverseLerp(boundsMin.y, boundsMax.y, 0));
  171. if (isolatedTexture) {
  172. region.u = 0;
  173. region.v = 1;
  174. region.u2 = 1;
  175. region.v2 = 0;
  176. region.x = 0;
  177. region.y = 0;
  178. } else {
  179. Texture2D tex = s.texture;
  180. Rect uvRect = TextureRectToUVRect(s.textureRect, tex.width, tex.height);
  181. region.u = uvRect.xMin;
  182. region.v = uvRect.yMax;
  183. region.u2 = uvRect.xMax;
  184. region.v2 = uvRect.yMin;
  185. region.x = (int)spineRect.x;
  186. region.y = (int)spineRect.y;
  187. }
  188. return region;
  189. }
  190. #region Runtime Repacking
  191. static readonly Dictionary<AtlasRegion, int> existingRegions = new Dictionary<AtlasRegion, int>();
  192. static readonly List<int> regionIndices = new List<int>();
  193. static readonly List<AtlasRegion> originalRegions = new List<AtlasRegion>();
  194. static readonly List<AtlasRegion> repackedRegions = new List<AtlasRegion>();
  195. static List<Texture2D>[] texturesToPackAtParam = new List<Texture2D>[1];
  196. static List<Attachment> inoutAttachments = new List<Attachment>();
  197. /// <summary>
  198. /// Fills the outputAttachments list with new attachment objects based on the attachments in sourceAttachments,
  199. /// but mapped to a new single texture using the same material.</summary>
  200. /// <remarks>Returned <c>Material</c> and <c>Texture</c> behave like <c>new Texture2D()</c>, thus you need to call <c>Destroy()</c>
  201. /// to free resources.
  202. /// This method caches necessary Texture copies for later re-use, which might steadily increase the texture memory
  203. /// footprint when used excessively. Set <paramref name="clearCache"/> to <c>true</c>
  204. /// or call <see cref="AtlasUtilities.ClearCache()"/> to clear this texture cache.
  205. /// You may want to call <c>Resources.UnloadUnusedAssets()</c> after that.
  206. /// </remarks>
  207. /// <param name="sourceAttachments">The list of attachments to be repacked.</param>
  208. /// <param name = "outputAttachments">The List(Attachment) to populate with the newly created Attachment objects.
  209. /// May be equal to <c>sourceAttachments</c> for in-place operation.</param>
  210. /// <param name="materialPropertySource">May be null. If no Material property source is provided, a material with
  211. /// default parameters using the provided <c>shader</c> will be created.</param>
  212. /// <param name="clearCache">When set to <c>true</c>, <see cref="AtlasUtilities.ClearCache()"/> is called after
  213. /// repacking to clear the texture cache. See remarks for additional info.</param>
  214. /// <param name="additionalTexturePropertyIDsToCopy">Optional additional textures (such as normal maps) to copy while repacking.
  215. /// To copy e.g. the main texture and normal maps, pass 'new int[] { Shader.PropertyToID("_BumpMap") }' at this parameter.</param>
  216. /// <param name="additionalOutputTextures">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  217. /// this array will be filled with the resulting repacked texture for every property,
  218. /// just as the main repacked texture is assigned to <c>outputTexture</c>.</param>
  219. /// <param name="additionalTextureFormats">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  220. /// this array will be used as <c>TextureFormat</c> at the Texture at the respective property.
  221. /// When <c>additionalTextureFormats</c> is <c>null</c> or when its array size is smaller,
  222. /// <c>textureFormat</c> is used where there exists no corresponding array item.</param>
  223. /// <param name="additionalTextureIsLinear">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  224. /// this array will be used to determine whether <c>linear</c> or <c>sRGB</c> color space is used at the
  225. /// Texture at the respective property. When <c>additionalTextureIsLinear</c> is <c>null</c>, <c>linear</c> color space
  226. /// is assumed at every additional Texture element.
  227. /// When e.g. packing the main texture and normal maps, pass 'new bool[] { true }' at this parameter, because normal maps use
  228. /// linear color space.</param>
  229. public static void GetRepackedAttachments (List<Attachment> sourceAttachments, List<Attachment> outputAttachments, Material materialPropertySource,
  230. out Material outputMaterial, out Texture2D outputTexture,
  231. int maxAtlasSize = 1024, int padding = 2, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  232. string newAssetName = "Repacked Attachments", bool clearCache = false, bool useOriginalNonrenderables = true,
  233. int[] additionalTexturePropertyIDsToCopy = null, Texture2D[] additionalOutputTextures = null,
  234. TextureFormat[] additionalTextureFormats = null, bool[] additionalTextureIsLinear = null) {
  235. Shader shader = materialPropertySource == null ? Shader.Find("Spine/Skeleton") : materialPropertySource.shader;
  236. GetRepackedAttachments(sourceAttachments, outputAttachments, shader, out outputMaterial, out outputTexture,
  237. maxAtlasSize, padding, textureFormat, mipmaps, newAssetName,
  238. materialPropertySource, clearCache, useOriginalNonrenderables,
  239. additionalTexturePropertyIDsToCopy, additionalOutputTextures,
  240. additionalTextureFormats, additionalTextureIsLinear);
  241. }
  242. /// <summary>
  243. /// Fills the outputAttachments list with new attachment objects based on the attachments in sourceAttachments,
  244. /// but mapped to a new single texture using the same material.</summary>
  245. /// <remarks>Returned <c>Material</c> and <c>Texture</c> behave like <c>new Texture2D()</c>, thus you need to call <c>Destroy()</c>
  246. /// to free resources.</remarks>
  247. /// <param name="sourceAttachments">The list of attachments to be repacked.</param>
  248. /// <param name = "outputAttachments">The List(Attachment) to populate with the newly created Attachment objects.
  249. /// May be equal to <c>sourceAttachments</c> for in-place operation.</param>
  250. /// <param name="materialPropertySource">May be null. If no Material property source is provided, a material with
  251. /// default parameters using the provided <c>shader</c> will be created.</param>
  252. /// <param name="additionalTexturePropertyIDsToCopy">Optional additional textures (such as normal maps) to copy while repacking.
  253. /// To copy e.g. the main texture and normal maps, pass 'new int[] { Shader.PropertyToID("_BumpMap") }' at this parameter.</param>
  254. /// <param name="additionalOutputTextures">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  255. /// this array will be filled with the resulting repacked texture for every property,
  256. /// just as the main repacked texture is assigned to <c>outputTexture</c>.</param>
  257. /// <param name="additionalTextureFormats">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  258. /// this array will be used as <c>TextureFormat</c> at the Texture at the respective property.
  259. /// When <c>additionalTextureFormats</c> is <c>null</c> or when its array size is smaller,
  260. /// <c>textureFormat</c> is used where there exists no corresponding array item.</param>
  261. /// <param name="additionalTextureIsLinear">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  262. /// this array will be used to determine whether <c>linear</c> or <c>sRGB</c> color space is used at the
  263. /// Texture at the respective property. When <c>additionalTextureIsLinear</c> is <c>null</c>, <c>linear</c> color space
  264. /// is assumed at every additional Texture element.
  265. /// When e.g. packing the main texture and normal maps, pass 'new bool[] { true }' at this parameter, because normal maps use
  266. /// linear color space.</param>
  267. public static void GetRepackedAttachments (List<Attachment> sourceAttachments, List<Attachment> outputAttachments, Shader shader,
  268. out Material outputMaterial, out Texture2D outputTexture,
  269. int maxAtlasSize = 1024, int padding = 2, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  270. string newAssetName = "Repacked Attachments",
  271. Material materialPropertySource = null, bool clearCache = false, bool useOriginalNonrenderables = true,
  272. int[] additionalTexturePropertyIDsToCopy = null, Texture2D[] additionalOutputTextures = null,
  273. TextureFormat[] additionalTextureFormats = null, bool[] additionalTextureIsLinear = null) {
  274. if (sourceAttachments == null) throw new System.ArgumentNullException("sourceAttachments");
  275. if (outputAttachments == null) throw new System.ArgumentNullException("outputAttachments");
  276. outputTexture = null;
  277. if (additionalTexturePropertyIDsToCopy != null && additionalTextureIsLinear == null) {
  278. additionalTextureIsLinear = new bool[additionalTexturePropertyIDsToCopy.Length];
  279. for (int i = 0; i < additionalTextureIsLinear.Length; ++i) {
  280. additionalTextureIsLinear[i] = true;
  281. }
  282. }
  283. // Use these to detect and use shared regions.
  284. existingRegions.Clear();
  285. regionIndices.Clear();
  286. // Collect all textures from original attachments.
  287. int numTextureParamsToRepack = 1 + (additionalTexturePropertyIDsToCopy == null ? 0 : additionalTexturePropertyIDsToCopy.Length);
  288. additionalOutputTextures = (additionalTexturePropertyIDsToCopy == null ? null : new Texture2D[additionalTexturePropertyIDsToCopy.Length]);
  289. if (texturesToPackAtParam.Length < numTextureParamsToRepack)
  290. Array.Resize(ref texturesToPackAtParam, numTextureParamsToRepack);
  291. for (int i = 0; i < numTextureParamsToRepack; ++i) {
  292. if (texturesToPackAtParam[i] != null)
  293. texturesToPackAtParam[i].Clear();
  294. else
  295. texturesToPackAtParam[i] = new List<Texture2D>();
  296. }
  297. originalRegions.Clear();
  298. if (!object.ReferenceEquals(sourceAttachments, outputAttachments)) {
  299. outputAttachments.Clear();
  300. outputAttachments.AddRange(sourceAttachments);
  301. }
  302. int newRegionIndex = 0;
  303. for (int attachmentIndex = 0, n = sourceAttachments.Count; attachmentIndex < n; attachmentIndex++) {
  304. Attachment originalAttachment = sourceAttachments[attachmentIndex];
  305. if (originalAttachment is IHasTextureRegion) {
  306. MeshAttachment originalMeshAttachment = originalAttachment as MeshAttachment;
  307. IHasTextureRegion originalTextureAttachment = (IHasTextureRegion)originalAttachment;
  308. Attachment newAttachment = (originalTextureAttachment.Sequence != null) ? originalAttachment :
  309. (originalMeshAttachment != null) ? originalMeshAttachment.NewLinkedMesh() :
  310. originalAttachment.Copy();
  311. IHasTextureRegion newTextureAttachment = (IHasTextureRegion)newAttachment;
  312. AtlasRegion region = newTextureAttachment.Region as AtlasRegion;
  313. if (region == null && originalTextureAttachment.Sequence != null)
  314. region = (AtlasRegion)originalTextureAttachment.Sequence.Regions[0];
  315. int existingIndex;
  316. if (existingRegions.TryGetValue(region, out existingIndex)) {
  317. regionIndices.Add(existingIndex);
  318. } else {
  319. existingRegions.Add(region, newRegionIndex);
  320. Sequence originalSequence = originalTextureAttachment.Sequence;
  321. if (originalSequence != null) {
  322. newTextureAttachment.Sequence = new Sequence(originalSequence);
  323. for (int i = 0, regionCount = originalSequence.Regions.Length; i < regionCount; ++i) {
  324. AtlasRegion sequenceRegion = (AtlasRegion)originalSequence.Regions[i];
  325. AddRegionTexturesToPack(numTextureParamsToRepack, sequenceRegion, textureFormat, mipmaps,
  326. additionalTextureFormats, additionalTexturePropertyIDsToCopy, additionalTextureIsLinear);
  327. originalRegions.Add(sequenceRegion);
  328. regionIndices.Add(newRegionIndex);
  329. newRegionIndex++;
  330. }
  331. } else {
  332. AddRegionTexturesToPack(numTextureParamsToRepack, region, textureFormat, mipmaps,
  333. additionalTextureFormats, additionalTexturePropertyIDsToCopy, additionalTextureIsLinear);
  334. originalRegions.Add(region);
  335. regionIndices.Add(newRegionIndex);
  336. newRegionIndex++;
  337. }
  338. }
  339. outputAttachments[attachmentIndex] = newAttachment;
  340. } else {
  341. outputAttachments[attachmentIndex] = useOriginalNonrenderables ? originalAttachment : originalAttachment.Copy();
  342. regionIndices.Add(NonrenderingRegion); // Output attachments pairs with regionIndices list 1:1. Pad with a sentinel if the attachment doesn't have a region.
  343. }
  344. }
  345. // Rehydrate the repacked textures as a Material, Spine atlas and Spine.AtlasAttachments
  346. Material newMaterial = new Material(shader);
  347. if (materialPropertySource != null) {
  348. newMaterial.CopyPropertiesFromMaterial(materialPropertySource);
  349. newMaterial.shaderKeywords = materialPropertySource.shaderKeywords;
  350. }
  351. newMaterial.name = newAssetName;
  352. Rect[] rects = null;
  353. for (int i = 0; i < numTextureParamsToRepack; ++i) {
  354. // Fill a new texture with the collected attachment textures.
  355. Texture2D newTexture = new Texture2D(maxAtlasSize, maxAtlasSize,
  356. (i > 0 && additionalTextureFormats != null && i - 1 < additionalTextureFormats.Length) ?
  357. additionalTextureFormats[i - 1] : textureFormat,
  358. mipmaps,
  359. (i > 0) ? additionalTextureIsLinear[i - 1] : false);
  360. newTexture.mipMapBias = AtlasUtilities.DefaultMipmapBias;
  361. List<Texture2D> texturesToPack = texturesToPackAtParam[i];
  362. if (texturesToPack.Count > 0) {
  363. Texture2D sourceTexture = texturesToPack[0];
  364. newTexture.CopyTextureAttributesFrom(sourceTexture);
  365. }
  366. newTexture.name = newAssetName;
  367. Rect[] rectsForTexParam = newTexture.PackTextures(texturesToPack.ToArray(), padding, maxAtlasSize);
  368. if (i == 0) {
  369. rects = rectsForTexParam;
  370. newMaterial.mainTexture = newTexture;
  371. outputTexture = newTexture;
  372. } else {
  373. newMaterial.SetTexture(additionalTexturePropertyIDsToCopy[i - 1], newTexture);
  374. additionalOutputTextures[i - 1] = newTexture;
  375. }
  376. }
  377. AtlasPage page = newMaterial.ToSpineAtlasPage();
  378. page.name = newAssetName;
  379. repackedRegions.Clear();
  380. for (int i = 0, n = originalRegions.Count; i < n; i++) {
  381. AtlasRegion oldRegion = originalRegions[i];
  382. AtlasRegion newRegion = UVRectToAtlasRegion(rects[i], oldRegion, page);
  383. repackedRegions.Add(newRegion);
  384. }
  385. // Map the cloned attachments to the repacked atlas.
  386. for (int attachmentIndex = 0, repackedIndex = 0, n = outputAttachments.Count;
  387. attachmentIndex < n;
  388. ++attachmentIndex, ++repackedIndex) {
  389. Attachment attachment = outputAttachments[attachmentIndex];
  390. IHasTextureRegion textureAttachment = attachment as IHasTextureRegion;
  391. if (textureAttachment != null) {
  392. if (textureAttachment.Sequence != null) {
  393. TextureRegion[] regions = textureAttachment.Sequence.Regions;
  394. textureAttachment.Region = repackedRegions[regionIndices[repackedIndex]];
  395. for (int r = 0, regionCount = regions.Length; r < regionCount; ++r) {
  396. regions[r] = repackedRegions[regionIndices[repackedIndex++]];
  397. }
  398. --repackedIndex;
  399. } else {
  400. textureAttachment.Region = repackedRegions[regionIndices[repackedIndex]];
  401. }
  402. textureAttachment.UpdateRegion();
  403. }
  404. }
  405. // Clean up.
  406. if (clearCache)
  407. AtlasUtilities.ClearCache();
  408. outputMaterial = newMaterial;
  409. }
  410. private static void AddRegionTexturesToPack (int numTextureParamsToRepack, AtlasRegion region,
  411. TextureFormat textureFormat, bool mipmaps, TextureFormat[] additionalTextureFormats,
  412. int[] additionalTexturePropertyIDsToCopy, bool[] additionalTextureIsLinear) {
  413. for (int i = 0; i < numTextureParamsToRepack; ++i) {
  414. Texture2D regionTexture = (i == 0 ?
  415. region.ToTexture(textureFormat, mipmaps) :
  416. region.ToTexture((additionalTextureFormats != null && i - 1 < additionalTextureFormats.Length) ?
  417. additionalTextureFormats[i - 1] : textureFormat,
  418. mipmaps, additionalTexturePropertyIDsToCopy[i - 1], additionalTextureIsLinear[i - 1]));
  419. texturesToPackAtParam[i].Add(regionTexture);
  420. }
  421. }
  422. /// <summary>
  423. /// Creates and populates a duplicate skin with cloned attachments that are backed by a new packed texture atlas
  424. /// comprised of all the regions from the original skin.</summary>
  425. /// <remarks>GetRepackedSkin is an expensive operation, preferably call it at level load time.
  426. /// No Spine.Atlas object is created so there is no way to find AtlasRegions except through the Attachments using them.
  427. /// Returned <c>Material</c> and <c>Texture</c> behave like <c>new Texture2D()</c>, thus you need to call <c>Destroy()</c>
  428. /// to free resources.
  429. /// This method caches necessary Texture copies for later re-use, which might steadily increase the texture memory
  430. /// footprint when used excessively. Set <paramref name="clearCache"/> to <c>true</c>
  431. /// or call <see cref="AtlasUtilities.ClearCache()"/> to clear this texture cache.
  432. /// You may want to call <c>Resources.UnloadUnusedAssets()</c> after that.
  433. /// </remarks>
  434. /// <param name="clearCache">When set to <c>true</c>, <see cref="AtlasUtilities.ClearCache()"/> is called after
  435. /// repacking to clear the texture cache. See remarks for additional info.</param>
  436. /// <param name="additionalTexturePropertyIDsToCopy">Optional additional textures (such as normal maps) to copy while repacking.
  437. /// To copy e.g. the main texture and normal maps, pass 'new int[] { Shader.PropertyToID("_BumpMap") }' at this parameter.</param>
  438. /// <param name="additionalOutputTextures">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  439. /// this array will be filled with the resulting repacked texture for every property,
  440. /// just as the main repacked texture is assigned to <c>outputTexture</c>.</param>
  441. /// <param name="additionalTextureFormats">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  442. /// this array will be used as <c>TextureFormat</c> at the Texture at the respective property.
  443. /// When <c>additionalTextureFormats</c> is <c>null</c> or when its array size is smaller,
  444. /// <c>textureFormat</c> is used where there exists no corresponding array item.</param>
  445. /// <param name="additionalTextureIsLinear">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  446. /// this array will be used to determine whether <c>linear</c> or <c>sRGB</c> color space is used at the
  447. /// Texture at the respective property. When <c>additionalTextureIsLinear</c> is <c>null</c>, <c>linear</c> color space
  448. /// is assumed at every additional Texture element.
  449. /// When e.g. packing the main texture and normal maps, pass 'new bool[] { true }' at this parameter, because normal maps use
  450. /// linear color space.</param>
  451. public static Skin GetRepackedSkin (this Skin o, string newName, Material materialPropertySource, out Material outputMaterial, out Texture2D outputTexture,
  452. int maxAtlasSize = 1024, int padding = 2, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  453. bool useOriginalNonrenderables = true, bool clearCache = false,
  454. int[] additionalTexturePropertyIDsToCopy = null, Texture2D[] additionalOutputTextures = null,
  455. TextureFormat[] additionalTextureFormats = null, bool[] additionalTextureIsLinear = null) {
  456. return GetRepackedSkin(o, newName, materialPropertySource.shader, out outputMaterial, out outputTexture,
  457. maxAtlasSize, padding, textureFormat, mipmaps, materialPropertySource,
  458. clearCache, useOriginalNonrenderables, additionalTexturePropertyIDsToCopy, additionalOutputTextures,
  459. additionalTextureFormats, additionalTextureIsLinear);
  460. }
  461. /// <summary>
  462. /// Creates and populates a duplicate skin with cloned attachments that are backed by a new packed texture atlas
  463. /// comprised of all the regions from the original skin.</summary>
  464. /// See documentation of <see cref="GetRepackedSkin"/> for details.
  465. public static Skin GetRepackedSkin (this Skin o, string newName, Shader shader, out Material outputMaterial, out Texture2D outputTexture,
  466. int maxAtlasSize = 1024, int padding = 2, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  467. Material materialPropertySource = null, bool clearCache = false, bool useOriginalNonrenderables = true,
  468. int[] additionalTexturePropertyIDsToCopy = null, Texture2D[] additionalOutputTextures = null,
  469. TextureFormat[] additionalTextureFormats = null, bool[] additionalTextureIsLinear = null) {
  470. outputTexture = null;
  471. if (o == null) throw new System.NullReferenceException("Skin was null");
  472. ICollection<Skin.SkinEntry> skinAttachments = o.Attachments;
  473. Skin newSkin = new Skin(newName);
  474. newSkin.Bones.AddRange(o.Bones);
  475. newSkin.Constraints.AddRange(o.Constraints);
  476. inoutAttachments.Clear();
  477. foreach (Skin.SkinEntry entry in skinAttachments) {
  478. inoutAttachments.Add(entry.Attachment);
  479. }
  480. GetRepackedAttachments(inoutAttachments, inoutAttachments, materialPropertySource, out outputMaterial, out outputTexture,
  481. maxAtlasSize, padding, textureFormat, mipmaps, newName, clearCache, useOriginalNonrenderables,
  482. additionalTexturePropertyIDsToCopy, additionalOutputTextures, additionalTextureFormats, additionalTextureIsLinear);
  483. int i = 0;
  484. foreach (Skin.SkinEntry originalSkinEntry in skinAttachments) {
  485. Attachment newAttachment = inoutAttachments[i++];
  486. newSkin.SetAttachment(originalSkinEntry.SlotIndex, originalSkinEntry.Name, newAttachment);
  487. }
  488. return newSkin;
  489. }
  490. public static Sprite ToSprite (this AtlasRegion ar, float pixelsPerUnit = 100) {
  491. return Sprite.Create(ar.GetMainTexture(), ar.GetUnityRect(), new Vector2(0.5f, 0.5f), pixelsPerUnit);
  492. }
  493. struct IntAndAtlasRegionKey {
  494. int i;
  495. AtlasRegion region;
  496. public IntAndAtlasRegionKey (int i, AtlasRegion region) {
  497. this.i = i;
  498. this.region = region;
  499. }
  500. public override int GetHashCode () {
  501. return i.GetHashCode() * 23 ^ region.GetHashCode();
  502. }
  503. }
  504. static Dictionary<IntAndAtlasRegionKey, Texture2D> CachedRegionTextures = new Dictionary<IntAndAtlasRegionKey, Texture2D>();
  505. static List<Texture2D> CachedRegionTexturesList = new List<Texture2D>();
  506. /// <summary>
  507. /// Frees up textures cached by repacking and remapping operations.
  508. ///
  509. /// Calling <see cref="AttachmentCloneExtensions.GetRemappedClone"/> with parameter <c>premultiplyAlpha=true</c>,
  510. /// <see cref="GetRepackedAttachments"/> or <see cref="GetRepackedSkin"/> will cache textures for later re-use,
  511. /// which might steadily increase the texture memory footprint when used excessively.
  512. /// You can clear this Texture cache by calling <see cref="AtlasUtilities.ClearCache()"/>.
  513. /// You may also want to call <c>Resources.UnloadUnusedAssets()</c> after that. Be aware that while this cleanup
  514. /// frees up memory, it is also a costly operation and will likely cause a spike in the framerate.
  515. /// Thus it is recommended to perform costly repacking and cleanup operations after e.g. a character customization
  516. /// screen has been exited, and if required additionally after a certain number of <c>GetRemappedClone()</c> calls.
  517. /// </summary>
  518. public static void ClearCache () {
  519. foreach (Texture2D t in CachedRegionTexturesList) {
  520. UnityEngine.Object.Destroy(t);
  521. }
  522. CachedRegionTextures.Clear();
  523. CachedRegionTexturesList.Clear();
  524. }
  525. /// <summary>Creates a new Texture2D object based on an AtlasRegion.
  526. /// If applyImmediately is true, Texture2D.Apply is called immediately after the Texture2D is filled with data.</summary>
  527. public static Texture2D ToTexture (this AtlasRegion ar, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  528. int texturePropertyId = 0, bool linear = false, bool applyPMA = false) {
  529. Texture2D output;
  530. IntAndAtlasRegionKey cacheKey = new IntAndAtlasRegionKey(texturePropertyId, ar);
  531. CachedRegionTextures.TryGetValue(cacheKey, out output);
  532. if (output == null) {
  533. Texture2D sourceTexture = texturePropertyId == 0 ? ar.GetMainTexture() : ar.GetTexture(texturePropertyId);
  534. Rect r = ar.GetUnityRect();
  535. int width = (int)r.width;
  536. int height = (int)r.height;
  537. output = new Texture2D(width, height, textureFormat, mipmaps, linear) { name = ar.name };
  538. output.CopyTextureAttributesFrom(sourceTexture);
  539. if (applyPMA)
  540. AtlasUtilities.CopyTextureApplyPMA(sourceTexture, r, output);
  541. else
  542. AtlasUtilities.CopyTexture(sourceTexture, r, output);
  543. CachedRegionTextures.Add(cacheKey, output);
  544. CachedRegionTexturesList.Add(output);
  545. }
  546. return output;
  547. }
  548. static Texture2D ToTexture (this Sprite s, TextureFormat textureFormat = SpineTextureFormat,
  549. bool mipmaps = UseMipMaps, bool linear = false, bool applyPMA = false) {
  550. Texture2D spriteTexture = s.texture;
  551. Rect r;
  552. if (!s.packed || s.packingMode == SpritePackingMode.Rectangle) {
  553. r = s.textureRect;
  554. } else {
  555. r = new Rect();
  556. r.xMin = Math.Min(s.uv[0].x, s.uv[1].x) * spriteTexture.width;
  557. r.xMax = Math.Max(s.uv[0].x, s.uv[1].x) * spriteTexture.width;
  558. r.yMin = Math.Min(s.uv[0].y, s.uv[2].y) * spriteTexture.height;
  559. r.yMax = Math.Max(s.uv[0].y, s.uv[2].y) * spriteTexture.height;
  560. #if UNITY_EDITOR
  561. if (s.uv.Length > 4) {
  562. Debug.LogError("When using a tightly packed SpriteAtlas with Spine, you may only access Sprites that are packed as 'FullRect' from it! " +
  563. "You can either disable 'Tight Packing' at the whole SpriteAtlas, or change the single Sprite's TextureImporter Setting 'MeshType' to 'Full Rect'." +
  564. "Sprite Asset: " + s.name, s);
  565. }
  566. #endif
  567. }
  568. Texture2D newTexture = new Texture2D((int)r.width, (int)r.height, textureFormat, mipmaps, linear);
  569. newTexture.CopyTextureAttributesFrom(spriteTexture);
  570. if (applyPMA)
  571. AtlasUtilities.CopyTextureApplyPMA(spriteTexture, r, newTexture);
  572. else
  573. AtlasUtilities.CopyTexture(spriteTexture, r, newTexture);
  574. return newTexture;
  575. }
  576. static Texture2D GetClone (this Texture2D t, TextureFormat textureFormat = SpineTextureFormat,
  577. bool mipmaps = UseMipMaps, bool linear = false, bool applyPMA = false) {
  578. Texture2D newTexture = new Texture2D((int)t.width, (int)t.height, textureFormat, mipmaps, linear);
  579. newTexture.CopyTextureAttributesFrom(t);
  580. if (applyPMA)
  581. AtlasUtilities.CopyTextureApplyPMA(t, new Rect(0, 0, t.width, t.height), newTexture);
  582. else
  583. AtlasUtilities.CopyTexture(t, new Rect(0, 0, t.width, t.height), newTexture);
  584. return newTexture;
  585. }
  586. static void CopyTexture (Texture2D source, Rect sourceRect, Texture2D destination) {
  587. if (SystemInfo.copyTextureSupport == UnityEngine.Rendering.CopyTextureSupport.None) {
  588. // GetPixels fallback for old devices.
  589. Color[] pixelBuffer = source.GetPixels((int)sourceRect.x, (int)sourceRect.y, (int)sourceRect.width, (int)sourceRect.height);
  590. destination.SetPixels(pixelBuffer);
  591. destination.Apply();
  592. } else {
  593. Graphics.CopyTexture(source, 0, 0, (int)sourceRect.x, (int)sourceRect.y, (int)sourceRect.width, (int)sourceRect.height, destination, 0, 0, 0, 0);
  594. }
  595. }
  596. static void CopyTextureApplyPMA (Texture2D source, Rect sourceRect, Texture2D destination) {
  597. Color[] pixelBuffer = source.GetPixels((int)sourceRect.x, (int)sourceRect.y, (int)sourceRect.width, (int)sourceRect.height);
  598. for (int i = 0, n = pixelBuffer.Length; i < n; i++) {
  599. Color p = pixelBuffer[i];
  600. float a = p.a;
  601. p.r = p.r * a;
  602. p.g = p.g * a;
  603. p.b = p.b * a;
  604. pixelBuffer[i] = p;
  605. }
  606. destination.SetPixels(pixelBuffer);
  607. destination.Apply();
  608. }
  609. static bool IsRenderable (Attachment a) {
  610. return a is IHasTextureRegion;
  611. }
  612. /// <summary>
  613. /// Get a rect with flipped Y so that a Spine atlas rect gets converted to a Unity Sprite rect and vice versa.</summary>
  614. static Rect SpineUnityFlipRect (this Rect rect, int textureHeight) {
  615. rect.y = textureHeight - rect.y - rect.height;
  616. return rect;
  617. }
  618. /// <summary>
  619. /// Gets the Rect of an AtlasRegion according to Unity texture coordinates (x-right, y-up).
  620. /// This overload relies on region.page.height being correctly set.</summary>
  621. static Rect GetUnityRect (this AtlasRegion region) {
  622. return region.GetSpineAtlasRect().SpineUnityFlipRect(region.page.height);
  623. }
  624. /// <summary>
  625. /// Gets the Rect of an AtlasRegion according to Unity texture coordinates (x-right, y-up).</summary>
  626. static Rect GetUnityRect (this AtlasRegion region, int textureHeight) {
  627. return region.GetSpineAtlasRect().SpineUnityFlipRect(textureHeight);
  628. }
  629. /// <summary>
  630. /// Returns a Rect of the AtlasRegion according to Spine texture coordinates. (x-right, y-down)</summary>
  631. static Rect GetSpineAtlasRect (this AtlasRegion region, bool includeRotate = true) {
  632. float width = region.packedWidth;
  633. float height = region.packedHeight;
  634. if (includeRotate && region.degrees == 270) {
  635. width = region.packedHeight;
  636. height = region.packedWidth;
  637. }
  638. return new Rect(region.x, region.y, width, height);
  639. }
  640. /// <summary>
  641. /// Denormalize a uvRect into a texture-space Rect.</summary>
  642. static Rect UVRectToTextureRect (Rect uvRect, int texWidth, int texHeight) {
  643. uvRect.x *= texWidth;
  644. uvRect.width *= texWidth;
  645. uvRect.y *= texHeight;
  646. uvRect.height *= texHeight;
  647. return uvRect;
  648. }
  649. /// <summary>
  650. /// Normalize a texture Rect into UV coordinates.</summary>
  651. static Rect TextureRectToUVRect (Rect textureRect, int texWidth, int texHeight) {
  652. textureRect.x = Mathf.InverseLerp(0, texWidth, textureRect.x);
  653. textureRect.y = Mathf.InverseLerp(0, texHeight, textureRect.y);
  654. textureRect.width = Mathf.InverseLerp(0, texWidth, textureRect.width);
  655. textureRect.height = Mathf.InverseLerp(0, texHeight, textureRect.height);
  656. return textureRect;
  657. }
  658. /// <summary>
  659. /// Creates a new Spine AtlasRegion according to a Unity UV Rect (x-right, y-up, uv-normalized).</summary>
  660. static AtlasRegion UVRectToAtlasRegion (Rect uvRect, AtlasRegion referenceRegion, AtlasPage page) {
  661. Rect tr = UVRectToTextureRect(uvRect, page.width, page.height);
  662. Rect rr = tr.SpineUnityFlipRect(page.height);
  663. int x = (int)rr.x;
  664. int y = (int)rr.y;
  665. int w = (int)rr.width;
  666. int h = (int)rr.height;
  667. if (referenceRegion.degrees == 270) {
  668. int tempW = w;
  669. w = h;
  670. h = tempW;
  671. }
  672. // Note: originalW and originalH need to be scaled according to the
  673. // repacked width and height, repacking can mess with aspect ratio, etc.
  674. int originalW = Mathf.RoundToInt((float)w * ((float)referenceRegion.originalWidth / (float)referenceRegion.width));
  675. int originalH = Mathf.RoundToInt((float)h * ((float)referenceRegion.originalHeight / (float)referenceRegion.height));
  676. int offsetX = Mathf.RoundToInt((float)referenceRegion.offsetX * ((float)w / (float)referenceRegion.width));
  677. int offsetY = Mathf.RoundToInt((float)referenceRegion.offsetY * ((float)h / (float)referenceRegion.height));
  678. float u = uvRect.xMin;
  679. float u2 = uvRect.xMax;
  680. float v = uvRect.yMax;
  681. float v2 = uvRect.yMin;
  682. if (referenceRegion.degrees == 270) {
  683. // at a 270 degree region, u2/v2 deltas and atlas width/height are swapped, and delta-v is negative.
  684. float du = uvRect.width; // u2 - u;
  685. float dv = uvRect.height; // v - v2;
  686. float atlasAspectRatio = (float)page.width / (float)page.height;
  687. u2 = u + (dv / atlasAspectRatio);
  688. v2 = v - (du * atlasAspectRatio);
  689. }
  690. return new AtlasRegion {
  691. page = page,
  692. name = referenceRegion.name,
  693. u = u,
  694. u2 = u2,
  695. v = v,
  696. v2 = v2,
  697. index = -1,
  698. width = w,
  699. originalWidth = originalW,
  700. height = h,
  701. originalHeight = originalH,
  702. offsetX = offsetX,
  703. offsetY = offsetY,
  704. x = x,
  705. y = y,
  706. rotate = referenceRegion.rotate,
  707. degrees = referenceRegion.degrees
  708. };
  709. }
  710. /// <summary>
  711. /// Convenience method for getting the main texture of the material of the page of the region.</summary>
  712. static Texture2D GetMainTexture (this AtlasRegion region) {
  713. Material material = (region.page.rendererObject as Material);
  714. return material.mainTexture as Texture2D;
  715. }
  716. /// <summary>
  717. /// Convenience method for getting any texture of the material of the page of the region by texture property name.</summary>
  718. static Texture2D GetTexture (this AtlasRegion region, string texturePropertyName) {
  719. Material material = (region.page.rendererObject as Material);
  720. return material.GetTexture(texturePropertyName) as Texture2D;
  721. }
  722. /// <summary>
  723. /// Convenience method for getting any texture of the material of the page of the region by texture property id.</summary>
  724. static Texture2D GetTexture (this AtlasRegion region, int texturePropertyId) {
  725. Material material = (region.page.rendererObject as Material);
  726. return material.GetTexture(texturePropertyId) as Texture2D;
  727. }
  728. static void CopyTextureAttributesFrom (this Texture2D destination, Texture2D source) {
  729. destination.filterMode = source.filterMode;
  730. destination.anisoLevel = source.anisoLevel;
  731. #if UNITY_EDITOR
  732. destination.alphaIsTransparency = source.alphaIsTransparency;
  733. #endif
  734. destination.wrapModeU = source.wrapModeU;
  735. destination.wrapModeV = source.wrapModeV;
  736. destination.wrapModeW = source.wrapModeW;
  737. }
  738. #endregion
  739. static float InverseLerp (float a, float b, float value) {
  740. return (value - a) / (b - a);
  741. }
  742. }
  743. }