TextureAtlas CNA-free
Easy3D / Data & Batching / TextureAtlas
Easy3D::TextureAtlas maps names to sub-rectangles of a single atlas texture and converts them to normalized UVs. It is deliberately small and CNA-free: it deals only in plain pixel rectangles and normalized UVs. Loading the actual texture is the caller's job (via CNA); this class only remembers where each region lives.
#include <Easy3D/TextureAtlas.hpp>
namespace Easy3D
{
struct AtlasRect; // pixel rectangle inside the atlas texture
struct UvRect; // normalized texture coordinates
class TextureAtlas;
}
The POD types
AtlasRect and UvRect are Easy3D-local PODs, since pixel/UV rectangles are not CNA concepts. This is the one deliberate exception to "use CNA types everywhere" — it keeps the atlas CNA-free and unit-testable on its own.
AtlasRect — a pixel rectangle
| Field | Type | Default | Meaning |
|---|---|---|---|
X | int | 0 | Left edge, in pixels. |
Y | int | 0 | Top edge, in pixels. |
Width | int | 0 | Width in pixels. |
Height | int | 0 | Height in pixels. |
UvRect — normalized texture coordinates
| Field | Type | Default | Meaning |
|---|---|---|---|
U0 | float | 0 | Top-left corner. |
V0 | float | 0 | |
U1 | float | 0 | Bottom-right corner. |
V1 | float | 0 |
UvRect also appears in BillboardItem and CubeItem, where the batches use {0,0,1,1} (the full texture) as the default region.
Construction & atlas size
TextureAtlas(int atlasWidth, int atlasHeight) noexcept
GetUv()/GetUvOrDefault() return a zeroed UvRect.[[nodiscard]] int AtlasWidth() const noexcept
[[nodiscard]] int AtlasHeight() const noexcept
Registering regions
int frameWidth, int frameHeight,
int columns, int rows,
int startX = 0, int startY = 0,
int spacingX = 0, int spacingY = 0)
Register a grid of named regions from a spritesheet: prefix + "_0", prefix + "_1", … in row-major order (first row left to right, then the next row).
Frame i sits at pixel (startX + col*(frameWidth+spacingX), startY + row*(frameHeight+spacingY)) with size frameWidth × frameHeight, where col = i % columns, row = i / columns. Internally calls Add() for each frame, so it shares Add()'s overwrite-on-duplicate-name behavior.
Throws std::invalid_argument if prefix is empty; if frameWidth, frameHeight, columns, or rows is ≤ 0; or if startX/startY/spacingX/spacingY is negative.
Lookup
noexcept: the lookup allocates a temporary std::string key, which can in principle throw std::bad_alloc.std::out_of_range if name is unknown.std::out_of_range if name is unknown. Returns a zeroed UvRect if the atlas size is unset (≤ 0).fallback if name is unknown — unlike GetUv(), an unknown name is not an error. Not noexcept (temporary key allocation). Returns a zeroed UvRect if the name exists but the atlas size is unset.[[nodiscard]] bool Empty() const noexcept
Strict vs. lenient lookup
| Situation | GetUv(name) | GetUvOrDefault(name, fb) |
|---|---|---|
| Name registered, atlas size set | UVs | UVs |
| Name registered, atlas size unset | zeroed UvRect | zeroed UvRect |
| Name unknown | throws std::out_of_range | returns fb |
GetUv() for regions that must exist (a typo is a bug you want to hear about) and GetUvOrDefault() for optional content, e.g. an animation frame that may be missing from an older spritesheet.UV math
UVs are computed by dividing pixel coordinates by the atlas dimensions:
U0 = X / atlasWidth V0 = Y / atlasHeight
U1 = (X + Width) / atlasWidth V1 = (Y + Height) / atlasHeight
Example — a spritesheet in one call
#include <Easy3D/TextureAtlas.hpp>
// A 256x256 spritesheet with 8 columns x 4 rows of 32x48 walk frames,
// starting at pixel (0, 64), no spacing between frames.
Easy3D::TextureAtlas atlas(256, 256);
atlas.AddGrid("walk", 32, 48, 8, 4, 0, 64);
// Registered as walk_0 .. walk_31, row-major.
atlas.Contains("walk_31"); // true
const auto rect = atlas.GetRect("walk_9"); // {32, 112, 32, 48}: col 1, row 1
const auto uv = atlas.GetUv("walk_9"); // {0.125, 0.4375, 0.25, 0.625}
// Lenient lookup with a fallback (full texture) for optional frames:
const auto maybe = atlas.GetUvOrDefault("walk_99", {0, 0, 1, 1});
What TextureAtlas does not do
- It does not load, own, or reference the texture itself — pair it with a texture you load via CNA.
- It does not pack rectangles; you tell it where regions live.
- It describes one texture. Use one
TextureAtlasinstance per atlas texture.