Einführung in OpenGL: Ein 3D-Text-Rendering-Tutorial
Veröffentlicht: 2022-03-11Mit der Verfügbarkeit von Tools wie DirectX und OpenGL ist das Schreiben einer Desktop-Anwendung, die 3D-Elemente rendert, heutzutage nicht sehr schwierig. Wie bei vielen Technologien gibt es jedoch manchmal Hindernisse, die es Entwicklern erschweren, in diese Nische einzusteigen. Im Laufe der Zeit hat der Wettlauf zwischen DirectX und OpenGL dazu geführt, dass diese Technologien für Entwickler zugänglicher wurden, zusammen mit einer besseren Dokumentation und einem einfacheren Prozess, ein erfahrener DirectX- oder OpenGL-Entwickler zu werden.
DirectX, eingeführt und verwaltet von Microsoft, ist eine Technologie, die für die Windows-Plattform spezifisch ist. Andererseits ist OpenGL eine plattformübergreifende API für den 3D-Grafikbereich, deren Spezifikation von der Khronos Group gepflegt wird.
In dieser Einführung in OpenGL werde ich erklären, wie man eine sehr einfache Anwendung zum Rendern von 3D-Textmodellen schreibt. Wir werden Qt/Qt Creator verwenden, um die Benutzeroberfläche zu implementieren, was es einfach macht, diese Anwendung auf mehreren Plattformen zu kompilieren und auszuführen. Der Quellcode des für diesen Artikel erstellten Prototyps ist auf GitHub verfügbar.
Das Ziel dieser einfachen Anwendung ist es, 3D-Modelle zu generieren, sie in einer Datei mit einem einfachen Format zu speichern und sie auf dem Bildschirm zu öffnen und zu rendern. Das 3D-Modell in der gerenderten Szene ist drehbar und zoombar, um ein besseres Gefühl für Tiefe und Dimension zu vermitteln.
Voraussetzungen
Bevor wir beginnen, müssen wir unsere Entwicklungsumgebung mit einigen nützlichen Tools für dieses Projekt vorbereiten. Als allererstes benötigen wir das Qt-Framework und relevante Utilities, die von www.qt.io heruntergeladen werden können. Es kann auch über den Standard-Paketmanager Ihres Betriebssystems verfügbar sein; Wenn das der Fall ist, sollten Sie es vielleicht zuerst damit versuchen. Dieser Artikel erfordert eine gewisse Vertrautheit mit dem Qt-Framework. Wenn Sie jedoch mit dem Framework nicht vertraut sind, fühlen Sie sich bitte nicht entmutigt, ihm zu folgen, da der Prototyp auf einigen ziemlich trivialen Funktionen des Frameworks beruht.
Sie können auch Microsoft Visual Studio 2013 unter Windows verwenden. Stellen Sie in diesem Fall sicher, dass Sie das entsprechende Qt-Add-in für Visual Studio verwenden.
An dieser Stelle möchten Sie vielleicht das Repository von GitHub klonen und ihm folgen, während Sie diesen Artikel durchlesen.
OpenGL-Übersicht
Wir beginnen mit der Erstellung eines einfachen Qt-Anwendungsprojekts mit einem einzelnen Dokument-Widget. Da es sich um ein einfaches Widget handelt, wird das Kompilieren und Ausführen nichts Nützliches erzeugen. Mit Qt Designer fügen wir ein „Datei“-Menü mit vier Elementen hinzu: „Neu…“, „Öffnen…“, „Schließen“ und „Beenden“. Sie finden den Code, der diese Menüelemente an die entsprechenden Aktionen im Repository bindet.
Wenn Sie auf „Neu…“ klicken, sollte ein Dialog erscheinen, der in etwa so aussieht:
Hier kann der Benutzer Text eingeben, eine Schriftart auswählen, die resultierende Modellhöhe anpassen und ein 3D-Modell generieren. Ein Klick auf „Erstellen“ sollte das Modell speichern und es auch öffnen, wenn der Benutzer die entsprechende Option in der unteren linken Ecke auswählt. Wie Sie sehen können, besteht das Ziel hier darin, einen vom Benutzer eingegebenen Text in ein 3D-Modell umzuwandeln und auf dem Display darzustellen.
Das Projekt wird eine einfache Struktur haben und die Komponenten werden in eine Handvoll C++- und Header-Dateien unterteilt:
createcharmodeldlg.h/cpp
Dateien enthalten von QDialog abgeleitete Objekte. Dies implementiert das Dialog-Widget, das es dem Benutzer ermöglicht, Text einzugeben, eine Schriftart auszuwählen und zu wählen, ob das Ergebnis in einer Datei gespeichert und/oder in 3D angezeigt werden soll.
gl_widget.h/cpp
Enthält die Implementierung des von QOpenGLWidget abgeleiteten Objekts. Dieses Widget wird zum Rendern der 3D-Szene verwendet.
mainwindow.h/cpp
Enthält die Implementierung des Widgets der Hauptanwendung. Diese Dateien wurden unverändert gelassen, da sie vom Qt Creator-Assistenten erstellt wurden.
main.cpp
Enthält die Funktion main(…), die das Widget der Hauptanwendung erstellt und auf dem Bildschirm anzeigt.
model2d_processing.h/cpp
Enthält die Funktionalität zur Erstellung einer 2D-Szene.
model3d.h/cpp
Enthält Strukturen, die 3D-Modellobjekte speichern und es Operationen ermöglichen, an ihnen zu arbeiten (Speichern, Laden usw.).
model_creator.h/cpp
Enthält die Implementierung einer Klasse, die die Erstellung eines 3D-Szenenmodellobjekts ermöglicht.
OpenGL-Implementierung
Der Kürze halber überspringen wir die offensichtlichen Details der Implementierung der Benutzeroberfläche mit Qt Designer und den Code, der das Verhalten der interaktiven Elemente definiert. Es gibt sicherlich einige weitere interessante Aspekte dieser Prototypanwendung, die nicht nur wichtig, sondern auch relevant für die Kodierung und das Rendering von 3D-Modellen sind, die wir behandeln möchten. Beispielsweise besteht der erste Schritt zum Konvertieren von Text in ein 3D-Modell in diesem Prototyp darin, den Text in ein monochromes 2D-Bild zu konvertieren. Sobald dieses Bild generiert ist, ist es möglich zu wissen, welche Pixel des Bildes den Text bilden und welche nur „leerer“ Raum sind. Es gibt einige einfachere Möglichkeiten, einfachen Text mit OpenGL zu rendern, aber wir verfolgen diesen Ansatz, um einige grundlegende Details des 3D-Renderings mit OpenGL abzudecken.
Um dieses Bild zu generieren, instanziieren wir ein QImage-Objekt mit dem Flag QImage::Format_Mono. Da wir nur wissen müssen, welche Pixel Teil des Textes sind und welche nicht, sollte ein monochromes Bild gut funktionieren. Wenn der Benutzer Text eingibt, aktualisieren wir dieses QImage-Objekt synchron. Basierend auf der Schriftgröße und Bildbreite versuchen wir unser Bestes, um den Text innerhalb der benutzerdefinierten Höhe anzupassen.
Als nächstes zählen wir alle Pixel auf, die Teil des Textes sind – in diesem Fall die schwarzen Pixel. Jedes Pixel wird hier als separate quadratische Einheit behandelt. Auf dieser Grundlage können wir eine Liste von Dreiecken erstellen, die Koordinaten ihrer Eckpunkte berechnen und sie in unserer 3D-Modelldatei speichern.
Jetzt, da wir unser eigenes einfaches Dateiformat für 3D-Modelle haben, können wir uns auf das Rendern konzentrieren. Für OpenGL-basiertes 3D-Rendering stellt Qt ein Widget namens QOpenGLWidget bereit. Um dieses Widget zu verwenden, können drei Funktionen überschrieben werden:
- initializeGl() - hier geht der Initialisierungscode hin
- paintGl() - diese Methode wird jedes Mal aufgerufen, wenn das Widget neu gezeichnet wird
- resizeGl(int w, int h) – diese Methode wird bei jeder Größenänderung mit der Breite und Höhe des Widgets aufgerufen
Wir werden das Widget initialisieren, indem wir die entsprechende Shader-Konfiguration in der initializeGl-Methode festlegen.
glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);
Die erste Zeile bewirkt, dass das Programm nur die gerenderten Pixel anzeigt, die uns näher sind, und nicht diejenigen, die sich hinter anderen Pixeln befinden und nicht sichtbar sind. Die zweite Zeile gibt die Flachschattierungstechnik an. Die dritte Zeile lässt das Programm Dreiecke rendern, unabhängig davon, in welche Richtung ihre Normalen zeigen.
Nach der Initialisierung rendern wir das Modell bei jedem Aufruf von paintGl auf dem Display. Bevor wir die Methode paintGl überschreiben, müssen wir den Puffer vorbereiten. Dazu erstellen wir zunächst ein Buffer-Handle. Dann binden wir das Handle an einen der Bindungspunkte, kopieren die Quelldaten in den Puffer und schließlich weisen wir das Programm an, den Puffer zu entbinden:
// Get the Qt object which allows to operate with buffers QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Create the buffer handle funcs.glGenBuffers(1, &handle); // Select buffer by its handle (so we'll use this buffer // further) funcs.glBindBuffer(GL_ARRAY_BUFFER, handle); // Copy data into the buffer. Being copied, // source data is not used any more and can be released funcs.glBufferData(GL_ARRAY_BUFFER, size_in_bytes, src_data, GL_STATIC_DRAW); // Tell the program we've finished with the handle funcs.glBindBuffer(GL_ARRAY_BUFFER, 0);
Innerhalb der überschreibenden paintGl-Methode verwenden wir ein Array von Scheitelpunkten und ein Array von normalen Daten, um die Dreiecke für jeden Frame zu zeichnen:

QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Vertex data glEnableClientState(GL_VERTEX_ARRAY);// Work with VERTEX buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hVertexes); // Use this one glVertexPointer(3, GL_FLOAT, 0, 0); // Data format funcs.glVertexAttribPointer(m_coordVertex, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Normal data glEnableClientState(GL_NORMAL_ARRAY);// Work with NORMAL buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hNormals);// Use this one glNormalPointer(GL_FLOAT, 0, 0); // Data format funcs.glEnableVertexAttribArray(m_coordNormal); // Shader attribute funcs.glVertexAttribPointer(m_coordNormal, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Draw frame glDrawArrays(GL_TRIANGLES, 0, (3 * m_model.GetTriangleCount())); // Rendering finished, buffers are not in use now funcs.glDisableVertexAttribArray(m_coordNormal); funcs.glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY);
Für eine verbesserte Leistung haben wir in unserer Prototypanwendung Vertex Buffer Object (VBO) verwendet. Dadurch können wir Daten im Videospeicher speichern und direkt zum Rendern verwenden. Eine alternative Methode dazu besteht darin, die Daten (Scheitelkoordinaten, Normalen und Farben) aus dem Rendering-Code bereitzustellen:
glBegin(GL_TRIANGLES); // Provide coordinates of triangle #1 glVertex3f( x[0], y[0], z[0]); glVertex3f( x[1], y[1], z[1]); glVertex3f( x[2], y[2], z[2]); // Provide coordinates of other triangles ... glEnd();
Dies mag wie eine einfachere Lösung erscheinen; Dies hat jedoch schwerwiegende Auswirkungen auf die Leistung, da die Daten dazu den Videospeicherbus durchlaufen müssen - ein relativ langsamer Prozess. Nach der Implementierung der paintGl-Methode müssen wir auf Shader achten:
m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, QString::fromUtf8( "#version 400\r\n" "\r\n" "layout (location = 0) in vec3 coordVertexes;\r\n" "layout (location = 1) in vec3 coordNormals;\r\n" "flat out float lightIntensity;\r\n" "\r\n" "uniform mat4 matrixVertex;\r\n" "uniform mat4 matrixNormal;\r\n" "\r\n" "void main()\r\n" "{\r\n" " gl_Position = matrixVertex * vec4(coordVertexes, 1.0);\r\n" " lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);\r\n" "}")); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( "#version 400\r\n" "\r\n" "flat in float lightIntensity;\r\n" "\r\n" "layout (location = 0) out vec4 FragColor;\r\n" "uniform vec3 fragmentColor;\r\n" "\r\n" "void main()\r\n" "{\r\n" " FragColor = vec4(fragmentColor * lightIntensity, 1.0);\r\n" "}")); m_shaderProgram.link(); m_shaderProgram.bind(); m_coordVertex = m_shaderProgram.attributeLocation(QString::fromUtf8("coordVertexes")); m_coordNormal = m_shaderProgram.attributeLocation(QString::fromUtf8("coordNormals")); m_matrixVertex = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixVertex")); m_matrixNormal = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixNormal")); m_colorFragment = m_shaderProgram.uniformLocation(QString::fromUtf8("fragmentColor"));
Bei OpenGL werden Shader mithilfe einer Sprache implementiert, die als GLSL bekannt ist. Die Sprache wurde entwickelt, um die Bearbeitung von 3D-Daten vor dem Rendern zu vereinfachen. Hier benötigen wir zwei Shader: Vertex-Shader und Fragment-Shader. Im Vertex-Shader transformieren wir die Koordinaten mit der Transformationsmatrix, um Rotation und Zoom anzuwenden und die Farbe zu berechnen. Im Fragment-Shader weisen wir dem Fragment Farbe zu. Diese Shader-Programme müssen dann kompiliert und mit dem Kontext verknüpft werden. OpenGL bietet einfache Möglichkeiten, die beiden Umgebungen zu überbrücken, sodass auf Parameter innerhalb des Programms zugegriffen oder von außen zugewiesen werden kann:
// Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);
Im Vertex-Shader-Code berechnen wir die neue Vertex-Position, indem wir die Transformationsmatrix auf die ursprünglichen Vertices anwenden:
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
Um diese Transformationsmatrix zu berechnen, berechnen wir einige separate Matrizen: Bildschirmskalierung, Szene verschieben, Skalieren, Rotieren und Zentrieren. Wir finden dann das Produkt dieser Matrizen, um die endgültige Transformationsmatrix zu berechnen. Beginnen Sie damit, die Modellmitte zum Ursprung (0, 0, 0) zu verschieben, der auch die Mitte des Bildschirms ist. Die Drehung wird durch die Interaktion des Benutzers mit der Szene unter Verwendung eines Zeigegeräts bestimmt. Der Benutzer kann auf die Szene klicken und herumziehen, um sie zu drehen. Wenn der Benutzer klickt, speichern wir die Cursorposition und nach einer Bewegung haben wir die zweite Cursorposition. Mit diesen beiden Koordinaten zusammen mit dem Szenenmittelpunkt bilden wir ein Dreieck. Nach einigen einfachen Berechnungen können wir den Rotationswinkel bestimmen und unsere Rotationsmatrix aktualisieren, um diese Änderung widerzuspiegeln. Für die Skalierung verlassen wir uns einfach auf das Mausrad, um den Skalierungsfaktor der X- und Y-Achse des OpenGL-Widgets zu ändern. Das Modell wird um 0,5 zurückversetzt, um es hinter der Ebene zu halten, von der aus die Szene gerendert wird. Um schließlich das natürliche Seitenverhältnis beizubehalten, müssen wir die Verringerung der Modellerweiterung entlang der längeren Seite anpassen (im Gegensatz zur OpenGL-Szene kann das Widget, in dem es gerendert wird, unterschiedliche physikalische Abmessungen entlang beider Achsen haben). Wenn wir all dies kombinieren, berechnen wir die endgültige Transformationsmatrix wie folgt:
void GlWidget::GetMatrixTransform(QMatrix4x4& matrixVertex, const Model3DEx& model) { matrixVertex.setToIdentity(); QMatrix4x4 matrixScaleScreen; double dimMin = static_cast<double>(qMin(width(), height())); float scaleScreenVert = static_cast<float>(dimMin / static_cast<double>(height())); float scaleScreenHorz = static_cast<float>(dimMin / static_cast<double>(width())); matrixScaleScreen.scale(scaleScreenHorz, scaleScreenVert, 1.0f); QMatrix4x4 matrixCenter; float centerX, centerY, centerZ; model.GetCenter(centerX, centerY, centerZ); matrixCenter.translate(-centerX, -centerY, -centerZ); QMatrix4x4 matrixScale; float radius = 1.0; model.GetRadius(radius); float scale = static_cast<float>(m_scaleCoeff / radius); matrixScale.scale(scale, scale, 0.5f / radius); QMatrix4x4 matrixTranslateScene; matrixTranslateScene.translate(0.0f, 0.0f, -0.5f); matrixVertex = matrixScaleScreen * matrixTranslateScene * matrixScale * m_matrixRotate * matrixCenter; }
Fazit
In dieser Einführung in das OpenGL-3D-Rendering haben wir eine der Technologien untersucht, die es ud ermöglichen, unsere Grafikkarte zum Rendern eines 3D-Modells zu verwenden. Dies ist viel effizienter als die Verwendung von CPU-Zyklen für den gleichen Zweck. Wir haben eine sehr einfache Schattierungstechnik verwendet und die Szene durch die Handhabung von Benutzereingaben mit der Maus interaktiv gemacht. Wir haben es vermieden, den Videospeicherbus zu verwenden, um Daten zwischen dem Videospeicher und dem Programm hin und her zu übertragen. Obwohl wir nur eine einzelne Textzeile in 3D gerendert haben, können kompliziertere Szenen auf sehr ähnliche Weise gerendert werden.
Um fair zu sein, dieses Tutorial hat kaum an der Oberfläche der 3D-Modellierung und des Renderings gekratzt. Dies ist ein umfangreiches Thema, und dieses OpenGL-Tutorial kann nicht behaupten, dass dies alles ist, was Sie wissen müssen, um 3D-Spiele oder Modellierungssoftware erstellen zu können. Der Zweck dieses Artikels ist es jedoch, Ihnen einen Einblick in diesen Bereich zu geben und zu zeigen, wie einfach Sie mit OpenGL beginnen können, um 3D-Anwendungen zu erstellen.