Irgendwann in Ihrem Grafikabenteuer möchten Sie Text über OpenGL ausgeben. Entgegen dem, was Sie vielleicht erwarten, ist es mit einer Bibliothek auf niedriger Ebene wie OpenGL ziemlich schwierig, eine einfache Zeile auf dem Bildschirm zu erhalten. Wenn Sie nicht mehr als 128 verschiedene Zeichen zum Zeichnen von Text benötigen, ist dies nicht schwierig. Schwierigkeiten treten auf, wenn die Zeichen nicht mit Höhe, Breite und Versatz übereinstimmen. Je nachdem, wo Sie wohnen, benötigen Sie möglicherweise mehr als 128 Zeichen. Aber was ist, wenn Sie Sonderzeichen, mathematische oder musikalische Zeichen möchten? Sobald Sie verstehen, dass das Zeichnen von Text nicht die einfachste Aufgabe ist, werden Sie feststellen, dass er höchstwahrscheinlich nicht zu einer so einfachen API wie OpenGL gehören sollte.
Da OpenGL keine Möglichkeit zum Rendern von Text bietet, liegen alle Schwierigkeiten in diesem Fall bei uns. Da es kein grafisches Grundelement "Symbol" gibt, müssen wir es selbst erfinden. Es gibt bereits vorgefertigte Beispiele: Zeichnen Sie ein Symbol über GL_LINES
, erstellen Sie 3D-Modelle von Symbolen oder zeichnen Sie Symbole auf flachen Vierecken im dreidimensionalen Raum.
Meistens sind Entwickler zu faul, um Kaffee zu trinken und die letzte Option zu wählen. Das Zeichnen dieser strukturierten Vierecke ist nicht so schwierig wie die Auswahl der richtigen Textur. In diesem Tutorial lernen wir einige Möglichkeiten kennen und schreiben unseren erweiterten, aber flexiblen Textrenderer mit FreeType.
InhaltTeil 3. Laden Sie 3D-Modelle herunter Teil 4. Erweiterte OpenGL-Funktionen Teil 5. Erweiterte Beleuchtung Klassiker: Raster-Schriftarten
Es war einmal in der Zeit der Dinosaurier. Zum Rendern von Text gehörte das Auswählen (oder Erstellen) einer Schriftart für die Anwendung und das Kopieren der gewünschten Zeichen auf eine große Textur, die als Bitmap-Schriftart bezeichnet wird. Diese Textur enthält alle notwendigen Zeichen in bestimmten Teilen. Diese Zeichen werden Glyphen genannt. Jeder Glyphe ist ein bestimmter Bereich von Texturkoordinaten zugeordnet. Jedes Mal, wenn Sie einen Charakter zeichnen, wählen Sie eine bestimmte Glyphe aus und zeichnen nur den gewünschten Teil auf einem flachen Quad.

Hier können Sie sehen, wie wir den Text "OpenGL" rendern würden. Wir nehmen die Rasterschrift und probieren die erforderlichen Glyphen aus der Textur aus. Dabei wählen wir sorgfältig die Texturkoordinaten aus, die wir über mehrere Vierecke zeichnen. Wenn Sie das Mischen aktivieren und den Hintergrund transparent halten, wird eine Zeichenfolge auf dem Bildschirm angezeigt. Diese Bitmap-Schriftart wurde mit dem Codehead-Bitmap-Schriftgenerator generiert.
Dieser Ansatz hat Vor- und Nachteile. Dieser Ansatz ist einfach zu implementieren, da Bitmap-Schriftarten bereits gerastert sind. Dies ist jedoch nicht immer bequem. Wenn Sie eine andere Schriftart benötigen, müssen Sie eine neue Bitmap-Schriftart generieren. Darüber hinaus werden durch Erhöhen der Zeichengröße schnell pixelige Kanten angezeigt. Darüber hinaus sind Bitmap-Schriftarten häufig an einen kleinen Zeichensatz gebunden, sodass Unicode-Zeichen höchstwahrscheinlich nicht angezeigt werden.
Diese Technik war vor nicht allzu langer Zeit beliebt (und bleibt immer noch beliebt), da sie sehr schnell ist und auf jeder Plattform funktioniert. Bisher gibt es jedoch andere Ansätze zum Rendern von Text. Eine davon ist das Rendern von TrueType-Schriftarten mit FreeType.
Moderne: FreeType
FreeType ist eine Bibliothek, die Schriftarten herunterlädt, in Bitmaps rendert und Unterstützung für einige schriftartenbezogene Vorgänge bietet. Diese beliebte Bibliothek wird unter Mac OS X, Java, Qt, PlayStation, Linux und Android verwendet. Die Möglichkeit, TrueType-Schriftarten zu laden, macht diese Bibliothek attraktiv genug.
Eine TrueType-Schriftart ist eine Sammlung von Glyphen, die nicht durch Pixel, sondern durch mathematische Formeln definiert sind. Wie bei Vektorbildern kann ein gerastertes Schriftbild basierend auf der bevorzugten Schriftgröße generiert werden. Mit TrueType-Schriftarten können Sie problemlos Glyphen unterschiedlicher Größe ohne Qualitätsverlust rendern.
FreeType kann von der offiziellen Website heruntergeladen werden . Sie können FreeType entweder selbst kompilieren oder gegebenenfalls vorkompilierte Versionen auf der Site verwenden. Denken Sie daran, Ihr Programm mit freetype.lib
zu freetype.lib
und sicherzustellen, dass der Compiler weiß, wo er nach den Header-Dateien suchen muss.
Fügen Sie dann die richtigen Header-Dateien hinzu:
#include <ft2build.h> #include FT_FREETYPE_H
Da FreeType etwas seltsam gestaltet ist (lassen Sie mich zum Zeitpunkt des Schreibens des Originals wissen, wenn sich etwas geändert hat), können Sie seine Header-Dateien nur im Stammverzeichnis des Ordners mit den Header-Dateien ablegen. #include <3rdParty/FreeType/ft2build.h>
FreeType auf andere Weise verbinden (z. B. #include <3rdParty/FreeType/ft2build.h>
), kann dies zu einem Konflikt mit der Header-Datei führen.
Was macht FreeType? Lädt TrueType-Schriftarten und generiert für jede Glyphe ein Bitmap-Bild und berechnet einige Glyphenmetriken. Wir können Bitmap-Bilder zum Generieren von Texturen und Positionieren jedes Glyphen in Abhängigkeit von den empfangenen Metriken erhalten.
Um eine Schriftart herunterzuladen, müssen wir FreeType initialisieren und die Schriftart als Gesicht laden (wie FreeType die Schriftart nennt). In diesem Beispiel laden wir die TrueType-Schriftart arial.ttf
, die aus dem Ordner C: / Windows / Fonts kopiert wurde.
FT_Library ft; if (FT_Init_FreeType(&ft)) std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; FT_Face face; if (FT_New_Face(ft, "fonts/arial.ttf", 0, &face)) std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;
Jede dieser FreeType-Funktionen gibt im Fehlerfall einen Wert ungleich Null zurück.
Nachdem wir die Gesichtsschrift geladen haben, müssen wir die gewünschte Schriftgröße angeben, die wir extrahieren werden:
FT_Set_Pixel_Sizes(face, 0, 48);
Diese Funktion legt die Breite und Höhe des Glyphen fest. Durch Setzen der Breite auf 0 (Null) ermöglicht FreeType die Berechnung der Breite in Abhängigkeit von der eingestellten Höhe.
Face FreeType enthält eine Sammlung von Glyphen. Wir können einige Glyphen FT_Load_Char
indem FT_Load_Char
aufrufen. Hier versuchen wir die Glyphe X
zu laden:
if (FT_Load_Char(face, 'X', FT_LOAD_RENDER)) std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
Indem wir FT_LOAD_RENDER
als eines der Download-Flags FT_LOAD_RENDER
, FT_LOAD_RENDER
wir FreeType an, eine 8-Bit-Graustufen-Bitmap zu erstellen, die wir dann wie FT_LOAD_RENDER
erhalten können:
face->glyph->bitmap;
Mit FreeType geladene Glyphen haben nicht die gleiche Größe wie bei Bitmap-Schriftarten. Eine mit FreeType generierte Bitmap ist die Mindestgröße für eine bestimmte Schriftgröße und reicht nur für ein Zeichen aus. Zum Beispiel ein Bitmap-Bild eines Glyphen .
viel kleiner als die Bitmap von Glyphe X
Aus diesem Grund lädt FreeType auch einige Metriken herunter, die angeben, welche Größe und wo sich ein einzelnes Zeichen befinden soll. Unten sehen Sie ein Bild, das zeigt, welche Metriken FreeType für jede Glyphe berechnet.

Jedes Symbol befindet sich auf der Grundlinie (horizontale Linie mit einem Pfeil). Einige befinden sich genau auf der Grundlinie ( X
), andere unter ( g
, p
). Diese Metriken bestimmen genau die Offsets, um Glyphen genau auf der Grundlinie zu positionieren, die Größe der Glyphen anzupassen und herauszufinden, wie viele Pixel Sie zum Zeichnen des nächsten Glyphen übrig lassen müssen. Das Folgende ist eine Liste der Metriken, die wir verwenden werden:
- width :
face->glyph->bitmap.width
in Pixel, Zugriff über face->glyph->bitmap.width
- height :
face->glyph->bitmap.rows
in Pixel, Zugriff über face->glyph->bitmap.rows
- BearingX : horizontaler Versatz des oberen linken Punkts des Glyphen relativ zum Ursprung, Zugriff über
face->glyph->bitmap_left
- BearingY : Vertikaler Versatz des oberen linken Punkts des Glyphen relativ zum Ursprung, Zugriff über
face->glyph->bitmap_top
- Voraus : horizontaler Versatz des Beginns der nächsten Glyphe in 1/64 Pixel relativ zum Ursprung, Zugriff über
face->glyph->advance.x
Wir können jedes Mal, wenn wir es auf dem Bildschirm zeichnen möchten, eine Glyphe eines Symbols laden, seine Metriken abrufen und eine Textur generieren, aber es ist keine gute Methode, Texturen für jedes Symbol in jedem Frame zu erstellen. Besser, wir speichern die generierten Daten irgendwo und fordern sie an, wenn wir sie brauchen. Wir definieren eine bequeme Struktur, die wir in std::map
speichern werden:
struct Character { GLuint TextureID;
In diesem Artikel werden wir unser Leben vereinfachen und nur die ersten 128 Zeichen verwenden. Für jedes Zeichen generieren wir eine Textur und speichern die erforderlichen Daten in einer Struktur vom Typ Character
, die wir den Characters
Typ std::map
hinzufügen. Somit werden alle zum Zeichnen eines Zeichens erforderlichen Daten für die zukünftige Verwendung gespeichert.
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
Innerhalb der Schleife erhalten wir für jedes der ersten 128 Zeichen eine Glyphe, generieren eine Textur, legen ihre Einstellungen fest und speichern die Metriken. Es ist interessant festzustellen, dass wir GL_RED
als Argumente für internalFormat
und Texturen format
. Eine durch Glyphen erzeugte Bitmap ist ein 8-Bit-Graustufenbild, von dem jedes Pixel 1 Byte belegt. Aus diesem Grund speichern wir den Bitmap-Puffer als Texturfarbwert. Dies wird erreicht, indem eine Textur erstellt wird, in der jedes Byte der roten Komponente der Farbe entspricht. Wenn wir 1 Byte zur Darstellung von Texturfarben verwenden, vergessen Sie nicht die Einschränkungen von OpenGL:
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
OpenGL erfordert, dass alle Texturen einen Versatz von 4 Byte haben, d. H. Ihre Größe muss ein Vielfaches von 4 Bytes sein (z. B. 8 Bytes, 4000 Bytes, 2048 Bytes) oder (und) sie sollten 4 Bytes pro Pixel verwenden (z. B. im RGBA-Format), aber da wir 1 Byte pro Pixel verwenden, können sie unterschiedlich sein Breite. Durch Einstellen des Ausrichtungsversatzes beim Entpacken (gibt es eine bessere Übersetzung?) Auf 1 beseitigen wir die Versatzfehler, die zu Segfaults führen können.
Wenn wir mit der Schriftart selbst fertig sind, sollten wir die FreeType-Ressourcen löschen:
FT_Done_Face(face);
Shader
Verwenden Sie zum Zeichnen von Glyphen den folgenden Vertex-Shader:
#version 330 core layout (location = 0) in vec4 vertex;
Wir kombinieren vec4
und Texturkoordinaten in einem vec4
. Der Vertex-Shader berechnet das Koordinatenprodukt mit der Projektionsmatrix und überträgt die Texturkoordinaten an den Fragment-Shader:
#version 330 core in vec2 TexCoords; out vec4 color; uniform sampler2D text; uniform vec3 textColor; void main() { vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); color = vec4(textColor, 1.0) * sampled; }
Der Fragment-Shader akzeptiert zwei globale Variablen - ein monochromes Bild des Glyphen und die Farbe des Glyphen selbst. Zuerst probieren wir den Farbwert des Glyphen aus. Da die Texturdaten in der roten Komponente der Textur gespeichert sind, wird nur die r
Komponente als Transparenzwert abgetastet. Durch Ändern der Transparenz der Farbe wird die resultierende Farbe für den Hintergrund der Glyphe transparent und für die wahren Pixel der Glyphe undurchsichtig. Wir multiplizieren auch RGB-Farben mit der Variablen textColor, um die Farbe des Texts zu ändern.
Damit unser Mechanismus funktioniert, müssen Sie das Mischen aktivieren:
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Als Projektionsmatrix haben wir eine orthographische Projektionsmatrix. Zum Zeichnen von Text ist tatsächlich keine perspektivische Matrix erforderlich, und die Verwendung der orthografischen Projektion ermöglicht es uns auch, alle Scheitelpunktkoordinaten in Bildschirmkoordinaten anzugeben, wenn wir die Matrix wie folgt einstellen:
glm::mat4 projection = glm::ortho(0.0f, 800.0f, 0.0f, 600.0f);
Wir setzen den unteren Rand der Matrix auf 0.0f
, den oberen auf die Höhe des Fensters. Infolgedessen nimmt die y
Koordinate Werte vom unteren Bildschirmrand ( y = 0
) bis zum oberen Bildschirmrand ( y = 600
) an. Dies bedeutet, dass der Punkt (0, 0)
und die untere linke Ecke des Bildschirms anzeigt.
Erstellen Sie abschließend VBO und VAO, um die Vierecke zu zeichnen. Hier reservieren wir genügend Speicher in VBO, damit wir die Daten aktualisieren können, um Zeichen zu zeichnen.
GLuint VAO, VBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);
Ein flaches Viereck erfordert 6 Eckpunkte mit 4 Gleitkommazahlen, daher reservieren wir 6 * 4 = 24
Speicher-Floats. Da wir die Scheitelpunktdaten häufig ändern werden, weisen wir den Speicher mit GL_DYNAMIC_DRAW
.
Zeigen Sie eine Textzeile auf dem Bildschirm an
Um eine Textzeile anzuzeigen, extrahieren wir die dem Symbol entsprechende Zeichenstruktur und berechnen die Abmessungen des Vierecks aus den Metriken des Symbols. Aus den berechneten Abmessungen des Vierecks erstellen wir im glBufferSubData
einen Satz von 6 Scheitelpunkten und aktualisieren die Scheitelpunktdaten mit glBufferSubData
.
RenderText
Funktion, die eine Zeichenfolge zeichnet:
void RenderText(Shader &s, std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color) {
Der Inhalt der Funktion ist relativ klar: die Berechnung von Ursprung, Größe und Eckpunkten des Vierecks. Beachten Sie, dass wir jede Metrik mit der scale
multipliziert haben. Aktualisieren Sie danach VBO und zeichnen Sie ein Quad.
Diese Codezeile erfordert einige Aufmerksamkeit:
GLfloat ypos = y - (ch.Size.y - ch.Bearing.y);
Einige Zeichen, wie z. B. p
und g
, werden deutlich unterhalb der Grundlinie gezeichnet. RenderText
bedeutet, dass das Quad deutlich unter dem y
Parameter der RenderText
Funktion liegen sollte. Der genaue Versatz y_offset
kann aus y_offset
ausgedrückt werden:

Um den Versatz zu berechnen, benötigen wir gerade Arme, um den Abstand herauszufinden, in dem sich das Symbol unterhalb der Grundlinie befindet. Dieser Abstand wird durch den roten Pfeil angezeigt. Offensichtlich ist y_offset = bearingY - height
und ypos = y + y_offset
.
Wenn alles richtig gemacht wurde, können Sie den Text wie folgt auf dem Bildschirm anzeigen:
RenderText(shader, "This is sample text", 25.0f, 25.0f, 1.0f, glm::vec3(0.5, 0.8f, 0.2f)); RenderText(shader, "(C) LearnOpenGL.com", 540.0f, 570.0f, 0.5f, glm::vec3(0.3, 0.7f, 0.9f));
Das Ergebnis sollte folgendermaßen aussehen:

Ein Beispielcode ist hier (Link zur Website des ursprünglichen Autors).
Deaktivieren Sie die Überblendung, um zu verstehen, welche Vierecke gezeichnet werden:

Aus dieser Figur ist ersichtlich, dass sich die meisten Vierecke auf einer imaginären Grundlinie befinden, obwohl einige Zeichen wie (
und p
nach unten verschoben sind.
Was kommt als nächstes?
Dieser Artikel zeigte, wie TrueType-Schriftarten mit FreeType gerendert werden. Dieser Ansatz ist flexibel, skalierbar und effizient bei verschiedenen Zeichencodierungen. Dieser Ansatz kann jedoch für Ihre Anwendung zu schwer sein, da für jedes Zeichen eine Textur erstellt wird. Produktive Bitmap-Schriftarten werden bevorzugt, da wir für alle Glyphen eine Textur haben. Der beste Ansatz besteht darin, die beiden Ansätze zu kombinieren und den besten Ansatz zu wählen: Generieren Sie im laufenden Betrieb eine Rasterschrift aus Glyphen, die mit FreeType heruntergeladen wurden. Dies erspart dem Renderer zahlreiche Texturwechsel und erhöht je nach Texturverpackung die Leistung.
FreeType hat jedoch noch einen weiteren Nachteil: Glyphen mit fester Größe. Dies bedeutet, dass mit zunehmender Größe der gerenderten Glyphe möglicherweise Schritte auf dem Bildschirm angezeigt werden und die Glyphe beim Drehen möglicherweise verschwommen aussieht. Valve hat dieses Problem vor einigen Jahren mithilfe signierter Distanzfelder gelöst (Link zum Webarchiv). Sie haben es sehr gut gemacht und es in 3D-Anwendungen gezeigt.
PS : Wir haben ein Telegramm Conf für die Koordination der Überweisungen. Wenn Sie ernsthaft bei der Übersetzung helfen möchten, sind Sie herzlich willkommen!