glyph atlas

glyph atlas with SDL2 and SDL2_ttf

5 min 919 words
Jason Short it's me!

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');
}

A Glyph Atlas

Sample Project

A sample project with the above code is maintained here

License

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.