OpenGL Programming/Modern OpenGL Tutorial Text Rendering 01

From Wikibooks, open books for an open world
Jump to: navigation, search
Basic text

Introduction[edit]

It is very likely that at one point you will want to draw text with OpenGL. It may seem like a very basic functionality of any drawing API, but you will not find any function in OpenGL that deals with text. There are good reasons for that: text is much more complicated than you think. If you are American and use a fixed-width font, then life is simple. You don't have more than 128 possible characters to think about, and they are all the same size. If you have a proportional-width font, things are already getting more difficult. If you are European, then 256 characters might be just enough for one language, but it is already impossible to represent all European languages with that. If you include the rest of the world, then you need more than 65536 (16-bit) characters, and text might need to be rendered from right to left or from top to bottom, and you might have to use other techniques than just drawing characters next to each other on the screen to produce something intelligible. Related to text rendering, you might also want to render mathematical equations, Feynman diagrams, chess board diagrams or sheet music. I hope you are convinced now that text rendering is a very high-level function, that has no place in a low-level graphics API such as OpenGL.

That said, we still want to draw text with OpenGL. There are various ways to do this:

  • Draw letter shapes using GL_LINES.
  • Draw filled letter shapes using GL_TRIANGLES.
  • Draw real 3D letters.
  • Draw letters using textured quads.
  • Draw complete sentences or paragraphs using a textured quads.
  • Draw text directly to the framebuffer with glDrawPixels() (not available in OpenGL ES 2.0).

In this tutorial, we will start with rendering very simple (US-ASCII) text using one textured quad per letter, or in font terminology, "glyphs". This technique is quite flexible, and if you are able to cache textures properly, it is also one of the fastest ways to render text. With a little care, the visual quality of the text is as good as the text rendered by your browser or word processor.

The FreeType library[edit]

In order to draw text, we will need some way to read a font, and have it converted into a format that we can use with OpenGL. Many operating systems have a standard way to read fonts, but there are also many libraries that can do this. A very good, well-known, cross-platform library is FreeType. It supports a lot of font formats, including TrueType and OpenType.

With FreeType, you can look up characters, query their dimensions, find out how to position them more or less correctly, and most importantly, provides you with a greyscale image of any character. This is exactly the functionality that we need for the text drawing method that we will use in this tutorial.

While FreeType allows you to access font data, it is not a text layout engine. That means it will not render whole lines of text or paragraphs for you. It also cannot automatically render diacritics, automatically use ligatures or reproduce other complex typographic features. If you need this functionality, you should use a text layout library such as Pango, and draw a whole text at a time instead of a character at a time. This can use more memory and will certainly be slower when dynamically changing text is drawn.

Using FreeType is quite simple. The following two lines should be added to the top of the source code to include the right headers:

#include <ft2build.h>
#include FT_FREETYPE_H

Before using any other FreeType function, we need to initialize the library:

FT_Library ft;
 
if(FT_Init_FreeType(&ft)) {
  fprintf(stderr, "Could not init freetype library\n");
  return 1;
}

Fonts and Glyphs[edit]

The term font can have different meanings, but generally we think of "Times New Roman" or "Helvetica" as fonts. We also make a distinction between regular, bold, italic, and other styles, so "Helvetica Bold" is a different font than "Helvetica Italic". If you use the FreeType library, you have to specify the full filename of the file containing the font you want to render text with. In FreeType, this is called a "face". For example, to load the regular FreeSans font from the current directory, we use:

FT_Face face;
 
if(FT_New_Face(ft, "FreeSans.ttf", 0, &face)) {
  fprintf(stderr, "Could not open font\n");
  return 1;
}

After we have loaded the face, we basically have only one parameter we can adjust, and that is the font size. To set it to a height of 48 pixels, we use:

FT_Set_Pixel_Sizes(face, 0, 48);

A face is basically a collection of glyphs. A glyph is usually a single character, but it can also be a diacritic or a ligature. A font can also contain more than one glyph for the same character, providing alternative renderings. (See for example the list of features of the excellent Linux Libertine font.) Even Unicode characters do not necessarily have a one-to-one mapping to font glyphs. We will forget about all this complexity though, and focus on the good old ASCII character set. For example, to get the glyph for the character "X" from the font, we use the following code:

if(FT_Load_Char(face, 'X', FT_LOAD_RENDER)) {
  fprintf(stderr, "Could not load character 'X'\n");
  return 1;
}

The FT_Load_Char() function will fill store all the information of that character into the face's "glyph slot", which can be accessed through face->glyph. Because we specified FT_LOAD_RENDER, FreeType will also have created an 8-bit greyscale image that can be accessed via face->glyph->bitmap. Because it is tedious to write face->glyph all the time, and because the pointer face->glyph never changes, we will define the following variable as a shortcut:

FT_GlyphSlot g = face->glyph;

We will use the following information in this tutorial:

g->bitmap.buffer 
Pointer to the 8-bit greyscale image of the glyph, rendered at the previously selected font size.
g->bitmap.width 
Width of the bitmap, in pixels.
g->bitmap.rows 
Height of the bitmap, in pixels.
g->bitmap_left 
Horizontal position relative to the cursor, in pixels.
g->bitmap_top 
Vertical position relative to the baseline, in pixels.
g->advance.x 
How far to move the cursor horizontally for the next character, in 1/64 pixels.
g->advance.y 
How far to move the cursor vertically for the next character, in 1/64 pixels, almost always 0.

Why all these values? The reason is that not every glyph has the same size. FreeType renders an image that is just large enough to contain the visible parts of the character. That means a period "." has only a very small bitmap, and the "X" will have a large bitmap. This is why it is important to know the width and height of the bitmap. The comma "," and apostrophe "'" might be rendered as the same bitmap, but the position relative to the baseline is very different. The "X" character starts at the baseline but extends to very high above it, while the "p" character doesn't go that high but dips below the baseline. These things make it necessary to know the offset of the bitmap relative to the cursor and baseline. Furthermore, the visible size of a character does not necessarily tell you how far to move the cursor for the next character. Think for example about the space character!

Shaders[edit]

For text rendering, we can usually settle for very basic shaders. Since text is basically two-dimensional, we could use an attribute vec2 for vertices, and another attribute vec2 for texture coordinates. But it is also possible to combine the vertex and texture coordinates into a single four-dimensional vector, and have the vertex shader split it in two:

#version 120
 
attribute vec4 coord;
varying vec2 texcoord;
 
void main(void) {
  gl_Position = vec4(coord.xy, 0, 1);
  texcoord = coord.zw;
}

Although this might not be directly obvious, the best way to draw text is to use a texture that contains only alpha values. The RGB color itself is set to the same value for all the pixels. Where the alpha value is 1 (opaque), the font color is drawn. Where it is 0 (transparent), the background color is drawn. Where the alpha value is between 0 and 1, the background color is allowed to mix with the font color. The fragment shader is as follows:

#version 120
 
varying vec2 texcoord;
uniform sampler2D tex;
uniform vec4 color;
 
void main(void) {
  gl_FragColor = vec4(1, 1, 1, texture2D(tex, texcoord).a) * color;
}

Although one could also have written vec4(color.rgb, texture2D(tex, texcoord).a), this fragment shader allows us to render transparent text. The fragment shader should be used in combination with blending:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Rendering lines of text[edit]

Before we can start rendering text, there are still some things that need initialization. First, we will use a single texture object to render all the glyphs:

GLuint tex;
glActiveTexture(GL_TEXTURE0);
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glUniform1i(uniform_tex, 0);

To prevent certain artifacts when a character is not rendered exactly on pixel boundaries, we should clamp the texture at the edges, and enable linear interpolation:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

It is also very important to disable the default 4-byte alignment restrictions that OpenGL uses for uploading textures and other data. Normally you won't be affected by this restriction, as most textures have a width that is a multiple of 4, and/or use 4 bytes per pixel. The glyph images are in a 1-byte greyscale format though, and can have any possible width. To ensure there are no alignment restrictions, we have to use this line:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

We also need to set up a vertex buffer object for our combined vertex and texture coordinates:

GLuint vbo;
glGenBuffers(1, &vbo);
glEnableVertexAttribArray(attribute_coord);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glVertexAttribPointer(attribute_coord, 4, GL_FLOAT, GL_FALSE, 0, 0);

We now have everything in place to render a line of text. The recipe we will use is simple. We start with a certain baseline (vertical) and cursor (horizontal) position, load the first character, upload it as a texture, draw it at the correct offset from the starting position, and then move the cursor to the next position. We repeat this for all the characters in the line.

void render_text(const char *text, float x, float y, float sx, float sy) {
  const char *p;
 
  for(p = text; *p; p++) {
    if(FT_Load_Char(face, *p, FT_LOAD_RENDER))
        continue;
 
    glTexImage2D(
      GL_TEXTURE_2D,
      0,
      GL_ALPHA,
      g->bitmap.width,
      g->bitmap.rows,
      0,
      GL_ALPHA,
      GL_UNSIGNED_BYTE,
      g->bitmap.buffer
    );
 
    float x2 = x + g->bitmap_left * sx;
    float y2 = -y - g->bitmap_top * sy;
    float w = g->bitmap.width * sx;
    float h = g->bitmap.rows * sy;
 
    GLfloat box[4][4] = {
        {x2,     -y2    , 0, 0},
        {x2 + w, -y2    , 1, 0},
        {x2,     -y2 - h, 0, 1},
        {x2 + w, -y2 - h, 1, 1},
    };
 
    glBufferData(GL_ARRAY_BUFFER, sizeof box, box, GL_DYNAMIC_DRAW);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
 
    x += (g->advance.x >> 6) * sx;
    y += (g->advance.y >> 6) * sy;
  }
}

The function render_text() takes 5 arguments: the string to render, the x and y start coordinates, and the x and y scale parameters. The last two should be chosen such that one glyph pixel corresponds to one screen pixel. Let's look at the display() function which draws the whole screen:

void display() {
  glClearColor(1, 1, 1, 1);
  glClear(GL_COLOR_BUFFER_BIT);
 
  GLfloat black[4] = {0, 0, 0, 1};
  glUniform4fv(uniform_color, 1, black);
 
  float sx = 2.0 / glutGet(GLUT_WINDOW_WIDTH);
  float sy = 2.0 / glutGet(GLUT_WINDOW_HEIGHT);
 
  render_text("The Quick Brown Fox Jumps Over The Lazy Dog",
              -1 + 8 * sx,   1 - 50 * sy,    sx, sy);
  render_text("The Misaligned Fox Jumps Over The Lazy Dog",
              -1 + 8.5 * sx, 1 - 100.5 * sy, sx, sy);
 
  glutSwapBuffers();
}

We start by clearing the screen to white, and setting the font color to black. Since we are not using any transformation matrix, we can simply calculate the scaling factors by dividing 2 by the screen's width and height. The first line (a well-known pangram) is aligned exactly to pixel coordinates. The second line is deliberately misaligned by half a pixel in each direction. The difference is obvious; the second line looks more fuzzy and some ugly artifacts are visible.

You might naively think that it would be better to have textures that are twice or more times bigger than how you draw them, so that OpenGL does the anti-aliasing. Unless you use multi-sampling or FSAA, this is not what will happen. It will always be better to have FreeType render the font at the right size, and render it correctly aligned. To illustrate, let's draw the 48 point font scaled down 2 and 4 times, and compare that to "unscaled" 24 and 12 point font sizes. Add the following to the display() function:

  FT_Set_Pixel_Sizes(face, 0, 48);
  render_text("The Small Texture Scaled Fox Jumps Over The Lazy Dog",
              -1 + 8 * sx,   1 - 175 * sy,   sx * 0.5, sy * 0.5);
  FT_Set_Pixel_Sizes(face, 0, 24);
  render_text("The Small Font Sized Fox Jumps Over The Lazy Dog",
              -1 + 8 * sx,   1 - 200 * sy,   sx, sy);
  FT_Set_Pixel_Sizes(face, 0, 48);
  render_text("The Tiny Texture Scaled Fox Jumps Over The Lazy Dog",
              -1 + 8 * sx,   1 - 235 * sy,   sx * 0.25, sy * 0.25);
  FT_Set_Pixel_Sizes(face, 0, 12);
  render_text("The Tiny Font Sized Fox Jumps Over The Lazy Dog",
              -1 + 8 * sx,   1 - 250 * sy,   sx, sy);

You should see that despite the linear texture interpolation of OpenGL, the quality of the downscaled text is worse than the unscaled text. There are several reasons for this. First is that by scaling the text, you are letting OpenGL anti-alias an already anti-aliased glyph image. Second, linear texture interpolation uses a weighted average of at most four texture elements, and is not really the same as calculating the average of a 2x2 or 4x4 region from the texture. Last but not least, the FreeType library will by default apply hinting to improve the contrast of the characters. The effect of hinting is lost when the pixels of the hinted glyph image are not mapped on-to-one to screen pixels.

Rendering colored and/or transparent text is easy, we just change the uniform color to our liking:

  FT_Set_Pixel_Sizes(face, 0, 48);
  render_text("The Solid Black Fox Jumps Over The Lazy Dog",
              -1 + 8 * sx,   1 - 430 * sy,   sx, sy);
 
  GLfloat red[4] = {1, 0, 0, 1};
  glUniform4fv(uniform_color, 1, red);
  render_text("The Solid Red Fox Jumps Over The Lazy Dog",
              -1 + 8 * sx,   1 - 330 * sy,   sx, sy);
  render_text("The Solid Red Fox Jumps Over The Lazy Dog",
              -1 + 28 * sx,  1 - 450 * sy,   sx, sy);
 
  GLfloat transparent_green[4] = {0, 1, 0, 0.5};
  glUniform4fv(uniform_color, 1, transparent_green);
  render_text("The Transparent Green Fox Jumps Over The Lazy Dog",
              -1 + 8 * sx,   1 - 380 * sy,   sx, sy);
  render_text("The Transparent Green Fox Jumps Over The Lazy Dog",
              -1 + 18 * sx,  1 - 440 * sy,   sx, sy);

Exercises:

  • Try changing the background color.
  • Try using GL_LUMINANCE and GL_INTENSITY instead of GL_ALPHA.
  • Try changing the blending to glBlendFunc(GL_SRC_ALPHA, GL_ZERO).
  • Try removing the call to glPixelStorei().
  • Try different texture wrapping and interpolation modes.
  • Try drawing the text "First line\nSecond line". What happened?
  • Draw the baselines and cursors for every character.
  • Add a transformation matrix and use it to rotate the text by 30 degrees.
  • Use a perspective transformation matrix, and look at the text from an oblique angle.

< OpenGL Programming

Browse & download complete code