using UnityEngine; using UnityEngine.U2D; using Unity.Collections; using Unity.Mathematics; using System.Collections.Generic; using System.Linq; using UnityEngine.Assertions; namespace UnityEngine.U2D.Common.Samples { public static class SpriteAtlasCommon { /// /// Internal class to represent a sprite's position and size in an atlas. /// internal class AtlasFit { public int2 position; public int2 size; public int index; }; /// /// Internal class to manage sprite packing data for a single atlas page. /// internal class AtlasPageData { public List packs; public int2 size; public int2 filled; public void Pack(AtlasFit atlasFit) { filled.x = filled.x > (atlasFit.position.x + atlasFit.size.x) ? filled.x : atlasFit.position.x + atlasFit.size.x; filled.y = filled.y > (atlasFit.position.y + atlasFit.size.y) ? filled.y : atlasFit.position.y + atlasFit.size.y; packs.Add(atlasFit); } }; /// /// Resizes a texture to specified dimensions using a temporary render texture with trilinear filtering. /// /// Texture to resize /// New width of the texture /// New height of the texture internal static void Scale(Texture2D tex, int width, int height) { Rect texR = new(0, 0, width, height); Blitter(tex, width, height); tex.Reinitialize(width, height); tex.ReadPixels(texR, 0, 0, true); tex.Apply(true); } /// /// Copies source texture into render texture with trilinear filtering for smooth scaling. /// /// Source texture to copy /// Width of the render texture /// Height of the render texture internal static void Blitter(Texture2D src, int width, int height) { src.filterMode = FilterMode.Trilinear; src.Apply(true); RenderTexture rtt = new(width, height, 32); Graphics.SetRenderTarget(rtt); GL.LoadPixelMatrix(0, 1, 1, 0); GL.Clear(true, true, new Color(0, 0, 0, 0)); Graphics.DrawTexture(new Rect(0, 0, 1, 1), src); } /// /// Copies sprite texture from source to atlas with boundary handling. /// /// Destination atlas texture /// Source sprite texture /// Source sprite X coordinate /// Source sprite Y coordinate /// Width of the sprite /// Height of the sprite /// Destination X coordinate /// Destination Y coordinate internal static void BlitSpriteToAtlas(Texture2D dstAtlas, Texture2D srcTexture, int sx, int sy, int width, int height, int dx, int dy) { dstAtlas.CopyPixels(srcTexture, 0, 0, sx, sy, width, height, 0, dx, dy); dstAtlas.CopyPixels(srcTexture, 0, 0, sx, sy, width, 1, 0, dx, dy); dstAtlas.CopyPixels(srcTexture, 0, 0, sx, sy, 1, height, 0, dx, dy); dstAtlas.CopyPixels(srcTexture, 0, 0, sx + width - 1, sy, 1, height - 1, 0, dx + width + 1, dy + 1); dstAtlas.CopyPixels(srcTexture, 0, 0, sx, sy + height - 1, width - 1, 1, 0, dx + 1, dy + height + 1); } /// /// Validates if two rectangles are non-overlapping for proper sprite placement. /// /// Sprite rectangle /// Atlas page rectangle /// True if rectangles are non-overlapping, false otherwise internal static bool CheckValid(Rect rect, Rect atlasRect) { return (rect.x > (atlasRect.x + atlasRect.width) || rect.y > (atlasRect.y + atlasRect.height) || atlasRect.x > (rect.x + rect.width) || atlasRect.y > (rect.y + rect.height)); } /// /// Attempts to place sprite in atlas page using random positions with collision checks. /// /// Sprite to place /// Atlas page to place in /// Number of random position attempts /// Valid position (x,y) if found, otherwise (-1,-1) internal static int2 FitSprite(AtlasFit atlasFit, AtlasPageData atlasPage, int randomFitTries) { var fitPosition = new int2(-1, -1); var fitFound = false; var filled = atlasPage.filled; for (int test = 0; test < randomFitTries; ++test) { var tx = Random.Range(0, filled.x); var ty = Random.Range(0, filled.y); if (tx + atlasFit.size.x > atlasPage.size.x || ty + atlasFit.size.y > atlasPage.size.y) { continue; } var fit = true; for (int check = 0; check < atlasPage.packs.Count; ++check) { var packed = atlasPage.packs[check]; var RectSprite = new Rect(tx, ty, atlasFit.size.x, atlasFit.size.y); var PackSprite = new Rect(packed.position.x, packed.position.y, packed.size.x, packed.size.y); if (!CheckValid(RectSprite,PackSprite)) { fit = false; break; } } if (fit) { fitFound = true; filled.x = fitPosition.x = tx; filled.y = fitPosition.y = ty; } } if (fitFound) { return fitPosition; } if (atlasFit.size.x < atlasPage.size.x - filled.x && atlasFit.size.y < atlasPage.size.y - filled.y) { fitPosition.x = filled.x; fitPosition.y = filled.y; } return fitPosition; } /// /// Packs sprite into available atlas pages with overflow handling. /// /// Sprite to pack /// List of atlas pages /// Number of random position attempts /// Total number of sprites /// Maximum size of an atlas page internal static void PackSprite(AtlasFit atlasFit, List atlasPages, int randomFitTries, int inputSpriteCount, int maxTextureSize) { var fit = false; foreach (var atlasPage in atlasPages) { var atlasPageData = atlasPage; var fitPosition = FitSprite(atlasFit, atlasPage, randomFitTries); if (fitPosition.x != -1) { atlasFit.position = fitPosition; atlasPage.Pack(atlasFit); fit = true; } } if (!fit) { var atlasPage = new AtlasPageData(); atlasPage.packs = new List(); atlasPage.size = new int2(maxTextureSize, maxTextureSize); atlasFit.position = new int2(0, 0); atlasPage.Pack(atlasFit); atlasPages.Add(atlasPage); } } /// /// Creates a sprite atlas at runtime by packing sprites into optimized texture pages and generating a final atlas texture with specified dimensions and scaling. /// /// The name of the atlas to create /// Array of sprites to pack into the atlas /// Texture format for the atlas /// Max Texture Width /// Max Texture Height /// Atlas Scale Multiplier public static void CreateSpriteAtlasAtRuntimeDemo(string nameTag, Sprite[] inputSprites, TextureFormat fmt, int width, int height, float multiplier) { var spriteCount = inputSprites.Length; var atlasPages = new List(); var spriteIndex = 0; inputSprites = inputSprites.OrderBy(x => x.textureRect.width * x.textureRect.height).Reverse().ToArray(); // Pack First foreach (var sprite in inputSprites) { if (null != sprite && null != sprite.texture) { var atlasFit = new AtlasFit(); atlasFit.index = spriteIndex; atlasFit.size = new int2((int)sprite.rect.size.x, (int)sprite.rect.size.y); PackSprite(atlasFit, atlasPages, 128, spriteCount, 1024); } spriteIndex++; } var atlasPacked = new AtlasPage[atlasPages.Count]; var pageIndex = 0; // Create AtlasPages. foreach (var inputAtlasPage in atlasPages) { var atlasPage = new AtlasPage(); var packedTexture = new Texture2D(width, height, fmt, false); packedTexture.name = nameTag; atlasPage.assets = new ObjectData[inputAtlasPage.packs.Count]; for (int i = 0; i < inputAtlasPage.packs.Count; ++i) { var atlasFit = inputAtlasPage.packs[i]; var srcRect = inputSprites[atlasFit.index].textureRect; BlitSpriteToAtlas(packedTexture, inputSprites[atlasFit.index].texture, (int)srcRect.x, (int)srcRect.y, (int)srcRect.width, (int)srcRect.height, atlasFit.position.x, atlasFit.position.y); var objectData = new ObjectData(); objectData.asset = inputSprites[atlasFit.index].GetEntityId(); objectData.packInfo = new Vector4(atlasFit.position.x, atlasFit.position.y, 0, 0); atlasPage.assets[i] = objectData; } var textureData = new TextureData[1]; Scale(packedTexture, (int)((float)packedTexture.width * multiplier), (int)((float)packedTexture.height * multiplier)); textureData[0].texture = packedTexture.GetEntityId(); textureData[0].mapName = "_MainTex"; atlasPage.packedTextures = textureData; atlasPacked[pageIndex++] = atlasPage; } var config = new SpriteAtlasRuntimeConfig(); config.scaleMultiplier = multiplier; SpriteAtlasManager.CreateSpriteAtlas(nameTag, config, atlasPacked); } } }