www.codeworx.org/opengl-tutorials/Tutorial 13: Bitmap-Fonts

Lektion 13: Bitmap-Fonts

Willkommen zu diesem Tutorial. Man steht (mit Sicherheit) irgendwann vor dem Problem, Text in ein OpenGL-Fenster zu bringen (Und sei es nur um sich die fps anzeigen zu lassen!). Das ganze gestaltet sich etwas schwieriger als man denken könnte, eine einfache Sofortlösung gibt es leider nicht. (Es sei denn man würde nach dem bisherigen Wissensstand anfangen, einzelne Buchstaben in ein Grafikprogramm zu laden und diese dann mühselig und einzeln in *.bmp's zu verwandeln und als Einzeltexturen anzuzeigen...muss aber nicht sein! ;)

Eine wesentlich schönere Möglichkeit ist es daher, die Fonts des Betriebssystems zu nutzen und direkt im Programm auszugeben. Am Ende soll eine Funktion herrauskommen die es ermöglicht ein ganze Buchstabenfolge zu übergeben und anzeigen zu lassen.

Als Ausgangsbasis soll wieder der code der ersten Lektion dienen, stdio.h wird zusätzlich inkludiert um die Ein- und Ausgabegfunktionen für Dateien zu nutzen. Mithilfe von stdarg.h lassen sich Variablen in Texten kovertieren und um die Sache auch mathematisch etwas interessanter zu gestalten, werden die Winkelfunktionen aus math.h genutzt.

#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
Es werden drei neue Variablen benötigt, "base" speichert die Nummer der ersten zu erstellenden Display-Liste. Jeder Buchstabe benötigt eine eigene Displayliste. Ein "A" bekäme die Displayliste 65, "B" die 66, "C" die 67 usw. Soll ein "A" auf dem Bildschirm erscheinen muss also die base-Liste+65 ausgeben werden. (Das Verfahren basiert auf der ascii-Tabelle, aber da muss eigentlich fürs Grundverständnis nicht zu weit ins Detail gegangen werden.)
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
bool active=TRUE; // Window Active Flag Set To TRUE By Default
bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc

Die folgende Funktion erstellt die eigentlich Fonts. Es werden 96 Display-Listen generiert, jede für einen anderen Buchstaben.

GLvoid BuildFont(GLvoid)				// Build Our Bitmap Font
{
HFONT font; // Windows Font ID
HFONT oldfont; // Used For Good House Keeping base = glGenLists(96); // Storage For 96 Characters ( NEW )

Hier wird die Höhe des Fonts festgelegt, nämlich auf 24 Pixel über der Grundlinie, daher ist die Zahl negativ.

	font = CreateFont( -24, 			// Height Of Font ( NEW )

Als nächstes wird die Breite eingestellt, "0" ist der Normalwert, die Buchstaben erscheinen, relativ zur Höhe, unverzerrt.

	0, 									// Width Of Font

Die folgenden Werte bleiben bei "0", sind hier unwichtig.

	0, 									// Angle Of Escapement
	0, 									// Orientation Angle

Hier läßt sich die "Dicke" der Buchstaben einstellen, erlaubt sind Werte zwischen 0 und 1000. Wer vordefinierte Werte mag, der kann die Folgenden benutzen:

FW_DONTCARE für 0
FW_NORMAL für 400
FW_BOLD für 700
und FW_BLACK für 900

In diesem Fall soll ein etwas dickerer Text reichen.

	FW_BOLD, 							// Font Weight

Die folgenden Werte stehen für kursiv, unterstrichen und durchgestrichen (Alles entweder true oder false.).

	FALSE, 								// Italic
	FALSE, 								// Underline
	FALSE, 								// Strikeout

Hier lassen sich landesspeziefische Werte einsetzen um Sonderzeichen darzustellen ( Einige Werte: CHINESEBIG5_CHARSET, GREEK_CHARSET, RUSSIAN_CHARSET, GERMAN_CHARSET, DEFAULT_CHARSET, ANSI, DEFAULT usw.). Sollen die netten Symbole aus Webdings oder Wingdings genutzt werden, ist SYMBOL_CHARSET die erste Wahl. Der internationale ANSI_CHARSET sollte aber ausreichen.

	ANSI_CHARSET, 						// Character Set Identifier

Sollte mehr als eine Schriftart mit dem gewünschten Namen vorhanden sein, wird mit OUT_TT_PRECIS versucht, die wesentlich besser aussehende *.ttf-Variante dieser Schriftart zu wählen.

	OUT_TT_PRECIS, 						// Output Precision

Der nächste Wert sollte so gelassen werden.

	CLIP_DEFAULT_PRECIS, 				// Clipping Precision

Die Ausgabequalität verdient besondere Aufmerksamkeit. Gültig sind Werte wie PROOF, DRAFT, NONANTIALIASED, DEFAULT und ANTIALIASED. Auf schnellen Geräten ist sicher ANTIALIASED_QUALITY das Beste, gerade bei sehr großen Schriftzügen wirken andere Darstellungsqualitäten kantig.

	ANTIALIASED_QUALITY, 				// Output Quality

Auch die Stilarten und -familien können das Aussehen des Fonts etwas verändern, erlaubt sind für den ersten Wert: DEFAULT_PITCH, FIXED_PITCH und VARIABLE_PITCH, für den zweiten FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS und FF_DONTCARE. Einfach mal etwas dran herrumspielen. Zuerst sind beide auf Standard gesetzt.

	FF_DONTCARE|DEFAULT_PITCH, 			// Family And Pitch


Jetzt das wichtigste, der Name der Schriftart. Man kann in einen Texteditor oder das "fonts"-Verzeichnis von Windoof gucken um die gültigen Werte zu ermitteln.

	"Courier New"); 					// Font Name

Die eben erstellte Schriftart wird ausgewählt und in die Displaylisten eingeteilt. oldfont zeigt auf das gewählte Objekt. Es werden die 96 Listen erstellt, beim Buchstaben 32 (Einem Leerzeichen) wird angefangen (Davor befinden sich ominöse Steuerbefehle und Sonderzeichen, die wir eigentlich nicht brauchen.). Die letzten beiden Zeilen löschen den erstellten font wieder (Natürlich nicht die *.ttf-Datei :) .

	oldfont = (HFONT)SelectObject(hDC, font); 	
	// Selects The Font We Want

	wglUseFontBitmaps(hDC, 32, 96, base); 		
	// Builds 96 Characters Starting At Character 32

	SelectObject(hDC, oldfont); 	// Selects The Font We Want
	DeleteObject(font); 			// Delete The Font
}

Die nächste Funktion wird beim beenden des Programms aufgerufen und löscht die 96 Displaylisten.

GLvoid KillFont(GLvoid) 			// Delete The Font List
{
   glDeleteLists(base, 96); 		// Delete All 96 Characters ( NEW )
}

Jetzt zu der eigentlich Textausgabe:

GLvoid glPrint(const char *fmt, ...) // Custom GL "Print" Routine
{

Der String "text" schafft Platz für 256 Chars. ap zeigt auf die Liste der beim Funktionsaufruf (möglicherweise) übergebenen Argumente.

	char text[256]; 				// Holds Our String
	va_list ap; 					// Pointer To List Of Arguments

Wenn nichts übergeben wurde, wird die Funktion wieder verlassen.

	if (fmt == NULL) 				// If There's No Text
		return; 					// Do Nothing

Die folgenden Zeilen ersetzen die eingesetzten Platzhalter "%i" oder"%f" durch die übergebenen Variablen. Damit lassen sich dann in der Textausgabe konkrete Werte aus Variablen auslesen und anzeigen (Wird später noch klarer werden.). Der fertig präparierte String wird dann in text gespeichert.

	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

glPushAttrib(GL_LIST_BIT) verhindert, das die neuen Listen andere, im Programm möglicherweise genutzten Listen nicht überschreiben.

Um die gewünschten Buchstaben zu finden, muss OpenGL durch "glListBase(base - 32);" wissen an welcher Stelle die Buchstabenlisten überhaupt beginnen. 32 muss subtrahiert werden, da die ersten 32 Buchstaben ausgelassen wurden.

	glPushAttrib(GL_LIST_BIT); 		// Pushes The Display List Bits ( NEW )
	glListBase(base - 32); 			// Sets The Base Character to 32 ( NEW )

Jetzt, da klar ist wo sich die erstellten Listen befinden, können sie gemäß der Eingabe ausgegeben werden.

glCallLists() ruft die angebenen Listen auf. Die Positionen und damit die Buchstaben befinden sich in Form von unsigned bytes in "text" und werden Stück für Stück übergeben. Es sind genau 256 Zeichen (0 bis 255) in gespeichert. Da alle Werte "0" ein Leerzeichen ergeben, werden an den übergebenen String Leerzeichen gehängt bis 256 Listen ausgeben sind. (strlen(text) gibt die Länge des Strings zurück.)

glPopAttrib(); setzt GL_LIST_BIT wieder zum Ausgangswert zurück.

	glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); // Draws The Display List    Text ( NEW )
	glPopAttrib(); // Pops The Display List Bits ( NEW )
}

Das einzige was an der Original-Funktion verändert werden muss, ist der Aufruf von BuildFont().

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
	BuildFont(); // Build The Font (NEW)
	
	return TRUE; // Initialization Went OK
}

Jetzt zur "Bildschirmausgabe". Zuerst das Standardprogramm, Bild und depth-Puffer leeren. Alles mit glLoadIdentity() zurücksetzen und eine Einheit in den Schirm zoomen, damit der Text genau vor dem Betrachter erscheint.

Egal wie tief gezoomt wird, der Text wird seine Größe wider Erwarten nicht verändern. Das hat den Vorteil das man ihn präziser postieren kann, wenn um eine Einheit in die Tiefe gezoomt wurde, kann der Text auf den X- und Y-Achsen jeweils zwischen -0.5f und +0.5f platziert werden. Wenn sich die Größe verändern soll, muss das bei der Initialisierung durch BuildFont() passieren.

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,-1.0f); // Move One Unit Into The Screen

Jetzt wird die Farbe manipuliert. Das passiert mit den drei rgb-Einzelkomponenten und den allseits beliebten Winkelfuntionen, dem Sinus und dem Cosinus, die beide nur Werte zwischen -1.0 und +1.0 zurückliefern. Durch ein paar Tricks wird das Bild niemals Schwarz, da Blau immer nur zwischen 1.5 und 0.5 liegt, Grün und Rot aber zwischen 1.0 und 0 schwanken.

	// 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)));

Etwas ähnliches wird auch mit der Position angestellt. Hierbei werden die Positionen auf der X- und Y-Achse verändert. Sodaß der Text auf dem Schirm Wellenbewegungen ausführt. X liegt immer zwischen -0.05 und +0.05, Y zwischen -0.35 und +0.35. (Wäre ja unschön wenn die Schrift den sichtbaren Bereich verläßt.

	// Position The Text On The Screen
	glRasterPos2f(-0.45f+0.05f*float(cos(cnt1)), 0.35f*float(sin(cnt2)));

Jetzt das Entscheidene, das Aufrufen der Textausgabe. Das passiert sehr benutzerfreundlich im alten Stil mit glPrint("Was auch immer dann auf dem Schirm stehen soll"). %7.2f ist hier ein Platzhalter an dessen Stelle der aktuelle Wert der Variable cnt1 rückt. 7 steht für maximal 7 auszugebende Stellen, die 2 besagt das maximal 2 Stellen nach dem Komma angezeigt werden sollen und das "f" steht für float, da cnt1 vom Typ float ist. Über diese Platzhalter, auch Symbole genannt, steht in der MSDN eine ganze Menge. (Aber mehr als "%i" und "%f" braucht man ja meistens nicht.)

	glPrint("Active OpenGL Text With NeHe - %7.2f", cnt1); 
	// Print GL Text To The Screen

Als letztes müssen die beiden Animationscounter noch bei jedem Frame erhöht werden, damit sich die Farbe ändert und Bewegung ins Spiel kommt.

	cnt1+=0.051f; 	// Increase The First Counter
	cnt2+=0.005f; 	// Increase The Second Counter
	return TRUE; 	// Everything Went OK
}

Die Aufräumarbeiten zum Schluss, beim beenden muss in KillGLWindow() die oben beschriebene Funktion KillFont() aufgerufen werden um die Listen wieder zu löschen.

	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
}

Das wärs. Ich hoffe es hat euch Spaß gemacht, ihr solltet jezt in der Lage sein euer Programme mit Textausgaben zu verfeinern.

happy coding, Jeff Molofee (NeHe)

Die Source Codes und Ausführbaren Dateien zu den Kursen liegen auf der Neon Helium Website

Übersetzt und leicht Modifiziert von Hans-Jakob Schwer 05.09.2k2, www.codeworx.org