From f0f1d7f8edc9610ab713dbff892747f2d96deeda Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Wed, 25 May 2022 12:37:51 +0200 Subject: [PATCH] added runtime texture region padding and other improvements --- CHANGELOG.md | 5 ++ MLEM.Data/DataTextureAtlas.cs | 6 +- MLEM.Data/RuntimeTexturePacker.cs | 120 ++++++++++++++++++++++-------- Sandbox/GameImpl.cs | 33 ++++++++ 4 files changed, 134 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1ce01..fb66776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,8 +49,13 @@ Removals - Marked Tooltip.Paragraph as obsolete in favor of new Paragraphs collection ### MLEM.Data +Additions +- Added the ability to add padding to RuntimeTexturePacker texture regions +- Added the ability to pack UniformTextureAtlas and DataTextureAtlas using RuntimeTexturePacker + Improvements - Premultiply textures when using RawContentManager +- Allow enumerating all region names of a DataTextureAtlas Fixes - Fixed SoundEffectReader incorrectly claiming it could read ogg and mp3 files diff --git a/MLEM.Data/DataTextureAtlas.cs b/MLEM.Data/DataTextureAtlas.cs index 2bac052..839b930 100644 --- a/MLEM.Data/DataTextureAtlas.cs +++ b/MLEM.Data/DataTextureAtlas.cs @@ -50,9 +50,13 @@ namespace MLEM.Data { } } /// - /// Returns an enumerable of all of the s in this atlas. + /// Returns an enumerable of all of the values in this atlas. /// public IEnumerable Regions => this.regions.Values; + /// + /// Returns an enumerable of all of the names in this atlas. + /// + public IEnumerable RegionNames => this.regions.Keys; private readonly Dictionary regions = new Dictionary(); diff --git a/MLEM.Data/RuntimeTexturePacker.cs b/MLEM.Data/RuntimeTexturePacker.cs index a8fc79c..d2ebe14 100644 --- a/MLEM.Data/RuntimeTexturePacker.cs +++ b/MLEM.Data/RuntimeTexturePacker.cs @@ -38,43 +38,93 @@ namespace MLEM.Data { private readonly bool forcePowerOfTwo; private readonly bool forceSquare; private readonly bool disposeTextures; + private readonly int padding; private int maxWidth; /// - /// Creates a new runtime texture packer with the given settings + /// Creates a new runtime texture packer with the given settings. /// /// The maximum width that the packed texture can have. Defaults to 2048. /// Whether the maximum width should be increased if there is a texture to be packed that is wider than . Defaults to false. - /// Whether the resulting should have a width and height that is a power of two - /// Whether the resulting should be square regardless of required size - /// Whether the original textures submitted to this texture packer should be disposed after packing - public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false) { + /// Whether the resulting should have a width and height that is a power of two. + /// Whether the resulting should be square regardless of required size. + /// Whether the original textures submitted to this texture packer should be disposed after packing. + /// The padding that each texture region should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding. + public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false, int padding = default) { this.maxWidth = maxWidth; this.autoIncreaseMaxWidth = autoIncreaseMaxWidth; this.forcePowerOfTwo = forcePowerOfTwo; this.forceSquare = forceSquare; this.disposeTextures = disposeTextures; + this.padding = padding; } /// - /// Adds a new texture to this texture packer to be packed. + /// Adds a new to this texture packer to be packed. + /// The passed is invoked in and provides the caller with the resulting dictionary of texture regions on the , mapped to their x and y positions on the original . + /// Note that the resulting data cannot be converted back into a , since the resulting texture regions might be scattered throughout the . + /// + /// The texture atlas to pack. + /// The result callback which will receive the resulting texture regions. + /// Whether the texture packer's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if the texture packer was constructed with a padding. + /// 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. + public void Add(UniformTextureAtlas atlas, Action> result, bool padWithPixels = false) { + var resultRegions = new Dictionary(); + for (var x = 0; x < atlas.RegionAmountX; x++) { + for (var y = 0; y < atlas.RegionAmountY; y++) { + var pos = new Point(x, y); + this.Add(atlas[pos], r => { + resultRegions.Add(pos, r); + if (resultRegions.Count >= atlas.RegionAmountX * atlas.RegionAmountY) + result.Invoke(resultRegions); + }, padWithPixels); + } + } + } + + /// + /// Adds a new to this texture packer to be packed. + /// The passed is invoked in and provides the caller with the resulting dictionary of texture regions on the , mapped to their name on the original . + /// Note that the resulting data cannot be converted back into a , since the resulting texture regions might be scattered throughout the . + /// + /// The texture atlas to pack. + /// The result callback which will receive the resulting texture regions. + /// Whether the texture packer's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if the texture packer was constructed with a padding. + /// 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. + public void Add(DataTextureAtlas atlas, Action> result, bool padWithPixels = false) { + var atlasRegions = atlas.RegionNames.ToArray(); + var resultRegions = new Dictionary(); + foreach (var region in atlasRegions) { + this.Add(atlas[region], r => { + resultRegions.Add(region, r); + if (resultRegions.Count >= atlasRegions.Length) + result.Invoke(resultRegions); + }, padWithPixels); + } + } + + /// + /// Adds a new to this texture packer to be packed. /// The passed is invoked in and provides the caller with the resulting texture region on the . /// - /// The texture to pack - /// The result callback which will receive the resulting texture region - public void Add(Texture2D texture, Action result) { - this.Add(new TextureRegion(texture), result); + /// The texture to pack. + /// The result callback which will receive the resulting texture region. + /// Whether the texture packer's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if the texture packer was constructed with a padding. + /// 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. + public void Add(Texture2D texture, Action result, bool padWithPixels = false) { + this.Add(new TextureRegion(texture), result, padWithPixels); } /// /// Adds a new to this texture packer to be packed. /// The passed is invoked in and provides the caller with the resulting texture region on the . /// - /// The texture to pack - /// The result callback which will receive the resulting texture region - /// 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 - public void Add(TextureRegion texture, Action result) { + /// The texture region to pack. + /// The result callback which will receive the resulting texture region. + /// Whether the texture packer's padding should be filled with a copy of the texture's border, rather than transparent pixels. This value only has an effect if the texture packer was constructed with a padding. + /// 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. + public void Add(TextureRegion texture, Action result, bool padWithPixels = false) { if (this.PackedTexture != null) throw new InvalidOperationException("Cannot add texture to a texture packer that is already packed"); if (texture.Width > this.maxWidth) { @@ -84,11 +134,11 @@ namespace MLEM.Data { throw new InvalidOperationException($"Cannot add texture with width {texture.Width} to a texture packer with max width {this.maxWidth}"); } } - this.textures.Add(new Request(texture, result)); + this.textures.Add(new Request(texture, result, padWithPixels)); } /// - /// Packs all of the textures and texture regions added using into one texture. + /// Packs all of the textures and texture regions added using into one texture. /// The resulting texture will be stored in . /// All of the result callbacks that were added will also be invoked. /// @@ -100,10 +150,8 @@ namespace MLEM.Data { // set pack areas for each request var stopwatch = Stopwatch.StartNew(); - foreach (var request in this.textures.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) { - var area = this.FindFreeArea(new Point(request.Texture.Width, request.Texture.Height)); - request.PackedArea = area; - } + foreach (var request in this.textures.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) + request.PackedArea = this.FindFreeArea(new Point(request.Texture.Width, request.Texture.Height)); stopwatch.Stop(); this.LastCalculationTime = stopwatch.Elapsed; @@ -122,14 +170,15 @@ namespace MLEM.Data { stopwatch.Restart(); using (var data = this.PackedTexture.GetTextureData()) { foreach (var request in this.textures) - CopyRegion(data, request); + this.CopyRegion(data, request); } stopwatch.Stop(); this.LastPackTime = stopwatch.Elapsed; // invoke callbacks foreach (var request in this.textures) { - request.Result.Invoke(new TextureRegion(this.PackedTexture, request.PackedArea)); + var packedArea = request.PackedArea.Shrink(new Point(this.padding)); + request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea)); if (this.disposeTextures) request.Texture.Texture.Dispose(); } @@ -154,6 +203,9 @@ namespace MLEM.Data { } private Rectangle FindFreeArea(Point size) { + size.X += this.padding * 2; + size.Y += this.padding * 2; + var pos = new Point(0, 0); var lowestY = int.MaxValue; while (true) { @@ -179,13 +231,21 @@ namespace MLEM.Data { } } - private static void CopyRegion(TextureExtensions.TextureData destination, Request request) { + private void CopyRegion(TextureExtensions.TextureData destination, Request request) { + var location = request.PackedArea.Location + new Point(this.padding); using (var data = request.Texture.Texture.GetTextureData()) { - for (var x = 0; x < request.Texture.Width; x++) { - for (var y = 0; y < request.Texture.Height; y++) { - var dest = request.PackedArea.Location + new Point(x, y); - var src = request.Texture.Position + new Point(x, y); - destination[dest] = data[src]; + for (var x = -this.padding; x < request.Texture.Width + this.padding; x++) { + for (var y = -this.padding; y < request.Texture.Height + this.padding; y++) { + Color srcColor; + if (!request.PadWithPixels && (x < 0 || y < 0 || x >= request.Texture.Width || y >= request.Texture.Height)) { + // if we're out of bounds and not padding with pixels, we make it transparent + srcColor = Color.Transparent; + } else { + // otherwise, we just use the closest pixel that is actually in bounds, causing the border pixels to be doubled up + var src = new Point(MathHelper.Clamp(x, 0, request.Texture.Width - 1), MathHelper.Clamp(y, 0, request.Texture.Height - 1)); + srcColor = data[request.Texture.Position + src]; + } + destination[location + new Point(x, y)] = srcColor; } } } @@ -202,11 +262,13 @@ namespace MLEM.Data { public readonly TextureRegion Texture; public readonly Action Result; + public readonly bool PadWithPixels; public Rectangle PackedArea; - public Request(TextureRegion texture, Action result) { + public Request(TextureRegion texture, Action result, bool padWithPixels) { this.Texture = texture; this.Result = result; + this.PadWithPixels = padWithPixels; } } diff --git a/Sandbox/GameImpl.cs b/Sandbox/GameImpl.cs index 201e9e1..8aeed78 100644 --- a/Sandbox/GameImpl.cs +++ b/Sandbox/GameImpl.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using FontStashSharp; using Microsoft.Xna.Framework; @@ -21,6 +23,7 @@ using MLEM.Textures; using MLEM.Ui; using MLEM.Ui.Elements; using MLEM.Ui.Style; +using MonoGame.Extended; using MonoGame.Extended.Tiled; using MonoGame.Extended.ViewportAdapters; @@ -314,6 +317,36 @@ namespace Sandbox { button.Size = new Vector2(30, 50); } this.UiSystem.Add("Keybinds", keybindPanel); + + var packer = new RuntimeTexturePacker(padding: 1); + var regions = new List(); + packer.Add(new UniformTextureAtlas(tex, 16, 16), r => { + regions.AddRange(r.Values); + Console.WriteLine($"Returned {r.Count} regions: {string.Join(", ", r.Select(kv => kv.Key + ": " + kv.Value.Area))}"); + }, true); + packer.Add(this.Content.LoadTextureAtlas("Textures/Furniture"), r => { + regions.AddRange(r.Values); + Console.WriteLine($"Returned {r.Count} regions: {string.Join(", ", r.Select(kv => kv.Key + ": " + kv.Value.Area))}"); + }, true); + packer.Pack(this.GraphicsDevice); + packer.PackedTexture.SaveAsPng(File.Create("_Packed.png"), packer.PackedTexture.Width, packer.PackedTexture.Height); + + this.OnDraw += (g, t) => { + this.SpriteBatch.Begin(samplerState: SamplerState.PointClamp); + var x = 0; + var y = 10; + foreach (var r in regions) { + const int sc = 5; + this.SpriteBatch.DrawRectangle(new Vector2(x, y), new Vector2(r.Width * sc, r.Height * sc), Color.Green); + this.SpriteBatch.Draw(r, new Vector2(x, y), Color.White, 0, Vector2.Zero, sc, SpriteEffects.None, 0); + x += r.Width * sc + 1; + if (x >= 1000) { + x = 0; + y += 50; + } + } + this.SpriteBatch.End(); + }; } protected override void DoUpdate(GameTime gameTime) {