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
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MrGameEng.Atlases\MrGameEng.Atlases.csproj" />
</ItemGroup>
</Project>
+58
View File
@@ -0,0 +1,58 @@
using System.Diagnostics;
using MrGameEng.Atlases;
if (args.Length < 2 || args.Contains("--help") || args.Contains("-h"))
{
Console.WriteLine(
"""
MrGameEng.AtlasTool packs a directory tree of images into texture atlases.
Usage: MrGameEng.AtlasTool <source-dir> <output-dir> [options]
Options:
--group-depth <n> directories forming one atlas (0 = single atlas; default 1)
--page-size <n> maximum page size in pixels (default 2048)
--padding <n> gap between images in pixels (default 2)
--root-name <name> atlas name for files above group depth (default "Atlas")
--force rebuild even when sources are unchanged
""");
return args.Length < 2 && !args.Contains("--help") && !args.Contains("-h") ? 1 : 0;
}
int Option(string name, int fallback)
{
var index = Array.IndexOf(args, name);
return index >= 0 && index + 1 < args.Length ? int.Parse(args[index + 1]) : fallback;
}
var rootNameIndex = Array.IndexOf(args, "--root-name");
var options = new AtlasBuildOptions
{
SourceDirectory = args[0],
OutputDirectory = args[1],
GroupDepth = Option("--group-depth", 1),
MaxPageSize = Option("--page-size", 2048),
Padding = Option("--padding", 2),
RootAtlasName = rootNameIndex >= 0 && rootNameIndex + 1 < args.Length ? args[rootNameIndex + 1] : "Atlas",
Force = args.Contains("--force"),
};
var stopwatch = Stopwatch.StartNew();
var result = AtlasBuilder.Build(options);
stopwatch.Stop();
foreach (var group in result.Groups)
{
Console.WriteLine(group.Skipped
? $" {group.Name}: up to date ({group.RegionCount} regions, {group.PageCount} pages)"
: $" {group.Name}: {group.RegionCount} regions -> {group.PageCount} pages");
}
foreach (var orphan in result.DeletedOrphans)
{
Console.WriteLine($" deleted orphan {orphan}");
}
var built = result.Groups.Count(g => !g.Skipped);
Console.WriteLine($"Done: {built} atlases built, {result.Groups.Count - built} up to date, {stopwatch.Elapsed.TotalSeconds:F1}s.");
return 0;