Part 7: Text
In the previous chapter we saw the need for text display in our applications. From the simplest task to display some basic information to the user, to the display of a series of menu options for the user to select.
Displaying text from a simple program is easy. In C we have the printf function which prints text to the screen. As we saw in the first part of the book in C++ the equivalent id the std::out stream.
In complex environments though like OpenGL things are not so easy. Here we must create the objects that represent the characters we want to display.
The simplest way to display text is to create a texture image on which to draw all the characters of a font, and then use it to render quads with those characters. This technique relies on the things we have learned before about drawing with textures.
Here we are going to present a simpler technique that relies on the Windows system and generates a drawing list based on any system font. This drawing list can be used to render any text on the screen.
In the second section of this part, we will see how we can use the Windows system again to create a font with solid characters that can be used in any scene.
2D text rendering
I gave a brief description how to render 2D text on the screen using a texture. The technique we are going to see here is like that, only this time, most of the job is done by the system. It is Windows and OpenGL that do all the dirty work of creating the texture and generating the render commands required to draw each character.
First we generate a storage for the drawing commands for each character. This is the list that we generate which can store 256 characters. Then we ask Windows to generate a font as if we were going to draw some text on the screen. Finally, we instruct the Windows implementation of OpenGL to create the font textures and store the appropriate drawing commands in the character list we created. The build_2d function in the cg_font class generates the 'font' for 2D text.
void cg_font::build_2d(const char* name, int size)
{
HFONT hFont;
HDC hDC = wglGetCurrentDC();
listBase = glGenLists(256);
if (strcmp(name, "symbol") == 0)
{
hFont = CreateFontA(-size, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, SYMBOL_CHARSET,
OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY,
FF_DONTCARE | DEFAULT_PITCH, name);
}
else
{
hFont = CreateFontA(-size, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET,
OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY,
FF_DONTCARE | DEFAULT_PITCH, name);
}
if (!hFont)
return;
SelectObject(hDC, hFont);
wglUseFontBitmaps(hDC, 0, 256, listBase);
}
Now that we have everything set up, we can use the font to render our text on the screen. This done by the render function. This function has similar argument list as the printf function of the C standard library. This was chosen for easier passing of variables and formating.
void cg_font::render(const char* str, ...)
{
char text[1512];
va_list args;
if (str == NULL)
return;
va_start(args, str);
vsprintf(text, str, args);
va_end(args);
glColor4f(color2d.x, color2d.y, color2d.z, color2d.w);
glWindowPos2i(screenPos.x, screenPos.y);
glPushAttrib(GL_LIST_BIT);
glListBase(listBase);
glCallLists((GLsizei)strlen(text), GL_UNSIGNED_BYTE, text);
glPopAttrib();
}
Our 2D font class does not need a shader because it is only used to set the rendering color. OpenGL has several functions to do it. We set all four components of the color using glColor4f function which takes four floating point values, one for each color component. Then we do some housekeeping, by storing the current state of OpenGL before we activate the bitmap font storage and call it to render our text. Then we restore the state of OpenGL, and we return from the rendering function.
3D text rendering
Our next tool to display text is fancier. It creates three dimensional objects from the character description in the system font. This way we have real 3D objects that can be incorporated in our scenes like any other object we have seen so far.
The creation process of the 3D font is the same as the 2D font we saw earlier. The function that does the job is called build_3d. The main difference when we generate 3D fonts is the function wglUseFontOutlines, we call to generate the 3d objects. This function has two arguments that we need to pay some attention to.
The first is the depth of the character. These characters are created by extrusion, so we need to provide the depth. The second argument is the buffer that receives the dimensions of each character. We will need these dimensions when drawing the text.
void cg_font::build_3d(const char* name, int size, float depth)
{
HFONT hFont;
HDC hDC = wglGetCurrentDC();
listBase = glGenLists(256); // create storage for 96 characters
if (strcmp(name, "symbol") == 0)
{
hFont = CreateFontA(-size, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, SYMBOL_CHARSET,
OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY,
FF_DONTCARE | DEFAULT_PITCH, name);
}
else
{
hFont = CreateFontA(size, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET,
OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY,
FF_DONTCARE | DEFAULT_PITCH, name);
}
if (!hFont)
return;
SelectObject(hDC, hFont);
wglUseFontOutlines(hDC, 0, 255, listBase, (FLOAT)0.0f, (FLOAT)depth, WGL_FONT_POLYGONS, gmf);
}
Since this font creates 3D objects we do not need a custom shader. Instead, we use the normal shader of our scene. We can have a custom shader if we want though.
Our font class allows us to align the text at the bottom-left, bottom-center or bottom-right point. This requires us to calculate the length of the text prior to rendering to calculate how to place the text in our scene.
There is one thing we need to address when we render our text. Every character is a different object and must be positioned in the scene. So, we define a different model matrix for each one changing the translation matrix based on the alignment and stepping along as we move to the next character.
The render function for the 3D text has two more arguments than the one in 2D text. These are the shader and the alignment we described before. The rest of the arguments are the same as before or the same as printf.
void cg_font::render(cg_shader* shader, int alignment, const char* str, ...)
{
cg_float length = 0;
char text[512];
va_list args;
if ((str == NULL))
return;
va_start(args, str);
vsprintf(text, str, args);
va_end(args);
// center the text
// find length of text
for (unsigned int loop = 0; loop < (strlen(text)); loop++)
{
// increase length by character's width
length += gmf[text[loop]].gmfCellIncX;
}
vec3 offset(0, 0, 0);
switch (alignment)
{
case ALIGN_CENTER:
offset.x = -length / 2;
break;
case ALIGN_LEFT:
break;
case ALIGN_RIGHT:
offset.x = -length;
break;
}
// draw the text
glPushAttrib(GL_LIST_BIT);
glListBase(listBase);
for (auto i = 0; i < strlen(text); ++i)
{
mat4 ttm = tmat;
translate_matrix(ttm, offset);
// putting translation at the end treats the text as a unit
mat4 ob_matrix = rmat * smat * ttm;
shader->set_mat4("model", ob_matrix);
glCallLists(1, GL_UNSIGNED_BYTE, &text[i]);
offset.x += gmf[text[i]].gmfCellIncX;
}
glPopAttrib();
}
Summary
In this chapter we addressed the rendering of text in OpenGL. However easy it may seem; we all know that text display is a major component in our communication with the user of our program.
We have covered the following:
- Two-dimensional text
- Three-dimensional text