├── README.md ├── Vector2Extensions.cs ├── SpriteData.cs ├── LICENSE ├── DynamicAtlasses ├── DynamicSpriteData.cs ├── GridAtlas.cs ├── AbsoluteAtlas.cs ├── TexturePacker.cs └── DynamicAtlas.cs └── Atlas.cs /README.md: -------------------------------------------------------------------------------- 1 | # DynamicSpriteSheets -------------------------------------------------------------------------------- /Vector2Extensions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | public static class Vector2Extensions 4 | { 5 | /// 6 | /// Scales vector's each coordinate with corresponding coordinate in scale vector. 7 | /// If some scale's coordinate is zero than it doesn't have effect on that coordinate. 8 | /// 9 | public static void InverseScale(this Vector2 vector, Vector2 scale) 10 | { 11 | if (scale.x != 0f) 12 | { 13 | vector.x /= scale.x; 14 | } 15 | if (scale.y != 0f) 16 | { 17 | vector.y /= scale.y; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SpriteData.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | public class SpriteData 4 | { 5 | public string Name { get; private set; } 6 | 7 | public int X { get; private set; } 8 | public int Y { get; private set; } 9 | public int Width { get; private set; } 10 | public int Height { get; private set; } 11 | 12 | public SpriteData(string name) 13 | { 14 | Name = name; 15 | } 16 | 17 | public void SetRect(Rect rect, float padding) 18 | { 19 | SetRect(Mathf.FloorToInt(rect.xMin + padding), Mathf.FloorToInt(rect.yMin + padding), Mathf.FloorToInt(rect.width - 2f * padding), 20 | Mathf.FloorToInt(rect.height - 2f * padding)); 21 | } 22 | 23 | public void SetRect(int x, int y, int width, int height) 24 | { 25 | X = x; 26 | Y = y; 27 | Width = width; 28 | Height = height; 29 | } 30 | 31 | public void SetName(string newName) 32 | { 33 | Name = newName; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 dusanst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /DynamicAtlasses/DynamicSpriteData.cs: -------------------------------------------------------------------------------- 1 | /// 2 | /// Sprite data used in dynamic atlases, 3 | /// has some additional info. 4 | /// 5 | public class DynamicSpriteData : SpriteData 6 | { 7 | /// 8 | /// Gets or sets number of references to the coresponding sprite. 9 | /// If count is 0 than nobody uses coresponding sprite. 10 | /// 11 | public int ReferenceCount { get; set; } 12 | 13 | /// 14 | /// True if sprite has been lost because atlas texture got lost. 15 | /// 16 | public bool SpriteLost { get; set; } 17 | 18 | /// 19 | /// Returns whether this coresponding sprite inside of atlas is currently used. 20 | /// If it is not used than it can be overriden by another sprite. 21 | /// 22 | public bool IsUsed() 23 | { 24 | return ReferenceCount > 0; 25 | } 26 | 27 | public DynamicSpriteData(string name) : base(name) {} 28 | 29 | /// 30 | /// Resets sprite's reference count; so it becomes unused. 31 | /// 32 | public void Reset() 33 | { 34 | ReferenceCount = 0; 35 | SpriteLost = false; 36 | } 37 | 38 | /// 39 | /// Increases reference count to sprite represented by this sprite data. 40 | /// 41 | public void Acquire() 42 | { 43 | if (ReferenceCount < 0) 44 | { 45 | // fail safe 46 | ReferenceCount = 0; 47 | } 48 | ReferenceCount++; 49 | } 50 | 51 | /// 52 | /// Decreases reference count to sprite represented by this sprite data. 53 | /// 54 | public void Release() 55 | { 56 | ReferenceCount--; 57 | if (ReferenceCount < 0) 58 | { 59 | // fail safe 60 | ReferenceCount = 0; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Atlas.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using System.Collections.Generic; 4 | 5 | public class Atlas 6 | { 7 | private List spriteList = new List(); 8 | private Dictionary spriteDictionary; 9 | 10 | public Texture Texture 11 | { 12 | get { return SpriteMaterial != null ? SpriteMaterial.mainTexture : null; } 13 | } 14 | 15 | public Material SpriteMaterial { get; set; } 16 | 17 | public List SpriteList 18 | { 19 | get { return spriteList; } 20 | set { spriteList = value; } 21 | } 22 | 23 | public SpriteData GetSprite(string name) 24 | { 25 | if (string.IsNullOrEmpty(name)) { return null; } 26 | 27 | if (spriteDictionary == null) 28 | { 29 | spriteDictionary = new Dictionary(); 30 | foreach (var spriteData in spriteList) 31 | { 32 | spriteDictionary.Add(spriteData.Name, spriteData); 33 | } 34 | } 35 | 36 | SpriteData foundSprite; 37 | spriteDictionary.TryGetValue(name, out foundSprite); 38 | return foundSprite; 39 | } 40 | 41 | public void ClearAllSpriteData() 42 | { 43 | if (spriteDictionary != null) 44 | { 45 | spriteDictionary.Clear(); 46 | spriteDictionary = null; 47 | } 48 | if (SpriteList != null) 49 | { 50 | SpriteList.Clear(); 51 | } 52 | } 53 | 54 | public void RenameSprite(string oldName, string newName) 55 | { 56 | SpriteData spriteData; 57 | if (spriteDictionary.TryGetValue(oldName, out spriteData)) 58 | { 59 | spriteDictionary.Remove(oldName); 60 | spriteData.SetName(newName); 61 | spriteDictionary.Add(newName, spriteData); 62 | } 63 | } 64 | 65 | public void AddSprite(DynamicSpriteData spriteData) 66 | { 67 | if (spriteData == null || spriteDictionary.ContainsKey(spriteData.Name)) 68 | { 69 | return; 70 | } 71 | spriteDictionary.Add(spriteData.Name, spriteData); 72 | SpriteList.Add(spriteData); 73 | } 74 | 75 | public void RemoveAllSpriteData(Predicate match) 76 | { 77 | if (match == null) 78 | { 79 | return; 80 | } 81 | 82 | for (int i = 0; i < SpriteList.Count; i++) 83 | { 84 | if (match(SpriteList[i])) 85 | { 86 | spriteDictionary.Remove(SpriteList[i].Name); 87 | } 88 | } 89 | 90 | SpriteList.RemoveAll(match); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DynamicAtlasses/GridAtlas.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections.Generic; 3 | 4 | /// 5 | /// Objects of this class should contain fixed sized textures. 6 | /// Textures are arranged in grid order inside of atlas texture. 7 | /// 8 | public class GridAtlas : DynamicAtlas 9 | { 10 | #region Fields 11 | 12 | private readonly int initialCapacity; 13 | 14 | #endregion 15 | 16 | #region Public Properties 17 | 18 | /// 19 | /// Width and height of the single sprite. If added textures have different sizes, than they will be stretched to this size 20 | /// but when they are applied to object of appropriate size texture will act as it is in the original size. 21 | /// 22 | public Vector2 SpriteSize { get; private set; } 23 | 24 | #endregion 25 | 26 | #region Constructors 27 | 28 | /// 29 | /// Creates a new texture atlas with defined max size and sprite size. 30 | /// 31 | /// The maximum size of the atlas. Max size x and y should be power of two. 32 | /// The size of the sprites in the atlas. 33 | /// Initial capacity is good to set if minimum number of sprites can be predicted, it is measured in number of sprites. 34 | public GridAtlas(string atlasName, Vector2 maxSize, Vector2 spriteSize, int initialCapacity) : base(atlasName, maxSize) 35 | { 36 | SpriteSize = spriteSize; 37 | this.initialCapacity = initialCapacity; 38 | } 39 | 40 | #endregion 41 | 42 | #region Private Methods 43 | 44 | protected override bool AddTextures(List textures) 45 | { 46 | // The number of sprites after adding all sprites. 47 | // Number of sprites cannot be reduced because unused are not removed just replaced with new ones. 48 | int spriteCount = Mathf.Max(Atlas.SpriteList.Count, textures.Count + UsedSpriteCount); 49 | if (spriteCount < initialCapacity) 50 | { 51 | spriteCount = initialCapacity; 52 | } 53 | 54 | // This is the needed texture size 55 | Vector2 newTextureSize = GetMinimumTextureSize(spriteCount, SpriteSize, (int)MaxSize.x); 56 | 57 | // If the required size is larger than we can do, abort the process 58 | if (newTextureSize.x > MaxSize.x || newTextureSize.y > MaxSize.y) 59 | { 60 | Debug.LogError(string.Format("Atlas: {0} with maximum size: {1} cannot support size {2}", AtlasName, MaxSize, newTextureSize)); 61 | return false; 62 | } 63 | 64 | bool atlasTextureChanged = false; 65 | 66 | if (AtlasTexture == null) 67 | { 68 | AtlasTexture = CreateRenderTexture(newTextureSize); 69 | Graphics.SetRenderTarget((RenderTexture)AtlasTexture); 70 | atlasTextureChanged = true; 71 | } 72 | else if (newTextureSize.x > AtlasTexture.width || newTextureSize.y > AtlasTexture.height) 73 | { 74 | RenderTexture newTexture = CreateRenderTexture(newTextureSize); 75 | Graphics.SetRenderTarget(newTexture); 76 | DrawOldAtlas(newTexture, CalculateSpritePosition); 77 | atlasTextureChanged = true; 78 | } 79 | else 80 | { 81 | Graphics.SetRenderTarget(AtlasTexture as RenderTexture); 82 | } 83 | 84 | DrawNewTextures(textures, CalculateSpritePosition, Atlas.SpriteList.Count, true); 85 | 86 | // Remove the render target 87 | Graphics.SetRenderTarget(null); 88 | 89 | if (atlasTextureChanged) 90 | { 91 | // TODO: Inform Sprites that are using this atlas that texture has changed 92 | } 93 | 94 | return true; 95 | } 96 | 97 | private Rect CalculateSpritePosition(int index, Texture newTexture) 98 | { 99 | float spriteWidth = SpriteSize.x + 2f * Padding; 100 | float spriteHeight = SpriteSize.y + 2f * Padding; 101 | 102 | int spritesPerWidth = Mathf.FloorToInt(newTexture.width / spriteWidth); 103 | float x = (index % spritesPerWidth) * spriteWidth; 104 | float y = (index / spritesPerWidth) * spriteHeight; 105 | return new Rect(x, y, spriteWidth, spriteHeight); 106 | } 107 | 108 | #endregion 109 | 110 | #region Public Methods 111 | 112 | public void ChangeSize(Vector2 maxSize, Vector2 spriteSize) 113 | { 114 | if (AtlasTexture == null) 115 | { 116 | // only if atlas is empty this action can be performed 117 | MaxSize = maxSize; 118 | SpriteSize = spriteSize; 119 | } 120 | } 121 | 122 | /// 123 | /// Returns the minimum required texture size to fit the sprites in it. The returned texture 124 | /// size will be a power of 2. 125 | /// 126 | /// The number of sprites needed to fit in. 127 | /// The size of the textures. 128 | /// The minimum size. 129 | public static Vector2 GetMinimumTextureSize(int spriteCount, Vector2 spriteSize, int maxTextureWidth) 130 | { 131 | int maxInOneRow = Mathf.FloorToInt(maxTextureWidth / (spriteSize.x + 2f * Padding)); 132 | if (maxInOneRow == 0) 133 | { 134 | Debug.LogError(string.Format("Sprites with size: {0} cannot fit in atlas with maximum width: {1}", spriteSize, maxTextureWidth)); 135 | return Vector2.zero; 136 | } 137 | if (maxInOneRow >= spriteCount) 138 | { 139 | int minWidth = Mathf.CeilToInt(spriteCount * (spriteSize.x + 2f * Padding)); 140 | return new Vector2(Mathf.NextPowerOfTwo(minWidth), Mathf.NextPowerOfTwo(Mathf.CeilToInt(spriteSize.y + 2f * Padding))); 141 | } 142 | 143 | int numberOfRows = Mathf.CeilToInt(((float)spriteCount) / maxInOneRow); 144 | int minHeight = Mathf.CeilToInt(numberOfRows * (spriteSize.y + 2f * Padding)); 145 | return new Vector2(maxTextureWidth, Mathf.NextPowerOfTwo(minHeight)); 146 | } 147 | 148 | #endregion 149 | } 150 | -------------------------------------------------------------------------------- /DynamicAtlasses/AbsoluteAtlas.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | /// 5 | /// Objects of this class can contain textures of differing sizes. 6 | /// As a result this atlas can be used for inserting various kinds of textures, 7 | /// unlike GridAtlas which is supposed to contain only textures with same size. 8 | /// 9 | public class AbsoluteAtlas : DynamicAtlas 10 | { 11 | #region Fields 12 | 13 | /// 14 | /// Reference to the packer used for packing textures in atlas. 15 | /// 16 | private readonly TexturePacker packer; 17 | 18 | /// 19 | /// Initial size of the atlas texture. 20 | /// 21 | private readonly Vector2 initialSize; 22 | 23 | private static readonly Vector2 DefaultInitialSize = new Vector2(64, 64); 24 | 25 | #endregion 26 | 27 | #region Constructors 28 | 29 | /// 30 | /// Creates AbsoluteAtlas. 31 | /// 32 | /// Max size of the atlas texture. 33 | public AbsoluteAtlas(string atlasName, Vector2 maxSize) : this(atlasName, maxSize, DefaultInitialSize) {} 34 | 35 | /// 36 | /// Creates AbsoluteAtlas. 37 | /// Setting initial size is good if minimum size of textures can be predicted. 38 | /// 39 | /// Max size of the atlas texture. Must be greater or equal than 64 x 64. 40 | /// Initial size of the atlas texture. Must be smaller or equal to MaxSize 41 | public AbsoluteAtlas(string atlasName, Vector2 maxSize, Vector2 initialSize) 42 | : base(atlasName, maxSize) 43 | { 44 | this.initialSize = initialSize; 45 | if (initialSize.x > maxSize.x || initialSize.y > maxSize.y) 46 | { 47 | Debug.LogError("Absolute atlas initial size can not be greater than maximum size of it. " + atlasName); 48 | } 49 | packer = new TexturePacker(initialSize, false); 50 | } 51 | 52 | #endregion 53 | 54 | #region Methods 55 | 56 | protected override bool AddTextures(List textures) 57 | { 58 | Vector2 minimalTextureSize = Vector2.zero; 59 | List texturesToAdd = new List(); 60 | List positions; 61 | if (TryFindingMinimalAtlasTextureSize(textures, ref minimalTextureSize) && packer.binWidth == (int)minimalTextureSize.x 62 | && packer.binHeight == (int)minimalTextureSize.y) 63 | { 64 | // Minimal texture size is equal to the current size of packer, just try adding new textures 65 | 66 | foreach (Texture texture in textures) 67 | { 68 | texturesToAdd.Add(new Rect(0, 0, texture.width + 2f * Padding, texture.height + 2f * Padding)); 69 | } 70 | 71 | positions = packer.Insert(texturesToAdd, TexturePacker.FreeRectChoiceHeuristic.RectBestShortSideFit); 72 | 73 | if (positions != null) 74 | { 75 | // there is enough position in texture, just draw new textures. 76 | 77 | bool atlasTextureNotCreated = AtlasTexture == null; 78 | if (atlasTextureNotCreated) 79 | { 80 | AtlasTexture = CreateRenderTexture(initialSize); 81 | } 82 | 83 | // Get ready to render 84 | Graphics.SetRenderTarget(AtlasTexture as RenderTexture); 85 | 86 | DrawNewTextures(textures, positions, 0, false); 87 | 88 | // Remove the render target 89 | Graphics.SetRenderTarget(null); 90 | 91 | if (atlasTextureNotCreated) 92 | { 93 | // TODO: Inform Sprites that are using this atlas that texture has changed 94 | } 95 | 96 | return true; 97 | } 98 | } 99 | 100 | // we don't need unused sprite data in list 101 | Atlas.RemoveAllSpriteData((spriteData) => !((DynamicSpriteData)spriteData).IsUsed()); 102 | 103 | numberOfUnused = 0; 104 | 105 | Vector2 newTextureSize = Vector2.zero; 106 | if (!TryFindingMinimalAtlasTextureSize(textures, ref newTextureSize)) 107 | { 108 | return false; 109 | } 110 | 111 | while (true) 112 | { 113 | texturesToAdd.Clear(); 114 | foreach (var data in Atlas.SpriteList) 115 | { 116 | texturesToAdd.Add(new Rect(0, 0, data.Width + 2f * Padding, data.Height + 2f * Padding)); 117 | } 118 | foreach (Texture texture in textures) 119 | { 120 | texturesToAdd.Add(new Rect(0, 0, texture.width + 2f * Padding, texture.height + 2f * Padding)); 121 | } 122 | 123 | packer.Init(newTextureSize, false); 124 | positions = packer.Insert(texturesToAdd, TexturePacker.FreeRectChoiceHeuristic.RectBestShortSideFit); 125 | 126 | if (positions != null) 127 | { 128 | RenderTexture newTexture = CreateRenderTexture(newTextureSize); 129 | // Get ready to render 130 | Graphics.SetRenderTarget(newTexture); 131 | 132 | DrawOldAtlas(newTexture, positions); 133 | DrawNewTextures(textures, positions, Atlas.SpriteList.Count, false); 134 | 135 | // Remove the render target 136 | Graphics.SetRenderTarget(null); 137 | 138 | // TODO: Inform Sprites that are using this atlas that texture has changed 139 | 140 | return true; 141 | } 142 | 143 | if (!TryResizingSmallerEdge(ref newTextureSize)) 144 | { 145 | return false; 146 | } 147 | } 148 | } 149 | 150 | /// 151 | /// Tries finding minimal atlas texture size for storing newTextures in Atlas. 152 | /// Calculations are based on minimal area which new textures should occupy. 153 | /// 154 | /// True, if minimal possible texture size is not greater than MaxSize; otherwise, false. 155 | /// New textures which should be added in Atlas. 156 | /// Minimal texture size is stored in this field if method returns true. 157 | private bool TryFindingMinimalAtlasTextureSize(List newTextures, ref Vector2 minimalTextureSize) 158 | { 159 | float minimalTextureArea = MinimalAtlasTextureArea(newTextures); 160 | 161 | if (minimalTextureArea > MaxSize.x * MaxSize.y) 162 | { 163 | return false; 164 | } 165 | 166 | minimalTextureSize = initialSize; 167 | while (true) 168 | { 169 | if (minimalTextureSize.x * minimalTextureSize.y >= minimalTextureArea) 170 | { 171 | return true; 172 | } 173 | if (!TryResizingSmallerEdge(ref minimalTextureSize)) 174 | { 175 | return false; 176 | } 177 | } 178 | } 179 | 180 | /// 181 | /// Returns minimal atlas texture area after adding newTextures in atlas. 182 | /// 183 | /// The atlas texture area. 184 | /// New textures which should be added in atlas. 185 | private float MinimalAtlasTextureArea(List newTextures) 186 | { 187 | float area = 0; 188 | foreach (var spriteData in Atlas.SpriteList) 189 | { 190 | area += (spriteData.Width + 2f * Padding) * (spriteData.Height + (2f * Padding)); 191 | } 192 | foreach (var texture in newTextures) 193 | { 194 | area += (texture.width + 2f * Padding) * (texture.height + 2f * Padding); 195 | } 196 | return area; 197 | } 198 | 199 | /// 200 | /// Resizes one vector's edge by factor of two if it can fit MaxSize after resizing. 201 | /// First tries resizing smaller edge. 202 | /// 203 | /// True, if resizing succeeded, otherwise false. 204 | /// New texture size. 205 | private bool TryResizingSmallerEdge(ref Vector2 newTextureSize) 206 | { 207 | if (newTextureSize.x * 2f <= MaxSize.x && newTextureSize.y * 2f <= MaxSize.y) 208 | { 209 | // we want to resize smaller side 210 | if (newTextureSize.x <= newTextureSize.y) 211 | { 212 | newTextureSize = new Vector2(newTextureSize.x * 2f, newTextureSize.y); 213 | } 214 | else 215 | { 216 | newTextureSize = new Vector2(newTextureSize.x, newTextureSize.y * 2f); 217 | } 218 | } 219 | else if (newTextureSize.x * 2f <= MaxSize.x) 220 | { 221 | newTextureSize = new Vector2(newTextureSize.x * 2f, newTextureSize.y); 222 | } 223 | else if (newTextureSize.y * 2f <= MaxSize.y) 224 | { 225 | newTextureSize = new Vector2(newTextureSize.x, newTextureSize.y * 2f); 226 | } 227 | else 228 | { 229 | return false; 230 | } 231 | return true; 232 | } 233 | 234 | #endregion 235 | } 236 | -------------------------------------------------------------------------------- /DynamicAtlasses/TexturePacker.cs: -------------------------------------------------------------------------------- 1 | 2 | // Based on the Public Domain MaxRectsBinPack.cpp source by Jukka Jylänki 3 | // https://github.com/juj/RectangleBinPack/ 4 | // 5 | // Ported to C# by Sven Magnus 6 | // This version is also public domain - do whatever you want with it. 7 | // 8 | using UnityEngine; 9 | using System.Collections; 10 | using System.Collections.Generic; 11 | 12 | public class TexturePacker 13 | { 14 | public int binWidth = 0; 15 | public int binHeight = 0; 16 | public bool allowRotations; 17 | 18 | public List usedRectangles = new List(); 19 | public List freeRectangles = new List(); 20 | 21 | public enum FreeRectChoiceHeuristic 22 | { 23 | RectBestShortSideFit, ///< -BSSF: Positions the rectangle against the short side of a free rectangle into which it fits the best. 24 | RectBestLongSideFit, ///< -BLSF: Positions the rectangle against the long side of a free rectangle into which it fits the best. 25 | RectBestAreaFit, ///< -BAF: Positions the rectangle into the smallest free rect into which it fits. 26 | RectBottomLeftRule, ///< -BL: Does the Tetris placement. 27 | RectContactPointRule ///< -CP: Choosest the placement where the rectangle touches other rects as much as possible. 28 | }; 29 | 30 | public TexturePacker (int width, int height, bool rotations) 31 | { 32 | Init(width, height, rotations); 33 | } 34 | 35 | public TexturePacker(Vector2 size, bool rotations): this((int)size.x, (int)size.y, rotations){} 36 | 37 | public void Init (int width, int height, bool rotations) 38 | { 39 | binWidth = width; 40 | binHeight = height; 41 | allowRotations = rotations; 42 | 43 | Rect n = new Rect(); 44 | n.x = 0; 45 | n.y = 0; 46 | n.width = width; 47 | n.height = height; 48 | 49 | usedRectangles.Clear(); 50 | 51 | freeRectangles.Clear(); 52 | freeRectangles.Add(n); 53 | } 54 | 55 | public void Init(Vector2 size, bool rotations) 56 | { 57 | Init((int)size.x, (int) size.y, rotations); 58 | } 59 | 60 | public Rect Insert (int width, int height, FreeRectChoiceHeuristic method) 61 | { 62 | Rect newNode = new Rect(); 63 | int score1 = 0; // Unused in this function. We don't need to know the score after finding the position. 64 | int score2 = 0; 65 | switch (method) 66 | { 67 | case FreeRectChoiceHeuristic.RectBestShortSideFit: newNode = FindPositionForNewNodeBestShortSideFit(width, height, ref score1, ref score2); break; 68 | case FreeRectChoiceHeuristic.RectBottomLeftRule: newNode = FindPositionForNewNodeBottomLeft(width, height, ref score1, ref score2); break; 69 | case FreeRectChoiceHeuristic.RectContactPointRule: newNode = FindPositionForNewNodeContactPoint(width, height, ref score1); break; 70 | case FreeRectChoiceHeuristic.RectBestLongSideFit: newNode = FindPositionForNewNodeBestLongSideFit(width, height, ref score2, ref score1); break; 71 | case FreeRectChoiceHeuristic.RectBestAreaFit: newNode = FindPositionForNewNodeBestAreaFit(width, height, ref score1, ref score2); break; 72 | } 73 | 74 | if (newNode.height == 0) 75 | return newNode; 76 | 77 | int numRectanglesToProcess = freeRectangles.Count; 78 | for (int i = 0; i < numRectanglesToProcess; ++i) 79 | { 80 | if (SplitFreeNode(freeRectangles[i], ref newNode)) 81 | { 82 | freeRectangles.RemoveAt(i); 83 | --i; 84 | --numRectanglesToProcess; 85 | } 86 | } 87 | 88 | PruneFreeList(); 89 | 90 | usedRectangles.Add(newNode); 91 | return newNode; 92 | } 93 | 94 | public Rect Insert(Vector2 size, FreeRectChoiceHeuristic method) 95 | { 96 | return Insert((int)size.x, (int)size.y, method); 97 | } 98 | 99 | /// 100 | /// Returns list of positions for newly added rects. 101 | /// Element in returned array corresponds to the element on same position in input array. 102 | /// 103 | /// Rects. 104 | /// Method. 105 | public List Insert (List rects, FreeRectChoiceHeuristic method) 106 | { 107 | if (rects == null || rects.Count == 0) 108 | { 109 | return null; 110 | } 111 | 112 | List sortedUsedRects = new List(new Rect[rects.Count]); 113 | List indexesInOriginalArray = new List(); 114 | for (int i = 0; i < rects.Count; i++) 115 | { 116 | indexesInOriginalArray.Add(i); 117 | } 118 | 119 | while (rects.Count > 0) 120 | { 121 | int bestScore1 = int.MaxValue; 122 | int bestScore2 = int.MaxValue; 123 | int bestRectIndex = -1; 124 | Rect bestNode = new Rect(); 125 | 126 | for (int i = 0; i < rects.Count; ++i) 127 | { 128 | int score1 = 0; 129 | int score2 = 0; 130 | Rect newNode = ScoreRect((int)rects[i].width, (int)rects[i].height, method, ref score1, ref score2); 131 | 132 | if (score1 < bestScore1 || (score1 == bestScore1 && score2 < bestScore2)) 133 | { 134 | bestScore1 = score1; 135 | bestScore2 = score2; 136 | bestNode = newNode; 137 | bestRectIndex = i; 138 | } 139 | } 140 | 141 | if (bestRectIndex == -1) 142 | return null; 143 | 144 | PlaceRect(bestNode); 145 | rects.RemoveAt(bestRectIndex); 146 | 147 | int indexInOriginalArray = indexesInOriginalArray[bestRectIndex]; 148 | sortedUsedRects[indexInOriginalArray] = bestNode; 149 | indexesInOriginalArray.RemoveAt(bestRectIndex); 150 | } 151 | 152 | return sortedUsedRects; 153 | } 154 | 155 | void PlaceRect (Rect node) 156 | { 157 | int numRectanglesToProcess = freeRectangles.Count; 158 | for (int i = 0; i < numRectanglesToProcess; ++i) 159 | { 160 | if (SplitFreeNode(freeRectangles[i], ref node)) 161 | { 162 | freeRectangles.RemoveAt(i); 163 | --i; 164 | --numRectanglesToProcess; 165 | } 166 | } 167 | 168 | PruneFreeList(); 169 | 170 | usedRectangles.Add(node); 171 | } 172 | 173 | Rect ScoreRect (int width, int height, FreeRectChoiceHeuristic method, ref int score1, ref int score2) 174 | { 175 | Rect newNode = new Rect(); 176 | score1 = int.MaxValue; 177 | score2 = int.MaxValue; 178 | switch (method) 179 | { 180 | case FreeRectChoiceHeuristic.RectBestShortSideFit: newNode = FindPositionForNewNodeBestShortSideFit(width, height, ref score1, ref score2); break; 181 | case FreeRectChoiceHeuristic.RectBottomLeftRule: newNode = FindPositionForNewNodeBottomLeft(width, height, ref score1, ref score2); break; 182 | case FreeRectChoiceHeuristic.RectContactPointRule: newNode = FindPositionForNewNodeContactPoint(width, height, ref score1); 183 | score1 = -score1; // Reverse since we are minimizing, but for contact point score bigger is better. 184 | break; 185 | case FreeRectChoiceHeuristic.RectBestLongSideFit: newNode = FindPositionForNewNodeBestLongSideFit(width, height, ref score2, ref score1); break; 186 | case FreeRectChoiceHeuristic.RectBestAreaFit: newNode = FindPositionForNewNodeBestAreaFit(width, height, ref score1, ref score2); break; 187 | } 188 | 189 | // Cannot fit the current rectangle. 190 | if (newNode.height == 0) 191 | { 192 | score1 = int.MaxValue; 193 | score2 = int.MaxValue; 194 | } 195 | 196 | return newNode; 197 | } 198 | 199 | /// Computes the ratio of used surface area. 200 | public float Occupancy () 201 | { 202 | ulong usedSurfaceArea = 0; 203 | for (int i = 0; i < usedRectangles.Count; ++i) 204 | usedSurfaceArea += (uint)usedRectangles[i].width * (uint)usedRectangles[i].height; 205 | 206 | return (float)usedSurfaceArea / (binWidth * binHeight); 207 | } 208 | 209 | Rect FindPositionForNewNodeBottomLeft (int width, int height, ref int bestY, ref int bestX) 210 | { 211 | Rect bestNode = new Rect(); 212 | //memset(bestNode, 0, sizeof(Rect)); 213 | 214 | bestY = int.MaxValue; 215 | 216 | for (int i = 0; i < freeRectangles.Count; ++i) 217 | { 218 | // Try to place the rectangle in upright (non-flipped) orientation. 219 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) 220 | { 221 | int topSideY = (int)freeRectangles[i].y + height; 222 | if (topSideY < bestY || (topSideY == bestY && freeRectangles[i].x < bestX)) 223 | { 224 | bestNode.x = freeRectangles[i].x; 225 | bestNode.y = freeRectangles[i].y; 226 | bestNode.width = width; 227 | bestNode.height = height; 228 | bestY = topSideY; 229 | bestX = (int)freeRectangles[i].x; 230 | } 231 | } 232 | if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) 233 | { 234 | int topSideY = (int)freeRectangles[i].y + width; 235 | if (topSideY < bestY || (topSideY == bestY && freeRectangles[i].x < bestX)) 236 | { 237 | bestNode.x = freeRectangles[i].x; 238 | bestNode.y = freeRectangles[i].y; 239 | bestNode.width = height; 240 | bestNode.height = width; 241 | bestY = topSideY; 242 | bestX = (int)freeRectangles[i].x; 243 | } 244 | } 245 | } 246 | return bestNode; 247 | } 248 | 249 | Rect FindPositionForNewNodeBestShortSideFit (int width, int height, ref int bestShortSideFit, ref int bestLongSideFit) 250 | { 251 | Rect bestNode = new Rect(); 252 | //memset(&bestNode, 0, sizeof(Rect)); 253 | 254 | bestShortSideFit = int.MaxValue; 255 | 256 | for (int i = 0; i < freeRectangles.Count; ++i) 257 | { 258 | // Try to place the rectangle in upright (non-flipped) orientation. 259 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) 260 | { 261 | int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - width); 262 | int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - height); 263 | int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert); 264 | int longSideFit = Mathf.Max(leftoverHoriz, leftoverVert); 265 | 266 | if (shortSideFit < bestShortSideFit || (shortSideFit == bestShortSideFit && longSideFit < bestLongSideFit)) 267 | { 268 | bestNode.x = freeRectangles[i].x; 269 | bestNode.y = freeRectangles[i].y; 270 | bestNode.width = width; 271 | bestNode.height = height; 272 | bestShortSideFit = shortSideFit; 273 | bestLongSideFit = longSideFit; 274 | } 275 | } 276 | 277 | if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) 278 | { 279 | int flippedLeftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - height); 280 | int flippedLeftoverVert = Mathf.Abs((int)freeRectangles[i].height - width); 281 | int flippedShortSideFit = Mathf.Min(flippedLeftoverHoriz, flippedLeftoverVert); 282 | int flippedLongSideFit = Mathf.Max(flippedLeftoverHoriz, flippedLeftoverVert); 283 | 284 | if (flippedShortSideFit < bestShortSideFit || (flippedShortSideFit == bestShortSideFit && flippedLongSideFit < bestLongSideFit)) 285 | { 286 | bestNode.x = freeRectangles[i].x; 287 | bestNode.y = freeRectangles[i].y; 288 | bestNode.width = height; 289 | bestNode.height = width; 290 | bestShortSideFit = flippedShortSideFit; 291 | bestLongSideFit = flippedLongSideFit; 292 | } 293 | } 294 | } 295 | return bestNode; 296 | } 297 | 298 | Rect FindPositionForNewNodeBestLongSideFit (int width, int height, ref int bestShortSideFit, ref int bestLongSideFit) 299 | { 300 | Rect bestNode = new Rect(); 301 | //memset(&bestNode, 0, sizeof(Rect)); 302 | 303 | bestLongSideFit = int.MaxValue; 304 | 305 | for (int i = 0; i < freeRectangles.Count; ++i) 306 | { 307 | // Try to place the rectangle in upright (non-flipped) orientation. 308 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) 309 | { 310 | int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - width); 311 | int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - height); 312 | int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert); 313 | int longSideFit = Mathf.Max(leftoverHoriz, leftoverVert); 314 | 315 | if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit)) 316 | { 317 | bestNode.x = freeRectangles[i].x; 318 | bestNode.y = freeRectangles[i].y; 319 | bestNode.width = width; 320 | bestNode.height = height; 321 | bestShortSideFit = shortSideFit; 322 | bestLongSideFit = longSideFit; 323 | } 324 | } 325 | 326 | if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) 327 | { 328 | int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - height); 329 | int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - width); 330 | int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert); 331 | int longSideFit = Mathf.Max(leftoverHoriz, leftoverVert); 332 | 333 | if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit)) 334 | { 335 | bestNode.x = freeRectangles[i].x; 336 | bestNode.y = freeRectangles[i].y; 337 | bestNode.width = height; 338 | bestNode.height = width; 339 | bestShortSideFit = shortSideFit; 340 | bestLongSideFit = longSideFit; 341 | } 342 | } 343 | } 344 | return bestNode; 345 | } 346 | 347 | Rect FindPositionForNewNodeBestAreaFit (int width, int height, ref int bestAreaFit, ref int bestShortSideFit) 348 | { 349 | Rect bestNode = new Rect(); 350 | //memset(&bestNode, 0, sizeof(Rect)); 351 | 352 | bestAreaFit = int.MaxValue; 353 | 354 | for (int i = 0; i < freeRectangles.Count; ++i) 355 | { 356 | int areaFit = (int)freeRectangles[i].width * (int)freeRectangles[i].height - width * height; 357 | 358 | // Try to place the rectangle in upright (non-flipped) orientation. 359 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) 360 | { 361 | int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - width); 362 | int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - height); 363 | int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert); 364 | 365 | if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit)) 366 | { 367 | bestNode.x = freeRectangles[i].x; 368 | bestNode.y = freeRectangles[i].y; 369 | bestNode.width = width; 370 | bestNode.height = height; 371 | bestShortSideFit = shortSideFit; 372 | bestAreaFit = areaFit; 373 | } 374 | } 375 | 376 | if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) 377 | { 378 | int leftoverHoriz = Mathf.Abs((int)freeRectangles[i].width - height); 379 | int leftoverVert = Mathf.Abs((int)freeRectangles[i].height - width); 380 | int shortSideFit = Mathf.Min(leftoverHoriz, leftoverVert); 381 | 382 | if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit)) 383 | { 384 | bestNode.x = freeRectangles[i].x; 385 | bestNode.y = freeRectangles[i].y; 386 | bestNode.width = height; 387 | bestNode.height = width; 388 | bestShortSideFit = shortSideFit; 389 | bestAreaFit = areaFit; 390 | } 391 | } 392 | } 393 | return bestNode; 394 | } 395 | 396 | /// Returns 0 if the two intervals i1 and i2 are disjoint, or the length of their overlap otherwise. 397 | int CommonIntervalLength (int i1start, int i1end, int i2start, int i2end) 398 | { 399 | if (i1end < i2start || i2end < i1start) 400 | return 0; 401 | return Mathf.Min(i1end, i2end) - Mathf.Max(i1start, i2start); 402 | } 403 | 404 | int ContactPointScoreNode (int x, int y, int width, int height) 405 | { 406 | int score = 0; 407 | 408 | if (x == 0 || x + width == binWidth) 409 | score += height; 410 | if (y == 0 || y + height == binHeight) 411 | score += width; 412 | 413 | for (int i = 0; i < usedRectangles.Count; ++i) 414 | { 415 | if (usedRectangles[i].x == x + width || usedRectangles[i].x + usedRectangles[i].width == x) 416 | score += CommonIntervalLength((int)usedRectangles[i].y, (int)usedRectangles[i].y + (int)usedRectangles[i].height, y, y + height); 417 | if (usedRectangles[i].y == y + height || usedRectangles[i].y + usedRectangles[i].height == y) 418 | score += CommonIntervalLength((int)usedRectangles[i].x, (int)usedRectangles[i].x + (int)usedRectangles[i].width, x, x + width); 419 | } 420 | return score; 421 | } 422 | 423 | Rect FindPositionForNewNodeContactPoint (int width, int height, ref int bestContactScore) 424 | { 425 | Rect bestNode = new Rect(); 426 | //memset(&bestNode, 0, sizeof(Rect)); 427 | 428 | bestContactScore = -1; 429 | 430 | for (int i = 0; i < freeRectangles.Count; ++i) 431 | { 432 | // Try to place the rectangle in upright (non-flipped) orientation. 433 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) 434 | { 435 | int score = ContactPointScoreNode((int)freeRectangles[i].x, (int)freeRectangles[i].y, width, height); 436 | if (score > bestContactScore) 437 | { 438 | bestNode.x = (int)freeRectangles[i].x; 439 | bestNode.y = (int)freeRectangles[i].y; 440 | bestNode.width = width; 441 | bestNode.height = height; 442 | bestContactScore = score; 443 | } 444 | } 445 | if (allowRotations && freeRectangles[i].width >= height && freeRectangles[i].height >= width) 446 | { 447 | int score = ContactPointScoreNode((int)freeRectangles[i].x, (int)freeRectangles[i].y, height, width); 448 | if (score > bestContactScore) 449 | { 450 | bestNode.x = (int)freeRectangles[i].x; 451 | bestNode.y = (int)freeRectangles[i].y; 452 | bestNode.width = height; 453 | bestNode.height = width; 454 | bestContactScore = score; 455 | } 456 | } 457 | } 458 | return bestNode; 459 | } 460 | 461 | bool SplitFreeNode (Rect freeNode, ref Rect usedNode) 462 | { 463 | // Test with SAT if the rectangles even intersect. 464 | if (usedNode.x >= freeNode.x + freeNode.width || usedNode.x + usedNode.width <= freeNode.x || 465 | usedNode.y >= freeNode.y + freeNode.height || usedNode.y + usedNode.height <= freeNode.y) 466 | return false; 467 | 468 | if (usedNode.x < freeNode.x + freeNode.width && usedNode.x + usedNode.width > freeNode.x) 469 | { 470 | // New node at the top side of the used node. 471 | if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.height) 472 | { 473 | Rect newNode = freeNode; 474 | newNode.height = usedNode.y - newNode.y; 475 | freeRectangles.Add(newNode); 476 | } 477 | 478 | // New node at the bottom side of the used node. 479 | if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) 480 | { 481 | Rect newNode = freeNode; 482 | newNode.y = usedNode.y + usedNode.height; 483 | newNode.height = freeNode.y + freeNode.height - (usedNode.y + usedNode.height); 484 | freeRectangles.Add(newNode); 485 | } 486 | } 487 | 488 | if (usedNode.y < freeNode.y + freeNode.height && usedNode.y + usedNode.height > freeNode.y) 489 | { 490 | // New node at the left side of the used node. 491 | if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width) 492 | { 493 | Rect newNode = freeNode; 494 | newNode.width = usedNode.x - newNode.x; 495 | freeRectangles.Add(newNode); 496 | } 497 | 498 | // New node at the right side of the used node. 499 | if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) 500 | { 501 | Rect newNode = freeNode; 502 | newNode.x = usedNode.x + usedNode.width; 503 | newNode.width = freeNode.x + freeNode.width - (usedNode.x + usedNode.width); 504 | freeRectangles.Add(newNode); 505 | } 506 | } 507 | 508 | return true; 509 | } 510 | 511 | void PruneFreeList () 512 | { 513 | for (int i = 0; i < freeRectangles.Count; ++i) 514 | for (int j = i + 1; j < freeRectangles.Count; ++j) 515 | { 516 | if (IsContainedIn(freeRectangles[i], freeRectangles[j])) 517 | { 518 | freeRectangles.RemoveAt(i); 519 | --i; 520 | break; 521 | } 522 | if (IsContainedIn(freeRectangles[j], freeRectangles[i])) 523 | { 524 | freeRectangles.RemoveAt(j); 525 | --j; 526 | } 527 | } 528 | } 529 | 530 | bool IsContainedIn (Rect a, Rect b) 531 | { 532 | return a.x >= b.x && a.y >= b.y 533 | && a.x + a.width <= b.x + b.width 534 | && a.y + a.height <= b.y + b.height; 535 | } 536 | } -------------------------------------------------------------------------------- /DynamicAtlasses/DynamicAtlas.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | 6 | /// 7 | /// The class responsible for creating atlases in real time. 8 | /// 9 | public abstract class DynamicAtlas 10 | { 11 | #region Constants & Readonly Fields 12 | 13 | /// 14 | /// How many pixels should be around the sprite inside of atlas texture. 15 | /// Sprite is the same size as it's texture, but it have Padding pixels on each of its sides, 16 | /// those pixels are copied from texture borders in order to prevent some strange effects that Bilinear filter mode produces. 17 | /// 18 | protected const float Padding = 1f; 19 | 20 | #endregion 21 | 22 | #region Fields 23 | 24 | private static Material atlasingMaterial; 25 | 26 | // TODO: Change to any implementation of Sprite Sheet you use 27 | private Atlas atlas; 28 | 29 | protected int numberOfUnused; 30 | 31 | /// 32 | /// Helper list made for reducing number of instantiations. 33 | /// 34 | private readonly List textureHelperList = new List(new Texture[] {null}); 35 | 36 | /// 37 | /// Helper list made for reducing number of instantiations. 38 | /// 39 | private readonly List stringHelperList = new List(new string[] {null}); 40 | 41 | private readonly string atlasName; 42 | 43 | #endregion 44 | 45 | #region Properties 46 | 47 | #region Public 48 | 49 | public string AtlasName 50 | { 51 | get { return atlasName; } 52 | } 53 | 54 | /// 55 | /// The maximum width and height of the atlas. Dimensions should be a power of 2 for optimized rendering. 56 | /// 57 | public Vector2 MaxSize { get; protected set; } 58 | 59 | /// 60 | /// The number of valid sprites in the atlas 61 | /// 62 | public int UsedSpriteCount 63 | { 64 | get { return Atlas.SpriteList.Count - numberOfUnused; } 65 | } 66 | 67 | private static Shader Shader 68 | { 69 | get 70 | { 71 | // TODO: Change to any shader you use for sprites 72 | return Shader.Find("Unlit/Texture"); 73 | } 74 | } 75 | 76 | #endregion 77 | 78 | #region Protected 79 | 80 | /// 81 | /// Returns Atlas that holds data about this dynamic atlas. 82 | /// 83 | protected Atlas Atlas 84 | { 85 | get 86 | { 87 | if (atlas == null) 88 | { 89 | // TODO: Instantiate Atlas. If it is a MonoBehaviour you can make them all on one shered game object (DynamicAtlasGO.AddComponent()) 90 | atlas = new Atlas {SpriteMaterial = new Material(Shader)}; 91 | } 92 | return atlas; 93 | } 94 | } 95 | 96 | /// 97 | /// The material used for rendering atlas texture. 98 | /// 99 | protected static Material AtlasingMaterial 100 | { 101 | get 102 | { 103 | if (atlasingMaterial == null) 104 | { 105 | atlasingMaterial = new Material(Shader); 106 | } 107 | return atlasingMaterial; 108 | } 109 | } 110 | 111 | /// 112 | /// Gets/sets texture used by Atlas. 113 | /// 114 | protected Texture AtlasTexture 115 | { 116 | get 117 | { 118 | if (atlas != null) 119 | { 120 | return atlas.Texture; 121 | } 122 | return null; 123 | } 124 | set { Atlas.SpriteMaterial.mainTexture = value; } 125 | } 126 | 127 | /// 128 | /// Gets the size of the current atlas texture. 129 | /// 130 | protected Vector2 TextureSize 131 | { 132 | get { return new Vector2(AtlasTexture.width, AtlasTexture.height); } 133 | } 134 | 135 | #endregion 136 | 137 | #endregion 138 | 139 | #region Constructors 140 | 141 | protected DynamicAtlas(string atlasName, Vector2 maxSize) 142 | { 143 | MaxSize = maxSize; 144 | this.atlasName = atlasName; 145 | } 146 | 147 | #endregion 148 | 149 | #region Public Methods 150 | 151 | public bool IsEmpty() 152 | { 153 | return AtlasTexture == null; 154 | } 155 | 156 | /// 157 | /// Acquires the one texture in atlas. 158 | /// If texture is already in atlas than reference count to texture is increased, 159 | /// and true is returned. 160 | /// 161 | /// True, if texture is in atlas; otherwise, false. 162 | public bool AcquireByName(string name) 163 | { 164 | if (atlas != null && name != null) 165 | { 166 | var spriteData = Atlas.GetSprite(name) as DynamicSpriteData; 167 | if (spriteData != null && !spriteData.SpriteLost) 168 | { 169 | if (!spriteData.IsUsed()) 170 | { 171 | numberOfUnused--; 172 | } 173 | 174 | spriteData.Acquire(); 175 | return true; 176 | } 177 | } 178 | return false; 179 | } 180 | 181 | /// 182 | /// Returns whether specified texture is currently in the atlas. 183 | /// True even if that texture is currently unused, or lost. 184 | /// 185 | public bool ContainsTexture(string name) 186 | { 187 | if (atlas != null && name != null) 188 | { 189 | var spriteData = Atlas.GetSprite(name); 190 | return spriteData != null; 191 | } 192 | return false; 193 | } 194 | 195 | /// 196 | /// Acquires the one texture in atlas. 197 | /// If texture is already in atlas than reference count to texture is increased, 198 | /// if texture is not in atlas than it is added in atlas. 199 | /// Texture is added in atlas if there is enough space in atlas. 200 | /// 201 | /// Texture to be acquired. 202 | /// True, if texture is added or is already in atlas; false, if texture inserting failed. 203 | public bool Acquire(Texture texture) 204 | { 205 | textureHelperList[0] = texture; 206 | bool retValue = Acquire(textureHelperList); 207 | if (textureHelperList.Count == 0) 208 | { 209 | textureHelperList.Add(null); 210 | } 211 | return retValue; 212 | } 213 | 214 | /// 215 | /// Acquires a list of textures to the atlas. 216 | /// If texture is already in atlas than reference count to texture is increased, 217 | /// if texture is not in atlas than it is added in atlas. 218 | /// Texture is added in atlas if there is enough space in atlas. 219 | /// 220 | /// Textures to be acquired. 221 | /// True, if all textures are added or already in atlas; otherwise, false. 222 | public bool Acquire(List textures) 223 | { 224 | if (textures == null) 225 | { 226 | return false; 227 | } 228 | 229 | // We want to increase reference count of textures which are already in atlas 230 | for (int i = 0; i < textures.Count; i++) 231 | { 232 | var data = Atlas.GetSprite(textures[i].name) as DynamicSpriteData; 233 | if (data != null) 234 | { 235 | if (!data.SpriteLost) 236 | { 237 | data.Acquire(); 238 | } 239 | else 240 | { 241 | OverwriteOldTexture(textures[i]); 242 | } 243 | } 244 | } 245 | 246 | // we don't won't either duplicate textures with same name in atlas, or null textures 247 | textures.RemoveAll((texture) => texture == null || Atlas.GetSprite(texture.name) != null); 248 | 249 | if (textures.Count > 0) 250 | { 251 | bool returnVal = AddTextures(textures); 252 | return returnVal; 253 | } 254 | // all textures already in atlas 255 | return true; 256 | } 257 | 258 | /// 259 | /// Releases texture with provided name in atlas if there is such. 260 | /// When the last user releases texture from atlas than it is marked as unused, and can be run over by other texture. 261 | /// 262 | /// Name of the texture that will be released. 263 | public void Release(string textureName) 264 | { 265 | stringHelperList[0] = textureName; 266 | Release(stringHelperList); 267 | } 268 | 269 | /// 270 | /// Releases all textures with provided names in atlas. 271 | /// When the last user releases texture from atlas than it is marked as unused, and can be run over by other texture. 272 | /// Requires: 273 | /// Every name in provided list is unique in that list. 274 | /// 275 | /// Names of textures that will be released. 276 | public void Release(List textureNames) 277 | { 278 | if (textureNames != null && textureNames.Count > 0 && atlas != null) 279 | { 280 | // If sprite's OnDestroy method is called after DynamicAtlasManager's OnApplicationQuit 281 | // calling Atlas property would cause creating new GameObject; Because it was previously deleted from DynamicAtlasManager. 282 | 283 | for (int i = 0; i < textureNames.Count; i++) 284 | { 285 | var spriteData = Atlas.GetSprite(textureNames[i]) as DynamicSpriteData; 286 | if (spriteData != null && spriteData.IsUsed()) 287 | { 288 | spriteData.Release(); 289 | if (!spriteData.IsUsed()) 290 | { 291 | numberOfUnused++; 292 | } 293 | } 294 | } 295 | } 296 | } 297 | 298 | public void OverwriteOldTexture(Texture texture) 299 | { 300 | textureHelperList[0] = texture; 301 | OverwriteOldTextures(textureHelperList); 302 | } 303 | 304 | /// 305 | /// Overwrites textures in atlas by provided textures. 306 | /// Provided textures should have same name as textures from atlas. 307 | /// 308 | public void OverwriteOldTextures(List textures) 309 | { 310 | // Get ready to render 311 | Graphics.SetRenderTarget(AtlasTexture as RenderTexture); 312 | 313 | // Mesh we will write to. 314 | Mesh mesh = new Mesh(); 315 | mesh.MarkDynamic(); 316 | 317 | // Initialize the data we need to fill 318 | Vector3[] positions = new Vector3[4]; 319 | Vector2[] uvs = new Vector2[4]; 320 | int[] triangles = new int[6]; 321 | 322 | for (int i = 0; i < textures.Count; i++) 323 | { 324 | var spriteData = Atlas.GetSprite(textures[i].name) as DynamicSpriteData; 325 | 326 | if (spriteData == null) 327 | { 328 | Debug.LogWarning("Can not find sprite with name: " + textures[i].name + " in atlas while trying to overwrite it"); 329 | continue; 330 | } 331 | 332 | if (!spriteData.IsUsed()) 333 | { 334 | numberOfUnused--; 335 | } 336 | 337 | spriteData.Acquire(); 338 | 339 | if (!spriteData.SpriteLost) 340 | { 341 | // someone already regenerated this sprite 342 | continue; 343 | } 344 | 345 | spriteData.SpriteLost = false; 346 | 347 | SetPosition(new Rect(spriteData.X - Padding, spriteData.Y - Padding, spriteData.Width + Padding, spriteData.Height + Padding), 348 | TextureSize, spriteData, positions, 0); 349 | 350 | // Set the UVs 351 | SetupUvs(uvs, textures[i]); 352 | 353 | // Set the triangles 354 | SetTriangles(0, triangles); 355 | 356 | mesh.Clear(); 357 | // Add the data to the mesh 358 | mesh.vertices = positions; 359 | mesh.uv = uvs; 360 | mesh.triangles = triangles; 361 | 362 | // Set the texture to render to the new atlas 363 | AtlasingMaterial.mainTexture = textures[i]; 364 | // Activate the material 365 | AtlasingMaterial.SetPass(0); 366 | 367 | // Draw the texture to the atlas 368 | Graphics.DrawMeshNow(mesh, Matrix4x4.identity); 369 | MarkTextureForReusage(); 370 | } 371 | 372 | // Cleanup the mesh, otherwise it will leak, unity won't do this on his own 373 | Object.Destroy(mesh); 374 | 375 | // Remove the render target 376 | Graphics.SetRenderTarget(null); 377 | } 378 | 379 | private static void SetupUvs(Vector2[] uvs, Texture texture) 380 | { 381 | uvs[0] = new Vector2(0 - (Padding / texture.width), 1 + (Padding / texture.height)); 382 | uvs[1] = new Vector2(1 + (Padding / texture.width), 1 + (Padding / texture.height)); 383 | uvs[2] = new Vector2(0 - (Padding / texture.width), 0 - (Padding / texture.height)); 384 | uvs[3] = new Vector2(1 + (Padding / texture.width), 0 - (Padding / texture.height)); 385 | } 386 | 387 | /// 388 | /// Marks all textures in atlas as unused. 389 | /// 390 | public void Clear() 391 | { 392 | if (atlas != null) 393 | { 394 | for (int i = 0; i < Atlas.SpriteList.Count; i++) 395 | { 396 | var spriteData = Atlas.SpriteList[i] as DynamicSpriteData; 397 | if (spriteData != null && spriteData.IsUsed()) 398 | { 399 | spriteData.Reset(); 400 | numberOfUnused++; 401 | } 402 | } 403 | } 404 | } 405 | 406 | /// 407 | /// Destroys the atlas and the resources that it uses. 408 | /// 409 | public void Destroy() 410 | { 411 | if (atlas != null) 412 | { 413 | numberOfUnused = 0; 414 | 415 | atlas.ClearAllSpriteData(); 416 | 417 | if (AtlasTexture != null) 418 | { 419 | Object.Destroy(AtlasTexture); 420 | AtlasTexture = null; 421 | } 422 | } 423 | } 424 | 425 | #endregion 426 | 427 | #region Protected Methods 428 | 429 | /// 430 | /// Adds a list of textures to the atlas. 431 | /// Requires: 432 | /// textures list is not null or empty. 433 | /// 434 | protected abstract bool AddTextures(List textures); 435 | 436 | /// 437 | /// Helper function that draws the old atlas texture to the new texture, and destroys the old AtlasTexture. 438 | /// Also updates sprite list of the atlas. 439 | /// If Atlas.spriteList is null or empty doesn't have effect. 440 | /// Requires: 441 | /// AtlasTexture is not null. 442 | /// Render target is set to newTexture. 443 | /// 444 | /// Texture in which to write old atlas. 445 | /// Positions of old textures inside of the new texture, should correspond to order in Atlas.spriteList. 446 | protected void DrawOldAtlas(RenderTexture newTexture, List texturePositions) 447 | { 448 | DrawOldAtlas(newTexture, (i, texture) => texturePositions[i]); 449 | } 450 | 451 | /// 452 | /// Helper function that draws the old atlas texture to the new texture, and destroys the old AtlasTexture. 453 | /// Also updates sprite list of the atlas. 454 | /// If Atlas.spriteList is null or empty doesn't have effect. 455 | /// Requires: 456 | /// AtlasTexture is not null. 457 | /// Render target is set to newTexture. 458 | /// 459 | /// Texture in which to write old atlas. 460 | /// Function that returns position of old texture inside of the new texture, depending on order in Atlas.spriteList. 461 | protected void DrawOldAtlas(RenderTexture newTexture, Func texturePositionFunc) 462 | { 463 | if (Atlas.SpriteList == null || Atlas.SpriteList.Count == 0) 464 | { 465 | AtlasTexture = newTexture; 466 | return; 467 | } 468 | 469 | Vector2 newTextureSize = new Vector2(newTexture.width, newTexture.height); 470 | 471 | // We will use one mesh to draw everything 472 | Mesh mesh = new Mesh(); 473 | 474 | // If we had made the atlas before, we will need to keep the old sprites intact 475 | // We will render the old part of the atlas as a single draw call, so we need some 476 | // variables to do this. 477 | // Initialize the data we need to fill 478 | int numberOfVertices = Atlas.SpriteList.Count * 4; 479 | Vector3[] positions = new Vector3[numberOfVertices]; 480 | Vector2[] uvs = new Vector2[numberOfVertices]; 481 | int[] triangles = new int[Atlas.SpriteList.Count * 6]; 482 | 483 | // Update all old sprites positions, uvs and triangles 484 | for (int index = 0; index < Atlas.SpriteList.Count; index++) 485 | { 486 | // Set the UV coordinates of sprite in old atlas texture 487 | SetUvs(index, uvs); 488 | // Set the positions of vertices and sprite data 489 | // This will change positions in spriteData, SetUvs must be called before 490 | SetPosition(texturePositionFunc(index, newTexture), newTextureSize, Atlas.SpriteList[index], positions, index); 491 | // Set the triangles 492 | SetTriangles(index, triangles); 493 | } 494 | 495 | // Add the data to the mesh 496 | mesh.vertices = positions; 497 | mesh.uv = uvs; 498 | mesh.triangles = triangles; 499 | 500 | // Set the texture to the atlasing material 501 | AtlasingMaterial.mainTexture = AtlasTexture; 502 | // Activate the material 503 | AtlasingMaterial.SetPass(0); 504 | 505 | // Draw the mesh to the new atlas 506 | Graphics.DrawMeshNow(mesh, Matrix4x4.identity); 507 | 508 | Object.Destroy(mesh); 509 | 510 | Object.Destroy(AtlasTexture); 511 | 512 | AtlasTexture = newTexture; 513 | 514 | MarkTextureForReusage(); 515 | } 516 | 517 | /// 518 | /// Draws all new textures on atlas texture and creates/updates list of sprite data. 519 | /// Requires: 520 | /// textures is not null or empty; texturePositions contain position for every texture starting from indexOfFirstTexturePosition. 521 | /// Render target is set to desired texture. 522 | /// 523 | /// Textures to be added to atlas texture. 524 | /// Positions of new textures in AtlasTexture. Positions of new textures to be added should start from index specified by third argument. 525 | /// Index of position of the first new texture in texturePositions list. 526 | /// Whether unused sprites should be swapped with new textures. 527 | protected void DrawNewTextures(List textures, List texturePositions, int indexOfFirstTexturePosition, bool fillUnusedPlaces) 528 | { 529 | DrawNewTextures(textures, (i, texture) => texturePositions[i], indexOfFirstTexturePosition, fillUnusedPlaces); 530 | } 531 | 532 | /// 533 | /// Draws all new textures on atlas texture and creates/updates list of sprite data. 534 | /// Requires: 535 | /// textures is not null or empty; texturePositions contain position for every texture starting from indexOfFirstTexturePosition. 536 | /// Render target is set to desired texture. 537 | /// 538 | /// Textures to be added to atlas texture. 539 | /// This func should return positions of new textures in AtlasTexture and it will be called with index of element and atlas texture. Positions of new textures to be added should start from index specified by third argument. 540 | /// Index of position of first texture in texturePositions list. 541 | /// Whether unused sprites should be swapped with new textures. 542 | protected void DrawNewTextures(List textures, Func texturePositionsFunc, int indexOfFirstTexturePosition, 543 | bool fillUnusedPlaces) 544 | { 545 | // Mesh we will write to. 546 | Mesh mesh = new Mesh(); 547 | mesh.MarkDynamic(); 548 | 549 | // Initialize the data we need to fill 550 | Vector3[] positions = new Vector3[4]; 551 | Vector2[] uvs = new Vector2[4]; 552 | int[] triangles = new int[6]; 553 | 554 | int j = 0; 555 | for (int i = 0; i < textures.Count; i++) 556 | { 557 | DynamicSpriteData spriteData = null; 558 | 559 | if (fillUnusedPlaces) 560 | { 561 | // first fill empty places, if there is no more empty places fill unused places 562 | Rect spritePosition = texturePositionsFunc(i + indexOfFirstTexturePosition, AtlasTexture); 563 | if (spritePosition.xMax > AtlasTexture.width || spritePosition.yMax > AtlasTexture.height) 564 | { 565 | spriteData = FindFirstUnusedSprite(ref j); 566 | } 567 | } 568 | 569 | if (spriteData != null) 570 | { 571 | // unused sprite found 572 | numberOfUnused--; 573 | Atlas.RenameSprite(spriteData.Name, textures[i].name); 574 | SetPosition(texturePositionsFunc(j - 1, AtlasTexture), TextureSize, spriteData, positions, 0); 575 | indexOfFirstTexturePosition--; 576 | } 577 | else 578 | { 579 | spriteData = new DynamicSpriteData(textures[i].name); 580 | Atlas.AddSprite(spriteData); 581 | 582 | // Set the positions, and sprite data. 583 | SetPosition(texturePositionsFunc(i + indexOfFirstTexturePosition, AtlasTexture), TextureSize, spriteData, positions, 0); 584 | } 585 | 586 | spriteData.Reset(); 587 | spriteData.Acquire(); 588 | 589 | // Set the UVs 590 | SetupUvs(uvs, textures[i]); 591 | 592 | // Set the triangles 593 | SetTriangles(0, triangles); 594 | 595 | mesh.Clear(); 596 | // Add the data to the mesh 597 | mesh.vertices = positions; 598 | mesh.uv = uvs; 599 | mesh.triangles = triangles; 600 | 601 | // Set the texture to render to the new atlas 602 | AtlasingMaterial.mainTexture = textures[i]; 603 | // Activate the material 604 | AtlasingMaterial.SetPass(0); 605 | 606 | // Draw the texture to the atlas 607 | Graphics.DrawMeshNow(mesh, Matrix4x4.identity); 608 | MarkTextureForReusage(); 609 | } 610 | 611 | // Cleanup the mesh, otherwise it will leak, unity won't do this on his own 612 | Object.Destroy(mesh); 613 | } 614 | 615 | protected RenderTexture CreateRenderTexture(Vector2 textureSize) 616 | { 617 | return new RenderTexture((int)textureSize.x, (int)textureSize.y, 0, RenderTextureFormat.ARGB32) {name = atlasName}; 618 | } 619 | 620 | #endregion 621 | 622 | #region Private Methods 623 | 624 | /// 625 | /// A helper method that sets the UVs in the UV array based on a sprite data. 626 | /// 627 | /// Index of sprite data in atlas sprite list. 628 | /// Array of uvs that will be filled. 629 | private void SetUvs(int index, Vector2[] uvs) 630 | { 631 | DynamicSpriteData sprite = Atlas.SpriteList[index] as DynamicSpriteData; 632 | if (sprite == null) 633 | { 634 | Debug.LogError("Sprite data in atlas is null or of wrong type."); 635 | return; 636 | } 637 | 638 | // Get the x, y, width and height in UV space 639 | float x = (sprite.X - Padding) / TextureSize.x; 640 | float y = (sprite.Y - Padding) / TextureSize.y; 641 | float w = (sprite.Width + 2f * Padding) / TextureSize.x; 642 | float h = (sprite.Height + 2f * Padding) / TextureSize.y; 643 | 644 | // Apply 645 | uvs[index * 4] = new Vector2(x, 1 - y); 646 | uvs[index * 4 + 1] = new Vector2(x + w, 1 - y); 647 | uvs[index * 4 + 2] = new Vector2(x, 1 - (y + h)); 648 | uvs[index * 4 + 3] = new Vector2(x + w, 1 - (y + h)); 649 | } 650 | 651 | /// 652 | /// A helper method that sets the triangle indices correctly. 653 | /// 654 | /// The index of the quad that has to be written. 655 | /// The array where to write the indices. 656 | private static void SetTriangles(int i, int[] triangles) 657 | { 658 | // The first triangle 659 | triangles[i * 6] = i * 4; 660 | triangles[i * 6 + 1] = i * 4 + 1; 661 | triangles[i * 6 + 2] = i * 4 + 2; 662 | 663 | // The second triangle 664 | triangles[i * 6 + 3] = i * 4 + 1; 665 | triangles[i * 6 + 4] = i * 4 + 3; 666 | triangles[i * 6 + 5] = i * 4 + 2; 667 | } 668 | 669 | /// 670 | /// A helper function that sets the position in the positions array. It also updates sprite position and size. 671 | /// 672 | /// Sprite screen size. 673 | /// Atlas texture size. 674 | /// SpriteData that is used for current texture. 675 | /// Array of positions that will be filled. 676 | /// Index in a positions array for current texture. 677 | private static void SetPosition(Rect texturePosition, Vector2 atlasTextureSize, SpriteData spriteData, Vector3[] positions, int index) 678 | { 679 | Vector2 spriteScreenSize = new Vector2(texturePosition.width, texturePosition.height) * 2f; 680 | spriteScreenSize.InverseScale(atlasTextureSize); 681 | 682 | Vector2 screenPosition = new Vector2(texturePosition.x, texturePosition.y); 683 | screenPosition.InverseScale(atlasTextureSize); 684 | screenPosition = screenPosition * 2f - new Vector2(1, 1); 685 | 686 | // Update sprite data with new position 687 | spriteData.SetRect(texturePosition, Padding); 688 | 689 | // Set the positions in an array 690 | positions[index * 4] = screenPosition; 691 | positions[index * 4 + 1] = screenPosition + new Vector2(spriteScreenSize.x, 0); 692 | positions[index * 4 + 2] = screenPosition + new Vector2(0, spriteScreenSize.y); 693 | positions[index * 4 + 3] = screenPosition + new Vector2(spriteScreenSize.x, spriteScreenSize.y); 694 | } 695 | 696 | /// 697 | /// Returns sprite data of first unused sprite starting from startingIndex. 698 | /// If such is not found returns null. 699 | /// 700 | private DynamicSpriteData FindFirstUnusedSprite(ref int index) 701 | { 702 | while (index < Atlas.SpriteList.Count) 703 | { 704 | var spriteData = Atlas.SpriteList[index] as DynamicSpriteData; 705 | index++; 706 | if (spriteData != null && (!spriteData.IsUsed())) 707 | { 708 | return spriteData; 709 | } 710 | } 711 | return null; 712 | } 713 | 714 | // TODO: This method needs to be triggered on every update (or occasionally) to regenerate texture if it's removed from memory. 715 | /// 716 | /// Checks if render texture is lost, and regenerates atlas's texture if that is case. 717 | /// 718 | public void UpdateHandler() 719 | { 720 | RenderTexture atlasTexture = AtlasTexture as RenderTexture; 721 | if (atlasTexture != null && !atlasTexture.IsCreated()) 722 | { 723 | if (atlas != null) 724 | { 725 | RenderTexture.active = atlasTexture; 726 | GL.Clear(true, true, new Color32(0, 0, 0, 0)); 727 | RenderTexture.active = null; 728 | for (int i = 0; i < atlas.SpriteList.Count; i++) 729 | { 730 | var spriteData = atlas.SpriteList[i] as DynamicSpriteData; 731 | if (spriteData == null) 732 | { 733 | Debug.LogError("Sprite data in atlas is null or of wrong type."); 734 | return; 735 | } 736 | 737 | spriteData.Reset(); 738 | spriteData.SpriteLost = true; 739 | } 740 | numberOfUnused = atlas.SpriteList.Count; 741 | // TODO: Inform Sprites that are using this atlas that texture has been changed 742 | } 743 | 744 | } 745 | } 746 | 747 | /// 748 | /// This will prevent Unity from throwing warning every time texture is written it before it's content was cleared. 749 | /// Because texture is reused multiple time this needs to be called after every write. 750 | /// 751 | private void MarkTextureForReusage() 752 | { 753 | ((RenderTexture)AtlasTexture).MarkRestoreExpected(); 754 | } 755 | 756 | #endregion 757 | } 758 | --------------------------------------------------------------------------------