glyph atlas
glyph atlas with SDL2 and SDL2_ttf
The Problem: Rendering Text
Generating textures from text on-the-fly can slow game loop rendering, particularly if the text is dynamic. Obviously pre-rendered text is one solution. Another possible optimization is to generate a glyph atlas of text characters, using the atlas to lookup the texture for individual graphemes that have been pre-generated and cached. Larger textures, say full sentences, can be further cached if necessary.
The Font Interface
The font
public interface might look something like the following. It manages its own references to a collection of glyphs that we'll create on-the-fly before the game loop starts.
typedef struct font font;
typedef struct font {
const glyph * (*putchar)(font * self,
SDL_Renderer * renderer,
SDL_Point dest,
char text
);
void (*write)(font * self,
SDL_Renderer * renderer,
SDL_Point point,
const char * text
);
/* State; see below */
font_data data;
} font;
The Glyph Structure
The glyph type might look like this. Our font
interface will hold an array of glyphs in its data
property.
typedef struct {
SDL_Texture * texture;
char * c;
int w;
int h;
} glyph;
Font Data
Finally, the font state. A further optimization omitted here is to save a reference to the printed strings, creating new textures only if the text has changed between renders. I'm not including this, as we're already getting deep in the weeds.
typedef struct {
TTF_Font * font;
int height;
char padding[4];
/* Text storage for graphemes; simplified here */
size_t text_buf_len;
size_t text_buf_capacity;
char * text_buf;
char * text_next;
/* the glyph atlas */
size_t glyphs_capacity;
size_t glyphs_len;
glyph * glyphs;
} font_data;
Take note of the glyph atlas. For simplicity, it's a sorted array of glyphs allocated to glyphs_capacity
. If the glyphs_len
extends to the capacity, we could resize the array.
An Aside on UTF-8 and Code Points
In our glyph type, we hold a pointer to an array of characters for a given glyph. UTF-8 is a variable width encoding format, which means that each code point is stored in one to four bytes. For example, the following UTF-8 code point 0xfffe, 0x00fb
represents the ff
ligature.
For simplicity, I'm ignoring UTF-8 code points. This code assumes everything is a single-byte character.
Constructing a Font
We need a function to construct the data and load the font. We might have something like this:
/* Create the SDL2 renderer and load the TTF here */
/* Then populate the font type: */
font f;
font_init(&f, renderer, ttf);
Where font_init
looks something like this:
static void font_init(font * self, SDL_Renderer * renderer, TTF_Font * ttf) {
font_data * data = &(self->data);
self->putchar = &font_putchar;
self->write = &font_write;
data->font = ttf;
data->height = TTF_FontHeight(ttf);
data->glyphs_len = 0;
data->glyphs_capacity = 256;
data->text_buf_len = 0;
data->text_buf_capacity = 2048;
data->glyphs = calloc(data->glyphs_capacity, sizeof * data->glyphs);
if(!data->glyphs) { abort(); }
data->text_buf = calloc(data->text_buf_capacity, sizeof * data->text_buf);
if(!data->text_buf) { abort(); }
data->text_next = data->text_buf;
font_build_atlas(self, ttf, renderer);
}
Initializing a Glyph Atlas
We now have a font object with an empty glyph atlas. We need to create the atlas. For this example, we'll iterate the ASCII character set, populating each member of the glyph array with an ASCII value. We could skip non-printable characters. We could also generate multi-byte UTF-8 graphemes and ligatures.
static void font_build_atlas(font * self, TTF_Font * ttf, SDL_Renderer * renderer) {
font_data * data = &(self->data);
for(size_t i = 0; i < data->glyphs_capacity; i++) {
if(data->text_buf_len + 1 >= data->text_buf_capacity) { abort(); }
glyph * g = data->glyphs + i;
g->c = data->text_next;
g->c[0] = (char)i; /* laziness */
g->c[1] = '\0';
data->text_next += sizeof * g->c + sizeof '\0';
data->text_buf_len++;
font_glyph_create_texture(ttf, renderer, g);
TTF_SizeUTF8(data->font, g->c, &(g->w), &(g->h));
data->glyphs_len++;
}
}
Creating a Glyph Texture
For each grapheme, we'll create a glyph using TTF_RenderUTF8_Blended
. This is a high-quality rendered surface that we'll convert to a texture and save with the glyph.
static void font_glyph_create_texture(TTF_Font * ttf, SDL_Renderer * renderer, glyph * g) {
static const SDL_Color foreground = { .r = 255, .g = 255, .b = 255, .a = 255 };
if(!g->c || *(g->c) == '\0') { return; }
SDL_Surface * surface = TTF_RenderUTF8_Blended(ttf, g->c, foreground);
g->texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_FreeSurface(surface), surface = NULL;
}
Write a Single Glyph to a Renderer
Given that we now have an atlas of glyph textures, we can draw a glyph to our renderer, like this.
static const glyph * font_putchar(font * self, SDL_Renderer * renderer, SDL_Point dest, char text) {
if(text == '\0') { return NULL; }
/* Lookup the glyph from the character */
const glyph * g = &self->data.glyphs[(size_t)text];
SDL_Rect d = {
.x = dest.x,
.y = dest.y,
.w = g->w,
.h = self->data.height
};
/* send it! */
SDL_RenderCopy(renderer, g->texture, NULL, &d);
return g;
}
Write a String of Glyphs to a Renderer
We can now use the putchar
method on our font
type to write a string of text to the renderer:
static void font_write(font * self, SDL_Renderer * renderer, SDL_Point point, const char * text) {
const char * t = text;
do {
const glyph * g = self->putchar(self, renderer, point, *t);
point.x += g->w;
} while(t++, *t != '\0');
}
Sample Project
A sample project with the above code is maintained here
License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Table of contents