1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-25 14:08:34 +01:00

Compare commits

...

4 commits

3 changed files with 54 additions and 22 deletions

View file

@ -58,6 +58,7 @@ 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 - Cache RuntimeTexturePacker texture data while packing to improve performance
- Improve RuntimeTexturePacker performance by checking against packed textures in the same order as packing
Fixes Fixes
- Fixed SoundEffectReader incorrectly claiming it could read ogg and mp3 files - Fixed SoundEffectReader incorrectly claiming it could read ogg and mp3 files

View file

@ -34,7 +34,8 @@ namespace MLEM.Data {
/// </summary> /// </summary>
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> texturesToPack = new List<Request>();
private readonly List<Request> alreadyPackedTextures = new List<Request>();
private readonly Dictionary<Texture2D, TextureData> dataCache = new Dictionary<Texture2D, TextureData>(); 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;
@ -68,17 +69,27 @@ namespace MLEM.Data {
/// <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="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="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> /// <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>
/// <param name="ignoreTransparent">Whether completely transparent texture regions in the <paramref name="atlas"/> should be ignored. If this is true, they will not be part of the <paramref name="result"/> collection either.</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, int padding = 0, bool padWithPixels = false) { public void Add(UniformTextureAtlas atlas, Action<Dictionary<Point, TextureRegion>> result, int padding = 0, bool padWithPixels = false, bool ignoreTransparent = false) {
var addedRegions = new List<TextureRegion>();
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++) {
var pos = new Point(x, y); var pos = new Point(x, y);
this.Add(atlas[pos], r => { var region = atlas[pos];
if (ignoreTransparent) {
if (IsTransparent(region))
continue;
}
this.Add(region, r => {
resultRegions.Add(pos, r); resultRegions.Add(pos, r);
if (resultRegions.Count >= atlas.RegionAmountX * atlas.RegionAmountY) if (resultRegions.Count >= addedRegions.Count)
result.Invoke(resultRegions); result.Invoke(resultRegions);
}, padding, padWithPixels); }, padding, padWithPixels);
addedRegions.Add(region);
} }
} }
} }
@ -138,7 +149,7 @@ namespace MLEM.Data {
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, padding, padWithPixels)); this.texturesToPack.Add(new Request(texture, result, padding, padWithPixels));
} }
/// <summary> /// <summary>
@ -153,15 +164,18 @@ namespace MLEM.Data {
throw new InvalidOperationException("Cannot pack a texture packer that is already packed"); throw new InvalidOperationException("Cannot pack a texture packer that is already packed");
// set pack areas for each request // 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(); var stopwatch = Stopwatch.StartNew();
foreach (var request in this.textures.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) foreach (var request in this.texturesToPack.OrderByDescending(t => t.Texture.Width * t.Texture.Height)) {
request.PackedArea = this.FindFreeArea(request); request.PackedArea = this.FindFreeArea(request);
this.alreadyPackedTextures.Add(request);
}
stopwatch.Stop(); stopwatch.Stop();
this.LastCalculationTime = stopwatch.Elapsed; this.LastCalculationTime = stopwatch.Elapsed;
// figure out texture size and generate texture // figure out texture size and generate texture
var width = this.textures.Max(t => t.PackedArea.Right); var width = this.alreadyPackedTextures.Max(t => t.PackedArea.Right);
var height = this.textures.Max(t => t.PackedArea.Bottom); var height = this.alreadyPackedTextures.Max(t => t.PackedArea.Bottom);
if (this.forcePowerOfTwo) { if (this.forcePowerOfTwo) {
width = ToPowerOfTwo(width); width = ToPowerOfTwo(width);
height = ToPowerOfTwo(height); height = ToPowerOfTwo(height);
@ -173,21 +187,22 @@ namespace MLEM.Data {
// copy texture data onto the packed texture // copy texture data onto the packed texture
stopwatch.Restart(); stopwatch.Restart();
using (var data = this.PackedTexture.GetTextureData()) { using (var data = this.PackedTexture.GetTextureData()) {
foreach (var request in this.textures) foreach (var request in this.alreadyPackedTextures)
this.CopyRegion(data, request); this.CopyRegion(data, request);
} }
stopwatch.Stop(); stopwatch.Stop();
this.LastPackTime = stopwatch.Elapsed; this.LastPackTime = stopwatch.Elapsed;
// invoke callbacks // invoke callbacks
foreach (var request in this.textures) { foreach (var request in this.alreadyPackedTextures) {
var packedArea = request.PackedArea.Shrink(new Point(request.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.texturesToPack.Clear();
this.alreadyPackedTextures.Clear();
this.dataCache.Clear(); this.dataCache.Clear();
} }
@ -197,7 +212,8 @@ namespace MLEM.Data {
public void Reset() { public void Reset() {
this.PackedTexture?.Dispose(); this.PackedTexture?.Dispose();
this.PackedTexture = null; this.PackedTexture = null;
this.textures.Clear(); this.texturesToPack.Clear();
this.alreadyPackedTextures.Clear();
this.dataCache.Clear(); this.dataCache.Clear();
this.LastCalculationTime = TimeSpan.Zero; this.LastCalculationTime = TimeSpan.Zero;
this.LastPackTime = TimeSpan.Zero; this.LastPackTime = TimeSpan.Zero;
@ -218,7 +234,7 @@ namespace MLEM.Data {
while (true) { while (true) {
var intersected = false; var intersected = false;
var area = new Rectangle(pos, size); var area = new Rectangle(pos, size);
foreach (var tex in this.textures) { foreach (var tex in this.alreadyPackedTextures) {
if (tex.PackedArea.Intersects(area)) { if (tex.PackedArea.Intersects(area)) {
pos.X = tex.PackedArea.Right; pos.X = tex.PackedArea.Right;
// when we move down, we want to move down by the smallest intersecting texture's height // when we move down, we want to move down by the smallest intersecting texture's height
@ -239,13 +255,7 @@ namespace MLEM.Data {
} }
private void CopyRegion(TextureData destination, Request request) { private void CopyRegion(TextureData destination, Request request) {
// we cache texture data in case multiple requests use the same underlying texture var data = this.GetCachedTextureData(request.Texture.Texture);
// this collection doesn't need to be disposed since we don't actually edit these textures
if (!this.dataCache.TryGetValue(request.Texture.Texture, out var data)) {
data = request.Texture.Texture.GetTextureData();
this.dataCache.Add(request.Texture.Texture, data);
}
var location = request.PackedArea.Location + new Point(request.Padding); var location = request.PackedArea.Location + new Point(request.Padding);
for (var x = -request.Padding; x < request.Texture.Width + request.Padding; x++) { for (var x = -request.Padding; x < request.Texture.Width + request.Padding; x++) {
for (var y = -request.Padding; y < request.Texture.Height + request.Padding; y++) { for (var y = -request.Padding; y < request.Texture.Height + request.Padding; y++) {
@ -263,6 +273,27 @@ namespace MLEM.Data {
} }
} }
private TextureData GetCachedTextureData(Texture2D texture) {
// we cache texture data in case multiple requests use the same underlying texture
// this collection doesn't need to be disposed since we don't actually edit these textures
if (!this.dataCache.TryGetValue(texture, out var data)) {
data = texture.GetTextureData();
this.dataCache.Add(texture, data);
}
return data;
}
private bool IsTransparent(TextureRegion region) {
var data = this.GetCachedTextureData(region.Texture);
for (var rX = 0; rX < region.Width; rX++) {
for (var rY = 0; rY < region.Height; rY++) {
if (data[region.U + rX, region.V + rY] != Color.Transparent)
return false;
}
}
return true;
}
private static int ToPowerOfTwo(int value) { private static int ToPowerOfTwo(int value) {
var ret = 1; var ret = 1;
while (ret < value) while (ret < value)

View file

@ -323,7 +323,7 @@ namespace Sandbox {
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))}");
}, 1, true); }, 1, true, 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))}");
@ -342,7 +342,7 @@ namespace Sandbox {
x += r.Width * sc + 1; x += r.Width * sc + 1;
if (x >= 1000) { if (x >= 1000) {
x = 0; x = 0;
y += 50; y += 250;
} }
} }
this.SpriteBatch.End(); this.SpriteBatch.End();