mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-25 05:58:35 +01:00
Made RuntimeTexturePacker padding be per request and improve performance by caching texture data
This commit is contained in:
parent
f0f1d7f8ed
commit
b9f2de8290
3 changed files with 58 additions and 43 deletions
|
@ -56,6 +56,7 @@ Additions
|
||||||
Improvements
|
Improvements
|
||||||
- Premultiply textures when using RawContentManager
|
- Premultiply textures when using RawContentManager
|
||||||
- Allow enumerating all region names of a DataTextureAtlas
|
- Allow enumerating all region names of a DataTextureAtlas
|
||||||
|
- Cache RuntimeTexturePacker texture data while packing to improve performance
|
||||||
|
|
||||||
Fixes
|
Fixes
|
||||||
- Fixed SoundEffectReader incorrectly claiming it could read ogg and mp3 files
|
- Fixed SoundEffectReader incorrectly claiming it could read ogg and mp3 files
|
||||||
|
|
|
@ -6,6 +6,7 @@ using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
using MLEM.Extensions;
|
using MLEM.Extensions;
|
||||||
using MLEM.Textures;
|
using MLEM.Textures;
|
||||||
|
using static MLEM.Extensions.TextureExtensions;
|
||||||
|
|
||||||
namespace MLEM.Data {
|
namespace MLEM.Data {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -34,11 +35,11 @@ namespace MLEM.Data {
|
||||||
public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime;
|
public TimeSpan LastTotalTime => this.LastCalculationTime + this.LastPackTime;
|
||||||
|
|
||||||
private readonly List<Request> textures = new List<Request>();
|
private readonly List<Request> textures = new List<Request>();
|
||||||
|
private readonly Dictionary<Texture2D, TextureData> dataCache = new Dictionary<Texture2D, TextureData>();
|
||||||
private readonly bool autoIncreaseMaxWidth;
|
private readonly bool autoIncreaseMaxWidth;
|
||||||
private readonly bool forcePowerOfTwo;
|
private readonly bool forcePowerOfTwo;
|
||||||
private readonly bool forceSquare;
|
private readonly bool forceSquare;
|
||||||
private readonly bool disposeTextures;
|
private readonly bool disposeTextures;
|
||||||
private readonly int padding;
|
|
||||||
|
|
||||||
private int maxWidth;
|
private int maxWidth;
|
||||||
|
|
||||||
|
@ -50,14 +51,12 @@ namespace MLEM.Data {
|
||||||
/// <param name="forcePowerOfTwo">Whether the resulting <see cref="PackedTexture"/> should have a width and height that is a power of two.</param>
|
/// <param name="forcePowerOfTwo">Whether the resulting <see cref="PackedTexture"/> should have a width and height that is a power of two.</param>
|
||||||
/// <param name="forceSquare">Whether the resulting <see cref="PackedTexture"/> should be square regardless of required size.</param>
|
/// <param name="forceSquare">Whether the resulting <see cref="PackedTexture"/> should be square regardless of required size.</param>
|
||||||
/// <param name="disposeTextures">Whether the original textures submitted to this texture packer should be disposed after packing.</param>
|
/// <param name="disposeTextures">Whether the original textures submitted to this texture packer should be disposed after packing.</param>
|
||||||
/// <param name="padding">The padding that each texture region should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false) {
|
||||||
public RuntimeTexturePacker(int maxWidth = 2048, bool autoIncreaseMaxWidth = false, bool forcePowerOfTwo = false, bool forceSquare = false, bool disposeTextures = false, int padding = default) {
|
|
||||||
this.maxWidth = maxWidth;
|
this.maxWidth = maxWidth;
|
||||||
this.autoIncreaseMaxWidth = autoIncreaseMaxWidth;
|
this.autoIncreaseMaxWidth = autoIncreaseMaxWidth;
|
||||||
this.forcePowerOfTwo = forcePowerOfTwo;
|
this.forcePowerOfTwo = forcePowerOfTwo;
|
||||||
this.forceSquare = forceSquare;
|
this.forceSquare = forceSquare;
|
||||||
this.disposeTextures = disposeTextures;
|
this.disposeTextures = disposeTextures;
|
||||||
this.padding = padding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -67,9 +66,10 @@ namespace MLEM.Data {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="atlas">The texture atlas to pack.</param>
|
/// <param name="atlas">The texture atlas to pack.</param>
|
||||||
/// <param name="result">The result callback which will receive the resulting texture regions.</param>
|
/// <param name="result">The result callback which will receive the resulting texture regions.</param>
|
||||||
/// <param name="padWithPixels">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.</param>
|
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
||||||
|
/// <param name="padWithPixels">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 <paramref name="padding"/> is greater than 0.</param>
|
||||||
/// <exception cref="InvalidOperationException">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.</exception>
|
/// <exception cref="InvalidOperationException">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.</exception>
|
||||||
public void Add(UniformTextureAtlas atlas, Action<Dictionary<Point, TextureRegion>> result, bool padWithPixels = false) {
|
public void Add(UniformTextureAtlas atlas, Action<Dictionary<Point, TextureRegion>> result, int padding = 0, bool padWithPixels = false) {
|
||||||
var resultRegions = new Dictionary<Point, TextureRegion>();
|
var resultRegions = new Dictionary<Point, TextureRegion>();
|
||||||
for (var x = 0; x < atlas.RegionAmountX; x++) {
|
for (var x = 0; x < atlas.RegionAmountX; x++) {
|
||||||
for (var y = 0; y < atlas.RegionAmountY; y++) {
|
for (var y = 0; y < atlas.RegionAmountY; y++) {
|
||||||
|
@ -78,7 +78,7 @@ namespace MLEM.Data {
|
||||||
resultRegions.Add(pos, r);
|
resultRegions.Add(pos, r);
|
||||||
if (resultRegions.Count >= atlas.RegionAmountX * atlas.RegionAmountY)
|
if (resultRegions.Count >= atlas.RegionAmountX * atlas.RegionAmountY)
|
||||||
result.Invoke(resultRegions);
|
result.Invoke(resultRegions);
|
||||||
}, padWithPixels);
|
}, padding, padWithPixels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,9 +90,10 @@ namespace MLEM.Data {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="atlas">The texture atlas to pack.</param>
|
/// <param name="atlas">The texture atlas to pack.</param>
|
||||||
/// <param name="result">The result callback which will receive the resulting texture regions.</param>
|
/// <param name="result">The result callback which will receive the resulting texture regions.</param>
|
||||||
/// <param name="padWithPixels">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.</param>
|
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
||||||
|
/// <param name="padWithPixels">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 <paramref name="padding"/> is greater than 0.</param>
|
||||||
/// <exception cref="InvalidOperationException">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.</exception>
|
/// <exception cref="InvalidOperationException">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.</exception>
|
||||||
public void Add(DataTextureAtlas atlas, Action<Dictionary<string, TextureRegion>> result, bool padWithPixels = false) {
|
public void Add(DataTextureAtlas atlas, Action<Dictionary<string, TextureRegion>> result, int padding = 0, bool padWithPixels = false) {
|
||||||
var atlasRegions = atlas.RegionNames.ToArray();
|
var atlasRegions = atlas.RegionNames.ToArray();
|
||||||
var resultRegions = new Dictionary<string, TextureRegion>();
|
var resultRegions = new Dictionary<string, TextureRegion>();
|
||||||
foreach (var region in atlasRegions) {
|
foreach (var region in atlasRegions) {
|
||||||
|
@ -100,7 +101,7 @@ namespace MLEM.Data {
|
||||||
resultRegions.Add(region, r);
|
resultRegions.Add(region, r);
|
||||||
if (resultRegions.Count >= atlasRegions.Length)
|
if (resultRegions.Count >= atlasRegions.Length)
|
||||||
result.Invoke(resultRegions);
|
result.Invoke(resultRegions);
|
||||||
}, padWithPixels);
|
}, padding, padWithPixels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,10 +111,11 @@ namespace MLEM.Data {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="texture">The texture to pack.</param>
|
/// <param name="texture">The texture to pack.</param>
|
||||||
/// <param name="result">The result callback which will receive the resulting texture region.</param>
|
/// <param name="result">The result callback which will receive the resulting texture region.</param>
|
||||||
/// <param name="padWithPixels">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.</param>
|
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
||||||
|
/// <param name="padWithPixels">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 <paramref name="padding"/> is greater than 0.</param>
|
||||||
/// <exception cref="InvalidOperationException">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.</exception>
|
/// <exception cref="InvalidOperationException">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.</exception>
|
||||||
public void Add(Texture2D texture, Action<TextureRegion> result, bool padWithPixels = false) {
|
public void Add(Texture2D texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
|
||||||
this.Add(new TextureRegion(texture), result, padWithPixels);
|
this.Add(new TextureRegion(texture), result, padding, padWithPixels);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -122,23 +124,25 @@ namespace MLEM.Data {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="texture">The texture region to pack.</param>
|
/// <param name="texture">The texture region to pack.</param>
|
||||||
/// <param name="result">The result callback which will receive the resulting texture region.</param>
|
/// <param name="result">The result callback which will receive the resulting texture region.</param>
|
||||||
/// <param name="padWithPixels">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.</param>
|
/// <param name="padding">The padding that the texture should have around itself. This can be useful if texture bleeding issues occur due to texture coordinate rounding.</param>
|
||||||
|
/// <param name="padWithPixels">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 <paramref name="padding"/> is greater than 0.</param>
|
||||||
/// <exception cref="InvalidOperationException">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.</exception>
|
/// <exception cref="InvalidOperationException">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.</exception>
|
||||||
public void Add(TextureRegion texture, Action<TextureRegion> result, bool padWithPixels = false) {
|
public void Add(TextureRegion texture, Action<TextureRegion> result, int padding = 0, bool padWithPixels = false) {
|
||||||
if (this.PackedTexture != null)
|
if (this.PackedTexture != null)
|
||||||
throw new InvalidOperationException("Cannot add texture to a texture packer that is already packed");
|
throw new InvalidOperationException("Cannot add texture to a texture packer that is already packed");
|
||||||
if (texture.Width > this.maxWidth) {
|
var paddedWidth = texture.Width + 2 * padding;
|
||||||
|
if (paddedWidth > this.maxWidth) {
|
||||||
if (this.autoIncreaseMaxWidth) {
|
if (this.autoIncreaseMaxWidth) {
|
||||||
this.maxWidth = texture.Width;
|
this.maxWidth = paddedWidth;
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidOperationException($"Cannot add texture with width {texture.Width} to a texture packer with max width {this.maxWidth}");
|
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, padWithPixels));
|
this.textures.Add(new Request(texture, result, padding, padWithPixels));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Packs all of the textures and texture regions added using <see cref="Add(Microsoft.Xna.Framework.Graphics.Texture2D,System.Action{MLEM.Textures.TextureRegion},bool)"/> into one texture.
|
/// Packs all of the textures and texture regions added using <see cref="Add(Microsoft.Xna.Framework.Graphics.Texture2D,System.Action{MLEM.Textures.TextureRegion},int,bool)"/> into one texture.
|
||||||
/// The resulting texture will be stored in <see cref="PackedTexture"/>.
|
/// The resulting texture will be stored in <see cref="PackedTexture"/>.
|
||||||
/// All of the result callbacks that were added will also be invoked.
|
/// All of the result callbacks that were added will also be invoked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -151,7 +155,7 @@ namespace MLEM.Data {
|
||||||
// set pack areas for each request
|
// set pack areas for each request
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
foreach (var request in this.textures.OrderByDescending(t => t.Texture.Width * t.Texture.Height))
|
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));
|
request.PackedArea = this.FindFreeArea(request);
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
this.LastCalculationTime = stopwatch.Elapsed;
|
this.LastCalculationTime = stopwatch.Elapsed;
|
||||||
|
|
||||||
|
@ -177,13 +181,14 @@ namespace MLEM.Data {
|
||||||
|
|
||||||
// invoke callbacks
|
// invoke callbacks
|
||||||
foreach (var request in this.textures) {
|
foreach (var request in this.textures) {
|
||||||
var packedArea = request.PackedArea.Shrink(new Point(this.padding));
|
var packedArea = request.PackedArea.Shrink(new Point(request.Padding));
|
||||||
request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea));
|
request.Result.Invoke(new TextureRegion(this.PackedTexture, packedArea));
|
||||||
if (this.disposeTextures)
|
if (this.disposeTextures)
|
||||||
request.Texture.Texture.Dispose();
|
request.Texture.Texture.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.textures.Clear();
|
this.textures.Clear();
|
||||||
|
this.dataCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -193,6 +198,7 @@ namespace MLEM.Data {
|
||||||
this.PackedTexture?.Dispose();
|
this.PackedTexture?.Dispose();
|
||||||
this.PackedTexture = null;
|
this.PackedTexture = null;
|
||||||
this.textures.Clear();
|
this.textures.Clear();
|
||||||
|
this.dataCache.Clear();
|
||||||
this.LastCalculationTime = TimeSpan.Zero;
|
this.LastCalculationTime = TimeSpan.Zero;
|
||||||
this.LastPackTime = TimeSpan.Zero;
|
this.LastPackTime = TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
|
@ -202,9 +208,10 @@ namespace MLEM.Data {
|
||||||
this.Reset();
|
this.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Rectangle FindFreeArea(Point size) {
|
private Rectangle FindFreeArea(Request request) {
|
||||||
size.X += this.padding * 2;
|
var size = new Point(request.Texture.Width, request.Texture.Height);
|
||||||
size.Y += this.padding * 2;
|
size.X += request.Padding * 2;
|
||||||
|
size.Y += request.Padding * 2;
|
||||||
|
|
||||||
var pos = new Point(0, 0);
|
var pos = new Point(0, 0);
|
||||||
var lowestY = int.MaxValue;
|
var lowestY = int.MaxValue;
|
||||||
|
@ -231,11 +238,17 @@ namespace MLEM.Data {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CopyRegion(TextureExtensions.TextureData destination, Request request) {
|
private void CopyRegion(TextureData destination, Request request) {
|
||||||
var location = request.PackedArea.Location + new Point(this.padding);
|
// we cache texture data in case multiple requests use the same underlying texture
|
||||||
using (var data = request.Texture.Texture.GetTextureData()) {
|
// this collection doesn't need to be disposed since we don't actually edit these textures
|
||||||
for (var x = -this.padding; x < request.Texture.Width + this.padding; x++) {
|
if (!this.dataCache.TryGetValue(request.Texture.Texture, out var data)) {
|
||||||
for (var y = -this.padding; y < request.Texture.Height + this.padding; y++) {
|
data = request.Texture.Texture.GetTextureData();
|
||||||
|
this.dataCache.Add(request.Texture.Texture, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
var location = request.PackedArea.Location + new Point(request.Padding);
|
||||||
|
for (var x = -request.Padding; x < request.Texture.Width + request.Padding; x++) {
|
||||||
|
for (var y = -request.Padding; y < request.Texture.Height + request.Padding; y++) {
|
||||||
Color srcColor;
|
Color srcColor;
|
||||||
if (!request.PadWithPixels && (x < 0 || y < 0 || x >= request.Texture.Width || y >= request.Texture.Height)) {
|
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
|
// if we're out of bounds and not padding with pixels, we make it transparent
|
||||||
|
@ -249,7 +262,6 @@ namespace MLEM.Data {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static int ToPowerOfTwo(int value) {
|
private static int ToPowerOfTwo(int value) {
|
||||||
var ret = 1;
|
var ret = 1;
|
||||||
|
@ -262,12 +274,14 @@ namespace MLEM.Data {
|
||||||
|
|
||||||
public readonly TextureRegion Texture;
|
public readonly TextureRegion Texture;
|
||||||
public readonly Action<TextureRegion> Result;
|
public readonly Action<TextureRegion> Result;
|
||||||
|
public readonly int Padding;
|
||||||
public readonly bool PadWithPixels;
|
public readonly bool PadWithPixels;
|
||||||
public Rectangle PackedArea;
|
public Rectangle PackedArea;
|
||||||
|
|
||||||
public Request(TextureRegion texture, Action<TextureRegion> result, bool padWithPixels) {
|
public Request(TextureRegion texture, Action<TextureRegion> result, int padding, bool padWithPixels) {
|
||||||
this.Texture = texture;
|
this.Texture = texture;
|
||||||
this.Result = result;
|
this.Result = result;
|
||||||
|
this.Padding = padding;
|
||||||
this.PadWithPixels = padWithPixels;
|
this.PadWithPixels = padWithPixels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -318,16 +318,16 @@ namespace Sandbox {
|
||||||
}
|
}
|
||||||
this.UiSystem.Add("Keybinds", keybindPanel);
|
this.UiSystem.Add("Keybinds", keybindPanel);
|
||||||
|
|
||||||
var packer = new RuntimeTexturePacker(padding: 1);
|
var packer = new RuntimeTexturePacker();
|
||||||
var regions = new List<TextureRegion>();
|
var regions = new List<TextureRegion>();
|
||||||
packer.Add(new UniformTextureAtlas(tex, 16, 16), r => {
|
packer.Add(new UniformTextureAtlas(tex, 16, 16), r => {
|
||||||
regions.AddRange(r.Values);
|
regions.AddRange(r.Values);
|
||||||
Console.WriteLine($"Returned {r.Count} regions: {string.Join(", ", r.Select(kv => kv.Key + ": " + kv.Value.Area))}");
|
Console.WriteLine($"Returned {r.Count} regions: {string.Join(", ", r.Select(kv => kv.Key + ": " + kv.Value.Area))}");
|
||||||
}, true);
|
}, 1, true);
|
||||||
packer.Add(this.Content.LoadTextureAtlas("Textures/Furniture"), r => {
|
packer.Add(this.Content.LoadTextureAtlas("Textures/Furniture"), r => {
|
||||||
regions.AddRange(r.Values);
|
regions.AddRange(r.Values);
|
||||||
Console.WriteLine($"Returned {r.Count} regions: {string.Join(", ", r.Select(kv => kv.Key + ": " + kv.Value.Area))}");
|
Console.WriteLine($"Returned {r.Count} regions: {string.Join(", ", r.Select(kv => kv.Key + ": " + kv.Value.Area))}");
|
||||||
}, true);
|
}, 1, true);
|
||||||
packer.Pack(this.GraphicsDevice);
|
packer.Pack(this.GraphicsDevice);
|
||||||
packer.PackedTexture.SaveAsPng(File.Create("_Packed.png"), packer.PackedTexture.Width, packer.PackedTexture.Height);
|
packer.PackedTexture.SaveAsPng(File.Create("_Packed.png"), packer.PackedTexture.Width, packer.PackedTexture.Height);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue