Add MrGameEng.Atlases: texture atlas builder, runtime loader and CLI tool
CI / build-test (push) Failing after 1m13s

AtlasBuilder packs a directory tree of loose images into atlas pages plus
JSON metadata (deterministic shelf packing, incremental rebuilds, orphan
cleanup); TextureAtlas loads them back handing out Texture2DRegions, so
sprites from one page batch into a single draw call. The asset handle
generator maps .atlas files to TextureAtlas and skips page images.
Demonstrated in the sample (Assets/Atlases + 'atlas' console command),
wrapped as tools/MrGameEng.AtlasTool for build scripts.

Documented dependency exception: Atlases depends on Graphics and Assets.
This commit is contained in:
Leonid Pershin
2026-06-11 08:18:30 +03:00
parent d6f19b9119
commit b318d1e795
21 changed files with 1209 additions and 1 deletions
+83
View File
@@ -0,0 +1,83 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MrGameEng.Atlases;
/// <summary>
/// Serializable description of one packed atlas: its page image files and the source-relative
/// region keys with their pixel rectangles. Stored as a JSON <c>.atlas</c> file next to the pages.
/// </summary>
public sealed class AtlasMetadata
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
/// <summary>Format version, bumped on breaking metadata changes.</summary>
public int Version { get; init; } = 1;
/// <summary>Atlas name (group key with '/' replaced by '.').</summary>
public string Name { get; init; } = "";
/// <summary>Maximum page size the atlas was built with (staleness check input).</summary>
public int PageSize { get; init; }
/// <summary>Padding in pixels the atlas was built with (staleness check input).</summary>
public int Padding { get; init; }
/// <summary>Page image files (relative to the metadata file), in page-index order.</summary>
public List<AtlasPage> Pages { get; init; } = [];
/// <summary>Packed regions, sorted by key.</summary>
public List<AtlasRegion> Regions { get; init; } = [];
/// <summary>Serializes this metadata to indented JSON.</summary>
public string ToJson() => JsonSerializer.Serialize(this, JsonOptions);
/// <summary>Parses metadata from JSON produced by <see cref="ToJson"/>.</summary>
public static AtlasMetadata FromJson(string json) =>
JsonSerializer.Deserialize<AtlasMetadata>(json, JsonOptions)
?? throw new InvalidDataException("Atlas metadata JSON deserialized to null.");
}
/// <summary>One page image of an atlas.</summary>
public sealed class AtlasPage
{
/// <summary>Image file name, relative to the metadata file.</summary>
public string File { get; init; } = "";
/// <summary>Page width in pixels.</summary>
public int Width { get; init; }
/// <summary>Page height in pixels.</summary>
public int Height { get; init; }
}
/// <summary>One packed source texture inside an atlas.</summary>
public sealed class AtlasRegion
{
/// <summary>
/// Region key: the source path relative to the build source root, forward slashes,
/// without the file extension (e.g. <c>Things/Pawn/Animal/Fox</c>).
/// </summary>
public string Key { get; init; } = "";
/// <summary>Index of the page containing this region.</summary>
public int Page { get; init; }
/// <summary>X of the region in page pixels.</summary>
public int X { get; init; }
/// <summary>Y of the region in page pixels.</summary>
public int Y { get; init; }
/// <summary>Region width in pixels.</summary>
[JsonPropertyName("w")]
public int Width { get; init; }
/// <summary>Region height in pixels.</summary>
[JsonPropertyName("h")]
public int Height { get; init; }
}