Rotierende Pixel
Der erste Teil des Tutorials hatte den grundlegenden Aufbau eines OpenGL-Programms unter Einbeziehung der GLUT-Bibliothek sowie die Modelview-Transformation zum Inhalt. In diesem Beitrag geht es um die Beleuchtung einer Szene und einfache Animationen.
- Christian Marten
Anhand einer kleinen Schreibtischlampe demonstrieren die beiden Beispielprogramme dieses Teils das Konzept der Matrix-Stacks und einige von OpenGLs Möglichkeiten, eine Szene zu beleuchten. Die Listings können hier nur in Auszügen wiedergegeben werden. Lauffähige Beispielprogramme sind über den iX-Listingsservice erhältlich.
Die Schreibtischlampe besteht aus einem Arm, modelliert durch zwei längliche Quader, und dem Kopf, dargestellt durch einen Zylinder mit aufgesetztem Kegelstumpf. Sie lässt sich an ihrem Fußgelenk drehen und kippen. Der Arm hat in seiner Mitte ein Scharniergelenk, über das er sich beugen lässt. Der Lampenkopf schließlich ist über ein weiteres Scharniergelenk am Arm befestigt.
Mit den im ersten Teil vorgestellten Techniken müsste jeder Lampenteil für sich eine Reihe von Transformationen durchlaufen, um alle Komponenten zu einer Lampe zusammenzusetzen und alle möglichen Gelenkstellungen zu simulieren. Ein Teil der Transformationen wäre für alle vier Objekte stets derselbe, ein zweiter wäre nur auf Untermengen anzuwenden. So müssten beispielsweise beim Rotieren der gesamten Lampe alle Objekte um denselben Winkel gedreht werden, eine Beugung im ‘Ellenbogengelenk’ jedoch würde sich nur auf Unterarm und Kopf auswirken.
OpenGL unterstützt die Implementierung solcher Strukturen durch die Möglichkeit, Zwischenresultate der Modelview-Matrix an beliebigen Stellen zu speichern. Anschließend können weitere Transformationen zur Modelview-Matrix hinzugefügt werden, und danach erfolgt das Rendering der Szenenobjekte mit den zusätzlichen Transformationen. Schließlich lassen sich die gespeicherten Zwischenzustände in umgekehrter Reihenfolge wieder restaurieren.
Funktionen rückwärts lesen
Das Programm sample2.c ist weitgehend identisch mit dem bereits in Teil 1 besprochenen sample1.c, lediglich eine Hand voll Variablen, die Tastaturbehandlung und die Display-Funktion wurden geändert beziehungsweise erweitert.
Da die als letzte im Quelltext auftretende Transformation die erste ist, die das Programm auf die darauf folgenden Primitive anwendet, liest man die Display-Funktion am besten von unten nach oben, beginnend auf Höhe des letzten Aufrufs von gluCylinder (siehe Listing 1). Wie am Präfix zu erkennen, entstammt diese Funktion der GL-Utilities-Bibliothek GLU, die auch eine Reihe weiterer Funktionen zum Zeichnen einfacher Körper wie Kugel oder Torus bereitstellt. Je nach übergebenen Radien zeichnet gluCylinder einen Zylinder, einen Kegel oder Kegelstumpf um die z-Achse. Als Parameter erwartet sie einen Zeiger auf ein zuvor (in Init) erstelltes Objekt vom Typ GLUquadricObj, den oberen und unteren Radius, die Höhe des Zylinders sowie die Anzahl der Sektoren und Scheiben, die die Methode für die Approximation des Zylinders verwenden soll.
Listing 1: Auszug aus sample2.c
Die Transformationen, die als letzte im Quelltext erscheinen, wirken als erste auf die nachfolgenden Primitive.
static void Display(void)
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
/* Drehung um y-Achse, Neigung und Platzierung auf dem Tisch */
glTranslatef (0.0, -1.0 ,-4.4);
glRotatef (spinAngle, 0.0, 1.0, 0.0);
glRotatef (baseAngle, 1.0, 0.0, 0.0);
/* Ausgabe des Oberarms */
glPushMatrix();
/* Wuerfel aus Bsp. 1 durch asymetrische Skalierung zu einem */
/* Quader deformieren */
glScalef (0.2, 1.0, 0.2);
glTranslatef (-0.5, 0.0 , -0.5);
DrawCube ();
glPopMatrix();
/* Lampenkopf + Unterarm an Spitze Oberarm verschieben */
glTranslatef (0.0, 1.0 , 0.0);
/* Kippen des Kopfes + Unterarms um Ellenbogenwinkel */
glRotatef (elbowAngle, 1.0, 0.0, 0.0);
/* Ausgabe des Unterarms */
glPushMatrix();
glScalef (0.2, 1.0, 0.2);
glTranslatef (-0.5, 0.0 , -0.5);
DrawCube ();
glPopMatrix();
/* Lampenkopf an Spitze Unterarm verschieben */
glTranslatef (0.0, 1.0 , 0.0);
/* Lampenkopf kippen: */
glRotatef (headAngle, 1.0, 0.0, 0.0);
/* Ausgabe des Lampenkopfes */
glPushMatrix();
gluCylinder (cone, 0.2, 0.2, 0.2, 20, 4);
glPopMatrix();
glTranslatef (0.0, 0.0 , 0.2);
gluCylinder (cone, 0.2, 0.4, 0.2, 20, 4);
glutSwapBuffers();
}
Die erste Transformation, die auf den Kegelstumpf wirkt, ist eine Verschiebung in z-Richtung um 0.2 Einheiten (glTranslate*). Dies schafft Platz für das Zeichnen des zum Lampenkopf gehörigen Zylinders über den zweiten Aufruf von gluCylinder. Der Zylinder erfährt jedoch nicht die Translation des Kegelstumpfs, da diese ja erst zur Modelview-Matrix hinzu multipliziert wird, nachdem das Rendering bereits erfolgt ist.
Als Nächstes dreht glRotate* Zylinder und Kegelstumpf um die x-Achse, was der Beugung des Lampenkopfes entspricht. Danach erfolgt eine Verschiebung um 1.0 Einheiten entlang der y-Achse (glTranslate*) hin zum Ende des Unterarms.
Diesen zeichnet die im ersten Kursteil vorgestellte Funktion DrawCube. Zunächst verschiebt glTranslate den Würfel so, dass er auf der x-z-Ebene steht und die y-Achse durch seinen Mittelpunkt verläuft. Anschließend staucht glScale* ihn in x- und z-Richtung, sodass er zu einem Quader mutiert.
DrawCube, glTranslate* und glScale* sind von dem Funktionenpaar glPushMatrix und glPopMatrix eingeklammert. Während glPushMatrix die momentane Modelview-Matrix sichert, restauriert glPopMatrix diese nach Ausführung der eingeschlossenen Funktionen wieder. Für alle nachfolgenden Kommandos sieht es also so aus, als hätten die zwischen glPushMatrix und glPopMatrix stehenden Transformationen nie stattgefunden. Translation und Skalierung wirken nicht auf den Lampenkopf.
Als Nächstes fügt sample2.c die Beugung des Ellenbogens zur Modelview-Matrix hinzu. Sie wirkt auf alle bisher gezeichneten Lampenteile gleichermaßen. Unterarm und Lampenkopf verschiebt das Programm dann um eine Einheit in y-Richtung, sodass Platz für den Oberarm entsteht. Diesen erzeugen dieselben Aufrufe wie zuvor den Unterarm.
Die letzten Transformationen betreffen Drehung und Neigung am Fuß der Lampe (glRotate*) sowie ihre Platzierung in der Szene (glTranslate*). Sie gelten für alle Lampenteile gleichermaßen.
Das Rückwärtslesen der Funktion hilft dabei, sich die Reihenfolge zu veranschaulichen, in der die Transformationen auf Primitive wirken. Tatsächlich werden die Funktionen natürlich wie in jedem Programm von oben nach unten abgearbeitet.
Baumstruktur der Transformationen
An den Blättern der Baumstruktur (siehe Abbildung 1) von sample2.c, der Display-Funktion, liegen die Ausgabeprimitive für Quader, Kegelstumpf und Zylinder. Beim Weg von einem Blatt zur Wurzel des Baumes trifft man der Reihe nach alle Transformationen an, die auf die Primitive am Blatt beim Rendering wirken.
Die Display-Funktion durchläuft diesen Baum in ‘Preorder’-Reihenfolge, arbeitet also zunächst die Anweisungen des Vaterknotens ab und besucht dann den linken und schließlich den rechten Ast. Bevor sie in den linken Ast verzweigt, erfolgt an jeder Gabelung ein Aufruf von glPushMatrix, der die aktuelle Modelview-Matrix sichert. Nachdem der linke Ast diese Matrix um einige lokale Transformationen ergänzt und seine Primitive ausgegeben hat, kehrt die Funktion zur Gabelung zurück und stellt die ursprüngliche Modelview-Matrix über einen Aufruf von glPopMatrix wieder her. Danach wandert sie in den rechten Ast, und das Spiel beginnt von neuem.
Technisch gesehen erfolgt die Speicherung der Modelview-Matrix auf einem Stack. Folgen mehrere Aufrufe von glPushMatrix aufeinander, ohne dass zwischen ihnen glPopMatrix aufgerufen wird, restauriert der erste Aufruf von glPopMatrix die vom zuletzt ausgeführten glPushMatrix gesicherte Matrix. Es wäre daher auch ohne weiteres möglich, in den linken Teilbäumen des Beispiels weitere Verästelungen einzubauen. OpenGL garantiert ausreichend Platz für mindestens 32 Matrizen auf dem Modelview-Stack.
Perspektivische Projektionen über Matrizen
Modelview ist nicht die einzige Matrix in der OpenGL-Welt. Tatsächlich erfolgen auch die Projektion der transformierten Objekte sowie Manipulationen an Texturen wiederum mit Hilfe von 4:4-Matrizen. Sowohl Projection- als auch Texture-Matrix besitzen wie Modelview ihren eigenen Stack, den dieselben Befehle manipulieren können. So lässt sich beispielsweise die aktuelle (perspektivische) Projection-Matrix für das Rendering von Primitiven unter einer Orthogonalprojektion sichern. Anschließend restauriert glPopMatrix einfach die alten Projektionseinstellungen. Die Funktion glMatrixMode legt fest, auf welchen Stack sich nachfolgende Matrixbefehle (glPushMatrix, glRotate*, LoadIdentity et cetera) beziehen.
Die Beispielprogramme arbeiten mit der Projection-Matrix in der Funktion Reshape (siehe Listing 2), die bei jeder Größenänderung des Ausgabefensters aufgerufen wird. Als erstes lässt sie den gesamten Clientbereich des Ausgabefensters mittels glViewport für das Rendering der Objekte zu. Der nachfolgende Aufruf von glMatrixMode legt fest, dass alle nachfolgenden Matrixoperationen sich auf die Projection-Matrix und ihren Stack beziehen. Anschließend erfolgt die Initialisierung dieser Matrix über den schon bekannten Aufruf von glLoadIdentity. Dies ist notwendig, weil OpenGL-Transformationen der Projection-Matrix genauso behandelt wie die von Modelview, also alle bisher angefallenen Transformationen zu einer Matrix zusammenfasst. Gerade bei der Projektion führt die Verkettung von Transformationen in der Regel zu ‘merkwürdigen’ Effekten.
Listing 2: Auszug aus sample2.c
Reshape sorgt dafür, dass die Objekte sich eventuellen Größenveränderungen des Ausgabefensters anpassen.
static void Reshape(int width, int height)
{
/* Darstellung auf gesamten Clientbereich des Fensters zulassen */
glViewport(0, 0, (GLint)width, (GLint)height);
/* Projektionsmatix initialisieren auf 60 Grad horizontales */
/* Sichtfeld, Verhaeltnis Breite:Hoehe = 1:1, Clipping fuer z<1 */
/* und z>200 */
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
/* angle, aspect, near Clip, far Clip */
gluPerspective(60.0, 1.0, 1.0, 200.0);
/* Modelview-Matrix wieder zur aktuellen Matrix machen */
glMatrixMode(GL_MODELVIEW);
}
Listing 3: Auszug aus sample3.c
glLight* setzt Farbe und Richtung des von Lichtquellen abgestrahlten Lichts sowie deren Positionen.
static void Init(void)
{
static float light0_ambient[] = {0.4, 0.4, 0.4, 1.0};
static float light0_diffuse[] = {0.8, 0.8, 0.8, 1.0};
static float light0_pos[] = {1.0, 1.0, 1.0, 0.0};
static float light1_ambient[] = {0.0, 0.0, 0.0, 1.0};
static float light1_diffuse[] = {1.0, 1.0, 1.0, 1.0};
static float lmodel_ambient[] = {0.0, 0.0, 0.0, 1.0};
glEnable(GL_DEPTH_TEST);
glPolygonMode (GL_FRONT_AND_BACK, GL_LINE);
/* Lichtquelle 0 Position sowie Farben fuer ambienten und diffusen */
/* Lichtanteil zuweisen */
glLightfv(GL_LIGHT0, GL_AMBIENT, light0_ambient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_diffuse);
glLightfv(GL_LIGHT0, GL_POSITION,light0_pos);
/* Lichtquelle 1 Farben fuer ambienten und diffusen Lichtanteil zuweisen */
/* Lichtkegel auf 45 Grad einstellen */
glLightfv(GL_LIGHT1, GL_AMBIENT, light1_ambient);
glLightfv(GL_LIGHT1, GL_DIFFUSE, light1_diffuse);
glLightf(GL_LIGHT1, GL_SPOT_CUTOFF, 45.0);
/* Ambientes Licht fuer die gesamte Szene setzen und verschiedene */
/* Materialeigenschaften fuer Vorder- und Rueckseite zulassen */
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);
/* Beide Lichtquellen sowie Beleuchtungsberechnung insgesamt einschalten */
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_LIGHT1);
cone = gluNewQuadric ();
}
gluPerspective definiert einen Pyramidenstumpf, der den sichtbaren Bereich (View Volume) umfasst. Teile eines Objekts, die nicht innerhalb des Pyramidenstumpfs liegen, schneidet die Funktion ab; vollständig außerhalb liegende Objekte stellt sie nicht dar. Die gluPerspective übergebenen Parameter vereinbaren ein Sichtfeld von 60°, ein Seitenverhältnis von 1 : 1 sowie eine vordere Clipping-Ebene bei z = 1 und eine hintere bei z = 200.
Der von einem Objekt eingenommene Platz relativ zur Breite des Pyramidenstumpfs einer perspektivischen Projektion ist umso größer, je näher das Objekt dem Betrachter ist. Anstelle der perspektivischen Projektion kann auch eine Orthogonalprojektion erfolgen. Hierzu muss man lediglich den Aufruf von
gluPerspective(60.0,1.0,1.0,20.0)
durch
glOrtho(-3.0,3.0,-3.0,3.0,1.0,20.0);
ersetzen. Bei einer Orthogonalprojektion definiert ein Quader das View Volume. Dadurch verliert das Auge zwar die Tiefeninformation, andererseits bleiben Längen- und Winkelverhältnisse erhalten. Aus diesem Grund wird die Orthogonalprojektion häufig im konstruktiven Bereich eingesetzt.
Abschließend erklärt Reshape die Modelview-Matrix erneut zur aktuellen Matrix. Damit wirken alle folgenden Transformationen wieder auf die Modelview-Transformation.
Licht anknipsen mit Shading
In den bisherigen Beispielen hat glColor* den Geometriepunkten ihre Farben explizit zugewiesen. glColor* verliert jedoch ihre Funktion, sowie die Beleuchtungsberechnung von OpenGL aktiviert wird. Die Farben einer Szene ergeben sich dann aus dem Wechselspiel von Materialeigenschaften und Lichtquellen.
OpenGL unterstützt mit Flat und Gouraud Shading zwei unterschiedliche Beleuchtungsmodelle, zwischen denen der Programmierer (fast) jederzeit über glShadeModel wechseln kann. Beim Flat Shading wird die Beleuchtung jedes Polygons gemäß der Lage seiner Flächennormalen zur Lichtquelle berechnet. Leider ermittelt OpenGL den Normalenvektor nicht selbstständig, sondern erwartet ihn über die Parameter der Funktion glNormal*. Ein Polygon erstrahlt in maximaler Leuchtkraft, wenn das Licht senkrecht darauf trifft, also ein Vektor in Richtung der Lichtstrahlen und der Normalenvektor der Fläche einen Winkel von 180° einschließen.
Im Gegensatz zum Flat Shading muss man bei der Gouraud-Schattierung für jeden Geometriepunkt, also beispielsweise Eckpunkte eines Polygons, eine Normalenrichtung angeben. OpenGL berechnet in diesem Fall die Ausleuchtung jedes Geometriepunkts und interpoliert die Farben für alle dazwischen liegenden Punkte eines Primitivs. Die State Machine von OpenGL merkt sich die aktuelle Normalenrichtung genauso wie die aktuelle Zeichenfarbe. Daher erfolgt die Angabe von Normalenvektoren beim Flat Shading normalerweise spätestens vor der Angabe der Koordinaten des letzten Punktes eines Primitivs, für Gouraud Shading vor jedem Punkt.
Normalenvektoren sollten für eine korrekte Beleuchtungsberechnung stets die Länge 1 haben. Allerdings normalisiert OpenGL die Normalenvektoren automatisch, wenn die Aufforderung dazu über glEnable(GL_NORMALIZE) erfolgt.
Materialeigenschaften bestimmen Reflektion
Jedem Primitiv können Materialeigenschaften zugewiesen werden, die Farbe und Oberflächenbeschaffenheit festlegen. Die der Funktion glMaterial* übergebenen Parameter bestimmen, wieviel Prozent jeder Grundfarbe des auftreffenden Lichts reflektiert werden. Dabei gibt es eine Unterscheidung nach ambientem Streulicht und diffusem, gerichtetem Licht, von denen jeweils unterschiedliche Farbanteile reflektiert werden können. Schließlich ermöglicht glMaterial* das Rendering von Glanzlichtern. Eine Oberfläche kann matt oder glänzend sein und das reflektierte Licht im Glanzpunkt eine andere Farbe haben als in den übrigen Bereichen der Oberfläche (bei spiegelnder Reflexion beispielsweise die Farbe der Lichtquelle).
Neben der Angabe von Primitiven mitsamt ihren Normalenvektoren und Materialeigenschaften erfordert die Beleuchtung einer Szene natürlich Lichtquellen. OpenGL unterstützt mindestens acht, die beliebig in der Szene positionierbar sind und Licht unterschiedlicher Farben abstrahlen können.
Die Funktion glLight* erwartet als erstes Argument die Angabe der Lichtquelle, GL_LIGHT0 bis GL_LIGHT7. Das zweite Argument legt die zu änderende Eigenschaft der ausgewählten Lichtquelle fest. So setzt GL_POSITION die Position der Lichtquelle, während GL_DIFFUSE die Farbe ihres diffusen Lichtanteils definiert. Im RGB-Modus wird sie wie die Zeichenfarbe als Triplett der Grundfarben Rot, Grün und Blau angegeben.
Neben dem gerichteten Licht jeder Lichtquelle ist die Szene zusätzlich von Streulicht erfüllt, das nach unzähligen Reflexionen des gerichteten Lichts die Szene überall gleichmäßig ausleuchtet. Ohne Streulicht würden die den Lichtquellen abgewandten Flächen schwarz erscheinen. Der Anteil einer Lichtquelle an diesem globalen Streulicht lässt sich durch den Parameter GL_AMBIENT bestimmen.
Lichtquellen können wie Scheinwerfer einen Kegel werfen oder ihr Licht gleichmäßig in alle Richtungen abstrahlen wie eine Glühbirne ohne Lampenschirm. Wird glLight* mit dem Parameter GL_SPOT_DIRECTION aufgerufen, strahlt die Lichtquelle nur noch in die angegebene Richtung. Ein zweiter Aufruf von glLight*, diesmal mit dem Parameter GL_SPOT_CUTOFF, legt die Größe des Lichtkegels fest.
Nach der Definition der Lichtquellen und Materialeigenschaften gilt es noch, die Beleuchtungsberechnung von OpenGL über glEnable(GL_LIGHTING) zu aktivieren und die einzelnen Lichtquellen mittels glEnable(GL_LIGHTx) einzuschalten. Dabei ist x die Nummer der einzuschaltenden Lichtquelle. sample3.c veranschaulicht die gerade besprochenen Beleuchtungstechniken. Es verwendet zwei Lichtquellen, von denen eine direkt in der Tischlampe sitzt und einen Lichtkegel gemäß der Lampenstellung abgibt. Die zweite steht hinter dem Betrachter und sorgt für die Ausleuchtung der Szene insgesamt.
Positions- und Richtungsangaben für Lichtquellen mittels glLight* unterliegen genauso der Modelview-Transformation wie die durch glVertex* spezifizierten Koordinaten von Punkten. Die Position und Richtung des Lichtkegels der Lampe berechnet OpenGL je nach Lampenstellung automatisch.
So praktisch die Transformation der Beleuchtungspositionen ist, so sehr kann sie dem Einsteiger Kopfzerbrechen bereiten. Für einen Walk Through durch eine beleuchtete Szene muss der Programmierer für jedes Bild zunächst die Modelview-Matrix über glLoadIdentity initialisieren und dann die View-Transformation vornehmen (also die Kamera in der Szene positionieren). Anschließend spezifiziert er die Lage der globalen Lichtquellen mittels glLight*, sodass diese ebenfalls der View-Transformation unterliegen. Erst nachdem die Positionen der globalen Lichtquellen gesetzt sind, können Modelltransformationen die Szenenobjekte manipulieren.
Christian Marten
arbeitet bei GE CompuNet in Hannover als Systemingenieur.
Literatur
[1] James Foley, Andries van Dam, Steven Feiner, John Hughes, Richard L. Phillips; Grundlagen der Computergraphik; Einführung, Konzepte, Methoden; Addison-Wesley, Bonn 1994
[2] Renate Kempf, Chris Frazier; OpenGL Reference Guide; Addison-Wesley, Reading 1997
[3] Mason Woo, Jackie Neider, Tom Davis; OpenGL Programming Guide; Addison-Wesley, Reading 1997
[4] Internet-Seite der OpenGL Open Community
[5] Internet-Seite; Homepage der Mesa-Bibliothek
iX-TRACT
- Anhand einer Schreibtischlampe demonstrieren die Beispielprogramme das Konzept der Matrix-Stacks und die Beleuchtungskonzepte von OpenGL.
- Duch die Möglichkeit, Zwischenresultate der Modelview-Matrix zu speichern, können Transformationen durchgeführt werden, die sich nur auf einzelne Teile eines Objekts auswirken.
- Positions- und Richtungsangaben für Lichtquellen unterliegen wie Geometrieangaben einzelner Punkte der Modelview-Transformation.
Tutorial Teil 1: Schnelle Pixel (ka)