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

FieldTypeDefaultMeaning
Xint0Left edge, in pixels.
Yint0Top edge, in pixels.
Widthint0Width in pixels.
Heightint0Height in pixels.

UvRect — normalized texture coordinates

FieldTypeDefaultMeaning
U0float0Top-left corner.
V0float0
U1float0Bottom-right corner.
V1float0

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() = default
TextureAtlas(int atlasWidth, int atlasHeight) noexcept
Create an atlas, optionally with the texture's pixel dimensions. The size is needed for UV computation — with a zero/unset size, GetUv()/GetUvOrDefault() return a zeroed UvRect.
void SetAtlasSize(int width, int height) noexcept
[[nodiscard]] int AtlasWidth() const noexcept
[[nodiscard]] int AtlasHeight() const noexcept
Set/read the atlas texture's pixel dimensions.

Registering regions

void Add(std::string name, const AtlasRect& rect)
Register (or overwrite) a named region. Adding an existing name replaces the previous rectangle.
void AddGrid(std::string_view prefix,
             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

[[nodiscard]] bool Contains(std::string_view name) const
Whether a region with this name exists. Not noexcept: the lookup allocates a temporary std::string key, which can in principle throw std::bad_alloc.
[[nodiscard]] const AtlasRect& GetRect(std::string_view name) const
Look up a region by name, in pixels. Throws std::out_of_range if name is unknown.
[[nodiscard]] UvRect GetUv(std::string_view name) const
Normalized UVs for a region, using the current atlas size. Throws std::out_of_range if name is unknown. Returns a zeroed UvRect if the atlas size is unset (≤ 0).
[[nodiscard]] UvRect GetUvOrDefault(std::string_view name, const UvRect& fallback = {}) const
Normalized UVs for a region, or 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]] std::size_t Count() const noexcept
[[nodiscard]] bool Empty() const noexcept
Number of registered regions / whether none are registered.

Strict vs. lenient lookup

SituationGetUv(name)GetUvOrDefault(name, fb)
Name registered, atlas size setUVsUVs
Name registered, atlas size unsetzeroed UvRectzeroed UvRect
Name unknownthrows std::out_of_rangereturns fb
Use 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