This tutorial is a sequel to the last tutorial. In tutorial 13 I taught you how to use Bitmap
Fonts. In this tutorial I'll teach you how to use Outline Fonts.
The way we create Outline fonts is fairly similar to the way we made Bitmap fonts in lesson 13.
However... Outline fonts are about 100 times more cool! You can size Outline fonts. Outline
font's can move around the screen in 3D, and outline fonts can have thickness! No more flat 2D
fonts. With Outline fonts, you can turn any font that's installed on your computer into a 3D
font for OpenGL, complete with proper normals so the characters light up really nice when light
shines on them.
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 basic GL code from lesson 1. We'll be adding the Math.h header file so
we can move the text around the screen in 3D using SIN and COS.
#include <windows.h> // Header File For Windows
#include <math.h> // Header File For Windows Math Library
#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 2 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 a variable called rot. Rot will be used to spin the text around on the screen
using both SIN and COS. It will also be used to pulse the colors.
GLuint base; // Base Display List For The Font Set
GLfloat rot; // Used To Rotate The Text
BOOL keys[256]; // Array Used For The Keyboard Routine
The following section of code builds the actual font similar to the way we made our Bitmap font.
Just like in lesson 13, this section of code was the hardest part for me to figure out.
GLYPHMETRICSFLOAT gmf[256] will hold our 256 floating point display list characters. '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 256 display lists using glGenLists(256).
After the display lists are created, the variable 'base' will hold the number of the first list.
GLvoid BuildFont(GLvoid) // Build Our Bitmap Font
{
GLYPHMETRICSFLOAT gmf[256]; // Address Buffer For Font Storage
HFONT font; // Windows Font ID
base = glGenLists(256); // Storage For 256 Characters
More fun stuff. We're going to create our Outline 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( -12, // Height Of Font
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. 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 'Comic Sans MS' with the name of the font you'd rather use.
"Comic Sans MS"); // Font Name
Now we select the font by relating it to our DC.
SelectObject(hDC, font); // Selects The Font We Created
Now for the new code. We build our Outline font using a new command wglUseFontOutlines. We
select our DC, the starting character, the number of characters to create and the 'base'
display list value. All very similar to the way we built our Bitmap font.
wglUseFontOutlines( hDC, // Select The Current DC
0, // Starting Character
255, // Number Of Display Lists To Build
base, // Starting Display Lists
That's not all however. We then set the deviation level. The closer to 0.0f, the smooth the
font will look. After we set the deviation, we get to set the font thickness. This describes
how thick the font is on the Z axis. 0.0f will produce a flat 2D looking font and 1.0f will
produce a font with some depth.
The parameter WGL_FONT_POLYGONS tells OpenGL to create a solid font using polygons. If we use
WGL_FONT_LINES instead, the font will be wireframe (made of lines).
The last parameter gmf points to the address buffer for the display list data.
0.0f, // Deviation From The True Outlines
0.2f, // Font Thickness In The Z Direction
WGL_FONT_POLYGONS, // Use Polygons, Not Lines
gmf); // Address Of Buffer To Recieve Data
}
The following code is pretty simple. It deletes the 256 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, 256); // Delete All 256 Characters
}
Now for my handy dandy GL text routine. You call this section of code with the command
glPrint("message goes here"). Exactly the same way you drew Bitmap fonts to the screen in
lesson 13. 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) tells OpenGL where to find the proper display list for each
character.
glPushAttrib(GL_LIST_BIT); // Pushes The Display List Bits
glListBase(base); // Sets The Base Character to 0
Now that OpenGL knows where the characters are located, we can tell it to write the text to the
screen. glCallLists writes the entire string of text to the screen at once by making multiple
display list calls.
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
still not sending any more than 255 characters. So we can use an UNSIGNED_BYTE. (a byte
represents a number from 0 - 255 which is exactly what we need). 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 character is. After the letter is drawn
to the screen, 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).
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); // Draws The Display List Text
glPopAttrib(); // Pops The Display List Bits
}
There are a few new lines at the end of the InitGL code. The line BuildFont() from lesson 13
is still there, along with code to do quick and dirty lighting. Light0 is predefined on most
video cards and will light up the scene nicely with no effort on my part on lighting has been
enabled :)
I've also added the command glEnable(GL_Color_Material). Because the characters are 3D objects
you need to enable Material Coloring, otherwise changing the color with glColor3f(r,g,b) will
not change the color of the text. If you're drawing shapes of your own to the screen while you
write text enable material coloring before you write the text, and disable it after you've
drawn the text, otherwise all the object on your screen will be colored.
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
glEnable(GL_LIGHT0); // Enable Default Light (Quick And Dirty)
glEnable(GL_LIGHTING); // Enable Lighting
glEnable(GL_COLOR_MATERIAL); // Enable Coloring Of Material
}
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 ten units into the screen. Outline
fonts look great in perspective mode. The further into the screen you translate, the smaller
the font becomes. The closer you translate, the larger the font becomes.
Outline fonts can also be manipulated by using the glScalef(x,y,z) command. If you want the
font 2 times taller, use glScalef(1.0f,2.0f,1.0f). the 2.0f is on the y axis, which tells OpenGL
to draw the list twice as tall. If the 2.0f was on the x axis, the character would be twice
as wide.
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,-10.0f); // Move Ten Units Into The Screen
After we've translated into the screen, we want the text to spin. The next 3 lines rotate the
screen on all three axes. I multiply rot by different numbers to make each rotation happen at
a different speed.
glRotatef(rot,1.0f,0.0f,0.0f); // Rotate On The X Axis
glRotatef(rot*1.2f,0.0f,1.0f,0.0f); // Rotate On The Y Axis
glRotatef(rot*1.4f,0.0f,0.0f,1.0f); // Rotate On The Z Axis
Now that the screen has been spun, we translate a little bit to the left, so that the text draws
from the left of center to the right of center. If we didn't translate, the text would start at
the center and move right. Then if we were to spin it, it wouldn't spin in the middle.
glTranslatef(-3.5f,0.0f,0.0f); // Move To The Left Before Drawing
Now for the crazy color cycling. As usual, I make use of the only variable that counts up (rot).
The colors pulse up and down using COS and SIN. I divide the value of rot by different numbers
so that each color isn't increasing at the same speed. The final results are nice.
// Pulsing Colors Based On The Rotation
glColor3f(1.0f*float(cos(rot/20.0f)),1.0f*float(sin(rot/25.0f)),1.0f-0.5f*float(cos(rot/17.0f)));
My favorite part... Writing the text to the screen. I've used the same command we used to write
Bitmap fonts to the screen. All you do to write the text to the screen is glPrint("{any text
you want}"). It's that easy.
glPrint("OpenGL With NeHe"); // Print GL Text To The Screen
The last thing to do is increase the rotation variable so the colors pulse and the text spins.
rot+=0.1f; // Increase The Rotation Variable
}
Just like in lesson 13, I've added a line of code to the WM_DESTROY / WM_CLOSE section.
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 256 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
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
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
}
}
At the end of this tutorial you should be able to use Outline Fonts in your own OpenGL projects.
Just like lesson 13, I've searched the net looking for a tutorial similar to this one, and have
found nothing. Could my site be the first to cover this topic in great detail while explaining
everything in easy to understand C code? 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 )