www.codeworx.org/opengl-tutorials/Tutorial 14: 3D-Fonts

Lektion 14: 3D-Fonts

Willkommen zu Tutorial Nr 14, das zu großen Teilen Nr. 13 entspricht, nur das die Fonts am Ende 3-dimensional erscheinen sollen, also in die Tiefe gehen. (Es gab früher mal einen eingebauten Bildschirmschoner in Windows der dem Ergebnis dieses Tuts ziemlich ähnlich sieht. ;)

Der code ist wieder sehr stark windowsorientiert und baut grundsätzlich auf dem ersten Tutorial auf.

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 zwei neue Variablen benötigt, "base" speichert die Nummer der ersten zu erstellenden Display-Liste, jeder Buchstabe benötigt eine eigene. (Später mehr dazu.) 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.). "rot" wird für die Drehung des auszugebenen Text und den Wechsel der Farben benötigt.
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] speichert Informationen über die Position und die Richtung für jede der 256 Displaylisten. Ein Buchstabe wird mit gmf[num] aufgerufen. "num" ist die Nummer der aktuellen Displayliste. Das wird später benötigt um den ausgegebenen Text genau in der Bildschirmitte zu postieren ohne Rücksicht auf die verschiedenen Breiten und die Anzahl der Buchstaben nehmen zu müssen. (Einfach mal das Beispiel kompilieren und ausführen.)

GLYPHMETRICSFLOAT gmf[256];	  // Storage For Information About Our Font

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); 
// Declaration For WndProc

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

GLvoid BuildFont(GLvoid)      // Build Our Bitmap Font
{
HFONT font; // Windows Font ID base = glGenLists(256); // Storage For 256 Characters

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

	font = CreateFont( -12,   // 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.

	"Comic Sans MS");             // Font Name

	SelectObject(hDC, font);      // Selects The Font We Created

Mit wglUseFontOutlines werden die 3D-Fonts erstellt. Der DC (Divice Context, man erinnere sich an die ersten Lektionen!), der erste Buchstabe und die Anzahl der zu erstellenden Buchstaben sowie die erste Displayliste wird übergeben. Das Verfahren ist dem aus Lektion 13 sehr ähnlich.

    wglUseFontOutlines(  hDC,   // Select The Current DC
                         0,     // Starting Character
                         255,   // Number Of Display Lists To Build
                         base,  // Starting Display Lists

Aber das wars noch nicht. Die Genauigkeit wird auf 0.0f festgelegt was das Ergebnis sehr glatt aussehen läßt. Als nächstes kommt die Tiefe der Fonts. Je größer der Wert ist, destso weiter reichen die Buchstaben in Z-Richtung. (0.0f würde einen zweidimensionalen Font erzeugen, was natürlich nicht Sinn der Übung ist).

WGL_FONT_POLYGONS weist OpenGL an einen massiven Font aus Polygonen zu erzeugen, GL_FONT_LINES ergibt ein Drahtgittermodell, was auch sehr gut aussieht, aber nicht beleuchtet werden kann.

In gmf sollen die fertigen Daten für die Ausgabe zwischengespeichert werden.

	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
}

Die nächste Funktion wird beim beenden des Programms aufgerufen und löscht die 255 Displaylisten (nur zur Sicherheit).

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

Jetzt zu der eigentlich Textausgabe:

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

"length" speichert die ermittelte Länge des Strings. "text" schafft Platz für 256 Chars. und ap zeigt auf die Liste der beim Funktionsaufruf (möglicherweise) übergebenen Argumente.

	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

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

Vielen Dank an Jim Williams dem wir den folgenden code verdanken, der die Länge des Textes berechnet. Die Schleife geht einmal durch den ganzen übergebenen Text, dabei wird die exakte länge mit strlen() bestimmt. Stück für Stück wird jetzt die Breite jedes einzelnen Buchstabens zur Gesamtlänge "length" addiert. gmf[text[loop]].gmfCellIncX enthält eben diese Einzelbreiten der Buchstaben, die ja nur noch in Form der Displaylisten vorhanden sind.

	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
	}

Jetzt wird nur noch der erstellt Text in der Mitte des Bildschirms platziert. Er wird also um genau die Hälfte seiner Länge in negativer X-Richtung bewegt und später dort ausgegeben.

	glTranslatef(-length/2,0.0f,0.0f); // Center Our Text On The Screen

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

Um die gewünschten Buchstaben zu finden, muss OpenGL durch "glListBase(base);" wissen an welcher Stelle die Buchstabenlisten beginnen.

	glPushAttrib(GL_LIST_BIT);        // Pushes The Display List Bits ( NEW )
	glListBase(base);            // Sets The Base Character to 0 ( 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 )
}

An der Originalfunktion müssen ein paar Sachen verändert werden. Zuerst soll etwas Licht in die Szenerie kommen, wir schlalten der Einfachkeit halber das Standardlicht ein, auch der Beleuchtungsmodus und die Färbung der Buchstaben müssen aktiviert werden. BuildFont(); gibt den Text dann aus.

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
}

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

Der Text soll sich um alle 3 Achsen drehen, die verschiedenen multiplizierten Werte, verändern die Geschwindigkeiten, damit sch der Text noch konfuser bewegt.

	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

Die Farbwerte werden mit dem rot-Zähler und dem Sinus verändert, die Teiler am Ende verhindern das sich die Wete alle in gleicher Weise ändern, sonst würde der Text ja nur zwischen schwarz und weiss "pendeln".

    // Pulsing Colors Based On Text Position
    glColor3f(1.0f*float(cos(rot/20.0f)),
              1.0f*float(sin(rot/25.0f)),
              1.0f-0.5f*float(cos(rot/17.0f)));

Jetzt das Entscheidene, das Aufrufen der Textausgabe. Das passiert sehr benutzerfreundlich im alten Stil a là 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. 3 steht für maximal 3 auszugebende Stellen, die 2 besagt das maximal 2 Stellen nach dem Komma angezeigt werden sollen und das "f" steht für float, da rot vom Typ float ist. (Durch 50 wird dividiert damit der Wert nicht so groß wird.) Ü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("NeHe - %3.2f",rot/50); // Print GL Text To The Screen

Als letztes muss der Rotationscounter noch vergrößert werden, damit sich die Farbe ändert und Drehung ins Spiel kommt.

	rot+=0.5f;                      // Increase The Rotation Variable
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 eure Programme mit 3D-Text zu verfeinern. Wäre zum Beispiel für Logos oder Highscorelisten in Spielen interessant.

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 15.02.2k4, www.codeworx.org