I’ve been experimenting with OpenGL lately. One of my experiments was rendering text in an OpenGL window. Now, there are several libraries out there that do the thing, they can even handle TrueType fonts and stuff, but I wanted something simple and I wanted to do it myself.
So, the idea is the following: you have a picture with letters, numbers and special symbols, you load it into your OpenGL application and then render lines of text using the symbols provided in the picture.
The solution I came up with is to store the picture with symbols as an OpenGL texture and render the text as series of texturized quads – applying the relevant piece of the texture to each of them. To make things faster, instructions for rendering each character are put into a display list. Let’s see how the whole thing is done.
We start by declaring a class that will create the texture and display lists. Nothing particularly interesting here.
#include "SDL/SDL.h"
#include "GL/gl.h"
#include "GL/glu.h" // for gluOrtho2D
#include <iostream>
#include <stdexcept>
class Font
{
public:
explicit Font(const char*);/* Builds a font from bitmap file */
~Font();
void render(const char*);
private:
SDL_Surface* surface; /* An SDL surface containing the characters */
GLuint texture; /* OpenGL texture */
GLuint dl; /* First of the 96 display lists used to draw printable characters */
};
Now let’s jump to the implementation of the Font ctor. It does several things:
- Loads a bitmap from a file
- Creates a new OpenGL texture, sets some options and sets image data for it
- Generates display lists, one per character. Each display list draws a quad with a different piece of the texture applied to it, depending on which character it is associated with
To load a bitmap from file, we use the SDL_LoadBMP function. After that we create a new texture with glGenTextures. Then we need to set image data for our texture. OpenGL expects raw image data, which is contained in the “pixels” field of the SDL_Surface structure. It is important for us to know how this data is arranged, because we’ll need to pass that information to the glTexImage2D function, which is used to specify the texture image. Namely, we’ll need to know the width and height of the bitmap, number of color components and pixel format. Note that the width and height of the bitmap should be a power of 2. I don’t know the exact reason for this limitation yet, but I think it might have something to do with memory alignment issues.
In this particular example, the bitmap that we’ll be using is a 1024×8 image that looks like this. It has 3 color components (R, G, B, there is no aplha component), and the pixel format is RGB (not BGR). In this piece of code, we simply make those assupmtions to keep things short and simple, but in a real-world application you’d probably want to take this information from the “format” field of the SDL_Surface structure.
After we’ve set the image data for the texture, it’s time to create the display lists. Note: this example only supports ASCII printable characters in the range from space to tilde (~). Also, we know how the glyphs are arranged in the bitmap and that the width and height of each glyph is exactly 8 pixels. If I wanted to make things more generic, I’d add the possibility to supply a separate text file that would provide texture coordinates for each symbol. Also, some people on the internet have suggested to use vertex buffers instead of display lists (because they have better performance), but I don’t know what they are and how to use them yet
/*
* Builds the font from a bitmap file.
* The entire file is read as a texture and then we create display lists
* for each printable character. Those lists just create a small quad, applying
* the relevant piece of the texture to it.
*/
Font::Font(const char* bitmap_file_name)
{
if(NULL == (surface = SDL_LoadBMP(bitmap_file_name))) throw std::runtime_error(SDL_GetError());
// Set up the texture
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, 3, surface->w, surface->h, 0, GL_RGB, GL_UNSIGNED_BYTE, surface->pixels);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
// Now compile the glyphs into display lists
dl = glGenLists(96);
for(char c = ' '; c <= '~'; ++c){
int idx = c - ' ';
glNewList(dl+idx, GL_COMPILE);
glBegin(GL_QUADS);
glTexCoord2i(8*idx, 0);
glVertex2i(0, 0);
glTexCoord2i(8*idx + 8, 0);
glVertex2i(8, 0);
glTexCoord2i(8*idx + 8, 8);
glVertex2i(8, 8);
glTexCoord2i(8*idx, 8);
glVertex2i(0, 8);
glEnd();
glEndList();
}
}
Now, let’s move on to the implementation of the render() method. The first thing we have to do is to make texture coordinates usable for our goal. By default, the origin of the texture coordinate system is at the upper-left corner of the image (0,0) and the lower right corner has the coordinates (1,1). This is not what we want. We want the lower-right corner to have coordinates (1024, 8), so that we can use pixel coordinates. This can be accomplished by modifying the texture matrix with glScalef. Then we just iterate through the string, calling the appropriate display lists. Note how we preserve the modelview matrix between calls and restore the previous texture matrix at the end of the procedure.
/*
* Renders a string on the screen
*/
void Font::render(const char* c)
{
glBindTexture(GL_TEXTURE_2D, texture);
glMatrixMode(GL_TEXTURE);
glPushMatrix();
glLoadIdentity();
glScalef(1.0/surface->w, 1.0/surface->h, 1.0);
glMatrixMode(GL_MODELVIEW);
for(int i = 0; i < strlen(c); ++i)
{
int idx = c[i] - ' ';
if(idx < 0 || idx >= 96) throw std::runtime_error("Non-printable character encountered");
glPushMatrix();
glTranslatef(8*i, 0, 0);
glCallList(dl + idx);
glPopMatrix();
}
glMatrixMode(GL_TEXTURE);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
}
The rest is pretty straightforward. It’s the dtor for Font, a routine to set up SDL and OpenGL and main(). Note how we’re able to use glColor3f to set the text color!
void prepare(); /* Prepares the environment (set up SDL and OpenGL */
int main()
{
try
{
prepare();
Font fnt("font.bmp");
glColor3f(1.0, 0.0, 0.0);
glTranslatef(208, 200, 0);
fnt.render("HELLO, OPENGL TEXTURE FONTS!");
SDL_GL_SwapBuffers();
SDL_Delay(30000);
}
catch(std::runtime_error& e)
{
std::cerr << "ERROR: " << e.what() << "\n";
return 1;
}
return 0;
}
/*
* Sets up SDL and OpenGL.
* Sets a 640x480 32-BPP video mode
* Sets a coordinate system for the OpenGL window, (0,0) being the top
* left corner, (640, 480) being the bottom right.
*/
void prepare()
{
// Initialize SDL and set video mode
if(SDL_Init(SDL_INIT_EVERYTHING) < 0) throw std::runtime_error(SDL_GetError());
if(NULL == SDL_SetVideoMode(640, 480, 32, SDL_OPENGL | SDL_GL_DOUBLEBUFFER)) throw std::runtime_error(SDL_GetError());
atexit(SDL_Quit);
// Prepare OpenGL
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0, 640, 480, 0);
glMatrixMode(GL_MODELVIEW);
glEnable(GL_TEXTURE_2D);
glClearColor(.0, .0, .0, .0);
}
/*
* Frees allocated resources (SDL surface, texture and dl's)
*/
Font::~Font()
{
SDL_FreeSurface(surface);
glDeleteTextures(1, &texture);
glDeleteLists(dl, 96);
}
If you want to compile it (and see this), don’t forget to link your executable against the SDL, GL and GLU libraries.
Recent Comments