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) {