Skip to content

Sprites and text

Everything Labrador draws is a textured rectangle: a sprite. Text is sprites cut from a font’s glyph atlas, and a solid rectangle is a sprite from a texture one pixel across. This guide covers the files, the classes that draw them, and the colours.

Kind File Made with
Texture .dds Any tool that writes DDS, such as texconv from Microsoft’s DirectXTex.
Font .spritefont MakeSpriteFont from Microsoft’s DirectX Tool Kit, which renders a system font into an atlas.
Sprite sheet .dds and .json with the same name A texture, plus a JSON file naming rectangles in it (below).

A texture can be uncompressed 32-bit colour (R8G8B8A8 or B8G8R8A8), 16-bit B4G4R4A4, or block-compressed BC1, BC2 or BC3. Anything else is refused at load, with the file named. Each file is listed in the manifest by name, without its extension.

There are three text classes, for three places text appears:

Class Is a Use it for
Label GameObject Text in a scene: added once, culled and drawn with everything else.
Text a drawable you draw yourself Text a state draws directly, without a scene.
UiText a widget A row in a menu that can take focus (Menus).

All three take the same core arguments: the text as a wide string, the font’s name from the manifest, a position, and the render resources the font is resolved against. The minimal sample’s greeting is a Label:

samples/minimal/states/hello_state.cpp
this->greeting_ = this->scene_->add(std::make_unique<Label>(
L"Hello from the engine.", font_name, this->position_, resources,

The font name is resolved to a handle in the constructor, so a misspelt name throws there, and the text is measured once whenever it changes rather than on every frame. set_text, set_position, set_colour and set_scale change it afterwards, from update().

Text is wide (std::wstring, written L"...") all the way to the renderer, so drawing it never converts or allocates. A font can only draw the characters it was made with. Before showing text that came from outside the game, such as a player’s name, check it with RenderResources::can_render(font, text).

Centring text means measuring it. RenderResources::measure_text(font, text) returns its size. LineSweeper’s pause menu centres each row this way, once, when the menu is built:

samples/linesweeper/states/pause_state.cpp
const auto centred = [&](const std::wstring& text, float y)
{
const Vector2F size = resources->measure_text(font, text);
return Vector2F((resolution.x - size.x) * 0.5f, y);
};

The engine has no layout system: positions are numbers your game works out, as here.

A sprite sheet is one texture with named rectangles in it. Its JSON file lists frames, each a named rectangle, and animation strips, each a run of equal-sized frames laid out left to right from a first frame:

{
"sprite_frames": [
{ "name": "coin", "position": { "x": 0, "y": 0 }, "size": { "w": 16, "h": 16 } },
{ "name": "hero_idle", "position": { "x": 0, "y": 16 }, "size": { "w": 16, "h": 32 },
"origin": { "x": 8, "y": 32 } }
],
"animation_strips": [
{ "name": "hero_walk", "first_frame": { "x": 16, "y": 16, "w": 16, "h": 32 },
"frame_count": 6, "frame_time": 0.1, "looping": true }
]
}

origin is optional, in texels from the frame’s top-left; it is the point a sprite is placed and rotated by, such as the middle of a character’s feet. A sheet whose packer rotated frames to fit them is refused, because the engine draws frames as they are stored. Every key is checked when the sheet loads, and a missing or mistyped one names the file and the key.

Two classes draw from a sheet, and both are designed to be inherited alongside GameObject, so your own class gets their drawing and keeps its own update:

  • TextureObject draws one named frame. Its constructor takes the sheet name and the frame name.
  • AnimationObject plays a strip. Its update(dt) advances the frame, and play, pause, stop, reset and set_animation_strip_and_reset control it from your own update().

Both have draw(draw_list, destination_rectangle) and draw(draw_list, position, scale), and a draw_with that takes the colour or flip as a parameter instead. Because several views can draw the same object at once, a tint or flip that varies from draw to draw is passed through draw_with rather than set on the object first. Neither sample uses a sprite sheet yet; the engine’s tests do.

There is no rectangle-drawing call, because a solid rectangle is a sprite. LineSweeper’s content includes a texture one white pixel across, and every block, panel and line on its screen is that pixel stretched and tinted. This is the darkening layer its pause menu draws over the game:

samples/linesweeper/states/pause_state.cpp
void draw(DrawList& draw_list) const override
{
draw_list.draw_sprite(this->block_, RectangleI(0, 0, 1, 1),
this->area_, faded(Colour(0.0f, 0.0f, 0.0f), scrim_alpha),
}

draw_sprite takes a source rectangle in texels, here the one pixel, and a destination rectangle in world space, here the whole screen.

Colour holds four floats from 0 to 1, and can be built from floats, from bytes (Colour(240, 152, 0)), or from a hex string. It has the usual named colours: Colour::white, Colour::goldenrod, Colour::dark_gray and so on.

Blending is premultiplied. A drawn pixel lands as source + destination × (1 − source alpha). For an ordinary translucent colour, multiply the colour by its alpha as well, which is what LineSweeper’s faded does:

samples/linesweeper/presentation/palette.h
constexpr labrador::Colour faded(const labrador::Colour& colour,
float alpha)
{
return labrador::Colour(colour.r * alpha, colour.g * alpha,
colour.b * alpha, alpha);
}

The same rule gives additive light for free. With alpha at zero the destination is untouched and the colour is simply added, so overlapping draws brighten towards white. That is LineSweeper’s glowing, and its whole particle effect:

samples/linesweeper/presentation/palette.h
constexpr labrador::Colour glowing(const labrador::Colour& colour,
float brightness)
{
return labrador::Colour(colour.r * brightness, colour.g * brightness,
colour.b * brightness, 0.0f);
}

DrawList::set_filter chooses between smooth (linear) and blocky (point) sampling when a texture is scaled, for every draw after it in that view. Pixel art usually wants point sampling. Changing the filter partway through a view is allowed and costs a little, so group draws by filter.

Development documentation (unreleased). Built from Labrador 862e08b of 2026-10-08.