From 17ce7b668db2989d1c28b4e4945aed4823c397ea Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Thu, 20 Oct 2022 23:59:42 +0200 Subject: [PATCH] Added the ability to add additional regions to a RuntimeTexturePacker after packing --- CHANGELOG.md | 1 + MLEM.Data/RuntimeTexturePacker.cs | 69 +++++++++++++++---------------- Tests/TexturePackerTests.cs | 23 +++++++++++ 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b90597..d23f682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Fixes ### MLEM.Data Additions - Added data, from, and copy instructions to DataTextureAtlas +- Added the ability to add additional regions to a RuntimeTexturePacker after packing Improvements - Allow data texture atlas pivots and offsets to be negative diff --git a/MLEM.Data/RuntimeTexturePacker.cs b/MLEM.Data/RuntimeTexturePacker.cs index 7f47435..4745839 100644 --- a/MLEM.Data/RuntimeTexturePacker.cs +++ b/MLEM.Data/RuntimeTexturePacker.cs @@ -35,8 +35,8 @@ namespace MLEM.Data { public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime; private readonly List texturesToPack = new List(); - private readonly List alreadyPackedTextures = new List(); - private readonly Dictionary firstPossiblePosForSizeCache = new Dictionary(); + private readonly List packedTextures = new List(); + private readonly Dictionary firstPossiblePosForSize = new Dictionary(); private readonly Dictionary dataCache = new Dictionary(); private readonly bool autoIncreaseMaxWidth; private readonly bool forcePowerOfTwo; @@ -71,7 +71,7 @@ namespace MLEM.Data { /// The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. /// Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if is greater than 0. /// Whether completely transparent texture regions in the should be ignored. If this is true, they will not be part of the collection either. - /// Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width. + /// Thrown when trying to add a texture width a width greater than the defined max width. public void Add(UniformTextureAtlas atlas, Action> result, int padding = 0, bool padWithPixels = false, bool ignoreTransparent = false) { var addedRegions = new List(); var resultRegions = new Dictionary(); @@ -104,7 +104,7 @@ namespace MLEM.Data { /// The result callback which will receive the resulting texture regions. /// The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. /// Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if is greater than 0. - /// Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width. + /// Thrown when trying to add a texture width a width greater than the defined max width. public void Add(DataTextureAtlas atlas, Action> result, int padding = 0, bool padWithPixels = false) { var atlasRegions = atlas.RegionNames.ToArray(); var resultRegions = new Dictionary(); @@ -125,7 +125,7 @@ namespace MLEM.Data { /// The result callback which will receive the resulting texture region. /// The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. /// Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if is greater than 0. - /// Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width. + /// Thrown when trying to add a texture width a width greater than the defined max width. public void Add(Texture2D texture, Action result, int padding = 0, bool padWithPixels = false) { this.Add(new TextureRegion(texture), result, padding, padWithPixels); } @@ -138,10 +138,8 @@ namespace MLEM.Data { /// The result callback which will receive the resulting texture region. /// The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. /// Whether the texture's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if is greater than 0. - /// Thrown when trying to add data to a packer that has already been packed, or when trying to add a texture width a width greater than the defined max width. + /// Thrown when trying to add a texture width a width greater than the defined max width. public void Add(TextureRegion texture, Action result, int padding = 0, bool padWithPixels = false) { - if (this.PackedTexture != null) - throw new InvalidOperationException("Cannot add texture to a texture packer that is already packed"); var paddedWidth = texture.Width + 2 * padding; if (paddedWidth > this.maxWidth) { if (this.autoIncreaseMaxWidth) { @@ -154,50 +152,54 @@ namespace MLEM.Data { } /// - /// Packs all of the textures and texture regions added using into one texture. - /// The resulting texture will be stored in . + /// Packs all of the textures and texture regions added using into one texture, which can be retrieved using . /// All of the result callbacks that were added will also be invoked. + /// This method can be called multiple times if regions are added after has already been called. When doing so, result callbacks of previous regions may be invoked again if the resulting has to be resized to accommodate newly added regions. /// /// The graphics device to use for texture generation - /// Thrown when calling this method on a texture packer that has already been packed public void Pack(GraphicsDevice device) { - if (this.PackedTexture != null) - throw new InvalidOperationException("Cannot pack a texture packer that is already packed"); - // set pack areas for each request // we pack larger textures first, so that smaller textures can fit in the gaps that larger ones leave var stopwatch = Stopwatch.StartNew(); foreach (var request in this.texturesToPack.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) { request.PackedArea = this.FindFreeArea(request); // if this is the first position that this request fit in, no other requests of the same size will find a position before it - this.firstPossiblePosForSizeCache[new Point(request.PackedArea.Width, request.PackedArea.Height)] = request.PackedArea.Location; - this.alreadyPackedTextures.Add(request); + this.firstPossiblePosForSize[new Point(request.PackedArea.Width, request.PackedArea.Height)] = request.PackedArea.Location; + this.packedTextures.Add(request); } stopwatch.Stop(); this.LastCalculationTime = stopwatch.Elapsed; - // figure out texture size and generate texture - var width = this.alreadyPackedTextures.Max(t => t.PackedArea.Right); - var height = this.alreadyPackedTextures.Max(t => t.PackedArea.Bottom); + // figure out texture size and regenerate texture if necessary + var width = this.packedTextures.Max(t => t.PackedArea.Right); + var height = this.packedTextures.Max(t => t.PackedArea.Bottom); if (this.forcePowerOfTwo) { width = RuntimeTexturePacker.ToPowerOfTwo(width); height = RuntimeTexturePacker.ToPowerOfTwo(height); } if (this.forceSquare) width = height = Math.Max(width, height); - this.PackedTexture = new Texture2D(device, width, height); + + // if we don't need to regenerate, we only need to add newly added regions + IEnumerable texturesToCopy = this.texturesToPack; + if (this.PackedTexture == null || this.PackedTexture.Width != width || this.PackedTexture.Height != height) { + this.PackedTexture?.Dispose(); + this.PackedTexture = new Texture2D(device, width, height); + // if we need to regenerate, we need to copy all regions since the old ones were deleted + texturesToCopy = this.packedTextures; + } // copy texture data onto the packed texture stopwatch.Restart(); using (var data = this.PackedTexture.GetTextureData()) { - foreach (var request in this.alreadyPackedTextures) + foreach (var request in texturesToCopy) this.CopyRegion(data, request); } stopwatch.Stop(); this.LastPackTime = stopwatch.Elapsed; - // invoke callbacks - foreach (var request in this.alreadyPackedTextures) { + // invoke callbacks for textures we copied + foreach (var request in texturesToCopy) { var packedArea = request.PackedArea.Shrink(new Point(request.Padding, request.Padding)); request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea) { Pivot = request.Texture.Pivot, @@ -207,18 +209,22 @@ namespace MLEM.Data { request.Texture.Texture.Dispose(); } - this.ClearTempCollections(); + this.texturesToPack.Clear(); + this.dataCache.Clear(); } /// - /// Resets this texture packer, disposing its and readying it to be re-used + /// Resets this texture packer entirely, disposing its , clearing all previously added requests, and readying it to be re-used. /// public void Reset() { this.PackedTexture?.Dispose(); this.PackedTexture = null; this.LastCalculationTime = TimeSpan.Zero; this.LastPackTime = TimeSpan.Zero; - this.ClearTempCollections(); + this.texturesToPack.Clear(); + this.packedTextures.Clear(); + this.firstPossiblePosForSize.Clear(); + this.dataCache.Clear(); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -231,12 +237,12 @@ namespace MLEM.Data { size.X += request.Padding * 2; size.Y += request.Padding * 2; - var pos = this.firstPossiblePosForSizeCache.TryGetValue(size, out var first) ? first : Point.Zero; + var pos = this.firstPossiblePosForSize.TryGetValue(size, out var first) ? first : Point.Zero; var lowestY = int.MaxValue; while (true) { var intersected = false; var area = new Rectangle(pos.X, pos.Y, size.X, size.Y); - foreach (var tex in this.alreadyPackedTextures) { + foreach (var tex in this.packedTextures) { if (tex.PackedArea.Intersects(area)) { pos.X = tex.PackedArea.Right; // when we move down, we want to move down by the smallest intersecting texture's height @@ -296,13 +302,6 @@ namespace MLEM.Data { return true; } - private void ClearTempCollections() { - this.texturesToPack.Clear(); - this.alreadyPackedTextures.Clear(); - this.firstPossiblePosForSizeCache.Clear(); - this.dataCache.Clear(); - } - private static int ToPowerOfTwo(int value) { var ret = 1; while (ret < value) diff --git a/Tests/TexturePackerTests.cs b/Tests/TexturePackerTests.cs index 1779cf7..49fd01d 100644 --- a/Tests/TexturePackerTests.cs +++ b/Tests/TexturePackerTests.cs @@ -80,6 +80,29 @@ namespace Tests { Assert.AreEqual(170, packer4.PackedTexture.Height); } + [Test] + public void TestPackMultipleTimes() { + using var packer = new RuntimeTexturePacker(1024); + + // pack the first time + var results = 0; + for (var i = 0; i < 10; i++) + packer.Add(new TextureRegion(this.testTexture, 0, 0, 64, 64), _ => results++); + packer.Pack(this.game.GraphicsDevice); + Assert.AreEqual(10, results); + + // pack without resizing + packer.Add(new TextureRegion(this.testTexture, 0, 0, 0, 0), _ => results++); + packer.Pack(this.game.GraphicsDevice); + Assert.AreEqual(11, results); + + // pack and force a resize + packer.Add(new TextureRegion(this.testTexture, 0, 0, 64, 64), _ => results++); + packer.Pack(this.game.GraphicsDevice); + // all callbacks are called again, so we add 11 again, as well as the callback we just added + Assert.AreEqual(2 * 11 + 1, results); + } + private static void StubResult(TextureRegion region) {} }