seuchomat.org v2

OpenGL - homogene Koordinaten


Arbeiten wir mit OpenGL im dreidimensionalen Raum, arbeiten wir grundsätzlich mit homogenen Koordinaten. Dabei handelt es sich um eine Erweiterung der X, Y- und Z-Komponenten eines Vektors um eine vierte Komponente, namentlich W. Das Konstrukt erscheint vielen Leuten etwas verwirrend, darum möchte ich mein Verständnis hierfür erläutern.

Verwenden wir im Alltag Koordinatenangaben, arbeiten wir meist im euklidischen Raum. Homogene Koordinaten sind nun nichts anderes als 4-Vektoren (X, Y, Z, W), wobei W ein Skalierungsfaktor ist. In OpenGL verwenden wir zur Darstellung einer perspektivisch korrekten Szene (weiter hinter liegende Objekte nähern sich dem Fluchtpunkt im unendlichen, werden also immer kleiner und konvergieren in die Mitte) eine Projektionsmatrix, die unsere Szene durch ein imaginäres View Frustum schickt, die letztendlich die Arbeit für uns übernimmt.

Zurück zu den homogenen Koordinaten: teilen wir unsere drei Komponenten X, Y, Z nun durch das W, so erhalten wir wieder unseren ursprünglichen Punkt. Das W ist standardmäßig 1.0 und wird durch die Projektionsmatrix mit transformiert. Haben wir eine orthographische Projektion (keine perspektivische Verzerrung, egal, wie weit etwas entfernt ist, es ist immer gleich groß), so wird das W nicht beeinflußt. Der Grund, warum wir nun homogene Koordinaten in Form von 4-Vektoren statt 3-Vektoren benutzen, lässt sich an verschiedenen Stellen finden.

Einerseits arbeiten wir zur Translation, Rotation und Skalierung in OpenGL mit Matrizen. Während Rotation und Skalierung mit 3x3-Matrizen abgebildet werden können, benötigen wir für die Translation eine 4x4-Matrix. Die Matrizenmultiplikation gibt vor, dass wenn wir einen Vektor mit einer Matrix multiplizieren möchten, wir so viele Zeilen im Vektor wie Spalten in der Matrix benötigen. Eine Multiplikation eines 3-Vektors mit einer 4x4-Matrix ist somit nicht möglich. Macht also Sinn, eine vierte Komponente einzufügen und diese auf 1 zu belassen. So können wir auch alle Transformationen (Translation, ...) in eine Matrix zusammen fassen, die sogenannte Model-Matrix.

Wie oben erwähnt wurde, wird das W durch die perspektivische Projektionsmatrix beeinflusst. Beim Verlassen des Vertex-Shaders werden die Koordinaten automatisch durch die vierte Komponente, das W dividiert (perspektivische Division). So werden die Koordinaten wieder normalisiert, wir haben normalisierte Gerätekoordinaten (NDC), die wir auf den Viewport projizieren können. Das machen wir, damit wir unsere perspektivische Verzerrung bekommen und Objekte, die ferner sind, kleiner werden. Warum wir nun nicht durch den Tiefenwert Z teilen? Das hat den Grund, dass zwar X/Z, Y/Z korrekt wären, aber Z/Z immer 1 ergäbe. Nach dem Vertex-Shader kommt noch der Depth Test, also das Prüfen der Tiefenwerte über den Depth Buffer. Der Tiefenwert des aktuellen Fragments würde somit immer 1 sein und unser Depth Test nicht richtig funktionieren, da wir den aktuell im Depth Buffer hinterlegten Tiefenwert immer mit 1 vergleichen würden statt mit dem von uns gewollt normalisiertem Wert. Es gibt auch die Möglichkeit, den Depth Test früher durchzuführen, das ist aber standardmäßig nicht der Fall.

Ein weiterer Grund ist, dass wir vor der perspektivischen Division und nach der Multiplikation mit Kamera- und Projektionsmatrix unsere Koordinaten im sogenannten Clip Space haben. Da die Division eine für den Computer aufwändige Operation ist, wird schon vorab geprüft, ob die einzelnen Koordinaten im Bereich -W..W liegen und können so beispielsweise durch den Sutherland-Hodgman Algorithmus (https://de.wikipedia.org/wiki/Algorithmus_von_Sutherland-Hodgman) geprüft und ggf. verworfen werden, da sie nicht sichtbar wären und somit keine Division erfolgen muss. Ist der Vertex-Shader durch, sind unsere Koordinaten normalisiert und können auf den Viewport anhand der von uns übermittelten Angaben auf den Viewport projiziert werden. Der Unterschied zwischen Clipping und Culling ist nun, dass beim Clipping vorab schon einzelne Vertices verworfen werden und eventuell das Objekt "zurecht geschnitten wird", während beim Culling die Objekte mehr oder minder direkt entfernt wird, wenn sie nicht sichtbar sind.

Letztlich lässt sich die W-Koordinate auch noch für einen kleinen Trick verwenden. Wollen wir die Beleuchtung berechnen, haben wir etwa Punktlicht- und Richtungslichtquellen (Point Light bzw. Directional Light). Bekanntlich ist eine Division durch W = 0.0 nicht definiert. Das nutzen wir beim Definieren der Koordinaten von Lichtquellen in unserer Szene aus: eine Punktlichtquelle liegt vor, wenn W = 1.0 ist, ein Richtungslicht, wenn W = 0.0 ist. Das prüfen wir in unserem Fragment-Shader. Letztlich spart uns das aufwändige Implementierungen im Shadercode, weil wir nur das W abfragen müssen.
Bei einem Richtungslicht sind alle Strahlen parallel zueinander und die Lichtquelle "unendlich weit entfernt", d. h. die Strahlungsintensität ist immer gleich. Ein Beispiel ist etwa die Simulation der Sonne. Da formal eine Division durch 0 einen Punkt im Unendlichen ergibt (Fluchtpunkt im Unendlichen), können wir das für uns nutzen.

Wer noch mehr Interesse an OpenGL hat, kann sich auf https://learnopengl.com informieren. Die Transformation der unterschiedlichen Koordinatensysteme in OpenGL ist recht komplex, aber letztlich hilft es, ein bisschen Verständnis dafür aufzubauen, was hinter den Kulissen passiert. Ein Artikel, der das ganze noch mal rechnerisch erklärt, lässt sich bei Khronos finden: https://www.khronos.org/opengl/wiki/Vertex_Transformation