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