OpenGL gibt es schon seit Langem und wenn man die gesammelten Dokumentationen im Internet liest, ist nicht immer ganz klar, welche Teile bereits veraltet sind und welche Teile immer noch nützlich und auf moderner Grafikhardware unterstützt werden. Vor diesem Hintergrund haben wir uns entschlossen einen neuen Einsteigerguide für OpenGL zu schreiben. In diesem Beitrag wird nur noch auf Artikel eingegangen, die auch heute noch aktuell sind.

Was ist OpenGL?

Wikipedia gibt einen guten Überblick über den Zweck und die Geschichte von OpenGL, aber wir werden hier noch einmal eine kurze Zusammenfassung geben. In seiner modernen Form ist OpenGL eine plattformübergreifende Bibliothek für die Anbindung an programmierbare Grafikprozessoren zum Rendern von Echtzeit-3D-Grafiken. Seine Verwendung ist in Spielen, CAD– und Datenvisualisierungsanwendungen üblich. Es begann in den frühen 90er Jahren als eine plattformübergreifende Standardisierung von SGIs proprietärer Graphics Library (GL), die die Grafikhardware in ihren High-End-Workstations antrieb. Einige Jahre später schoben GLQuake und die Voodoo-Grafikbeschleuniger von 3dfx 3D-Beschleuniger in den Mainstream und OpenGL wurde neben Microsofts proprietärer Direct3D-Bibliothek zum Standard für die Steuerung von Grafikbeschleunigern in Consumer-PCs. In den letzten Jahren hat die Khronos-Gruppe die Verantwortung für den OpenGL-Standard übernommen, indem Sie diesen weiterentwickelt hat, um die Funktionen moderner programmierbarer Grafikprozessoren zu unterstützen und eine Nutzung auf Internetseiten und mobilen Endgeräten zu ermöglichen. Daneben wurden veraltete Funktionen, die frühere Versionen der Bibliothek überladen haben, verworfen.

Eine weitere Entwicklung der letzten Zeit ist die Einführung von GPU-Bibliotheken (GPGPU), darunter Nvidias CUDA und Khronos OpenGL. Diese Bibliotheken implementieren Dialekte von C mit zusätzlichen Data parallelism Features, so dass die GPU für allgemeine Berechnungen verwendet werden kann, ohne dass sie innerhalb des grafikorientierten Rahmens von OpenGL arbeiten muss. Diese GPGPU-Frameworks ersetzen OpenGL jedoch nicht, da ihr Hauptzweck nicht die Grafikprogrammierung ist, bieten sie lediglich Zugriff auf die Recheneinheiten eines Grafikprozessors, ohne dessen grafikspezifische Hardware zu berücksichtigen. Sie können jedoch als Zubehör zu OpenGL fungieren. Sowohl CUDA als auch OpenGL können Puffer des GPU-Speichers mit OpenGL gemeinsam nutzen und Daten zwischen GPGPU-Programmen und der Grafik-Pipeline austauschen. Wir werden hier nicht weiter auf GPGPU eingehen. Wir werden uns hier darauf konzentrieren, OpenGL für Grafikaufgaben zu verwenden.

Um den weiteren Ausführungen folgen zu können, sollten Sie über Programmierkenntnisse in C verfügen, sie müssen sich aber nicht mit OpenGL oder der Grafikprogrammierung auskennen. Eine grundlegende Algebra und Geometrie wird ihnen gut weiterhelfen können. Wir werden auf OpenGL 2.0 eingehen und vermeiden über API-Features zu diskutieren, die in OpenGL 3 oder OpenGL ES veraltet oder entfernt worden sind. Zusätzlich zu OpenGL werden wir zwei weitere Hilfsbibliotheken verwenden: GLUT (GL Utility Toolkit), das eine plattformübergreifende Schnittstelle zwischen dem Window-System und OpenGL bietet und GLEW (GL Extension Wrangler), welche den Umgang mit verschiedenen Versionen von OpenGL und ihren Erweiterungen vereinfacht.

Wo bekomme ich OpenGL, GLUT und GLEW?

OpenGL kommt standardmäßig in irgendeiner Form auf MacOS X, Windows und den meisten Linux-Distributionen zum Einsatz. Wenn Sie diesem Tutorial folgen möchten, müssen sie sicherstellen, dass ihre OpenGL-Implementierung mindestens Version 2.0 unterstützt. Die OpenGL-Implementierung von MacOS X unterstützt immer OpenGL 2.0, zumindestens in der Software, wenn der Grafikkartentreiber dies nicht unterstützt. Unter Windows sind Sie darauf angewiesen, dass ihre Grafikkartentreiber OpenGL 2 oder höher bereitstellen. Sie können den kostenlosen OpenGL Extensions Viewer von RealTech verwenden, um zu sehen, welche OpenGL-Version ihr Treiber unterstützt. Die OpenGL-Treiber von Nvidia und AMD unterstützen mindestens OpenGL 2.0 auf allen Grafikkarten, die in den letzten vier Jahren veröffentlicht wurden. Anwender von Intel Onboard- oder älteren Grafikkarten haben weniger Glück. Für einen Fallback bietet Mesa 3D eine Open-Source-, Cross-Plattform-Software OpenGL 2.1-Implementierung, die unter Windows und fast allen Unix-Plattformen funktioniert.

Mesa ist auch die am weitesten verbreitete OpenGL-Implementierung unter Linux, wo es auch mit dem X-Server zusammenarbeitet, um OpenGL mit Grafikhardware unter Verwendung von „direct rendering Infrastructure (DRI)“-Treibern zu verbinden. Sie können sehen, ob ihr spezieller DRI-Treiber OpenGL 2.0 unterstützt, indem Sie den glxinfo-Befehl von einem xterm ausführen. Wenn OpenGL 2.0 auf ihrer Hardware nicht unterstützt wird, können Sie den Treiber deaktivieren, um auf die Software-Implementierung von Mesa zurückzugreifen. Nvidia stellt auch ihre eigene proprietäre OpenGL-Implementierung für Linux bereit, die auf ihre eigenen GPUs abzielt. Diese Implementierung sollte OpenGL 2.0 oder höher auf jeder aktuellen Nvidia-Karte bereitstellen.

Um GLUT und GLEW zu installieren, suchen sie nach den Binärpaketen auf ihren jeweiligen Seiten. MacOS X wird mit vorinstallierten GLUT ausgeliefert. Die meisten Linux-Distributionen haben GLUT und GLEW über ihr Paketsystem verfügbar, obwohl Sie für GLUT möglicherweise die optionalen „non-free“ Paket-Repositories ihrer Distribution aktivieren müssen, da ihre Lizenz technisch gesehen nicht Open Source ist. Es gibt einen Open-Source-GLUT-Klon mit der Bezeichnung OpenGLUT, wenn Sie ein Verfechter solcher Dinge sind.

Wenn Sie ein erfahrener C-Programmierer sind, sollten Sie in der Lage sein, diese Bibliotheken zu installieren und Sie problemlos in ihrer Entwicklungsumgebung zum Laufen zu bringen. Aber bevor wir tiefer in die Materie einsteigen, werden wir im Folgenden ein paar Konzepte für große Bilder durchgehen. In diesem Beitrag werden wir die Grafikpipeline eines typischen Renderingauftrags erläutern.

Die Grafikpipeline.

Aus datenschutzrechtlichen Gründen benötigt YouTube Ihre Einwilligung um geladen zu werden. Mehr Informationen finden Sie unter Datenschutzerklärung.

Seit den Anfängen von Echtzeit-3D ist das Dreieck der Pinsel mit dem Szenen gezeichnet werden. Obwohl moderne Grafikprozessoren alle möglichen auffälligen Effekte ausführen können, um dieses Geheimnis zu vertuschen, sind Dreiecke unter all den Shadings immer noch das Medium, in dem sie arbeiten. Die Grafikpipeline, die OpenGL implementiert, spiegelt dies wieder. Das Host-Programm füllt OpenGL-verwaltete Speicherpuffer mit Vertices-Arrays. Diese Vertices werden in den Bildschirmraum projiziert, zu Dreiecken zusammengesetzt und zu pixelgroßen Fragmenten gerastert. Schließlich werden den Fragmenten Farbwerte zugewiesen und in den Framebuffer gezeichnet. Moderne Grafikprozessoren erhalten ihre Flexibilität, indem sie die Stufen „Projekt in den Bildschirmraum“ und „Farbwerte zuweisen“ an hochladbare Programme, sogenannte Shader, delegieren. Schauen wir uns die Etappen ein wenig genauer an:

Die Vertex– und Element-Arrays.

Ein Renderjob beginnt seine Reise durch die Pipeline in einem Satz von einem oder mehreren Vertex-Puffern, die mit Arrays von Vertex-Attributen gefüllt sind. Diese Attribute werden als Input für den Vertex-Shader verwendet. Zu den üblichen Vertex-Attributen gehören die Lage des Vertex im 3D-Raum und ein oder mehrere Sätze von Texturkoordinaten, die den Vertex auf einen Sample-Punkt auf einer oder mehreren Texturen abbilden. Der Satz von Vertex-Puffern, die Daten an einen Renderjob liefern, wird zusammenfassend als Vertex-Array bezeichnet. Wenn ein Renderjob übergeben wird, liefern wir ein zusätzliches Element-Array, ein Array von Indizes in das Vertexarray, das auswählt, welche Vertices in die Pipeline eingespeist werden. Die Reihenfolge der Indizes steuert auch, wie die Eckpunkte später zu Dreiecken zusammengesetzt werden.

Einheitlicher Zustand und Texturen.

Ein Renderingjob hat auch einen einheitlichen Status, der den Shadern in jedem programmierbaren Stadium der Pipeline einen Satz gemeinsamer, schreibgeschützter Werte zur Verfügung stellt. Dies erlaubt es dem Shader-Programm, Parameter zu nehmen, die nicht zwischen Vertices oder Fragmenten wechseln. Der einheitliche Zustand umfasst Texturen, das sind ein-, zwei- oder dreidimensionale Arrays, die von Shadern abgetastet werden können. Wie der Name schon sagt, werden Texturen häufig verwendet, um Texturbilder auf Oberflächen abzubilden. Sie können auch als Lookup-Tabellen für vorberechnete Funktionen oder als Datensätze für verschiedene Arten von Effekten verwendet werden.

Der Vertex-Shader.

Die GPU beginnt, indem sie jeden ausgewählten Vertex aus dem Vertex-Array liest und durch den Vertex-Shader führt, ein Programm, das eine Reihe von Vertex-Attributen als Eingänge verwendet und einen neuen Satz von Attributen ausgibt, die als variierende Werte bezeichnet werden und dem Rasterizer zugeführt werden. Der Vertex-Shader berechnet mindestens die projizierte Position des Vertex im Bildschirmbereich. Der Vertex kann auch andere, variierende Ausgaben erzeugen, wie z.B. Farb- oder Texturkoordinaten, damit der Rasterizer über die Oberfläche der Dreiecke, die den Vertex verbinden, hinweggeht.

Dreiecksanordnung.

Die GPU verbindet dann die projizierten Eckpunkte zu Dreiecken. Dazu werden die Eckpunkte in der durch das Elementarray festgelegten Reihenfolge genommen und in Dreiergruppen gruppiert. Die Eckpunkte können auf verschiedene Arten gruppiert werden:

  • Nehmen Sie alle drei Elemente als eigenständiges Dreieck.
  • Erstellen Sie einen Dreiecksstreifen, indem Sie die letzten beiden Eckpunkte jedes Dreiecks als die ersten beiden Eckpunkte des nächsten Dreiecks wiederverwenden.
  • Bilden Sie einen Dreiecksfächer, der das erste Element mit jedem nachfolgenden Element verbindet.

Das Diagramm zeigt, wie sich die drei verschiedenen Modi verhalten. Strips und Lüfter benötigen beide nur einen neuen Index pro Dreieck im Elementarray nach den ersten drei und tauschen die Flexibilität unabhängiger Dreiecke gegen zusätzliche Speichereffizienz im Elementarray.

Rasterung.

Der Rasterizer nimmt jedes Dreieck, schneidet es ab und verwirft Teile, die sich außerhalb des Bildschirm befinden und zerlegt die verbleibenden sichtbaren Teile in pixelgroße Fragmente. Wie bereits erwähnt, werden die unterschiedlichen Ausgaben des Vertex-Shaders auch über die gerastete Oberfläche jedes Dreiecks interpoliert, wobei jedem Fragment ein glatter Gradient von Werten zugewiesen wird. Wenn der Vertex-Shader beispielsweise jedem Vertex einen Farbwert zuweist, wird der Rasterizer diese Farben über die pixelige Oberfläche mischen.

Der Fragment-Shader.

Die erzeugten Fragmente durchlaufen dann ein ein anderes Programm, den Fragment-Shader. Der Fragment-Shader empfängt die vom Vertex-Shader ausgegebenen variablen Werte und wird vom Rasterizer als Input interpoliert. Es gibt Farb- und Tiefenwerte aus, die dann in den Framebuffer gezeichnet werden. Häufige Fragment-Shader-Operationen umfassen Texture Mapping und Lighting. Da der Fragment-Shader für jedes Pixel unabhängig arbeitet, kann er die anspruchsvollsten Spezialeffekte ausführen, ist aber auch der leistungsempfindlichste Teil der Grafik-Pipeline.

Framebuffer, Testen und Blenden.

Ein Framebuffer ist das endgültige Ziel für die Ausgabe eines Renderjobs. Zusätzlich zu dem standardmäßigen Framebuffer, den OpenGL ihnen zum Zeichnen auf dem Bildschirm bietet, können Sie mit den meisten modernen OpenGL-Implementierungen Framebuffer-Objekte erstellen, die in Offscreen-Renderbuffer oder Texturen gezeichnet werden. Diese Texturen können dann als Input für andere Renderjobs verwendet werden. Ein Framebuffer ist mehr als ein einzelnes 2D-Bild. Zusätzlich zu einem oder mehreren Farbpuffern kann ein Framebuffer einen Depth buffer und/oder Stencil buffer haben, die beide optional Fragmente filtern, bevor sie in den Framebuffer gezogen werden. Das Depth Testing verwirft Fragmente von Objekten, die sich hinter den bereits gezeichneten Objekten befinden und das Stencil Testing verwendet Formen, die in den Stencil buffer gezeichnet wurden, um den zeichnungsfähigen Teil des Framebuffers zu begrenzen und „schabloniert“ den Renderjob. Fragmente, die diese Prozesse überleben, haben ihren Farbwert Alpha gemischt mit den zu überschreibenden Farbwert gemischt und die endgültigen Werte für Farbe, Tiefe und Schablone werden in die entsprechenden Puffer gezeichnet.

Schlußfolgerung.

Das ist der Prozess vom Vertex-Puffer bis zum Framebuffer, den ihre Daten durchlaufen, wenn Sie einen einzigen „Draw“-Aufruf in OpenGL durchführen. Das Rendern einer Szene beinhaltet in der Regel mehrere Zeichenjobs, das Wechseln von Texturen, anderen einheitlichen Zuständen oder Shadern zwischen den Übergängen und die Verwendung der Depth- und Stencilbuffer des Framebuffers, um die Ergebnisse der einzelnen Übergänge zu kombinieren. Nun, da wir den allgemeinen Datenfluss des 3D-Renderings abgedeckt haben, können wir ein einfaches Programm schreiben, um zu sehen, wie OpenGL alles möglich macht.

Vielen Dank fürs Lesen.