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.