AtlasUtilities.cs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. /******************************************************************************
  2. * Spine Runtimes License Agreement
  3. * Last updated January 1, 2020. Replaces all prior versions.
  4. *
  5. * Copyright (c) 2013-2020, 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
  13. * or 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
  27. * THE 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. var material = new Material(shader);
  54. if (materialPropertySource != null) {
  55. material.CopyPropertiesFromMaterial(materialPropertySource);
  56. material.shaderKeywords = materialPropertySource.shaderKeywords;
  57. }
  58. material.mainTexture = t;
  59. var page = material.ToSpineAtlasPage();
  60. float width = t.width;
  61. float height = t.height;
  62. var 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. var material = new Material(shader);
  92. if (materialPropertySource != null) {
  93. material.CopyPropertiesFromMaterial(materialPropertySource);
  94. material.shaderKeywords = materialPropertySource.shaderKeywords;
  95. }
  96. var 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. var page = material.ToSpineAtlasPage();
  101. var 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. var newPage = new AtlasPage {
  109. rendererObject = m,
  110. name = m.name
  111. };
  112. var 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. var 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. var 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. var material = new Material(shader);
  141. if (materialPropertySource != null) {
  142. material.CopyPropertiesFromMaterial(materialPropertySource);
  143. material.shaderKeywords = materialPropertySource.shaderKeywords;
  144. }
  145. var 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. var page = material.ToSpineAtlasPage();
  150. var region = s.ToAtlasRegion(true);
  151. region.page = page;
  152. return region;
  153. }
  154. internal static AtlasRegion ToAtlasRegion (this Sprite s, bool isolatedTexture = false) {
  155. var 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.rect.SpineUnityFlipRect(s.texture.height);
  164. region.width = (int)spineRect.width;
  165. region.originalWidth = (int)spineRect.width;
  166. region.height = (int)spineRect.height;
  167. region.originalHeight = (int)spineRect.height;
  168. region.offsetX = spineRect.width * (0.5f - InverseLerp(boundsMin.x, boundsMax.x, 0));
  169. region.offsetY = spineRect.height * (0.5f - InverseLerp(boundsMin.y, boundsMax.y, 0));
  170. if (isolatedTexture) {
  171. region.u = 0;
  172. region.v = 1;
  173. region.u2 = 1;
  174. region.v2 = 0;
  175. region.x = 0;
  176. region.y = 0;
  177. } else {
  178. Texture2D tex = s.texture;
  179. Rect uvRect = TextureRectToUVRect(s.textureRect, tex.width, tex.height);
  180. region.u = uvRect.xMin;
  181. region.v = uvRect.yMax;
  182. region.u2 = uvRect.xMax;
  183. region.v2 = uvRect.yMin;
  184. region.x = (int)spineRect.x;
  185. region.y = (int)spineRect.y;
  186. }
  187. return region;
  188. }
  189. #region Runtime Repacking
  190. static readonly Dictionary<AtlasRegion, int> existingRegions = new Dictionary<AtlasRegion, int>();
  191. static readonly List<int> regionIndices = new List<int>();
  192. static readonly List<AtlasRegion> originalRegions = new List<AtlasRegion>();
  193. static readonly List<AtlasRegion> repackedRegions = new List<AtlasRegion>();
  194. static List<Texture2D>[] texturesToPackAtParam = new List<Texture2D>[1];
  195. static List<Attachment> inoutAttachments = new List<Attachment>();
  196. /// <summary>
  197. /// Fills the outputAttachments list with new attachment objects based on the attachments in sourceAttachments,
  198. /// but mapped to a new single texture using the same material.</summary>
  199. /// <remarks>Returned <c>Material</c> and <c>Texture</c> behave like <c>new Texture2D()</c>, thus you need to call <c>Destroy()</c>
  200. /// to free resources.
  201. /// This method caches necessary Texture copies for later re-use, which might steadily increase the texture memory
  202. /// footprint when used excessively. Set <paramref name="clearCache"/> to <c>true</c>
  203. /// or call <see cref="AtlasUtilities.ClearCache()"/> to clear this texture cache.
  204. /// You may want to call <c>Resources.UnloadUnusedAssets()</c> after that.
  205. /// </remarks>
  206. /// <param name="sourceAttachments">The list of attachments to be repacked.</param>
  207. /// <param name = "outputAttachments">The List(Attachment) to populate with the newly created Attachment objects.
  208. /// May be equal to <c>sourceAttachments</c> for in-place operation.</param>
  209. /// <param name="materialPropertySource">May be null. If no Material property source is provided, a material with
  210. /// default parameters using the provided <c>shader</c> will be created.</param>
  211. /// <param name="clearCache">When set to <c>true</c>, <see cref="AtlasUtilities.ClearCache()"/> is called after
  212. /// repacking to clear the texture cache. See remarks for additional info.</param>
  213. /// <param name="additionalTexturePropertyIDsToCopy">Optional additional textures (such as normal maps) to copy while repacking.
  214. /// To copy e.g. the main texture and normal maps, pass 'new int[] { Shader.PropertyToID("_BumpMap") }' at this parameter.</param>
  215. /// <param name="additionalOutputTextures">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  216. /// this array will be filled with the resulting repacked texture for every property,
  217. /// just as the main repacked texture is assigned to <c>outputTexture</c>.</param>
  218. /// <param name="additionalTextureFormats">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  219. /// this array will be used as <c>TextureFormat</c> at the Texture at the respective property.
  220. /// When <c>additionalTextureFormats</c> is <c>null</c> or when its array size is smaller,
  221. /// <c>textureFormat</c> is used where there exists no corresponding array item.</param>
  222. /// <param name="additionalTextureIsLinear">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  223. /// this array will be used to determine whether <c>linear</c> or <c>sRGB</c> color space is used at the
  224. /// Texture at the respective property. When <c>additionalTextureIsLinear</c> is <c>null</c>, <c>linear</c> color space
  225. /// is assumed at every additional Texture element.
  226. /// When e.g. packing the main texture and normal maps, pass 'new bool[] { true }' at this parameter, because normal maps use
  227. /// linear color space.</param>
  228. public static void GetRepackedAttachments (List<Attachment> sourceAttachments, List<Attachment> outputAttachments, Material materialPropertySource,
  229. out Material outputMaterial, out Texture2D outputTexture,
  230. int maxAtlasSize = 1024, int padding = 2, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  231. string newAssetName = "Repacked Attachments", bool clearCache = false, bool useOriginalNonrenderables = true,
  232. int[] additionalTexturePropertyIDsToCopy = null, Texture2D[] additionalOutputTextures = null,
  233. TextureFormat[] additionalTextureFormats = null, bool[] additionalTextureIsLinear = null) {
  234. Shader shader = materialPropertySource == null ? Shader.Find("Spine/Skeleton") : materialPropertySource.shader;
  235. GetRepackedAttachments(sourceAttachments, outputAttachments, shader, out outputMaterial, out outputTexture,
  236. maxAtlasSize, padding, textureFormat, mipmaps, newAssetName,
  237. materialPropertySource, clearCache, useOriginalNonrenderables,
  238. additionalTexturePropertyIDsToCopy, additionalOutputTextures,
  239. additionalTextureFormats, additionalTextureIsLinear);
  240. }
  241. /// <summary>
  242. /// Fills the outputAttachments list with new attachment objects based on the attachments in sourceAttachments,
  243. /// but mapped to a new single texture using the same material.</summary>
  244. /// <remarks>Returned <c>Material</c> and <c>Texture</c> behave like <c>new Texture2D()</c>, thus you need to call <c>Destroy()</c>
  245. /// to free resources.</remarks>
  246. /// <param name="sourceAttachments">The list of attachments to be repacked.</param>
  247. /// <param name = "outputAttachments">The List(Attachment) to populate with the newly created Attachment objects.
  248. /// May be equal to <c>sourceAttachments</c> for in-place operation.</param>
  249. /// <param name="materialPropertySource">May be null. If no Material property source is provided, a material with
  250. /// default parameters using the provided <c>shader</c> will be created.</param>
  251. /// <param name="additionalTexturePropertyIDsToCopy">Optional additional textures (such as normal maps) to copy while repacking.
  252. /// To copy e.g. the main texture and normal maps, pass 'new int[] { Shader.PropertyToID("_BumpMap") }' at this parameter.</param>
  253. /// <param name="additionalOutputTextures">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  254. /// this array will be filled with the resulting repacked texture for every property,
  255. /// just as the main repacked texture is assigned to <c>outputTexture</c>.</param>
  256. /// <param name="additionalTextureFormats">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  257. /// this array will be used as <c>TextureFormat</c> at the Texture at the respective property.
  258. /// When <c>additionalTextureFormats</c> is <c>null</c> or when its array size is smaller,
  259. /// <c>textureFormat</c> is used where there exists no corresponding array item.</param>
  260. /// <param name="additionalTextureIsLinear">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  261. /// this array will be used to determine whether <c>linear</c> or <c>sRGB</c> color space is used at the
  262. /// Texture at the respective property. When <c>additionalTextureIsLinear</c> is <c>null</c>, <c>linear</c> color space
  263. /// is assumed at every additional Texture element.
  264. /// When e.g. packing the main texture and normal maps, pass 'new bool[] { true }' at this parameter, because normal maps use
  265. /// linear color space.</param>
  266. public static void GetRepackedAttachments (List<Attachment> sourceAttachments, List<Attachment> outputAttachments, Shader shader,
  267. out Material outputMaterial, out Texture2D outputTexture,
  268. int maxAtlasSize = 1024, int padding = 2, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  269. string newAssetName = "Repacked Attachments",
  270. Material materialPropertySource = null, bool clearCache = false, bool useOriginalNonrenderables = true,
  271. int[] additionalTexturePropertyIDsToCopy = null, Texture2D[] additionalOutputTextures = null,
  272. TextureFormat[] additionalTextureFormats = null, bool[] additionalTextureIsLinear = null) {
  273. if (sourceAttachments == null) throw new System.ArgumentNullException("sourceAttachments");
  274. if (outputAttachments == null) throw new System.ArgumentNullException("outputAttachments");
  275. outputTexture = null;
  276. if (additionalTexturePropertyIDsToCopy != null && additionalTextureIsLinear == null) {
  277. additionalTextureIsLinear = new bool[additionalTexturePropertyIDsToCopy.Length];
  278. for (int i = 0; i < additionalTextureIsLinear.Length; ++i) {
  279. additionalTextureIsLinear[i] = true;
  280. }
  281. }
  282. // Use these to detect and use shared regions.
  283. existingRegions.Clear();
  284. regionIndices.Clear();
  285. // Collect all textures from original attachments.
  286. int numTextureParamsToRepack = 1 + (additionalTexturePropertyIDsToCopy == null ? 0 : additionalTexturePropertyIDsToCopy.Length);
  287. additionalOutputTextures = (additionalTexturePropertyIDsToCopy == null ? null : new Texture2D[additionalTexturePropertyIDsToCopy.Length]);
  288. if (texturesToPackAtParam.Length < numTextureParamsToRepack)
  289. Array.Resize(ref texturesToPackAtParam, numTextureParamsToRepack);
  290. for (int i = 0; i < numTextureParamsToRepack; ++i) {
  291. if (texturesToPackAtParam[i] != null)
  292. texturesToPackAtParam[i].Clear();
  293. else
  294. texturesToPackAtParam[i] = new List<Texture2D>();
  295. }
  296. originalRegions.Clear();
  297. if (!object.ReferenceEquals(sourceAttachments, outputAttachments)) {
  298. outputAttachments.Clear();
  299. outputAttachments.AddRange(sourceAttachments);
  300. }
  301. int newRegionIndex = 0;
  302. for (int attachmentIndex = 0, n = sourceAttachments.Count; attachmentIndex < n; attachmentIndex++) {
  303. var originalAttachment = sourceAttachments[attachmentIndex];
  304. if (originalAttachment is IHasRendererObject) {
  305. var originalMeshAttachment = originalAttachment as MeshAttachment;
  306. Attachment newAttachment = (originalMeshAttachment != null) ? originalMeshAttachment.NewLinkedMesh() : originalAttachment.Copy();
  307. var region = ((IHasRendererObject)newAttachment).RendererObject as AtlasRegion;
  308. int existingIndex;
  309. if (existingRegions.TryGetValue(region, out existingIndex)) {
  310. regionIndices.Add(existingIndex);
  311. } else {
  312. originalRegions.Add(region);
  313. for (int i = 0; i < numTextureParamsToRepack; ++i) {
  314. Texture2D regionTexture = (i == 0 ?
  315. region.ToTexture(textureFormat, mipmaps) :
  316. region.ToTexture((additionalTextureFormats != null && i - 1 < additionalTextureFormats.Length) ?
  317. additionalTextureFormats[i - 1] : textureFormat,
  318. mipmaps, additionalTexturePropertyIDsToCopy[i - 1], additionalTextureIsLinear[i - 1]));
  319. texturesToPackAtParam[i].Add(regionTexture);
  320. }
  321. existingRegions.Add(region, newRegionIndex);
  322. regionIndices.Add(newRegionIndex);
  323. newRegionIndex++;
  324. }
  325. outputAttachments[attachmentIndex] = newAttachment;
  326. } else {
  327. outputAttachments[attachmentIndex] = useOriginalNonrenderables ? originalAttachment : originalAttachment.Copy();
  328. regionIndices.Add(NonrenderingRegion); // Output attachments pairs with regionIndices list 1:1. Pad with a sentinel if the attachment doesn't have a region.
  329. }
  330. }
  331. // Rehydrate the repacked textures as a Material, Spine atlas and Spine.AtlasAttachments
  332. var newMaterial = new Material(shader);
  333. if (materialPropertySource != null) {
  334. newMaterial.CopyPropertiesFromMaterial(materialPropertySource);
  335. newMaterial.shaderKeywords = materialPropertySource.shaderKeywords;
  336. }
  337. newMaterial.name = newAssetName;
  338. Rect[] rects = null;
  339. for (int i = 0; i < numTextureParamsToRepack; ++i) {
  340. // Fill a new texture with the collected attachment textures.
  341. var newTexture = new Texture2D(maxAtlasSize, maxAtlasSize,
  342. (i > 0 && additionalTextureFormats != null && i - 1 < additionalTextureFormats.Length) ?
  343. additionalTextureFormats[i - 1] : textureFormat,
  344. mipmaps,
  345. (i > 0) ? additionalTextureIsLinear[i - 1] : false);
  346. newTexture.mipMapBias = AtlasUtilities.DefaultMipmapBias;
  347. var texturesToPack = texturesToPackAtParam[i];
  348. if (texturesToPack.Count > 0) {
  349. var sourceTexture = texturesToPack[0];
  350. newTexture.CopyTextureAttributesFrom(sourceTexture);
  351. }
  352. newTexture.name = newAssetName;
  353. var rectsForTexParam = newTexture.PackTextures(texturesToPack.ToArray(), padding, maxAtlasSize);
  354. if (i == 0) {
  355. rects = rectsForTexParam;
  356. newMaterial.mainTexture = newTexture;
  357. outputTexture = newTexture;
  358. } else {
  359. newMaterial.SetTexture(additionalTexturePropertyIDsToCopy[i - 1], newTexture);
  360. additionalOutputTextures[i - 1] = newTexture;
  361. }
  362. }
  363. var page = newMaterial.ToSpineAtlasPage();
  364. page.name = newAssetName;
  365. repackedRegions.Clear();
  366. for (int i = 0, n = originalRegions.Count; i < n; i++) {
  367. var oldRegion = originalRegions[i];
  368. var newRegion = UVRectToAtlasRegion(rects[i], oldRegion, page);
  369. repackedRegions.Add(newRegion);
  370. }
  371. // Map the cloned attachments to the repacked atlas.
  372. for (int i = 0, n = outputAttachments.Count; i < n; i++) {
  373. var a = outputAttachments[i];
  374. if (a is IHasRendererObject)
  375. a.SetRegion(repackedRegions[regionIndices[i]]);
  376. }
  377. // Clean up.
  378. if (clearCache)
  379. AtlasUtilities.ClearCache();
  380. outputMaterial = newMaterial;
  381. }
  382. /// <summary>
  383. /// Creates and populates a duplicate skin with cloned attachments that are backed by a new packed texture atlas
  384. /// comprised of all the regions from the original skin.</summary>
  385. /// <remarks>GetRepackedSkin is an expensive operation, preferably call it at level load time.
  386. /// No Spine.Atlas object is created so there is no way to find AtlasRegions except through the Attachments using them.
  387. /// Returned <c>Material</c> and <c>Texture</c> behave like <c>new Texture2D()</c>, thus you need to call <c>Destroy()</c>
  388. /// to free resources.
  389. /// This method caches necessary Texture copies for later re-use, which might steadily increase the texture memory
  390. /// footprint when used excessively. Set <paramref name="clearCache"/> to <c>true</c>
  391. /// or call <see cref="AtlasUtilities.ClearCache()"/> to clear this texture cache.
  392. /// You may want to call <c>Resources.UnloadUnusedAssets()</c> after that.
  393. /// </remarks>
  394. /// <param name="clearCache">When set to <c>true</c>, <see cref="AtlasUtilities.ClearCache()"/> is called after
  395. /// repacking to clear the texture cache. See remarks for additional info.</param>
  396. /// <param name="additionalTexturePropertyIDsToCopy">Optional additional textures (such as normal maps) to copy while repacking.
  397. /// To copy e.g. the main texture and normal maps, pass 'new int[] { Shader.PropertyToID("_BumpMap") }' at this parameter.</param>
  398. /// <param name="additionalOutputTextures">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  399. /// this array will be filled with the resulting repacked texture for every property,
  400. /// just as the main repacked texture is assigned to <c>outputTexture</c>.</param>
  401. /// <param name="additionalTextureFormats">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  402. /// this array will be used as <c>TextureFormat</c> at the Texture at the respective property.
  403. /// When <c>additionalTextureFormats</c> is <c>null</c> or when its array size is smaller,
  404. /// <c>textureFormat</c> is used where there exists no corresponding array item.</param>
  405. /// <param name="additionalTextureIsLinear">When <c>additionalTexturePropertyIDsToCopy</c> is non-null,
  406. /// this array will be used to determine whether <c>linear</c> or <c>sRGB</c> color space is used at the
  407. /// Texture at the respective property. When <c>additionalTextureIsLinear</c> is <c>null</c>, <c>linear</c> color space
  408. /// is assumed at every additional Texture element.
  409. /// When e.g. packing the main texture and normal maps, pass 'new bool[] { true }' at this parameter, because normal maps use
  410. /// linear color space.</param>
  411. public static Skin GetRepackedSkin (this Skin o, string newName, Material materialPropertySource, out Material outputMaterial, out Texture2D outputTexture,
  412. int maxAtlasSize = 1024, int padding = 2, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  413. bool useOriginalNonrenderables = true, bool clearCache = false,
  414. int[] additionalTexturePropertyIDsToCopy = null, Texture2D[] additionalOutputTextures = null,
  415. TextureFormat[] additionalTextureFormats = null, bool[] additionalTextureIsLinear = null) {
  416. return GetRepackedSkin(o, newName, materialPropertySource.shader, out outputMaterial, out outputTexture,
  417. maxAtlasSize, padding, textureFormat, mipmaps, materialPropertySource,
  418. clearCache, useOriginalNonrenderables, additionalTexturePropertyIDsToCopy, additionalOutputTextures,
  419. additionalTextureFormats, additionalTextureIsLinear);
  420. }
  421. /// <summary>
  422. /// Creates and populates a duplicate skin with cloned attachments that are backed by a new packed texture atlas
  423. /// comprised of all the regions from the original skin.</summary>
  424. /// See documentation of <see cref="GetRepackedSkin"/> for details.
  425. public static Skin GetRepackedSkin (this Skin o, string newName, Shader shader, out Material outputMaterial, out Texture2D outputTexture,
  426. int maxAtlasSize = 1024, int padding = 2, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  427. Material materialPropertySource = null, bool clearCache = false, bool useOriginalNonrenderables = true,
  428. int[] additionalTexturePropertyIDsToCopy = null, Texture2D[] additionalOutputTextures = null,
  429. TextureFormat[] additionalTextureFormats = null, bool[] additionalTextureIsLinear = null) {
  430. outputTexture = null;
  431. if (o == null) throw new System.NullReferenceException("Skin was null");
  432. var skinAttachments = o.Attachments;
  433. var newSkin = new Skin(newName);
  434. newSkin.Bones.AddRange(o.Bones);
  435. newSkin.Constraints.AddRange(o.Constraints);
  436. inoutAttachments.Clear();
  437. foreach (var entry in o.Attachments) {
  438. inoutAttachments.Add(entry.Attachment);
  439. }
  440. GetRepackedAttachments(inoutAttachments, inoutAttachments, materialPropertySource, out outputMaterial, out outputTexture,
  441. maxAtlasSize, padding, textureFormat, mipmaps, newName, clearCache, useOriginalNonrenderables,
  442. additionalTexturePropertyIDsToCopy, additionalOutputTextures, additionalTextureFormats, additionalTextureIsLinear);
  443. int i = 0;
  444. foreach (var originalSkinEntry in o.Attachments) {
  445. var newAttachment = inoutAttachments[i++];
  446. newSkin.SetAttachment(originalSkinEntry.SlotIndex, originalSkinEntry.Name, newAttachment);
  447. }
  448. return newSkin;
  449. }
  450. public static Sprite ToSprite (this AtlasRegion ar, float pixelsPerUnit = 100) {
  451. return Sprite.Create(ar.GetMainTexture(), ar.GetUnityRect(), new Vector2(0.5f, 0.5f), pixelsPerUnit);
  452. }
  453. struct IntAndAtlasRegionKey {
  454. int i;
  455. AtlasRegion region;
  456. public IntAndAtlasRegionKey (int i, AtlasRegion region) {
  457. this.i = i;
  458. this.region = region;
  459. }
  460. public override int GetHashCode () {
  461. return i.GetHashCode() * 23 ^ region.GetHashCode();
  462. }
  463. }
  464. static Dictionary<IntAndAtlasRegionKey, Texture2D> CachedRegionTextures = new Dictionary<IntAndAtlasRegionKey, Texture2D>();
  465. static List<Texture2D> CachedRegionTexturesList = new List<Texture2D>();
  466. /// <summary>
  467. /// Frees up textures cached by repacking and remapping operations.
  468. ///
  469. /// Calling <see cref="AttachmentCloneExtensions.GetRemappedClone"/> with parameter <c>premultiplyAlpha=true</c>,
  470. /// <see cref="GetRepackedAttachments"/> or <see cref="GetRepackedSkin"/> will cache textures for later re-use,
  471. /// which might steadily increase the texture memory footprint when used excessively.
  472. /// You can clear this Texture cache by calling <see cref="AtlasUtilities.ClearCache()"/>.
  473. /// You may also want to call <c>Resources.UnloadUnusedAssets()</c> after that. Be aware that while this cleanup
  474. /// frees up memory, it is also a costly operation and will likely cause a spike in the framerate.
  475. /// Thus it is recommended to perform costly repacking and cleanup operations after e.g. a character customization
  476. /// screen has been exited, and if required additionally after a certain number of <c>GetRemappedClone()</c> calls.
  477. /// </summary>
  478. public static void ClearCache () {
  479. foreach (var t in CachedRegionTexturesList) {
  480. UnityEngine.Object.Destroy(t);
  481. }
  482. CachedRegionTextures.Clear();
  483. CachedRegionTexturesList.Clear();
  484. }
  485. /// <summary>Creates a new Texture2D object based on an AtlasRegion.
  486. /// If applyImmediately is true, Texture2D.Apply is called immediately after the Texture2D is filled with data.</summary>
  487. public static Texture2D ToTexture (this AtlasRegion ar, TextureFormat textureFormat = SpineTextureFormat, bool mipmaps = UseMipMaps,
  488. int texturePropertyId = 0, bool linear = false, bool applyPMA = false) {
  489. Texture2D output;
  490. IntAndAtlasRegionKey cacheKey = new IntAndAtlasRegionKey(texturePropertyId, ar);
  491. CachedRegionTextures.TryGetValue(cacheKey, out output);
  492. if (output == null) {
  493. Texture2D sourceTexture = texturePropertyId == 0 ? ar.GetMainTexture() : ar.GetTexture(texturePropertyId);
  494. Rect r = ar.GetUnityRect();
  495. int width = (int)r.width;
  496. int height = (int)r.height;
  497. output = new Texture2D(width, height, textureFormat, mipmaps, linear) { name = ar.name };
  498. output.CopyTextureAttributesFrom(sourceTexture);
  499. if (applyPMA)
  500. AtlasUtilities.CopyTextureApplyPMA(sourceTexture, r, output);
  501. else
  502. AtlasUtilities.CopyTexture(sourceTexture, r, output);
  503. CachedRegionTextures.Add(cacheKey, output);
  504. CachedRegionTexturesList.Add(output);
  505. }
  506. return output;
  507. }
  508. static Texture2D ToTexture (this Sprite s, TextureFormat textureFormat = SpineTextureFormat,
  509. bool mipmaps = UseMipMaps, bool linear = false, bool applyPMA = false) {
  510. var spriteTexture = s.texture;
  511. Rect r;
  512. if (!s.packed || s.packingMode == SpritePackingMode.Rectangle) {
  513. r = s.textureRect;
  514. } else {
  515. r = new Rect();
  516. r.xMin = Math.Min(s.uv[0].x, s.uv[1].x) * spriteTexture.width;
  517. r.xMax = Math.Max(s.uv[0].x, s.uv[1].x) * spriteTexture.width;
  518. r.yMin = Math.Min(s.uv[0].y, s.uv[2].y) * spriteTexture.height;
  519. r.yMax = Math.Max(s.uv[0].y, s.uv[2].y) * spriteTexture.height;
  520. #if UNITY_EDITOR
  521. if (s.uv.Length > 4) {
  522. Debug.LogError("When using a tightly packed SpriteAtlas with Spine, you may only access Sprites that are packed as 'FullRect' from it! " +
  523. "You can either disable 'Tight Packing' at the whole SpriteAtlas, or change the single Sprite's TextureImporter Setting 'MeshType' to 'Full Rect'." +
  524. "Sprite Asset: " + s.name, s);
  525. }
  526. #endif
  527. }
  528. var newTexture = new Texture2D((int)r.width, (int)r.height, textureFormat, mipmaps, linear);
  529. newTexture.CopyTextureAttributesFrom(spriteTexture);
  530. if (applyPMA)
  531. AtlasUtilities.CopyTextureApplyPMA(spriteTexture, r, newTexture);
  532. else
  533. AtlasUtilities.CopyTexture(spriteTexture, r, newTexture);
  534. return newTexture;
  535. }
  536. static Texture2D GetClone (this Texture2D t, TextureFormat textureFormat = SpineTextureFormat,
  537. bool mipmaps = UseMipMaps, bool linear = false, bool applyPMA = false) {
  538. var newTexture = new Texture2D((int)t.width, (int)t.height, textureFormat, mipmaps, linear);
  539. newTexture.CopyTextureAttributesFrom(t);
  540. if (applyPMA)
  541. AtlasUtilities.CopyTextureApplyPMA(t, new Rect(0, 0, t.width, t.height), newTexture);
  542. else
  543. AtlasUtilities.CopyTexture(t, new Rect(0, 0, t.width, t.height), newTexture);
  544. return newTexture;
  545. }
  546. static void CopyTexture (Texture2D source, Rect sourceRect, Texture2D destination) {
  547. if (SystemInfo.copyTextureSupport == UnityEngine.Rendering.CopyTextureSupport.None) {
  548. // GetPixels fallback for old devices.
  549. Color[] pixelBuffer = source.GetPixels((int)sourceRect.x, (int)sourceRect.y, (int)sourceRect.width, (int)sourceRect.height);
  550. destination.SetPixels(pixelBuffer);
  551. destination.Apply();
  552. } else {
  553. Graphics.CopyTexture(source, 0, 0, (int)sourceRect.x, (int)sourceRect.y, (int)sourceRect.width, (int)sourceRect.height, destination, 0, 0, 0, 0);
  554. }
  555. }
  556. static void CopyTextureApplyPMA (Texture2D source, Rect sourceRect, Texture2D destination) {
  557. Color[] pixelBuffer = source.GetPixels((int)sourceRect.x, (int)sourceRect.y, (int)sourceRect.width, (int)sourceRect.height);
  558. for (int i = 0, n = pixelBuffer.Length; i < n; i++) {
  559. Color p = pixelBuffer[i];
  560. float a = p.a;
  561. p.r = p.r * a;
  562. p.g = p.g * a;
  563. p.b = p.b * a;
  564. pixelBuffer[i] = p;
  565. }
  566. destination.SetPixels(pixelBuffer);
  567. destination.Apply();
  568. }
  569. static bool IsRenderable (Attachment a) {
  570. return a is IHasRendererObject;
  571. }
  572. /// <summary>
  573. /// Get a rect with flipped Y so that a Spine atlas rect gets converted to a Unity Sprite rect and vice versa.</summary>
  574. static Rect SpineUnityFlipRect (this Rect rect, int textureHeight) {
  575. rect.y = textureHeight - rect.y - rect.height;
  576. return rect;
  577. }
  578. /// <summary>
  579. /// Gets the Rect of an AtlasRegion according to Unity texture coordinates (x-right, y-up).
  580. /// This overload relies on region.page.height being correctly set.</summary>
  581. static Rect GetUnityRect (this AtlasRegion region) {
  582. return region.GetSpineAtlasRect().SpineUnityFlipRect(region.page.height);
  583. }
  584. /// <summary>
  585. /// Gets the Rect of an AtlasRegion according to Unity texture coordinates (x-right, y-up).</summary>
  586. static Rect GetUnityRect (this AtlasRegion region, int textureHeight) {
  587. return region.GetSpineAtlasRect().SpineUnityFlipRect(textureHeight);
  588. }
  589. /// <summary>
  590. /// Returns a Rect of the AtlasRegion according to Spine texture coordinates. (x-right, y-down)</summary>
  591. static Rect GetSpineAtlasRect (this AtlasRegion region, bool includeRotate = true) {
  592. if (includeRotate && (region.degrees == 90 || region.degrees == 270))
  593. return new Rect(region.x, region.y, region.height, region.width);
  594. else
  595. return new Rect(region.x, region.y, region.width, region.height);
  596. }
  597. /// <summary>
  598. /// Denormalize a uvRect into a texture-space Rect.</summary>
  599. static Rect UVRectToTextureRect (Rect uvRect, int texWidth, int texHeight) {
  600. uvRect.x *= texWidth;
  601. uvRect.width *= texWidth;
  602. uvRect.y *= texHeight;
  603. uvRect.height *= texHeight;
  604. return uvRect;
  605. }
  606. /// <summary>
  607. /// Normalize a texture Rect into UV coordinates.</summary>
  608. static Rect TextureRectToUVRect (Rect textureRect, int texWidth, int texHeight) {
  609. textureRect.x = Mathf.InverseLerp(0, texWidth, textureRect.x);
  610. textureRect.y = Mathf.InverseLerp(0, texHeight, textureRect.y);
  611. textureRect.width = Mathf.InverseLerp(0, texWidth, textureRect.width);
  612. textureRect.height = Mathf.InverseLerp(0, texHeight, textureRect.height);
  613. return textureRect;
  614. }
  615. /// <summary>
  616. /// Creates a new Spine AtlasRegion according to a Unity UV Rect (x-right, y-up, uv-normalized).</summary>
  617. static AtlasRegion UVRectToAtlasRegion (Rect uvRect, AtlasRegion referenceRegion, AtlasPage page) {
  618. var tr = UVRectToTextureRect(uvRect, page.width, page.height);
  619. var rr = tr.SpineUnityFlipRect(page.height);
  620. int x = (int)rr.x, y = (int)rr.y;
  621. int w, h;
  622. if (referenceRegion.degrees == 90 || referenceRegion.degrees == 270) {
  623. w = (int)rr.height;
  624. h = (int)rr.width;
  625. } else {
  626. w = (int)rr.width;
  627. h = (int)rr.height;
  628. }
  629. int originalW = Mathf.RoundToInt((float)w * ((float)referenceRegion.originalWidth / (float)referenceRegion.width));
  630. int originalH = Mathf.RoundToInt((float)h * ((float)referenceRegion.originalHeight / (float)referenceRegion.height));
  631. int offsetX = Mathf.RoundToInt((float)referenceRegion.offsetX * ((float)w / (float)referenceRegion.width));
  632. int offsetY = Mathf.RoundToInt((float)referenceRegion.offsetY * ((float)h / (float)referenceRegion.height));
  633. if (referenceRegion.degrees == 270) {
  634. w = (int)rr.width;
  635. h = (int)rr.height;
  636. }
  637. float u = uvRect.xMin;
  638. float u2 = uvRect.xMax;
  639. float v = uvRect.yMax;
  640. float v2 = uvRect.yMin;
  641. return new AtlasRegion {
  642. page = page,
  643. name = referenceRegion.name,
  644. u = u,
  645. u2 = u2,
  646. v = v,
  647. v2 = v2,
  648. index = -1,
  649. width = w,
  650. originalWidth = originalW,
  651. height = h,
  652. originalHeight = originalH,
  653. offsetX = offsetX,
  654. offsetY = offsetY,
  655. x = x,
  656. y = y,
  657. rotate = referenceRegion.rotate,
  658. degrees = referenceRegion.degrees
  659. };
  660. }
  661. /// <summary>
  662. /// Convenience method for getting the main texture of the material of the page of the region.</summary>
  663. static Texture2D GetMainTexture (this AtlasRegion region) {
  664. var material = (region.page.rendererObject as Material);
  665. return material.mainTexture as Texture2D;
  666. }
  667. /// <summary>
  668. /// Convenience method for getting any texture of the material of the page of the region by texture property name.</summary>
  669. static Texture2D GetTexture (this AtlasRegion region, string texturePropertyName) {
  670. var material = (region.page.rendererObject as Material);
  671. return material.GetTexture(texturePropertyName) as Texture2D;
  672. }
  673. /// <summary>
  674. /// Convenience method for getting any texture of the material of the page of the region by texture property id.</summary>
  675. static Texture2D GetTexture (this AtlasRegion region, int texturePropertyId) {
  676. var material = (region.page.rendererObject as Material);
  677. return material.GetTexture(texturePropertyId) as Texture2D;
  678. }
  679. static void CopyTextureAttributesFrom (this Texture2D destination, Texture2D source) {
  680. destination.filterMode = source.filterMode;
  681. destination.anisoLevel = source.anisoLevel;
  682. #if UNITY_EDITOR
  683. destination.alphaIsTransparency = source.alphaIsTransparency;
  684. #endif
  685. destination.wrapModeU = source.wrapModeU;
  686. destination.wrapModeV = source.wrapModeV;
  687. destination.wrapModeW = source.wrapModeW;
  688. }
  689. #endregion
  690. static float InverseLerp (float a, float b, float value) {
  691. return (value - a) / (b - a);
  692. }
  693. }
  694. }