Lesson 8: Drawing Text

Drawing Text in OpenGL

Drawing text is fairly important on occasion. There are a few possible approaches to drawing text in OpenGL. I'll outline four approaches and their pros and cons.

  1. You can use bitmaps, not the kind that uses an image file, but a certain OpenGL construct that I haven't shown yet. Each character is represented as a bitmap. Each pixel in the bitmap has a bit, which is 1 if the pixel is colored and 0 if it is transparent. Each frame, you'd send the bitmaps for the characters to the graphics card. The graphics card would then bypass the usual 3D transformations and just draw the pixels right on the top of the window. I'm not a fan of this approach. It's slow, as you have to send each bitmap to the graphics card each frame, which is a lot of data. The method is also inflexible; you can't scale or transform the characters very well. You can do it in GLUT using glutBitmapCharacter, whose documentation is at this site. But again, there are a lot of disadvantages to the technique.
  2. You can represent characters using textures. Each character would correspond to a certain part of some texture, with some of the pixels in the texture white and the rest transparent (which I haven't shown how to do yet). You would draw a quadrilateral for each character and map the appropriate part of the appropriate texture to it. This approach is alright; it gives you some flexibility as to how and where you draw characters in 3D. It's also pretty fast. But the characters wouldn't scale too well; they'll look pixelated if you zoom in too far.
  3. You can draw a bunch of lines in 3D, using GL_LINES (which I also haven't shown yet, although you can probably guess how GL_LINES works). This technique is fast and does allow scaling and otherwise transforming characters. However, the characters would look better if they covered an area rather than a perimeter. Also, it's fairly tedious to figure out a set of lines to represent each character. You can draw outlined text in GLUT using glutStrokeCharacter, whose documentation is at this site.
  4. You can draw a bunch of polygons in 3D. This technique also allows us to transform characters well. It even lets us give the characters 3D depth, so that they look 3D rather than flat. However, it's slower than drawing lines and using textures. Also, it's even more annoying to figure out how to describe each character as a set of polygons than it is to figure out how to describe one as a set of lines.

How We'll Draw Text

Of the four techniques presented for drawing text in OpenGL, we'll be using the last one. I've already done most of the work for it. Using the open-source 3D modeling program Blender, I used the "add text" feature to come up with a 3D model for each of the 95 printable ASCII characters. I used the decimate tool to reduce their polygon count, so that they average around 40 polygons each. I gave each character some 3D depth, saved each character to a separate file, and used a program to load the files and output them into a special file format I designed. Then, I wrote code to load the models from the file and display them using handy t3dDraw2D and t3dDraw3D functions, which I will describe later.

Details aside, the basic idea is that there is a file with all of the positions of the 3D polygons for the different characters. The t3dDraw2D and t3dDraw3D functions take care of drawing the appropriate triangles. The functions themselves use some OpenGL techniques I haven't show yet, in order to make them draw as quickly as possible.

How fast are these functions? The 2D drawing function draws about 40 triangles per character. Graphics cards can draw millions of triangles per second. So, we would expect the function to be able to draw about 1,000,000 / 40 = 25,000 characters per second, which is about what I observed in a little test I rigged up. So, if you're not drawing tons of characters, it should be sufficient, but if you are, you might want to switch to using glutStrokeCharacter to draw lines rather than polygons.

The Source Code

We're going to put 3D text on each of the four sides of a square, so that our program will look like this:

Drawing 3D text screenshot

Let's look at the source.

//Computes a scaling value so that the strings
float computeScale(const char* strs[4]) {
    float maxWidth = 0;
    for(int i = 0; i < 4; i++) {
        float width = t3dDrawWidth(strs[i]);
        if (width > maxWidth) {
            maxWidth = width;
        }
    }
    
    return 2.6f / maxWidth;
}

Each side of the square will have a length of 3. We want the longest string to take up 2.6 units on the square, so we use a computeScale function to determine the factor by which we should scale the text. We go through each of the four strings, use t3dDrawWidth to determine the draw width of the strings as a multiple of the height of the font. We take 2.6 divided by the maximum width to be our scaling factor.

//The four strings that are drawn
const char* STRS[4] = {"Video", "Tutorials", "Rock", ".com"};

The array STRS contains the strings that we will draw.

void cleanup() {
    t3dCleanup();
}

Our cleanup function for this project calls t3dCleanup(), from the header "text3d.h", which frees the memory used by the text drawing functionality.

void initRendering() {
    //...
    t3dInit();
}

In our initRendering function, we have to set up some stuff for drawing 3D text. Namely, we have to load the positions of the triangles for each character from the file "charset". So we call t3DInit(), which is also from text3d.h.

void drawScene() {
    //...
    
    //Draw the strings along the sides of a square
    glScalef(_scale, _scale, _scale);
    glColor3f(0.3f, 1.0f, 0.3f);
    for(int i = 0; i < 4; i++) {
        glPushMatrix();
        glRotatef(90 * i, 0, 1, 0);
        glTranslatef(0, 0, 1.5f / _scale);
        t3dDraw3D(STRS[i], 0, 0, 0.2f);
        glPopMatrix();
    }
    
    //...
}

Here's where we draw the 3D text. First, we scale by the appropriate factor. Then, for each string, we move to the appropriate side of the cube and use t3dDraw3D to draw the string.

The t3dDraw3D has five parameters. You only see four parameters here. That's because we omitted the fifth parameter, so that it will use the default value. If you look at text3d.h, you can see the way that this is accomplished in C++:

void t3dDraw3D(std::string str,
               int hAlign, int vAlign,
               float depth,
               float lineHeight = 1.5f);

Where we have float lineHeight = 1.5f, we're telling the compiler to use a default value of 1.5 for the last parameter if it is omitted.

The first parameter is the string to draw. The second is the horizontal alignment of the string; a negative number is a left alignment, 0 is a centered alignment, and a positive number is a right alignment. The third parameter is the vertical alignment of the string; a negative number is a top alignment, 0 is a centered alignment, and a positive number is a bottom alignment. (You could draw text with multiple lines if you wanted to, using newline characters.) The fourth parameter is the 3D depth of the character, as a multiple of the height of the font. The fifth parameter is the height of each line, as a multiple of the height of the font. It could be used to indicate the spacing between lines, if we were drawing text with multiple lines. But we're not, so we'll just use the default value of 1.5.

If you want to draw text without depth, where all of the polygons are in the same plane, you could call t3dDraw2D, which has the same parameters, except that it omits the depth of the text (since there is no depth). This is faster, since there are fewer polygons to draw, but it doesn't give us the nice-looking 3D text.

int main(int argc, char** argv) {
    //...
    
    _scale = computeScale(STRS);
    
    //...
}

Finally, in our main function, we compute the factor by which we're scaling the text by calling the computeScale function we saw earlier.

There we have it! We've made some 3D text in OpenGL.

Next is "Lesson 9: Animation".