Tutorial Lektion 5
Inhaltsverzeichnis
Artenvielfalten und Ihre Folgen
Vorwort
Und wieder einmal aufraffen und etwas tippen. Es ist wirklich nicht immer leicht solche Tutorials zu schreiben, vor allem wenn man mal wieder eine Null-Bock-Phase hat. Ich hoffe doch sehr, dass auch Ihr dafür Verständnis habt, den… auch ich bin ein Mensch… egal wie viele Augen und Beine ich habe *sg* Gut… genug "gefaxt", es geht wieder um den Ernst im Leben: Delphi
Ich habe wirklich eine zeitlang überlegt, was wir machen sollten. Einige haben sich Lichter gewünscht, aber ich konnte mich nicht dazu aufraffen… Stattdessen habe ich irgendwie Lust gehabt, mal was anderes zu machen, etwas wo man etwas kreativ sein kann und was auch mir Spaß macht! :-D Und deswegen, habe ich mir gedacht, dass wir das eine oder andere Wissen, welches zwischendurch behandelt wurde noch etwas vertiefen sollten… einfach in dem wir andere Möglichkeiten aufzeigen, elegant ein Problem zu umschiffen und auch den einen oder anderen Effekt erzielen!
Und genau darum geht es auch diesmal! Ich denke, dass sich dieses Tutorial weitestgehend nur an die richten wird, die bereits ein solides Wissen in OpenGL und Delphi haben, ansonsten wird es wohl schwer sein mir zu folgen, ich werde einiges an Wissen woraus setzen! Wer folgen kann, wird dann mit einem Wissen belohnt werden, dass ihm das eine oder andere Probleme sehr elegant umschiffen lässt in dem man einfach seine Software entsprechend mit OpenGL optimiert! Den nur wer sein Handwerk bis ins Detail beherrscht, darf sich Meister nennen ;)
Unbekannte Zeichen-Arten
OpenGL-Maxime
Wer nicht gerade erst jetzt hier eingestiegen ist, wird sicherlich bereits bemerkt haben, dass OpenGL streng genommen nach einem sehr einfachen Prinzip arbeitet. Ständig wird an unserer "Zustand-Maschine" etwas manipuliert und mit Matrizen setzen wir die Positionen fest. Jedoch nur an einer einzigen Stelle kommen alle diese Werte zusammen. Nämlich dann, wenn etwas gerendert wird. Genau in diesem Moment werden alle Werte "zusammengerechnet" und erzeugen etwas Sichtbares auf dem Bildschirm. In den meisten Fällen wird dies Eben zwischen glBegin und glEnd geschehen. Und genau diese beiden Funktionen wollen wir nun näher betrachten. Interessant hierbei ist nämlich der Parameter von glBegin…
Streng genommen definieren wir nur eine Anzahl von Punkten zwischen glBegin und glEnd, der Parameter bei glBegin bestimmt aber letztendlich wie diese Verstanden werden sollen. Nur als kleine Übersicht alle verfügbaren Parameter:
GL_POINTS GL_LINES GL_LINE_STRIP GL_LINE_LOOP GL_TRIANGLES GL_TRIANGLE_STRIP GL_TRIANGLE_FAN GL_QUADS GL_QUAD_STRIP GL_POLYGON
Ich denke aus ethischen Gründen werde ich nun darauf verzichten Euch erneut zu erklären wofür, die bisher für Euch bekannten Parameter (gl_Triangle und gl_Quad) gut sind… jeder wird sich denken können, dass das erste immer per 3er Punkte ein Dreieck bildet, dass zweiter aus 4 Punkten ein Quadrat erzeugt… (mist… *g*) ;)
Zunächst war der Strich
Interessanter, da neu für uns, sind die restlichen Parameter, auch wenn diese sich weitestgehend selbst erklären. Gl_Points z.B. … könnte es vielleicht bedeuten, dass OpenGL alle mit glVertex3f definierten Punkte auch nur als Punkte zeichnet?
Scheint zu stimmen… ;) Es ist nun auch nicht besonders schwer herzuleiten, was OpenGL mit Vertex-Definitionen macht, die mit gl_Lines beginnen. Versuchen wir doch mal ein Dreieck damit zu zeichnen! Ergo brauchen wir 4 Punkte (der letzte muss, auf den ersten verweisen)…
Doch was ist das? Dies ist kein böswilliger Trick, den ich auf Euch spielen will, sondern folgender Code, ist dafür verantwortlich:
glBegin(GL_LINES); glVertex3f(-1,0,0); glVertex3f(0,1,0); glVertex3f(1,0,0); glVertex3f(-1,0,0); glEnd;
Sieht soweit alles in Ordnung aus - ist es auch! Der Fehler liegt nämlich nicht am Code, sondern an uns selbst. Gl_Lines bewirkt nämlich nicht, dass alle Punkte miteinander verbunden werden, sondern nur, dass jeweils Punkte 1 mit Punkte2, Punkte 3 mit Punkt 4 etc. verbunden werden, d.h. immer zweiter Pärchen. Um das Ergebnis zu erreichen, welches wir angestrebt haben (und zwar ohne 6 Punkte zu definieren), wäre in diesem Fall nämlich gl_Line_Strip gewesen, was nämlich bewirkt, dass der "Zeichenstift" immer von seiner letzten Position zum nächst definierten Punkte einen Strich zieht. Dementsprechend sieht dann unser Dreieck mit praktisch dem gleichen Source wie folgt aus:
hr merkt bereits jetzt wie viele Möglichkeiten einen OpenGL bietet mit nur wenigen Zeilen eine Menge zu verändern. Man bedenkt, dass wir hier nur sehr wenige Punkte haben, allerdings kann man eine Menge Leistung rausschlagen, wenn man statt gl_Lines, gl_Line_Strip verwendet, da einfach weniger Punkte abgearbeitet werden müssen. Logisch oder?
GL_LINE_LOOP
Der Parameter entspricht praktisch gesehen GL_LINE_STRIP, nur das der letzte Punkt mit dem ersten verbunden wird. Man bräuchte mit diesem Parameter also nur 3 Punkte… das Optimum für unser Dreieck! ;)
Artenvielfalt
Natürlich lassen sich solche Zeichenoperationen auch durch eine Menge anderer Faktoren beeinflussen. Vielleicht hat sich der eine oder andere ja bereits gefragt, wie man es schafft, dass der Rahmen des Dreiecks dicker gezeichnet wird. Sicherlich könnte man nun beginnen und ganz leicht versetzt daneben noch ein Dreieck zu zeichnen. Dies würde dann natürlich einen kleinen Tick größer wirken.
Die Lösung liegt allerdings viel näher - den OpenGL bietet hierfür eine hauseigene Lösung:
glLineWidth(3);
Wir stellen einfach ein, wie OpenGL die Linien Rastern soll. Wir nehmen in diesem Fall die dreifache Dicke von der normalen Einstellung:
Auch werden einige von Euch sicherlich die unschönen Treppchen kennen, die vor allem bei solch einfache Konstruktionen wie diesm dickeren Dreieck auftreten können. Die Lösung dagegen heißt bekanntlich "AntiAliasing" … verschlägt es Euch die Sprache, dass ich gleich mit solch derben Geschützen auffahre?
glEnable(GL_LINE_SMOOTH); glDisable(GL_LINE_SMOOTH);
Da verschlägt es einem die Sprache oder? Das war es nämlich auch bereits wieder. Wir schalten einfach einen Zustand um und fertig war die Geschichte! Der Zustands-Maschine von OpenGL sei dank, müssen wir nur noch sagen, was gemacht werden soll, ein lästiges "Wie?" entfällt komplett
Und noch ein kleines Beispiel dafür, wie einfach OpenGL nicht nur Informationen schreiben lässt, sondern diese auch wieder preisgibt. Haben wir vergessen wie Dick wir die Linien eingestellt haben? Kein Problem, den OpenGL bietet folgende Funktionen um zu ermitteln, auf welchen Wert ein "Zustand" geschaltet ist:
glGetBooleanv, glGetDoublev, glGetFloatv, glGetIntegerv
Ich glaube ich brauche nicht wieder damit zu beginnen darauf hinzuweisen, dass man bei jeder Abfrage auch den richtigen Variablen-Typen verwenden sollte *g*
glGetIntegerv(gl_line_width,@myint);
Und schon haben wir in myint die Stärke mit der OpenGL momentan alle Linien zeichnen soll. Das geht mit fast allen Zuständen, die OpenGL haben kann. Ich denke, das Grundprinzip ist recht leicht verständlich. Beachtet auch, dass OpenGL bei solchen Funktionen immer einen Pointer auf eine Variable erwartet (sprich besser: eine Adresse) und nicht die Variable selbst!
Achso… vergaß ich zu erwähnen, dass wir auch weitere Funktionen auf Striche anwenden können? Zum Beispiel glColor, um den Strich einzufärben? Auch der Z-Buffer lasst sich darauf anwenden, eben alles, was man auch bei einem Dreieck tun könnte (wovon man aber zwingend absehen sollte eine Linie zu texturieren, um keine unnötigen Berechnungen durchzuführen)
Ein Dreieck, hat drei Ecken…
Nun dreht sich erstmal alles um Dreiecke. Den auch hierfür bietet OpenGL mehre Möglichkeiten, wie die Reihenfolge der Punkte verstanden wird:
GL_TRIANGLES GL_TRIANGLE_STRIP GL_TRIANGLE_FAN
Zunächst widmen wir uns GL_TRIANGLES! Dieser Parameter sollte uns allen noch geläufig sein. Jeweils 3 Punkte werden zusammen zu einem eigenständigen Dreieck verbunden. Nicht neues für uns, bereits im ersten Tutorial könnt ihr den Beweis finden, dass es klappt :-D
Sicherlich werdet ihr Euch denken können, dass dies sehr hilfreich ist, wenn man nur ein Dreieck zeichnen möchte, nicht jedoch, sobald man mehre in einem Rutsch auf dem Bildschirm bringen möchte. Hierzu bietet sich dann eher GL_TRIANGLE_STRIP an:
glBegin(GL_TRIANGLE_STRIP); glColor3f(1,0,0); glVertex3f(-1,0,0); glColor3f(1,1,0); glVertex3f(0,1,0); glColor3f(1,1,1); glVertex3f(1,0,0); glColor3f(0,1,1); glVertex3f(2,1,0); glColor3f(0,1,0); glVertex3f(3,0,0); glEnd;
Wem das ganze zu schwer erscheint, sollte er sich die glColor3f weglassen, die habe ich reingesetzt, damit das ganze auch schön aussieht ;) (Programmierer lieben sinnlosen Spielkram der glänzt und bunt ist). Wer sich nur auf die Punkte konzentriert und sich die Position im Kopfe vorstellt, wird ein Gebilde wie folgt vorstellen können:
Streng genommen macht OpenGL nichts anderes, als die ersten drei Eckpunkte zu nehmen und ein Dreieck daraus zu rendern. Anschließend fällt der erste Punkt weg und es wird das Dreieck zwischen 2,3,4 gerendert usw. Im Anschluss dieses Kapitels werden wir nochmals hieraus zurückkommen und das Culling erklären!
Ventilatoren und OpenGL
Tja, und nur die wenigstens wissen, dass auch ein Ventilator ein Dreieck sein kann *hust* Okay, begnügen wir uns hier lieber mit dem englischen Begriff "Fan". (Das ist nicht der Kerl, der vorm Fenster steht, einem gierig anstarrt und laut ruft "Ich will ein Kind von Dir…" … nein, sicher nicht ;)
Vielmehr sollten wir uns wirklich mal bildlich einen Fahrrad-Reifen vorstellen. Von den Außenseiten verlaufen die einzelnen Speichen alle zu einem Mittelpunkt. Nach einem ähnlichen Render-Prinzip funktioniert auch GL_TRIANGLE_FAN. Der einfachheitshalber werden wir hier jedoch kein komplexes Objekt anfertigen, sondern nur eine Möglichkeit zeigen, wie man mit dieser Einstellung sinnvoll ein Objekt zeichnen kann! In unserem Fall nehmen wir einfach einen Drachen (nein… nicht das Fabelwesen), den auch dieser ist ein Ventilator (Chaos… perfekt….) ;)
Bevor nun jeder abdreht, schauen wir uns doch mal das Objekt an, von dem ich sprach:
In diesem Fall benötigen wir nur 6 (!) Punkte, um dieses Gebilde zu erzeugen. Zentraler Ausgangspunkt ist hierbei die Nummer 1. Wie man erkennen kann besteht das erste Dreieck aus 1,2,3. Das zweite aus 1,3,4… für die abstrakt denkenden Menschen lässt sich daraus folgern, dass OpenGL die Punkte wie folgt abarbeitet 1, n+1, n+2. Oder um es neudeutsch zu sagen, der erste Punkte wird immer mit zwei weiteren verbunden. Im Gegensatz zum Strip fällt allerdings nicht der erste Punkt raus, sondern immer der zweite.
Gl_Triangle_Fans und Gl_Triangle_Strips sind wohl die besten Möglichkeiten, um die Anzahl der im Video-Speicher befindlichen Daten zu begrenzen. Allerdings lässt sich schnell erahnen, dass nicht jeder dieser Methoden wirklich komplexe Objekte zulässt. Man sollte sie jedoch nicht total ignorieren, sondern mit Verstand einsetzen. Ach so, bevor ich es vergesse, der Code dafür:
glBegin(GL_TRIANGLE_FAN); glColor3f(1,1,0); glVertex3f(0,0,0); glColor3f(1,0,0); glVertex3f(0,1,0); glVertex3f(1,0,0); glColor3f(0,0,1); glVertex3f(0,-3,0); glColor3f(1,0,0); glVertex3f(-1,0,0); glColor3f(1,0,0); glVertex3f(0,1,0); glEnd;
Man beachte, dass Punkte 2 und Punkte 6 identisch sind, da er sonst das letzte Dreieck weglassen würde ;)
Quadratisch, praktisch, gut!
Das ist ne wahrer Marathon geworden. Schlimm, wenn man bedenkt, dass wir uns hier nur mit einem Parameter für eine Funktion beschäftigen. Ihr versteht, warum ich so oft geschrieben haben "lassen wir es lieber und klären nicht jeden Parameter" ;) Aber gut, ich halte es für sehr wichtig, solche Dinge zu beherrschen, weil es einfach zu den Grundlagen dazugehört! Immerhin bleiben nicht mehr sonderlich viele übrig, als ran an die letzten drei!
GL_QUADS
Sollte auch niemanden von uns mehr sonderlich schockieren können. Jeweils vier übergebende Punkte werden zusammengefasst zu einem Quadrat… nichts weiter Aufregendes. Eigentlich sollten inzwischen auch die GL_QUAD_STRIP nichts wirklich erstaunliches mehr liefern. Wir übergeben zunächst vier Punkte und bei jedem Durchgang entfallen die beiden ersten und werden durch die nächsten zwei ersetzt. OpenGL erstellt dann daraus jedes Mal ein Dreieck. Bitte nicht verwechseln mit der Punkt-Reihenfolge von GL_QUADS. Folgendes Beispiel sollte die Problematik verdeutlichen:
glBegin(GL_QUAD_STRIP); glColor3f(1,0,0); glVertex3f(0,0,0); glVertex3f(0,1,0); glColor3f(0,0,1); glVertex3f(1,0,0); glVertex3f(1,1,0); glColor3f(0,1,0); glVertex3f(2,0,0); glVertex3f(2,1,0); glColor3f(0,1,1); glVertex3f(3,0,0); glVertex3f(3,1,0); glEnd;
Unser erstes Quadrat besteht aus den Punkten 1,2,3,4, dass zweite aus 3,4,5,6 etc. Ich denke, dass das Prinzip dahinter leicht verständlich ist. Anbieten tut sich diese Lösung meist in Schleifen, wenn man längere solcher Quad-Strukturen braucht, die aneinander gereiht sind
Vieleckerei
Und zu guter letzt GL_POLYGON, was wohl am einfachsten nachzuvollziehen ist ;) Es wird einfach eine Liste von Punkten übergeben und daraus wird schlicht und ergreifend dann ein Vieleck gemacht. Wie variantenreich dies werden kann, könnt ihr Euch vorstellen. Vor allem komplexere und exotischere Formen lassen sich damit darstellen, auch wenn es in diesem Fall eher Leitung kostet als wirklich einbringt. Zeichnet also niemals ein Quadrat mit GL_POLYGON.
Eine mögliche Form wäre zum Beispiel diese:
glBegin(GL_POLYGON); glColor3f(1,0,0); glVertex3f(0,0,0); glColor3f(0,1,0); glVertex3f(3,0,0); glColor3f(0,0,1); glVertex3f(4,1,0); glColor3f(1,1,0); glVertex3f(2,2,0); glColor3f(0,1,1); glVertex3f(-2,3,0); glColor3f(1,1,1); glVertex3f(-1,1,0); glEnd;
Ich denke zusammen mit dem Code sollte es keine weiteren Fragen mehr dazu geben. Am besten setzt Ihr Euch nun alle einmal hin und wendet das Wissen Testweise an, denn es muss sitzen und stellt vor allem, wenn es um optimierte Programmierung geht ein absolutes Grundwissen da! Ihr werdet mit etwas praktischer Erfahrung schnell die Grenzen finden, die die einzelnen Parametern mit sich bringen z.B. bei der Texturierung von Objekten!
Ein Quadrat besteht aus einem Face (Fläche), ein Quadrat zusammengesetzt aus zwei Triangle aus zwei. Man könnte zum Beispiel letzteres mit zwei Texturen versehen, ersteres nur mit einer!
Das Culling-Verfahren
Wahrheiten und Wirklichkeiten
- Dramatik
Bevor wir uns mit der eigentlichen Technik des Cullings befassen, möchte ich eine radikale Aufklärung betreiben, die Euer Weltbild für immer verändern wird. Seit gewarnt, dass Ihr nach dem lesen der folgenden Zeilen, Eure Quake3-Gegner mit ganz anderen Augen sehen werdet und eventuell nie wieder zu Eurer alten Denkweise zurückkehren werden könnt!
Habt Ihr Euch bereits gefragt wie die kommerziellen Spiele es schaffen ohne Ruckler (*hust), super Grafiken auf den Bildschirm zu bringen, die auch nach der Optimierung immer noch durch grafische Qualität überzeugen können? Nun, es gibt viele Möglichkeiten seine Szenen zu optimieren, dass wohl einfachste und auch mit am effizientesten ist das so genannte Culling-Verfahren.
So sieht unser Protagonist wie gewohnt aus. Wir sehen ihn wie er liebt und lebt. (ja…) Um es philosophisch auszudrücken, sehen wir hier jedoch nur die halbe Wahrheit… ich stelle die wage Behauptung aus, dass wir der Rückseite von ihm gar nicht sehen können und daher auch nicht sagen können, ob sie existiert! Jedes Mal, wenn wir uns um ihn herum bewegen sehen wir nie seine Rückseite. Nun… das wäre auch nicht weiter fatal, wenn ich nicht sofort eine weitere These aufstellen würde: "Der Kerl hat gar keine Rückseite" :-O
"Aber! Wenn wir um ihn herum gehen, dann sehen wir doch seine Rückseite, also ist sie da!", könnte man mir nun skeptisch zurufen. Tja… beweist mir das Gegenteil und schickt einen Wetteinsatz an mich, ich werde Euch beweisen, dass er immer nur eine Seite hat und seine Rückseite erst erzeugt wird, wenn sie auch benötigt wird. Es würde dann aussage gegen Aussage stehen und ihr könntet mir nicht beweisen, dass Ihr Recht habt, den wenn wir um ihn herum gehen, würden wir ja wieder seine Rückseite nicht sehen. Ich hingegen würde den Beweis antreten und Euch mitteilen, dass wir ja einfach mal uns die Rückseite ansehen, dann allerdings auf die Forderseite verzichten müssen:
Was auf den ersten Blick wie ein verschwommendes Etwas aussieht entpuppt sich auf den zweiten Blick bereits als unser Soldat vom ersten Bild. Skeptisch wird man sich sicherlich fragen, was gesehen ist. Und wenn man genau durch ihn durch sieht, erkennen wir, dass wir hier z.B. seine Hose sehen… und seinen Kopf auch irgendwie von … hinten! Was ist geschehen? Wir betrachten Ihn von vorne und sehen, was hinten ist! (Oh Gott… eine ganze Wirklichkeit stürzt in sich ein *sg* ).
Natürlich ist das verschwommendes Ding da oben nicht ein grafisches Ziel, was wir erreichen wollen, sondern nur der Beweis dafür, dass jede Figur zwei Seiten hat (Erkenntnis, aufschreiben!) und wir in OpenGL bestimmen können, welche Seite wir von Vorne oder Hinten betrachten können.
Ich gebe ja auch zu, dass ich ein wenig getrickst habe und im oberen Bild, die Figur zweimal genredert habe, davon einmal die Vorderseite mit Alpha Blending, weil auf einem unbewegten Bild es schwer zu erkennen wäre, welches die Vorder und welches die Rückseite ist, da wir die Texturen eben gespiegelt auf dem Modell sehen würden.
Eine abstrakte Wahrheit…
An sich klingt bisher doch auch noch alles recht logisch oder? Den wieso sollte OpenGL die Rückseite von Objekten rendern, wenn man sie gar nicht sehen kann. Grob würde dies eben die doppelte Arbeit sein, die sinnlos getätigt wird. Der Laie wird nun vor Freude an die Decke springen, der erfahrene Programmierer skeptisch die Falten runzeln. "Wie erkennt OpenGL, den das es sich um die Rückseite handelt!". Gute Frage oder? Des Lösung-Rätsel sollte ein Blick auf die Uhr zeigen… (<== nein, er spinnt nicht (Anm. v. Flo2))
Aber was meine ich damit? Was haben unsere Objekte mit einer Uhr gemeinsam? Was zunächst einem komisch vorkommt ist eigentlich logisch: Es ist die Laufrichtung! Unsere Uhr sollte im Normalfall im Uhrzeigersinn laufen (… scheiß Übergang!). Die Entgegengesetzte Richtung nennen wir dann entgegen des Uhrzeigersinnes. Das wird nun vielleicht den einen oder anderen dazu gebracht haben ein leises Aua von sich zu geben und sich mit der Hand an die Rübe zu schlagen. Diese Erkenntnis ist jedoch grundlegend ^__-
Den auch unsere Objekt haben eine gewissen Definitionsreihen folge. Wir werden die Problematik im Weiteren an Hand eines Quadrates verfolgen ;) Folgender Code ist nicht gleich…
glBegin(GL_QUADS); glColor3f(1,0,0); glVertex3f(0,0,0); glColor3f(0,1,0); glVertex3f(1,0,0); glColor3f(1,1,1); glVertex3f(1,1,0); glColor3f(0,0,1); glVertex3f(0,1,0); glEnd; glBegin(GL_QUADS); glColor3f(1,0,0); glVertex3f(0,0,0); glColor3f(1,1,0); glVertex3f(0,1,0); glColor3f(1,1,1); glVertex3f(1,1,0); glColor3f(0,0,1); glVertex3f(1,0,0); glEnd;
Wer das Culling nicht verstanden hat, wird sich fragen, wo der unterschied liegt, den beide Stücke beschreiben ein und das gleiche Dreieck. Der Unterschied wird erst deutlich, wenn wir uns die Definitionsreihenfolge vor Augen führen.
Und genau hier ist auch für OpenGL die Definition von "Rückseite". Normalerweise erwartet OpenGL nämlich, dass alle Punkte gegen den Uhrzeigersinn definiert werden, also so wie beim ersten Quadrat. OpenGL würde in seinem Normalzustand dies dann als die Vorderseite ansehen. Mit einer einzigen Zeile können wir dieses Verhalten jedoch auch verändern:
glFrontFace(GL_CW); //Counter-Wise glFrontFace(GL_CCW); // Counter Clock-Wise
Es ist jedoch zu empfehlen diese Reihenfolge beizubehalten, da andere Programmierer, die Euren Source lesen, ebenfalls davon ausgehen werden, dass ihr die Punkte gegen den Uhrzeigersinn definieren werdet. Nur bei einigen Optimierungsverfahren macht ein umschalten wirklich Sinn.
… und eine verlogene Wirklichkeit
Vielleicht springt mal wieder jemand auf und wird mich beschuldigen totalen Mist erzählt zu haben… immerhin zeichnet er vielleicht bei Euch mit beiden Code-Schnipseln die Quadrate? Das liegt einfach daran, dass OpenGL das Culling standardgemäß deaktiviert hat und somit auch beide Seiten rendert. Wie man Culling aktivieren kann, ist schon fast ratbar:
glEnable(GL_CULL_FACE);
Mit glDisable dementsprechend können wir es wieder deaktivieren. Sollten wir beide Quadrate nebeneinander gerendert haben und eben an den Grundeinstellungen nichts verändert haben, sollte mit der Aktivierung dieser Seite nur noch das linke Dreieck angezeigt werden, weil OpenGL es als Vorderseite ansieht. Wollen wir, dass er nur die "Rückseiten" rendert, so lässt sich dies erfolgreich mit einem weiteren Funktionsauruf realisieren:
glCullface(GL_FRONT);
Mit glCullface können wir immer die Seite angeben, die OpenGL vernachlässigen soll. In diesem Fall wäre dies dann die Vorderseite, die nicht gezeichnet werden würde (oder eben das rechte Dreieck).
glCullface(GL_BACK);
Würde wieder den ursprünglichen Status herstellen. Zusammengefasst : glCullFace definiert, ob Vorder oder Rückseite weggelassen werden sollen, glFrontFace definiert, wie die Vorderseite definiert ist (im oder gegen Uhrzeigersinn!) und mit glEnable müssen wir das Culling zunächst aktivieren (und das sollten wir auch immer tun, wenn es möglich ist!).
Ich hoffe, ich habe es halbwegsverständlich erklärt. Wenn jemand sich jetzt fragt, wie man nun die Rückseite sehen kann, wenn wir ein Quadrat haben, dass im Uhrzeigersinn definiert ist… Wenn wir von vorne drauf sehen, werden wir es nicht sehen, da die Punkte in der falschen Reihenfolge definiert sind. Bewegen wir uns durch das Dreieck hin durch und drehen uns um, so werden wir feststellen, dass die Punkte wieder in der "richtigen" Reihenfolge definiert sind und OpenGL sie rendern wird. Wir haben dies hier nur bei sehr simplen Gebilden betrachtet, aber auch bei komplexen ist das Prinzip gleich!
Auch bei möchte ich noch einmal ein gutes Beispiel sehen, wo man bei komplexeren Objekten das Culling gut erkennen kann. Nämlich unserer Landschaft aus dem vierten Tutorial:
Dies sieht zunächst nach einem schweren Grafikfehler aus… ist es jedoch nicht. Wir betrachten unsere Landschaft hier nur von einer Seite, die der Spieler nicht sehen würde, nämlich von der Unterseite. Wir sehen die Landschaft von unten bei aktivieren Culling (Anm.: Beim vierten Tutorial ist KEIN Culling von mir aktiviert worden!) und da ich die Quadrate alle richtig definiert habe sehen wir auch nur noch die, die dem Spieler zu gewannt sind. Etwas Fantasie benötigt man schon dazu, um dies wieder zu erkennen, allerdings sieht man es oben rechts doch noch recht gut. Ich hoffe, dass spätestens dieses kleine Beispiel es noch verständlicher gemacht hat, wenn nicht… schaut Euch die Samples an und fragt dann im Forum nach ;)
BTW: Für psychische Folgen, die beim Lesen dieses Tutorials entstanden sind, übernimmt der Autor keine Haftung. Auch er hat bisher keinen glaubwürdigen Beweis dafür gefunden, dass seine Mitmenschen eine Rückseite haben. Auch die Betrachtung dieser mit Hilfe eines Spiegels kann eine zusätzlich Render-Routine der Engine sein, in der wir leben. Es ist jedoch höchst wahrscheinlich, dass sie keine haben, weil … wer sollte die ganze Rechenleistung aufbringen, um die Vorder- und Rückseite aller Menschen zu rendern! :-D
Für das Protokoll
Grundgedanken zu Display-Listen
Bisher sind wir sicherlich noch nicht in die Verlegenheit gekommen, komplexere Programme zu schreiben. Hat es doch jemand bereits versucht, so wird er schnell gemerkt haben, dass man ohne eine solide Organisation keine Chance hat, ein größeres Projekt zu verwirklichen. Die goldene Regel für eine gelungene Organisation ist sehr einfach! Redundanzen vermeiden um jeden Preis. Man sollte nicht wenn man z.B. ein Quad zeichnen will, diesen 20.000 Mal hintereinander erzeugt. Vielleicht tut es ja auch eine Schleife? Genauso, wie man nicht mehrfach ein Modell in den Speicher laden sollte, wenn es bereits einmal geladen wurde.
Das klingt nun sicherlich wie eine große Verarschung meinerseits. Aber wer jetzt hier geschmunzelt hat, sollte aufpassen, dass ihm nicht gleich das Grinsen im Gesichte stecken bleibt. Würdest Ihr Euer Programm richtig aufbauen? Versuchen eben doppelte Daten zu vermeiden, wo man es nur kann? Wer halbwegs geschickt vorgeht, wird mit Prozeduren, die er immer wieder aufrufen kann eine Menge sinnlosen Code vermeiden. Zum Beispiel wenn wir einen Würfel zeichnen wollen! Wir würden diesen in einer Funktion zeichnen lassen und dann einfach diese Prozedur aufrufen, wenn er gerendert werden soll.
Wünschen wir ihn an einer anderen Stelle in der Welt, so rufen wir einfach die entsprechende Matrix vor dem rendern auf und schon haben wir zwei Würfel in einer Welt mit nur einer Prozedur erzeugt… und die Hälfte an Code gespart. Das lässt sich doch sehen! Auch können wir nun zahlreiche andere Dinge mir in diese Funktion setzen, z.B. eine andere Farbe. Das klingt doch sehr nach einem richtige Ansatz!
Vorsicht! Aufnahme
OpenGL bietet für ein solches vorgehen ein eigenes System und eine Anzahl von Funktionen, die man unter den Namen "Display-Listen" zusammenfassen kann. Dies kann man sich so vorstellen, dass man einen Namen für eine Liste vergibt (nicht irritieren lassen, ein Name ist in diesem Fall eine Nummer!) und sagt OpenGL, dass alles, was jetzt geschieht mitprotokolliert werden soll. Man kann nun anfangen Objekte zu zeichnen, Farben, Texturen, Materialen, einfach alles was einem in den Sinn kommt zu rendern und OpenGL wird dann all dies in einer Liste zusammen fassen. Es steht dann im fortan für uns auf Abruf zur Verfügung.
Technisch gesehen ließen sich wohl diese Listen auch mit Prozeduren realisieren, allerdings gibt es auch einen weiteren Vorteil (angeblich… ich konnte ihn nicht nachweisen, evtl. nur einen Sinn in seiner Frühzeit, wo es keine 3D-Karten gab oder nur bei extrem vielen Objekten spürbar…). OpenGL kann diese Befehle nämlich schneller durchführen, weil es die Operationen praktisch vormerkt. Wie auch immer man es sehen mag, Display-Listen sind unglaublich praktisch bei der Programmierung und meist weicht die anfängliche Skepsis durch Begeisterung (und wenn nicht, gehört es zum guten Ton es zu wissen, weil man es häufiger sehen wird)
Zunächst holen wir uns von OpenGL eine Nummer ab, unter welcher wir die Display-Liste später erreichen werden. Diese wird im Idealfall von Typ Integer sein. Der Aufruf ist kinderleicht:
Displaylist := glGenLists(1);
glGenLists liefert und eben den gewünschten Wert zurück. Wir können auch stattdessen ein Array of GLUint nehmen und gleich mehre anfordern.
Der Rest ist dann praktisch aufgebaut wie ein glBegin und glEnd, nur dass die entsprechenden Funktionen diesmal glNewList und glEndList heißen und wie folgt verwendet werden:
glNewList(DisplayList,GL_COMPILE); […] glEndList;
Das GL_COMPILE bewirkt schlicht und ergreifend, dass sich OpenGL die verwendeten Funktionen merkt. Alternativ könnten wie auch noch GL_COMPILE_AND_EXECUTE einsetzen, wenn wir wollen, dass die Liste auch gleich ausgeführt wird. Praktisch alles was zwischen diesen beiden Funktionen steht wird auch mitgeschrieben. Ausnahmen gibt es wir immer und ich werde dies Mal aufführen, ich denke, dass man erahnen kann, warum diese nicht mit unterstützt werden. Es gibt praktisch keinen Sinn diese mit aufzuzeichnen:
glColorPointer, glDeleteLists, glDisableClientState, glEdgeFlagPointer, glEnableClientState, glFeedbackBuffer, glFinish, glFlush, glGenLists, glIndexPointer,glInterleavedArrays, glIsEnabled, glIsList, glNormalPointer, glPopClientAttrib,glPixelStore, glPushClientAttrib, glReadPixels, glRenderMode, glSelectBuffer, glTexCoordPointer, glVertexPointer und alle glGet Routinen
Und Cut!
Idealerweise erzeugen wir diese Listen nicht bei jedem Render-Vorgang, sondern nur einmal im Init und führen diese dann nur noch beim Rendern aus. Dies geschieht mit der Funktion glCallList und sollte genauso leicht zu beherrschen sein, wie die bisherigen auch:
glCallList(displaylist);
Wir übergeben einfach den Namen der Liste und schon wird alles was wir drinnen aufgezeichnet haben auch ausgeführt… Perfekt, oder? Das ganze ist doch recht leicht hand zu haben und sollte niemanden vor einem großen Problem stellen.
Im Sample haben wir ein kleines Astroiden-Feld nachgebildet, eben mit Hilfe dieser Display-Listen. Ich denke, dass dort anschaulich gezeigt wird, wie man sie sinnvoll einsetzen kann. Auch die Demo von Jan Horn "Biohazzard" zeigt eindrucksvoll, wie man es am Geschicktesten machen kann ;)
- g* Ach ja… einen kleinen Nachteil haben die Display-Listen doch noch! (ich weiß, dass ist jetzt so, als würde ich euch ins ein tiefes Becken tauchen lassen und am Boden steht: "Du hast die Sauerstoff-Falsche vergessen…") Sie verbrauchen relativ viel Speicher! Alle Vorgänge werden nämlich direkt im Arbeitsspeicher geschoben. Eine gesunde Mischung zwischen normalen Rendering und Display-Listen ist also anzuraten, den wenn der Speicher voll ist, hilft keine Optimierung mehr, dass Programm vorm ruckeln zu schützen ;)
Auch sollte man bedenken, dass ein Aufbau z.B. eines Quads sich nicht mehr sehr optimieren lässt. Richtig bringen werden Euch die Display-Listen nur dann etwas, wenn ihr mit komplexen Gebilden arbeitet, die sich immer wieder holen. Würdest ihr z.B. bei jedem Render-Durchgang ein Model verändern, also seine Form (nicht seine Farbe), so würde eine Display-Liste eher hinderlich als nützlich sein!
Nachwort
Bitte nicht traurig sein, wenn hier bereits wieder Ende ist ;) Ich weiß, dass ich in diesem Tutorial mehr versprochen habe, als letztendlich drin ist, aber ich habe vermehrt zu hören bekommen, dass ich dazu neige, wahre Monster-Tutorials zu machen und wollte dem hier mal entgegen wirken, da ich noch rund das doppelte an Stoff gehabt hätte. Drum ist hier nun erstmal Schluss! Es ist nicht ganz das geworden, was zunächst geplant war, aber ich denke, dass das Ziel, nämlich das Grundwissen zu vertiefen erfüllt werden konnte!
Das schreiben dieses Tutorials hat mir sehr viel Spaß gemacht, da ich mich teilweise auch selbst momentan damit beschäftige und es auch unglaublich interessant finde. In diesem Sinne möchte ich dann auch beim sechsten Tutorial weiter machen und mal eine kleine Exkursion unternehmen, nämlich Möglichkeiten aufzuzeigen, Daten aus einem Modellierer in unsere Software zu bekommen. Die Direct3D-Welt hat gesagt, dass sie bereit ist für einen Kampf ist - also sollen sie ihn auch bekommen ^__-
Den mit OpenGL lässt sich weitaus mehr machen als man zunächst erahnt und da die meisten Samples immer nur Dreiecke zeigen wollen wir doch mal etwas Komplexeres machen! Ihr dürft also gespannt sein und wie immer gilt… Ideen und Vorschläge sind herzlich willkommen ;) Im Gegensatz zu D3D hat man bei OpenGL erkannt, dass eine Grafik-API keine Model- und Texture-Loader bieten sollte. Hier ist dann eine Hardcodierung notwendig, die wir allerdings nicht als Nachteil ansehen sollten, sondern vielmehr die Chancen nutzen, unsere Programme mit eigenen Routinen zu optimieren. ;)
Have Fun!
Euer Phobeus