IdentifiantMot de passe
Mot de passe oublié ?Je m'inscris ! (gratuit)

NeHe Productions: OpenGL Lesson #14

Date de publication : 31/03/2006 , Date de mise à jour : 31/03/2006

Par Jeff Molofee ( NeHe ) (Autres articles)

Lesson: 14 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 the Bitmap font 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 characters. With Outline fonts, you can turn any font 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 another font tutorial.

We start off with the typical code from lesson 1. We'll be adding the stdio.h header file for standard input/output operations; the stdarg.h header file to parse the text and convert variables to text, and finally 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 <stdio.h>						 // Header File For Standard Input/Output	( ADD )
#include <stdarg.h>						 // Header File For Variable Argument Routines	( 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
HDC		hDC=NULL;					 // Private GDI Device Context
HGLRC		hRC=NULL;					 // Permanent Rendering Context
HWND		hWnd=NULL;					 // Holds Our Window Handle
HINSTANCE	hInstance;					 // Holds The Instance Of The Application
We're going to add 2 new variables. 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	( ADD )
GLfloat	rot;							 // Used To Rotate The Text		( ADD )
bool	keys[256];						 // Array Used For The Keyboard Routine
bool	active=TRUE;						 // Window Active Flag Set To TRUE By Default
bool	fullscreen=TRUE;					 // Fullscreen Flag Set To Fullscreen Mode By Default
GLYPHMETRICSFLOAT gmf[256] will hold information about the placement and orientation for each of our 256 outline font display lists. We select a letter by using gmf[num]. num is the number of the display list we want to know something about. Later in the code I'll show you how to find out the width of each character so that you can automatically center the text on the screen. Keep in mind that each character can be a different width. glyphmetrics will make our lives a whole lot easier.
GLYPHMETRICSFLOAT gmf[256];					 // Storage For Information About Our Font
LRESULT	CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);		 // Declaration For WndProc
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.

'HFONT font' will hold our Windows font ID.

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
	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 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). It's also important to note that if you use GL_FONT_LINES, normals will not be generated so lighting will not work properly.

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 char string fmt.
GLvoid glPrint(const char *fmt, ...)				 // Custom GL "Print" Routine
The first line below sets up a variable called length. We'll use this variable to find out how our string of text is. The second line creates storage space for a 256 character string. text is the string we will end up printing to the screen. The third line creates a pointer that points to the list of arguments we pass along with the string. If we send any variables along with the text, this pointer will point to them.
	float		length=0;				 // Used To Find The Length Of The Text
	char		text[256];				 // Holds Our String
	va_list		ap;					 // Pointer To List Of Arguments
The next two lines of code check to see if there's anything to display? If there's no text, fmt will equal nothing (NULL), and nothing will be drawn to the screen.
	if (fmt == NULL)					 // If There's No Text
		return;						 // Do Nothing
The following three lines of code convert any symbols in the text to the actual numbers the symbols represent. The final text and any converted symbols are then stored in the character string called "text". I'll explain symbols in more detail down below.
	va_start(ap, fmt);					 // Parses The String For Variables
	 vsprintf(text, fmt, ap);				 // And Converts Symbols To Actual Numbers
	va_end(ap);						 // Results Are Stored In Text
Thanks to Jim Williams for suggesting the code below. I was centering the text manually. His method works alot better :)

We start off by making a loop that goes through all the text character by character. strlen(text) gives us the length of our text. After we've set up the loop, we will increase the value of length by the width of each character. When we are done the value stored in length will be the width of our entire string. So if we were printing "hello" and by some fluke each character was exactly 10 units wide, we'd increase the value of length by the width of the first letter 10. Then we'd check the width of the second letter. The width would also be 10, so length would become 10+10 (20). By the time we were done checking all 5 letters length would equal 50 (5*10).

The code that gives us the width of each character is gmf[text[loop]].gmfCellIncX. Remember that gmf stores information out each display list. If loop is equal to 0 text[loop] will be the first character in our string. If loop is equal to 1 text[loop] will be the second character in our string. gmfCellIncX tells us how wide the selected character is. gmfCellIncX is actually the distance that our display moves to the right after the character has been drawn so that each character isn't drawn on top of eachother. Just so happens that distance is our width :) You can also find out the character height with the command gmfCellIncY. This might come in handy if you're drawing text vertically on the screen instead of horizontally.

NOTE: Curt Werline adds - When you calculate the dimensions of the text string, you are using gmfCellIncX for the width and you suggest using gmfCellIncY for the height. These values are offsets and not true dimensions. To get the true dimensions you should use gmfBlackBoxX and gmfBlackBoxY.
	for (unsigned int loop=0;loop<(strlen(text));loop++)	 // Loop To Find Text Length
		length+=gmf[text[loop]].gmfCellIncX;		 // Increase Length By Each Characters Width
Finally we take the length that we calculate and make it a negative number (because we have to move left of center to center our text). We then divide the length by 2. We don't want all the text to move left of center, just half the text!
	glTranslatef(-length/2,0.0f,0.0f);			 // Center Our Text On The Screen
We then 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 for you.

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
Resizing code is exactly the same as the code in Lesson 1 so we'll skip over it.

There are a few new lines at the end of the InitGL code. The line BuildFont() from lesson 13 is still there, along with new 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 :)

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.
int InitGL(GLvoid)						 // All Setup For OpenGL Goes Here
	glShadeModel(GL_SMOOTH);				 // Enable Smooth Shading
	glClearColor(0.0f, 0.0f, 0.0f, 0.5f);			 // Black Background
	glClearDepth(1.0f);					 // Depth Buffer Setup
	glEnable(GL_DEPTH_TEST);				 // Enables Depth Testing
	glDepthFunc(GL_LEQUAL);					 // The Type Of Depth Testing To Do
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);	 // Really Nice Perspective Calculations
	glEnable(GL_LIGHT0);					 // Enable Default Light (Quick And Dirty)	( NEW )
	glEnable(GL_LIGHTING);					 // Enable Lighting				( NEW )
	glEnable(GL_COLOR_MATERIAL);				 // Enable Coloring Of Material			( NEW )
	BuildFont();						 // Build The Font				( ADD )
	return TRUE;						 // Initialization Went OK
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.
int DrawGLScene(GLvoid)						 // Here's Where We Do All The Drawing
	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.5f,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 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
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 have to do to write the text to the screen is glPrint("{any text you want}"). It's that easy!

In the code below we'll print NeHe, a space, a dash, a space, and then whatever number is stored in rot divided by 50 (to slow down the counter a bit). If the number is larger that 999.99 the 4th digit to the left will be cut off (we're requesting only 3 digits to the left of the decimal place). Only 2 digits will be displayed after the decimal place.
 	glPrint("NeHe - %3.2f",rot/50);				 // Print GL Text To The Screen
Then we increase the rotation variable so the colors pulse and the text spins.
	rot+=0.5f;						 // Increase The Rotation Variable
	return TRUE;						 // Everything Went OK
The last thing to do is add KillFont() to the end of KillGLWindow() just like I'm showing below. It's important to add this line. It cleans things up before we exit our program.
	if (!UnregisterClass("OpenGL",hInstance))		 // Are We Able To Unregister Class
		MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
		hInstance=NULL;					 // Set hInstance To NULL
	KillFont();						 // Destroy The Font
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 )

Valid XHTML 1.1!Valid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2006 Nehe Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.