using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Extensions; using MLEM.Textures; namespace MLEM.Data { /// /// A runtime texture packer provides the user with the ability to combine multiple instances into a single texture. /// Packing textures in this manner allows for faster rendering, as fewer texture swaps are required. /// The resulting texture segments are returned as instances. /// public class RuntimeTexturePacker : IDisposable { private readonly List textures = new List(); private readonly bool autoIncreaseMaxWidth; private readonly bool forcePowerOfTwo; private readonly bool forceSquare; /// /// The generated packed texture. /// This value is null before is called. /// public Texture2D PackedTexture { get; private set; } /// /// The time that it took to calculate the required areas the last time that was called /// public TimeSpan LastCalculationTime { get; private set; } /// /// The time that it took to copy the texture data from the invidiual textures onto the the last time that was called /// public TimeSpan LastPackTime { get; private set; } /// /// The time that took the last time it was called /// public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime; private int maxWidth; /// /// 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 public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false) { this.maxWidth = maxWidth; this.autoIncreaseMaxWidth = autoIncreaseMaxWidth; this.forcePowerOfTwo = forcePowerOfTwo; this.forceSquare = forceSquare; } /// /// Adds a new texture 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); } /// /// 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) { if (this.PackedTexture != null) throw new InvalidOperationException("Cannot add texture to a texture packer that is already packed"); if (texture.Width > this.maxWidth) { if (this.autoIncreaseMaxWidth) { this.maxWidth = texture.Width; } else { 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)); } /// /// 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. /// /// 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 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; } stopwatch.Stop(); this.LastCalculationTime = stopwatch.Elapsed; // figure out texture size and generate texture var width = this.textures.Max(t => t.PackedArea.Right); var height = this.textures.Max(t => t.PackedArea.Bottom); if (this.forcePowerOfTwo) { width = ToPowerOfTwo(width); height = ToPowerOfTwo(height); } if (this.forceSquare) width = height = Math.Max(width, height); this.PackedTexture = new Texture2D(device, width, height); // copy texture data onto the packed texture stopwatch.Restart(); using (var data = this.PackedTexture.GetTextureData()) { foreach (var request in this.textures) 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)); this.textures.Clear(); } /// /// Resets this texture packer, disposing its and readying it to be re-used /// public void Reset() { this.PackedTexture?.Dispose(); this.PackedTexture = null; this.textures.Clear(); this.LastCalculationTime = TimeSpan.Zero; this.LastPackTime = TimeSpan.Zero; } /// public void Dispose() { this.Reset(); } private Rectangle FindFreeArea(Point size) { var pos = new Point(0, 0); var lowestY = int.MaxValue; while (true) { var intersected = false; var area = new Rectangle(pos, size); foreach (var tex in this.textures) { 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 if (lowestY > tex.PackedArea.Bottom) lowestY = tex.PackedArea.Bottom; intersected = true; break; } } if (!intersected) return area; if (pos.X + size.X > this.maxWidth) { pos.X = 0; pos.Y = lowestY; lowestY = int.MaxValue; } } } private static void CopyRegion(TextureExtensions.TextureData destination, Request request) { 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]; } } } } private static int ToPowerOfTwo(int value) { var ret = 1; while (ret < value) ret <<= 1; return ret; } private class Request { public readonly TextureRegion Texture; public readonly Action Result; public Rectangle PackedArea; public Request(TextureRegion texture, Action result) { this.Texture = texture; this.Result = result; } } } }