OpenGL Programming/Modern OpenGL Tutorial Text Rendering 02

Optimized text rendering with a texture atlas

Introduction

edit

In the first text rendering tutorial, we were uploading a new texture to the graphics card for each character that we drew. This is of course very wasteful. It would be much better to have all possible character images stored permanently on the graphics card. A simple way to do this is to have many textures, one for each character, so that we just have to switch between textures when drawing a quad. However, this means we can only draw a quad at a time, and if we have a lot of text that means a lot of OpenGL calls. A slightly more complex way is to have a single, large texture that contains all the characters, and to properly set the texture coordinates for each quad so that the right character will be rendered. This is also known as a texture atlas.

Creating a texture atlas

edit

A texture atlas is basically a big texture which contains many small images that are packed together. If all the subimages have the same size, it is rather easy to create a tightly packed atlas. However, we have already seen that the glyph images that FreeType produces have wildly varying sizes. This is true even when using monospaced fonts! Although there are various methods to efficiently pack a set of arbitrarily sized rectangles, we will use a very simple method in this tutorial: we will put all characters next to each other in a single row.

Before we can create the atlas itself though, we need to know the combined width of all the glyph images, and the height of the tallest glyph. Assuming we have already initialized FreeType, have loaded a font, and set the font size, it is a simple matter:

FT_GlyphSlot g = face->glyph;
int w = 0;
int h = 0;

for(int i = 32; i < 128; i++) {
  if(FT_Load_Char(face, i, FT_LOAD_RENDER)) {
    fprintf(stderr, "Loading character %c failed!\n", i);
    continue;
  }

  w += g->bitmap.width;
  h = std::max(h, g->bitmap.rows);
}

/* you might as well save this value as it is needed later on */
int atlas_width = w;

Remember that the variable "g" is just a shortcut to save some typing, and in that spirit, we also used the max() function, so you should put #include <algorithm> at the top of the source. We skip the first 32 ASCII characters, since they are just control codes.

Now that we know the width and height of our atlas, we can create an empty texture for it:

GLuint tex;
glActiveTexture(GL_TEXTURE0);
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, w, h, 0, GL_ALPHA, GL_UNSIGNED_BYTE, 0);

We used a null pointer to tell OpenGL that we will fill in the contents of the texture later. Again, don't forget about the GL_UNPACK_ALIGNMENT. Now we are ready to paste the glyph images into the texture atlas. We will use the very convenient glTexSubImage2D() function for this:

int x = 0;

for(int i = 32; i < 128; i++) {
  if(FT_Load_Char(face, i, FT_LOAD_RENDER))
    continue;

  glTexSubImage2D(GL_TEXTURE_2D, 0, x, 0, g->bitmap.width, g->bitmap.rows, GL_ALPHA, GL_UNSIGNED_BYTE, g->bitmap.buffer);

  x += g->bitmap.width;
}

That's it, our texture atlas is finished... except that we should try to remember where in the atlas we can find individual characters.

Caching glyph metrics and texture offsets

edit

When we are building our vertex buffer, we need to know the right texture coordinates to use. Most importantly, we need to know the x offset, the rest of the information can be found in FreeType's face->glyph struct. However, calling FT_Load_Char all the time is also not so efficient, so the best thing to do is to store all the information we need to render text in our own cache. Since we only use the ASCII character set, we can just make an array containing the information for all ASCII characters:

struct character_info {
  float ax; // advance.x
  float ay; // advance.y
  
  float bw; // bitmap.width;
  float bh; // bitmap.rows;
  
  float bl; // bitmap_left;
  float bt; // bitmap_top;
  
  float tx; // x offset of glyph in texture coordinates
} c[128];

In the for loop where we upload the glyph images to the texture, we fill in this array:

  c[*p].ax = g->advance.x >> 6;
  c[*p].ay = g->advance.y >> 6;

  c[*p].bw = g->bitmap.width;
  c[*p].bh = g->bitmap.rows;

  c[*p].bl = g->bitmap_left;
  c[*p].bt = g->bitmap_top;

  c[*p].tx = (float)x / w;

Although the struct contains only members of type float, you could use (u)int16_t or even (u)int8_t for them, depending on the maximum size of the glyphs and the atlas. Since we are dealing with ASCII, we could also have omitted the copy of the g->advance.y value, since it should be 0 anyway. If you want to go beyond ASCII though, it is best not to make any assumptions.

Rendering lines of text using the atlas

edit

Now that we really have all the information we need, we can build a vertex buffer that contains all the information necessary to render a complete line of text. Let's adapt the render_text() function from the previous tutorial to use the texture atlas:

void render_text(const char *text, float x, float y, float sx, float sy) {
  struct point {
    GLfloat x;
    GLfloat y;
    GLfloat s;
    GLfloat t;
  } coords[6 * strlen(text)];

  int n = 0;

  for(const char *p = text; *p; p++) { 
    float x2 =  x + c[*p].bl * sx;
    float y2 = -y - c[*p].bt * sy;
    float w = c[*p].bw * sx;
    float h = c[*p].bh * sy;

    /* Advance the cursor to the start of the next character */
    x += c[*p].ax * sx;
    y += c[*p].ay * sy;

    /* Skip glyphs that have no pixels */
    if(!w || !h)
      continue;

    coords[n++] = (point){x2,     -y2    , c[*p].tx,                                            0};
    coords[n++] = (point){x2 + w, -y2    , c[*p].tx + c[*p].bw / atlas_width,   0};
    coords[n++] = (point){x2,     -y2 - h, c[*p].tx,                                          c[*p].bh / atlas_height}; //remember: each glyph occupies a different amount of vertical space
    coords[n++] = (point){x2 + w, -y2    , c[*p].tx + c[*p].bw / atlas_width,   0};
    coords[n++] = (point){x2,     -y2 - h, c[*p].tx,                                          c[*p].bh / atlas_height};
    coords[n++] = (point){x2 + w, -y2 - h, c[*p].tx + c[*p].bw / atlas_width,                 c[*p].bh / atlas_height};
  }

  glBufferData(GL_ARRAY_BUFFER, sizeof coords, coords, GL_DYNAMIC_DRAW);
  glDrawArrays(GL_TRIANGLES, 0, n);
}

First we define a struct to hold the vertex and texture coordinates for each point, to make it slightly easier to write the code to fill in all the values. We need 6 points for each character we will render, since we have to use GL_TRIANGLES instead of a GL_TRIANGLE_STRIP to be able to render separate quads. In the loop, we perform the same calculations as before, but using the cached glyph metrics. We add an optimisation to skip drawing glyphs that have zero width and/or height, such as the space character. After the loop has filled in our vertex buffer, we just need to upload it to the graphics card, and tell it to render all the triangles.

Exercises

edit
  • Make a struct atlas that holds the texture and cache for a given font and font size, so that you do not have any global variables anymore. Make it so you can pass a pointer to such as struct to render_text().
  • If you draw misaligned text, you should see much more rendering artifacts than in the previous tutorial. Can you explain why this happens? How would you prevent this from happening?
  • Use an index buffer object so that you only need to have 4 points per character in the vertex buffer. Is this a worthwhile optimization?
  • Since we are dealing with pixel-aligned coordinates, we don't really need floating point coordinates. OpenGL also supports 16 bit integer coordinates (GL_SHORT). Convert the glyph metrics cache and vertex buffer to use that instead, and change the shaders as necessary.
  • If you have a VBO with GL_SHORT coordinates, is it still worthwhile to use an IBO?
  • The texture atlas techniques also makes it easy to cache the VBO (and IBO), so that you don't have to recalculate anything if the text doesn't change. It is also very common that only the last part of the text changes. How could you optimize this?

Troubleshooting

edit

If you are having trouble getting it to render correctly, go through this list.

  • In newer versions of OpenGL the value GL_ALPHA is deprecated for use in the function glTexImage2D. Replace it with GL_RED and modify the shader to read the red channel to fix this. See the glTexImage2D documentation for list of valid values.


< OpenGL Programming

Browse & download complete code