Computational Sciences Center

OpenGL

Sämtlich von einem Computer verarbeiteten Daten sind letzten Endes durch Zahlen dargestellt, die geeignet interpretiert werden müssen, um für Menschen verständlich zu sein. Ein üblicher Ansatz besteht darin, die Daten zu visualisieren, sie also in Bilder oder Videos zu übersetzen.

Bei großen Datenmengen ist es wichtig, dass diese Visualisierung möglichst schnell erfolgt. Falls beispielsweise die Daten von einer Computersimulation berechnet werden, wäre es wünschenswert, dass die grafische Darstellung zügig auf Veränderungen der Modellparameter reagiert, so dass wir mit der Simulation interagieren können.

Dafür ist eine hohe Rechenleistung erforderlich, aber angesichts der Einschränkungen der menschlichen Wahrnehmung nur eine relativ geringe Genauigkeit der Berechnungen. Für derartige Aufgaben werden spezialisierte Grafikprozessoren eingesetzt, die häufig gemeinsam mit spezialisiertem Grafikspeicher ein Subsystem bilden, das durch den Hauptprozessor gesteuert werden muss.

Ein weit verbreiteter Industriestandard für die Programmierung solcher Grafiksysteme ist OpenGL. Ältere Fassungen des Standards gingen von einer nur wenig flexiblen Grafikhardware aus, moderne Fassungen erlauben es uns, die Hardware fast völlig frei zu programmieren. Der Preis dieser Flexibilität ist, dass schon eine einfache Aufgabe ein relativ umfangreiches Programm erfordert.

FreeGLUT

OpenGL-Anwendungen müssen mit dem Betriebssystem und der grafischen Benutzerschnittstelle interagieren können, um beispielsweise Fenster zu öffnen oder auf Tastendrücke zu reagieren. Eine nützliche Bibliothek, die diese Aufgaben auf verschiedenen gängigen Betriebssystemen übernimmt, ist FreeGLUT. Die Bibliothek definiert eine Reihe von Funktionen, mit denen sich Callbacks definieren lassen. Diese Callbacks sind Funktionen, die aufgerufen werden, sobald bestimmte Ereignisse eintreten. So gibt es beispielsweise das display-Callback, das aufgerufen wird, wenn der Inhalt eines Fenster gezeichnet werden soll, und das reshape-Callback, dass dafür zuständig ist, auf Veränderungen der Fensterabmessungen zu reagieren.

Die unten stehenden Beispielprogramme können per Makefile übersetzt werden.

Initialisierung: window

Unser erstes Beispielprogramm richtet ein Fenster ein und zeichnet in diesem Fenster ein weißes Quadrat auf schwarzem Hintergrund. Diese an sich einfache Aufgabe erfordert mit FreeGLUT und OpenGL eine Reihe von Schritten: Wir müssen das Fenster anlegen, reshape- und display-Callbacks definieren, mit der Hilfsbibliothek GLEW das OpenGL-System initialisieren, die zu zeichnende Geometrie definieren und schließlich dafür sorgen, dass sie auch gezeichnet wird.

Die letzten beiden Schritte sind in OpenGL etwas umständlich: Die Geometriedaten müssen in einem vertex array abgelegt werden, das wiederum einen vertex buffer enthält, der die Darstellung der Daten im Grafikspeicher repräsentiert. Das Zeichnen erfolgt mittels zweier Programme, die auf der Grafikhardware ausgeführt werden. Die Geometrie wird durch eine Folge von Punkten dargestellt, beispielsweise den Eckpunkten eines Dreiecks. Diese Punkte werden von dem vertex shader genannten Programm verarbeitet, beispielsweise um ihre Koordinaten geeignet zu transformieren. In unserem Fall genügt ein einfaches Programm, dass die Koordinaten des vertex arrays mit einer Transformationsmatrix multipliziert und in die vordefinierte Variable gl_Position schreibt. Wenn alle Punkte vorliegen, konstruiert OpenGL aus ihnen Liniensegmente oder Dreiecke, die dann in Fragmente zerlegt werden, also Kandidaten für Bildpunkte auf dem Monitor. Diese Fragmente werden einem zweiten Programm überantwortet, dem fragment shader, der im Wesentlichen die Aufgabe hat, die Farbe der Bildpunkte festzulegen. In unserem Fall ist diese Farbe konstant weiß. Beide shader müssen übersetzt und miteinander verbunden werden, bevor wir schließlich etwas zeichnen können.

glut1_window.c

Farbe: color

In unserem zweiten Beispielprogramm sollen die einzelnen Punkte unseres Quadrats unterschiedliche Farben aufweisen. Dazu erweitern wir das vertex array um ein Feld, das für jeden Punkt einen Farbton festlegt, und den vertex shader um eine Zeile, die aus dieser Zahl die zu verwendende Farbe ermittelt und sie an den fragment shader weitergibt.

glut2_color.c

Dreidimensionale Grafik: perspective

Um dreidimensionale Geometrien angemessen darzustellen, müssen wir sie perspektivisch korrekt in unser Fenster projizieren. Mit dem Strahlensatz ist das kein Problem, im einfachsten Fall genügt eine Division durch die z-Koordinate. Diese Operation lässt sich besonders elegant mit Hilfe homogener Koordinaten darstellen: Wir erweitern die dreidimensionalen Koordinatenvektoren um eine vierte Dimension und führen die Konvention ein, dass nach unseren Transformationen einmal der gesamte Vektor so skaliert werden soll, dass diese vierte Koordinate gleich eins ist, dass also die drei Koordinaten durch die vierte dividiert werden müssen. Diesen Schritt übernimmt OpenGL für uns, so dass wir lediglich eine geeignete 4x4-Matrix für die Transformation zu finden brauchen.

Dabei ist noch die Besonderheit zu beachten, dass die z-Koordinate geeignet mittransformiert werden sollte. Im vorliegenden Beispiel sehen wir sie zwar nicht, weil die Darstellung auf dem Monitor nur zweidimensional ist, bei späteren Anwendungen wird sie allerdings benötigt, um zu entscheiden, ob Teile der Geometrie von anderen Teilen verdeckt werden.

glut3_perspective.c

Interaktion: mouse

Wir erhalten einen wesentlich besseren Eindruck von der Form eines geometrischen Objekts, wenn wir es drehen und aus unterschiedlichen Perspektiven betrachten können. Mit den mouse- und motion-Callbacks lässt sich diese Aufgabe einfach realisieren: Im mouse-Callback merken wir uns die aktuelle Position der Maus und die aktuellen Rotationswinkel, im motion-Callback registrieren wir, wie weit sich die Maus von der gemerkten Position entfernt hat und passen die Winkel entsprechend an, um anschließend mit einem Aufruf der Funktion glutPostRedisplay zu veranlassen, dass die Grafik mit den neuen Winkeln gezeichnet wird.

glut4_mouse.c, transformations.h, transformations.c

Punktbasierte Beleuchtung: vtxlighting

Um von einfachen Liniengrafiken zu realistischeren Darstellungen zu wechseln, können wir ein Beleuchtungsmodell verwenden, das beschreibt, wie die einzelnen Bildpunkte beleuchtet werden sollen. Je realistischer das Modell ist, desto überzeugender wirkt die Grafik.

Wir beschränken uns auf das Phong-Modell, das die Farbe eines Bildpunkts aus drei Quellen ermittelt: Das Umgebungslicht (ambient lighting), das von der Geometrie eines Punkts unabhängig ist. Das an dem Punkt gestreute Licht (diffuse lighting), das davon abhängt, in welchem Winkel ein von einer Lichtquelle ausgehende Lichtstrahl die Oberfläche trifft. Und das an dem Punkt reflektierte Licht (specular lighting), das davon abhängt, in welchem Winkel der reflektierte Lichtstrahl zu der Blickrichtung steht.

Für diffuse lighting und specular lighting müssen wir jedem Punkt einen Einheitsnormalenvektor zuordnen, der senkrecht auf der dargestellten Oberfläche stehen soll. Diesen Vektor tragen wir in das vertex array ein, damit er dem vertex shader zur Verfügung steht. Darüber hinaus legen wir auch für jeden Punkt eine Farbe fest, die ebenfalls im vertex array vermerkt wird. Im Interesse einer hohen Geschwindigkeit führt unser Programm sämtliche Beleuchtungsberechnungen nur für Eckpunkte der verwendeten Dreiecke, aber nicht für Fragmente durch, so dass die tatsächliche Farbe sich durch lineare Interpolation zwischen den Eckpunkten ergibt.

glut5_vtxlighting.c

Fragmentbasierte Beleuchtung: frglighting

Eine realistische Beleuchtung erhalten wir, wenn wir die Beleuchtungsberechnungen für jedes Fragment eines Dreiecks statt nur für die Eckpunkte durchführen. In diesem Fall ist der vertex shader lediglich dafür zuständig, die Punktkoordinaten zu transformieren und an den fragment shader weiter zu reichen, der dann für jedes Fragment einen Normalenvektor konstruiert und die Farbe mit dem Phong-Modell berechnet.

glut6_frglighting.c

GTK+ 3.x

Für anspruchsvollere Programme, die neben der reinen OpenGL-Zeichenfläche noch weitere Elemente einer Benutzerschnittstelle bieten sollen, empfiehlt es sich, auf eine Bibliothek wie GTK+ zurück zu greifen. Seit der Version 3.16 bietet GTK+ die Möglichkeit, das Widget GtkGLArea zu verwenden, das uns eine Zeichenfläche für OpenGL-Befehle zur Verfügung stellt.

Das Makefile muss natürlich etwas angepasst werden, um die für GTK+ erforderlichen Bibliotheken und Headerdateien zu berücksichtigen.

Bei den ersten Beispielprogrammen

gtkgl1_window.c, gtkgl2_color.c, gtkgl3_perspective.c

halten sich die Änderungen gegenüber ihren FreeGLUT-Gegenstücken im Rahmen: Die Initialisierung ist ein Callback für das Signal realize, das ausgelöst wird, sobald die Zeichenfläche angelegt wird. Das Callback für das Zeichnen heißt nicht display, sondern render, und der Einfachheit halber kann sie auch die Aufgaben übernehmen, die zuvor dem reshape-Callback zufielen.

Bei den interaktiven Beispielprogrammen

gtkgl4_mouse.c, gtkgl5_vtxlighting.c, gtkgl6_frglighting.c

ändert sich im Prinzip auch nicht viel, es kommen die button- und motion-Callbacks hinzu, um auf Mausbewegungen reagieren zu können. Eine Besonderheit ist hier, dass wir mit der Funktion gtk_widget_add_events dafür sorgen müssen, dass die entsprechenden Signale auch tatsächlich unsere Zeichenfläche erreichen.