Welcome to yet another Tutorial. This time on I'll be teaching you how to use Bitmap Fonts.
You may be saying to yourself "what's so hard about putting text onto the screen". If you've
ever tried it, it's not easy!
Sure you can load up an art program, write text onto an image, load the image into your OpenGL
program, turn on blending then map the text onto the screen. But this is time consuming, the
final result usually looks blurry or blocky depending on the type of filtering you use, and
unless your image has an alpha channel your text will end up transparent (blended with the
objects on the screen) once it's mapped to the screen.
If you've ever used Wordpad, Microsoft Word or some other Word Processor, you may have noticed
all the different types of Font's avaialable. This tutorial will teach you how to use the
exact same fonts in your own OpenGL programs. As a matter of fact... Any font you install on
your computer can be used in your demos.
Not only do Bitmap Fonts looks 100 times better than graphical fonts. You can change the text
on the fly. No need to make textures for each word you want to write to the screen. Just
position the text on the screen and use my handy new gl command to write the text to the
screen.
I tried to make the command as simple as possible. All you do is type glPrint("Hello"). It's
that easy. Anyways. You can by the long intro that I'm pretty happy with this tutorial. It
took me roughly 1 1/2 hours to create the program. Why so long? Because there is literally
no information available on using Bitmap Fonts, unless of course you enjoy MFC code. In order
to keep the code simple I decided it would be nice if I wrote it all in simple to understand
C code :)
A small note, this code is Windows specific. It uses the wgl functions of Windows to build the
font. Apparently Apple has agl support that should do the same thing, and X has glx.
Unfortunately I can't guarantee this code is portable. If anyone has platform independant code
to draw fonts to the screen, send it my way and I'll write a 3rd font tutorial.
We start off with the typical code from lesson 1. We'll be adding the Math.h header file so
we can move the text around the screen using SIN and COS.
#include <windows.h> // Header File For Windows
#include <math.h> // Header File For Windows Math Library ( ADD )
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <gl\glaux.h> // Header File For The GLaux Library
static HGLRC hRC; // Permanent Rendering Context
static HDC hDC; // Private GDI Device Context
We're going to add 3 new variables as well. 'base' will hold the number of the first display
list we create. Each character requires it's own display list. The character 'A' is 65 in
the display list, 'B' is 66, 'C' is 67, etc. So 'A' would be stored in display list base+65.
Next we add two counters (cnt1 & cnt2). These counters will count up at different rates, and
are used to move the text around the screen using SIN and COS. This creates a semi-random
looking movement on the screen. We'll also use the counters to control the color of the
letters (more on this later).
GLuint base; // Base Display List For The Font Set
GLfloat cnt1; // 1st Counter Used To Move Text & For Coloring
GLfloat cnt2; // 2nd Counter Used To Move Text & For Coloring
BOOL keys[256]; // Array Used For The Keyboard Routine
The following section of code builds the actual font. This was the most difficult part of the
code to write. 'HFONT font' in simple english tells Windows we are going to be manipulating
a Windows font.
Next we define 'base'. We do this by creating a group of 96 display lists using glGenLists(96).
After the display lists are created, the variable 'base' will hold the number of the first list.
GLvoid BuildFont(GLvoid) // Build Our Bitmap Font
{
HFONT font; // Windows Font ID
base = glGenLists(96); // Storage For 96 Characters ( NEW )
Now for the fun stuff. We're going to create our font. We start off by specifying the size
of the font. You'll notice it's a negative number. By putting a minus, we're telling windows
to find us a font based on the CHARACTER height. If we use a positive number we match the font
based on the CELL height.
font = CreateFont( -24, // Height Of Font ( NEW )
Then we specify the cell width. You'll notice I have it set to 0. By setting values to 0,
windows will use the default value. You can play around with this value if you want. Make
the font wide, etc.
0, // Width Of Font
Angle of Escapement will rotate the font. Unfortunately this isn't a very useful feature.
Unless your at 0, 90, 180, and 270 degrees, the font usually gets cropped to fit inside it's
invisible square border. Orientation Angle quoted from MSDN help Specifies the angle, in tenths
of degrees, between each character's base line and the x-axis of the device. Unfortunately I have
no idea what that means :(
0, // Angle Of Escapement
0, // Orientation Angle
Font weight is a great parameter. You can put a number from 0 - 1000 or you can use one of
the predefined values. FW_DONTCARE is 0, FW_NORMAL is 400, FW_BOLD is 700 and FW_BLACK is 900.
There are alot more predefined values, but those 4 give some good variety. The higher the
value, the thicker the font (more bold).
FW_BOLD, // Font Weight
Italic, Underline and Strikeout can be either TRUE or FALSE. Basically if underline is TRUE,
the font will be underlined. If it's FALSE it wont be. Pretty simple :)
FALSE, // Italic
FALSE, // Underline
FALSE, // Strikeout
Character set Identifier describes the type of Character set you wish to use. There are too
many types to explain. CHINESEBIG5_CHARSET, GREEK_CHARSET, RUSSIAN_CHARSET, DEFAULT_CHARSET,
etc. ANSI is the one I use, although DEFAULT would probably work just as well.
If you're interested in using a font such as Webdings or Wingdings, you need to use
SYMBOL_CHARSET instead of ANSI_CHARSET.
ANSI_CHARSET, // Character Set Identifier
Output Precision is very important. It tells Windows what type of character set to use if
there is more than one type available. OUT_TT_PRECIS tells Windows that if there is more than
one type of font to choose from with the same name, select the TRUETYPE version of the font.
Truetype fonts always look better, especially when you make them large. You can also use
OUT_TT_ONLY_PRECIS, which ALWAYS trys to use a TRUETYPE Font.
OUT_TT_PRECIS, // Output Precision
Clipping Precision is the type of clipping to do on the font if it goes outside the clipping
region. Not much to say about this, just leave it set to default.
CLIP_DEFAULT_PRECIS, // Clipping Precision
Output Quality is very important.you can have PROOF, DRAFT, NONANTIALIASED, DEFAULT or
ANTIALIASED. We all know that ANTIALIASED fonts look good :) Antialiasing a font is the
same effect you get when you turn on font smoothing in Windows. It makes everything look less
jagged.
ANTIALIASED_QUALITY, // Output Quality
Next we have the Family and Pitch settings. For pitch you can have DEFAULT_PITCH,
FIXED_PITCH and VARIABLE_PITCH, and for family you can have FF_DECORATIVE, FF_MODERN,
FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE. Play around with them to find out what they do.
I just set them both to default.
FF_DONTCARE|DEFAULT_PITCH, // Family And Pitch
Finally... We have the actual name of the font. Boot up Microsoft Word or some other text
editor. Click on the font drop down menu, and find a font you like. To use the font,
replace 'Courier New' with the name of the font you'd rather use.
"Courier New"); // Font Name
Now we select the font by relating it to our DC, and build the 96 display lists starting at
character 32 (which is a blank space). You can build all 256 if you'd like, just make sure
you build 256 display lists using glGenLists. Make sure you delete all 245 display lists when
you quit the program, and make sure you set 32 to 0 and 96 to 255 in the line below.
SelectObject(hDC, font); // Selects The Font We Created ( NEW )
wglUseFontBitmaps(hDC, 32, 96, base); // Builds 96 Characters Starting At Character 32 ( NEW )
}
The following code is pretty simple. It deletes the 96 display lists from memory starting at
the first list specified by 'base'. I'm not sure if windows would do this for you, but it's
better to be safe than sorry :)
GLvoid KillFont(GLvoid) // Delete The Font
{
glDeleteLists(base, 96); // Delete All 96 Characters ( NEW )
}
Now for my handy dandy GL text routine. You call this section of code with the command
glPrint("message goes here"). The text is stored in the character string *text.
GLvoid glPrint(char *text) // Custom GL "Print" Routine
{
These two lines of code check to see if there's anything to display? If there's no text, text
will equal nothing (NULL), and nothing will be drawn to the screen.
if (text == NULL) // If There's No Text
return; // Do Nothing
If there is text, we push the GL_LIST_BIT, this prevents glListBase from affecting any other
display lists we may be using in our program.
The command glListBase(base-32) is a little hard to explain. Say we draw the letter 'A', it's
number was 65. Without glListBase(base-32) OpenGL wouldn't know where to find this letter.
It would look for it at display list 65. Now if base was equal to 1000, 'A' would actually
be stored at display list 1065. So by setting a base starting point, OpenGL knows where to
get the proper display list from. The reason we subtract 32 is because we never made the first
32 display lists. We skipped them. So we have to let OpenGL know this by subtracting 32
from the base value. I hope that makes sense.
glPushAttrib(GL_LIST_BIT); // Pushes The Display List Bits ( NEW )
glListBase(base - 32); // Sets The Base Character to 32 ( NEW )
Now that OpenGL knows where the Letters are located, we can tell it to write the text to the
screen. glCallLists is a very interesting command. It's capable of putting more than one
display list on the screen at a time.
The line below does the following. First it tells OpenGL we're going to be displaying lists
to the screen. strlen(text) finds out how many letters we're going to send to the screen.
Next it needs to know what the largest list number were sending to it is going to be. We're
not sending any more than 255 characters. So we can use an UNSIGNED_BYTE. (remember a byte
is any value from 0 - 255). Finally we tell it what to display by passing the string 'text'.
In case you're wondering why the letters don't pile on top of eachother. Each display list
for each character knows where the right side of the letter is. After the letter is drawn,
OpenGL translates to the right side of the drawn letter. The next letter or object drawn will
be drawn starting at the last location GL translated to, which is to the right of the last
letter.
Finally we pop the GL_LIST_BIT setting GL back to how it was before we set our base setting
using glListBase(base-32).
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); // Draws The Display List Text ( NEW )
glPopAttrib(); // Pops The Display List Bits ( NEW )
}
The only thing different in the Init code is the line BuildFont(). This jumps to the code above
that builds the font so OpenGL can use it later on.
GLvoid InitGL(GLsizei Width, GLsizei Height) // Will Be Called Right After The GL Window Is Created
{
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Clear The Background Color To Black
glClearDepth(1.0); // Enables Clearing Of The Depth Buffer
glDepthFunc(GL_LESS); // The Type Of Depth Test To Do
glEnable(GL_DEPTH_TEST); // Enables Depth Testing
glShadeModel(GL_SMOOTH); // Enables Smooth Color Shading
glMatrixMode(GL_PROJECTION); // Select The Projection Matrix
glLoadIdentity(); // Reset The Projection Matrix
// Calculate The Aspect Ratio Of The Window
gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix
BuildFont(); // Build The Font ( ADD )
}
Resizing code is exactly the same as the code in Lesson 1.
GLvoid ReSizeGLScene(GLsizei Width, GLsizei Height) // Handles Window Resizing
{
if (Height==0) // Is Window Too Small (Divide By Zero Error)
Height=1; // If So Make It One Pixel Tall
// Reset The Current Viewport And Perspective Transformation
glViewport(0, 0, Width, Height);
glMatrixMode(GL_PROJECTION); // Select The Projection Matrix
glLoadIdentity(); // Reset The Projection Matrix
// Calculate The Aspect Ratio Of The Window
gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix
}
Now for the drawing code. We start off by clearing the screen and the depth buffer. We call
glLoadIdentity() to reset everything. Then we translate one unit into the screen. If we don't
translate, the text wont show up. Bitmap fonts work better when you use an ortho projection
rather than a perspective projection, but ortho looks bad, so to make it work in projection,
translate.
You'll notice that if you translate even deeper into the screen the size of the font does not
shrink like you'd expect it to. What actually happens when you translate deeper is that you
have more control over where the text is on the screen. If you tranlate 1 unit into the screen,
you can place the text anywhere from -0.5 to +0.5 on the X axis. If you tranlate 10 units into the
screen, you place the text from -5 to +5. It just gives you more control instead of using
decimal places to position the text at exact locations. Nothing will change the size of the
text. Not even glScalef(x,y,z). If you want the font bigger or smaller, make it bigger or
smaller when you create it!
GLvoid DrawGLScene(GLvoid) // Handles Rendering
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer
glLoadIdentity(); // Reset The View
glTranslatef(0.0f,0.0f,-1.0f); // Move One Unit Into The Screen
Now we use some fancy math to make the colors pulse. Don't worry if you don't understand
what I'm doing. I like to take advantage of as many variables and stupid tricks as I can to
achieve results :)
In this case I'm using the two counters we made to move the text around the screen to change
the red, green and blue colors. Red will go from -1.0 to 1.0 using COS and counter 1. Green
will also go from -1.0 to 1.0 using SIN and counter 2. Blue will go from 0.5 to 1.5 using
COS and counter 1 and 2. That way blue will never be 0, and the text should never completely
fade out. Stupid, but it works :)
// Pulsing Colors Based On Text Position
glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),1.0f-0.5f*float(cos(cnt1+cnt2)));
Now for a new command. glRasterPos2f(x,y) will position the Bitmapped Font on the screen. The
center of the screen is still 0,0. Notice there's no Z position. Bitmap Fonts only use the
X axis and Y axis. Because we translate one unit into the screen, the far left is -0.5, and
the far right is +0.5. You'll notice that I move 0.2 pixels to the left on the X axis. This
moves the text into the center of the screen. Otherwise it would be more to the right of the
screen because it's drawn from the center to the right.
The fancy(?) math does pretty much the same thing as the color setting math does. It moves
the text on the x axis from -0.55 to +0.15 (remember, we subtract 0.2 right off the start).
This keeps the text on the screen at all times. It swings left to right using COS and counter
1. It moves from -0.35 to +0.35 on the Y axis using SIN and counter 2.
// Position The Text On The Screen
glRasterPos2f(-0.2f+0.35f*float(cos(cnt1)), 0.35f*float(sin(cnt2)));
Now for my favorite part... Writing the actual text to the screen. I tried to make it super
easy, and very user friendly. You'll notice it looks alot like an OpenGL call, combined with
the good old fashioned Print statement :) All you do to write the text to the screen is
glPrint("{any text you want}"). It's that easy. The text will be drawn onto the screen at
the exact spot you positioned it.
glPrint("OpenGL With NeHe"); // Print GL Text To The Screen
The last thing to do is increase both the counters by different amounts so the colors pulse and
the text moves.
cnt1+=0.01f; // Increase The First Counter
cnt2+=0.0081f; // Increase The Second Counter
}
Only one thing has changed in this section of code. The change can be found in the WM_DESTROY
/ WM_CLOSE section of code.
LRESULT CALLBACK WndProc( HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
{
RECT Screen; // Used Later On To Get The Size Of The Window
GLuint PixelFormat; // Pixel Format Storage
static PIXELFORMATDESCRIPTOR pfd= // Pixel Format Descriptor
{
sizeof(PIXELFORMATDESCRIPTOR), // Size Of This Pixel Format Descriptor
1, // Version Number (?)
PFD_DRAW_TO_WINDOW | // Format Must Support Window
PFD_SUPPORT_OPENGL | // Format Must Support OpenGL
PFD_DOUBLEBUFFER, // Must Support Double Buffering
PFD_TYPE_RGBA, // Request An RGBA Format
16, // Select A 16Bit Color Depth
0, 0, 0, 0, 0, 0, // Color Bits Ignored (?)
0, // No Alpha Buffer
0, // Shift Bit Ignored (?)
0, // No Accumulation Buffer
0, 0, 0, 0, // Accumulation Bits Ignored (?)
16, // 16Bit Z-Buffer (Depth Buffer)
0, // No Stencil Buffer
0, // No Auxiliary Buffer (?)
PFD_MAIN_PLANE, // Main Drawing Layer
0, // Reserved (?)
0, 0, 0 // Layer Masks Ignored (?)
};
switch (message) // Tells Windows We Want To Check The Message
{
case WM_CREATE: // Window Creation
hDC = GetDC(hWnd); // Gets A Device Context For The Window
PixelFormat = ChoosePixelFormat(hDC, &pfd); // Finds The Closest Match To The Pixel Format We Set Above
if (!PixelFormat) // No Matching Pixel Format?
{
MessageBox(0,"Can't Find A Suitable PixelFormat.","Error",MB_OK|MB_ICONERROR);
PostQuitMessage(0); // This Sends A 'Message' Telling The Program To Quit
break; // Prevents The Rest Of The Code From Running
}
if(!SetPixelFormat(hDC,PixelFormat,&pfd)) // Can We Set The Pixel Mode?
{
MessageBox(0,"Can't Set The PixelFormat.","Error",MB_OK|MB_ICONERROR);
PostQuitMessage(0); // This Sends A 'Message' Telling The Program To Quit
break; // Prevents The Rest Of The Code From Running
}
hRC = wglCreateContext(hDC); // Grab A Rendering Context
if(!hRC) // Did We Get One?
{
MessageBox(0,"Can't Create A GL Rendering Context.","Error",MB_OK|MB_ICONERROR);
PostQuitMessage(0); // This Sends A 'Message' Telling The Program To Quit
break; // Prevents The Rest Of The Code From Running
}
if(!wglMakeCurrent(hDC, hRC)) // Can We Make The RC Active?
{
MessageBox(0,"Can't Activate GLRC.","Error",MB_OK|MB_ICONERROR);
PostQuitMessage(0); // This Sends A 'Message' Telling The Program To Quit
break; // Prevents The Rest Of The Code From Running
}
GetClientRect(hWnd, &Screen); // Grab Screen Info For The Current Window
InitGL(Screen.right, Screen.bottom); // Initialize The GL Screen Using Screen Info
break;
case WM_DESTROY: // Windows Being Destroyed
case WM_CLOSE: // Windows Being Closed
ChangeDisplaySettings(NULL, 0); // Disable Fullscreen Mode
The next line of code is new. As soon as the GL window is closed, KillFont() will jump to the
section of code above that deletes all 96 characters of the font we created. This is more than
likely a good thing to do just to be on the safe side. (avoid memory leaks, etc).
KillFont(); // Deletes The Font Display List ( ADD )
wglMakeCurrent(hDC,NULL); // Make The DC Current
wglDeleteContext(hRC); // Kill The RC
ReleaseDC(hWnd,hDC); // Free The DC
PostQuitMessage(0); // Quit The Program
break;
case WM_KEYDOWN: // Key Being Held Down
keys[wParam] = TRUE; // Make That Keys Cell True
break;
case WM_KEYUP: // Key Is Released
keys[wParam] = FALSE; // Make That Keys Cell False
break;
case WM_SIZE: // Resizing The Screen
ReSizeGLScene(LOWORD(lParam),HIWORD(lParam)); // Resize To The New Window Size
break;
default:
return (DefWindowProc(hWnd, message, wParam, lParam)); // Pass Windows Messages
}
return (0);
}
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg; // Windows Message Structure
WNDCLASS wc; // Windows Class Structure Used To Set Up The Type Of Window
HWND hWnd; // Storage For Window Handle
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = (WNDPROC) WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = NULL;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = "OpenGL WinClass";
if(!RegisterClass(&wc))
{
MessageBox(0,"Failed To Register The Window Class.","Error",MB_OK|MB_ICONERROR);
return FALSE;
}
hWnd = CreateWindow(
"OpenGL WinClass",
"Jeff Molofee's GL Code Tutorial ... NeHe 2000", // Title Appearing At The Top Of The Window
WS_POPUP | // Popup Window
WS_CLIPCHILDREN | // Clip Child Windows
WS_CLIPSIBLINGS, // Clip Sibling Windows
0, 0, // The Position Of The Window On The Screen
640, 480, // The Width And Height Of The WIndow
NULL,
NULL,
hInstance,
NULL);
if(!hWnd) // Was Window Created?
{
MessageBox(0,"Window Creation Error.","Error",MB_OK|MB_ICONERROR);
return FALSE;
}
I've changed the fullscreen code a little bit in this tutorial in an attempt to make these
programs more Voodoo 3 friendly, and in an attempt to make the code as clean as possible. The
only change is the new line memset(&dmScreenSettings, 0, sizeof(DEVMODE)). What this does is
clears out the section of ram where we store all the fullscreen settings before we actually
change to fullscreen mode. That way any random values that might have been stored in the ram
are wiped out.
DEVMODE dmScreenSettings; // Developer Mode
memset(&dmScreenSettings, 0, sizeof(DEVMODE)); // Clear Room To Store Settings ( NEW )
dmScreenSettings.dmSize = sizeof(DEVMODE); // Size Of The Devmode Structure
dmScreenSettings.dmPelsWidth = 640; // Screen Width
dmScreenSettings.dmPelsHeight = 480; // Screen Height
dmScreenSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT; // Pixel Mode
ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN); // Switch To Full Screen
ShowWindow(hWnd, SW_SHOW); // Show Our Window
UpdateWindow(hWnd); // Update The Window
SetFocus(hWnd); // Set Focus On The Window
wglMakeCurrent(hDC,hRC); // Make DC/RC Current
while (1) // Endless Loop
{
// Process All Messages
while (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
{
if (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
return TRUE;
}
}
DrawGLScene(); // Draw The Scene
SwapBuffers(hDC); // Swap Screen Buffers
if (keys[VK_ESCAPE]) SendMessage(hWnd,WM_CLOSE,0,0); // If ESC Is Pressed Quit
}
}
That's it... Everything you need to know in order to use Bitmap Fonts in your own OpenGL
projects. I've searched the net looking for a tutorial similar to this one, and have found
nothing. Perhaps my site is the first to cover this topic in easy to understand C code?
Anyways. Enjoy the tutorial, and happy coding!
Jeff Molofee (NeHe)
* DOWNLOAD Visual C++ Code For This Lesson.
* DOWNLOAD Delphi Code For This Lesson.
( Conversion by Marc Aarts )