Perspektive und Z-Buffer - C++ OpenGL Tutorials

Inhaltsverzeichnis[Verbergen]

Bis jetzt haben wir zwar schon ein Fenster erstellt und in dieses Fenster mit OpenGL ein Dreieck hineingezeichnet. Das schaut zwar schon recht hübsch aus, aber wir wollen doch schließlich irgendwann einmal eine dreidimensionale Szene Rendern, und dafür fehlt uns eben noch die dritte Dimension, und die Möglichkeit Objekte durch verschiedenste Transformationen an den richtigen Platz und in die richtige Form zu bringen.
In diesem Kapitel werden wir uns mit einer dreidimensionalen Projektion und der korrekten Sichtbarkeit der Objekte mit dem Z-Buffer beschäftigen, und so schon bald zu den ersten "richtigen" 3D-Bildern kommen.

1. Das Koordinatensystem von OpenGL

OpenGL - Koordinatensystem In OpenGL werden alle Objekte in einem dreidimensionalen kartesischem Koordinatensystem positioniert. Dabei handelt es sich um ein sogenanntes "Rechtshändiges Koordinatensystem". Wie wir im Bild mit dem Monitor erkennen können verläuft dabei die gedachte X-Achse von Links nach Rechts waagrecht durch den Bildschirm. Senkrecht dazu, von unten nach Oben verläuft die Y-Achse, und aus dem Monitor hinaus, also quasi durch uns hindurch läuft noch die Z-Achse. Hierbei sollten vor allem Umsteiger von DirectX (auch ich zähle mich dazu :-) ) aufpassen, da dort fast immer ein linkshändiges Koordinatensystem verwendet wird, bei dem die Z-Achse genau in die entgegengesetzte Richtung verläuft, nämlich in den Bildschirm hinein.

2. Projektionen - Von 3D Koordinaten zum Bild

Da es zur Zeit noch keine Hologrammprojektoren gibt, sondern nur Bildschirme, die natürlich nur zweidimensionale Koordinaten darstellen können, müssen wir bzw. eigentlich die Grafikkarte aus den dreidimensionalen Koordinaten zweidimensionale (Bildschirm-)koordinaten machen, die unser Monitor dann nur mehr ausgeben braucht.

2.1. Einen bunten Würfel rendern

Wir gehen wieder vom Programmgerüst des vorigen Tutorials aus, zeichen jetzt aber einen bunten Würfel statt dem Dreieck, da man an ihm vor allem die Projektion viel besser demonstrieren kann. Der Würfel hat die Kantenlänge 1 und befindet sich im Mittelpunkt unseres Koordinatensystems. Damit wir ihn auch zu sehen bekommen, müssen wir die Projektionsmatrix etwas anpassen, und da wir im ganzen Programmablauf eigentlich immer die gleiche Projektionsmatrix verwenden wollen, können wir dieses Mal den Code etwas optimieren und die Projektionsmatrix nur einmal vor der Endlosschleife setzen. Das gleiche machen wir auch mit der Farbe zum Löschen des Bildbuffers, da wir sie nicht ändern und da, wie wir ja bereits wissen sollten OpenGL als Statemachine aufgebaut ist und deshalb auch alle Werte erhalten bleiben bis ein neuer Wert gesetzt wird.

//...

//--------------------------
// Die Projektion festlegen:
// Diesmal außerhalb der
// Endlosschleife.
//--------------------------

glMatrixMode( GL_PROJECTION ); // Den richtigen Stack aktivieren
glLoadIdentity();              // Die Matrix zurücksetzen

// Eine orthogonale Projektionsmatrix zum Stack
// dazu multiplizieren.

glOrtho( -2, 2, 1.5, -1.5, -1, 1 );  // Dieses Mal Sichtbereich auf auf
                                     // -2 bis 2 in x-Richtung,
                                     // -1.5 bis 1.5 in y-Richtung und
                                     // -1 bis 1 in z-Richtung begrenzt

//--------------------------
// Farbe zum Löschen setzen
// (Auch außerhalb der
// Endlosschleife.
//--------------------------

glClearColor( 1.0, 0.5, 0.4, 0.0 );

// Und noch einmal die Endlosschleife
while(true)
{
  if( !pollEvents() ) break;      // Nachrichten verarbeiten
  glClear( GL_COLOR_BUFFER_BIT ); //Bildbuffer löschen

  //----------------------
  // Den Quader zeichnen:
  //----------------------

  glBegin( GL_QUADS ); // Wir bauen den Würfel aus Quadraten (Quads) auf

    glColor3f(1, 0,   0  );   // Ab jetzt werden alle gezeichneten Punkte rot
      glVertex3f( 1,  1, -1);
      glVertex3f( 1, -1, -1);
      glVertex3f(-1, -1, -1);
      glVertex3f(-1,  1, -1);

    glColor3f(0, 1,   0  );   // Ab jetzt werden alle gezeichneten Punkte grün
      glVertex3f( 1,  1,  1);
      glVertex3f(-1,  1,  1);
      glVertex3f(-1, -1,  1);
      glVertex3f( 1, -1,  1);

    glColor3f(0, 0,   1  );
      glVertex3f( 1,  1, -1);
      glVertex3f( 1,  1,  1);
      glVertex3f( 1, -1,  1);
      glVertex3f( 1, -1, -1);

    glColor3f(1, 1,   0  );
      glVertex3f( 1, -1, -1);
      glVertex3f( 1, -1,  1);
      glVertex3f(-1, -1,  1);
      glVertex3f(-1, -1, -1);

    glColor3f(0, 0.5, 1  );
      glVertex3f(-1, -1, -1);
      glVertex3f(-1, -1,  1);
      glVertex3f(-1,  1,  1);
      glVertex3f(-1,  1, -1);

    glColor3f(1, 0.1, 0.8);
      glVertex3f( 1,  1,  1);
      glVertex3f( 1,  1, -1);
      glVertex3f(-1,  1, -1);
      glVertex3f(-1,  1,  1);

  glEnd(); // Wir sind fertig mit dem Zeichnen

  SDL_GL_SwapBuffers(); // Bildbuffer vertauschen
}

//...

Wenn wir dieses Programm jetzt kompilieren und ausführen sollte sich wieder unser Fenster öffnen, mit dem Unterschied, das dieses Mal ein grünes Quadrat den Großteil des Fensters ausfüllen sollte. Da das aber noch nicht das gewünschte Ergebnis ist gehen wir gleich weiter zu einer perspektivischen Projektion.

2.2. Eine perspektivische Projektion - Let's go 3D

Bis jetzt haben wir immer eine orthogonale Projektion verwendet. Dabei werden die x- und y-Koordinaten der Punkte einfach unabhängig von der z-Koordinate direkt in Bildschirmkoordinaten transformiert. Dabei werden die Koordinaten so skaliert, dass die Koordinaten der sichtbaren Punkte auf allen Achsen zwischen -1 und 1 liegen. Dadurch entsteht ein einfaches "flaches" Bild aller gerenderten Objekte.

Wir wollen aber ein scheinbar dreidimensionales Bild erzeugen und werden deshalb eine perspektivische Projektion einsetzen. Dabei wird die Größe von Objekten bzw. die Position von Punkten auch von deren z-Koordinaten beeinflusst, so dass weiter entfernte Objekte kleiner dargestellt werden, und so die Illusion einer Perspektive erzeugt wird.

2.2.1. Das View-Frustum

OpenGL view frustum Die Projektionsmatrix die wir benötigen muss also einen pyramidenstumpfförmigen Sichtbereich erzeugen. Dieser Sichtbereich wird meistens Frustum oder Viewfrustum genannt. Das ist also der Bereich in dem die dort vorhandenen Objekte auch auf den Bildschirm ausgegeben werden. Alles außerhalb wird durch sogenanntes 'clipping' abgeschnitten. Rechts sehen wir ein Viewfrustum für unsere gewünschte perspektivische Projektion. Abgegrenzt wird der sichtbare Bereich nach vorne und nach hinten durch zwei sogenannte Clipping-Planes. Für die seitlichen Begrenzungen gibt man einfach die Maße der Near-Clipping-Plane (= nahe Clipping Ebene) an. Dann wird der Bereich auf der hinteren Clipping Ebene berechnet, indem man davon ausgeht, dass sich die virtuelle Kamera im Ursprung befindet. Dann müssen wir nur vom Ursprung durch die Eckpunkte der Near-Clipping-Plane vier Geraden durchlegen, die uns dann im Schnitt mit der Far-Clipping-Plane deren Eckpunkte liefern.

Die gerade besprochene Theorie ist zwar sehr wichtig für das Verständnis, aber in OpenGL müssen wir davon fast nichts selbst berechnen sondern nur die Funktion 'glFrustum' aufrufen die wie folgt definiert ist:

void glFrustum( GLdouble left,
                GLdouble right,
                GLdouble bottom,
                GLdouble top,
                GLdouble zNear,
                GLdouble zFar );

Mit left, right, bottom und top geben wir die seitlichen Begrenzungen der Near-Clipping-Plane an und mit zNear und zFar geben wir die Entfernungen der beiden Clipping-Ebenen an. Wichtig dabei ist, dass zNear und zFar unbedingt größer als null sein müssen. Optimal ist es wenn die nahe Clippingebene möglichst weit entfernt ist und die fern Clippingebene möglichst nah ist, da man dabei zum Beispiel bei der Berechnung der Sichtbarkeit meistens die besten Ergebnisse erzielt.

2.2.2. Setzen der Projektion

Jetzt ist es endlich soweit, wir können nun eine perspektivische Projektion aufsetzen. Dazu ersetzen wir den Aufruf der Funktion 'glOrtho' einfach mit der gerade besprochenen Funktion 'glFrustum':

// Eine perspektivische Projektion setzen.
glFrustum( -1.6, 1.6, -1.2, 1.2, 1.5, 6.5 ); // Die Near-Clipping-Plane
                                             // befindet sich in einer
                                             // Entfernung von 1.5 Einheiten
                                             // hat die Abmessungen 3.2 * 2.4
                                             // Einheiten.
                                             // Die Far-Clipping-Plane ist 6.5
                                             // Einheiten entfernt.

Wenn wir das Programm jetzt ausführen, dann werden wir zwar ein nettes, rötliches Fenster sehen, aber von unserem dreidimensionalen Würfel fehlt noch jede Spur. Bei einem genaueren Blick auf die Werte der Projektion und die Koordinaten des Würfels können wir sofort erkennen, dass das auch gar nicht zu erwarten war, da der Würfel in z-Richtung von -1 bis 1 reicht, aber durch die Projektion erst Punkte ab einer z-Koordinate von kleiner -1.5 sichtbar sind.

2.3. Transformieren des Würfels

Damit wir unseren Würfel auch zu sehen bekommen werden wir ihn einfach etwas aus dem Ursprung hinausverschieben. Das können wir einfach mit der Funktion 'glTranslatef' machen, die wie folgt definiert ist:

void glTranslatef( GLfloat x, GLfloat y, GLfloat z );

x,y und z geben die Werte an um die alle nachfolgend gerenderten Objekte in die jeweilige Richtung verschoben werden sollen. Dieser Vorgang wird auch oft Translation genannt und ist neben der Rotation eine der wichtigsten Transformationen bei der Programmierung von Spielen.
Damit wir auch die Projektion gut erkennen können rotieren wir noch den Würfel etwas mit der wie folgt definierten Funktion 'glRotatef':

void glRotatef( GLfloat angle, GLfloat x, GLfloat y, GLfloat z );

Mit dieser Funktion werden alle nach dem Funktionsaufruf gezeichnete Objekte um den mit angle im Gradmaß angegeben Winkel um die mit x,y und z abgegebene Achse rotiert (=Rotation).
Mit folgendem Codeausschnitt bringen wir unseren Würfel jetzt also in die richtige Lage:

glTranslatef(0,0,-3.5); // Um 3.5 Einheiten in den
                        // Bildschirm hinein verschieben.
glRotatef(60,1,1,0);    // 60° um die Achse (1,1,0)
                        // rotieren.

Da vom Setzen der Projektion immer noch der dazugehörige Matrixstack aktiviert ist, würde der Code zu ungewünschten Ergebnissen führen. Deshalb wählen wir zuerst noch die 'Modelview'-Matrix aus, die für alle Transformationen zuständig ist. Dannach können wir jetzt korrekt die besprochenen Transformationen durchführen.
Da sich auch diese Operationen während des Ablaufs des Programmes nicht ändern können wir sie auch wie folgt vor der Endlosschleife setzen:

// ...

// Projektion festlegen

// ...

//--------------------------
// Die Transformation
// festlegen:
//--------------------------

glMatrixMode( GL_MODELVIEW ); // Matrixstack wählen
glLoadIdentity();             // Die Matrix zurücksetzen

glTranslatef(0,0,-3.5); // Um 3.5 Einheiten in den
                        // Bildschirm hinein verschieben.
glRotatef(60,1,1,0);    // 60° um die Achse (1,1,0)
                        // rotieren.

// Die Endlosschleife
while(true)
{
  // ...

Wenn wir unser Programm jetzt kompilieren und ausführen dann bekommen wir schon unseren transformierten und perspektivisch verzerrten Würfel, aber es ist immer noch nicht ganz das gewünschte Ergebnis, da irgendwie die vordere Fläche des Würfels fehlt. Die Lösung des Problems ist der sogenannte Z-Buffer.

3. Der Z-Buffer

3.1. Problem beim Rendern mehrerer Dreiecke

Wie wir gesehen haben führt das Rendern von mehreren Dreiecken leicht zu Problemen mit der Sichtbarkeit, da später gerenderte Dreiecke einfach bereits gerenderte Dreiecke überschreiben. Als simple Lösung könnten wir doch einfach die Dreiecke nach ihrer Entfernung zum Bildschirm sortieren und dann in der richtigen Reihenfolge rendern. Doch diese Lösung funktioniert nur auf den ersten Blick, da wir schnell zwei Probleme erkennen können:

  • Bei unserem Würfel haben wir nur wenige Dreiecke, aber bei komplexeren Szenen werden wie es mit deutlich mehr Dreiecken zu tun bekommen, so dass es kaum mehr in Echtzeit möglich sein wird sie zu sortieren.
  • Weiters kann man mit diesem Ansatz auch schwer den Schnitt von zwei Dreiecken betrachten, wo ja keines der beiden Dreiecke das jeweils andere verdecken sollte.

3.2. Ein weiterer Puffer

Man könnte diese Probleme vielleicht mit irgendwelchen speziellen Techniken etwas verkleinern, doch wir werden uns damit nicht weiter beschäftigen, da sich eine einfache aber geniale Technik durchgesetzt hat, nämlich der Z-Buffer. Er ist einfach ein weiterer Bildpuffer in dem aber keine Bildpunkte, sondern immer der Tiefenwert der an der selben Stelle im Bildpuffer gezeichneten Pixel stehen.
Wenn man nun ein Dreieck zeichnet dann werden alle Pixel normal in den Bildpuffer geschrieben aber zusätzlich auch noch die z-Koordinate jedes Pixels in den Z-Buffer eingetragen. Wenn nun ein weiteres Dreieck gerendert werden soll und beim schreiben eines Pixels bereits ein kleinerer Wert im Z-Buffer steht, dann wird dieses Pixel verworfen, da es sich hinter dem bereits gerenderten Pixel befindet uns somit nicht sichtbar ist.

3.3. Und jetzt der Code

Da die Verwendung des Z-Buffers standardmäßig deaktiviert ist müssen wir sie noch aktivieren und zwar mit der Funktion 'glEnable':

glEnable( GLenum cap );

Für cap müssen wir eine Konstante angeben, mit der wir sagen was wir denn eigentlich aktivieren wollen. Für den Z-Buffer rufen wir diese Funktion mit 'GL_DEPTH_TEST' auf, womit wir die Verwendung des Z-Buffers und die Durchführung des Tiefentests aktivieren.
Wir müssen also nur folgende Zeile vor der Endlosschleife einfügen und schon wird der Z-Buffer verwendet:

glEnable( GL_DEPTH_TEST ); // Tiefentest (mit dem Z-Buffer) aktivieren

Da wir natürlich in jedem Renderdurchgang den gleichen Z-Buffer verwenden und so auch immer die Tiefenwerte des vorherigen Durchgangs im Z-Buffer erhalten bleiben, würden wir sehr schnell keine Dreiecke zu sehen bekommen, da die Werte im Z-Buffer mit jedem näher gerenderten Dreieck größer werden. Deshalb übergeben wir der Funktion 'glClear', mit der wir schon den Bildpuffer löschen noch ein weiteres Flag 'GL_DEPTH_BUFFER_BIT', mit dem dann auch der Z-Buffer wieder zurückgesetzt wird. Wir müssen also nur mehr den alten Aufruf der Funktion 'glClear' mit dem folgenden Aufruf ersetzen:

glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); //Bildpuffer und Z-Buffer zurücksetzen

4. Ausblick

Jetzt sind wir schon so weit, dass wir eine richtige dreidimensionale Szene rendern können. Damit wir auch aufwendigere Szenen rendern können werden wir uns im nächsten Tutorial noch mit weiteren Transformationen und deren Kombinationen beschäftigen und dann später auch mit Maus- und Tastatureingaben noch Interaktion hineinbringen.

Nächstes Tutorial: Transformationen - C++ OpenGL Tutorials

5. Quellcode

Hier gibts wieder den vollständigen Quellcode dieses Tutorials zum Downloaden:

icon Quellcode - C++ OpenGL Tutorial 03 (5.59 kB 2008-12-13 17:01:52)

Und hier gibt es noch einmal die benötigte Klasse zum Initialisieren von OpenGL:

icon Klasse zum Initialisieren von OpenGL (3.42 kB 2008-07-13 00:52:46)


Wünsche, Anregungen und Kritik bitte über das Kontaktformular oder direkt an Diese E-Mail-Adresse ist gegen Spambots geschützt! Sie müssen JavaScript aktivieren, damit Sie sie sehen können. an mich schicken.

Aktualisiert ( Montag, 07. November 2011 um 00:19 Uhr )