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