Tutorial StencilSpiegel
Inhaltsverzeichnis
- 1 Realistische Spiegelungen - Spiel, Spaß und Spannung mit dem Stencil-Puffer
Realistische Spiegelungen - Spiel, Spaß und Spannung mit dem Stencil-Puffer
Vorwort
Ave!
Herzlich willkommen zu meinem ersten Tutorial für DGL. Einige Leser dürften schon die von mir auf meiner Seite veröffentlichen Tutorials kennen, von nun an werde ich jedoch verstärkt Tutorials für die DGL veröffentlichen.
In dieser Lektion geht es um die realistische Darstellung von Spiegelungen mit Hilfe des Stencilpuffers.
Diese Technik findet langsam aber sicher Verbreitung in der Spieleindustrie. Das hat zum einen den Grund, das spiegelnde Flächen in der realen Welt sehr häufig vorkommen, und ihre Nachahmung einer 3D-Welt noch ein Stück mehr an Realitätsnähe verleiht. Der weitere Grund, warum realistische Spiegelungen erst seit kurzem verwendet werden, ist die Tatsache das die Szene mindestens zwei mal gerendert werden muß, und das die Grafikkarte einen Stencilpuffer mitbringen muß. Moderne 3D-Beschleuniger besitzen ihn natürlich, aber die damals so weit verbreitete Vodoo-Serie besaß diesen nicht.
Der Stencil-Puffer
Wie schon erwähnt ist der Stencilpuffer ein wichtigste Mittel um realistische Spiegelungen (und noch viele andere interessante Dinge) zu realisieren. Wie das Wort Stencil (=Schablone) schon andeutet, kann man in diesem Puffer eine Schablone ablegen, die später bestimmt welcher Bereich des Bildschirms überzeichnet, und welcher Bereich unberührt bleibt.
Die Tiefe des Stencilpuffers ist je nach Hardware unterschiedlich, inzwischen kann man aber auf jeder aktuelleren Karte von mindestens 8-Bit für diesen Puffer (oft werden dann die 24-Bit für den Tiefenpuffer mit diesen kombiniert um ein DWORD zu erhalten) ausgehen.
Ein praktisches Anwendungsbeispiel für die Nutzung dieses Puffers zur Begrenzung der zu überzeichnenden Fläche wäre z.B. die Cockpitansicht einer Flugsimulation. Zuerst wird der Stencilpuffer aktiviert, und dann wird das Cockpit in diesen Puffer "hineingezeichnet" und als Ausschlussmaske benutzt. Überall dort wo das Cockpit im Stencilpuffer einen Pixel hinterlassen hat wird später im Farbpuffer nicht gezeichnet.Dadurch muss das Cockpit also nur einmal gezeichnet werden. Die folgende Bildreihe verdeutlicht dies :
Cockpit-Textur (oder 3D-Modell) (wird in den Stencilpuffer gezeichnet) |
Inhalt des Stencil-Puffers (Schwarz : Stencil-Wert = 0) (Weiß : Stencil-Wert = 1) |
Auf dem Bildschirm sichtbarer Teil der Szene | Endergebnis |
Jetzt fragt ihr euch bestimmt, was dieses Cokpit-Beispiel mit unserer Spiegelung zu tun hat?
Ganz einfach : Zum einen erleichtert es das Verständnis der Funktionsweise des Stencilpuffers, was unbedingt notwendig ist, und zum anderen funktioniert die Sache mit dem Spiegel genau umgekehrt.
Während der Stencilpuffer im Cockpit-Beispiel dazu genutzt wird, den Teil an dem sich das Cockpit befindet vor dem Überschreiben durch andere Farbwerte zu bewahren, wird der Puffer bei einer Spiegelung dazu genutzt, den Bereich ausserhalb des Spiegels vorm Überschreiben zu schützen. So wird also nur der Teil der Szene gezeichnet, die sich auch "im" Spiegel befindet.
Wozu der Stencilpuffer also gut ist, lässt sich anhand der folgenden zwei Screenshots wohl am besten verdeutlichen. Während der Spiegel im linken Bild korrekt ist, und das Spiegelbild ausserhalb der Spieglfläche aufgrund des Stenciltests nicht gezeichnet wird, ist das Spiegelbild rechts falsch. Hier wurde der Stenciltest deaktiviert :
Clipping Plane
Ein weiterer, für unseren Zweck notwendiger Begriff sind die Clipping Planes (zu Deutsch : Schnittflächen). Das ist aber nichts mathematisch unmögliches, sondern eine ganz einfache Fläche, an der die Geometrie "abgeschnitten" wird. D.h., dass alles was auf der per Vorzeichen definierten Seite dieser Schnittfläche liegt einfach nicht gezeichnet wird.
OpenGL bringt von Haus aus schonmal sechs solcher Clipping-Planes mit, die zusammen das Frustum ergeben, also der Quader, an dem die Geometrie aufgrund ihrer Sichtbarkeit "abgeschnitten" wird.
Darüberhinaus kann man jedoch selbst noch zusätzliche Schnittflächen definieren. Laut OpenGL-Vorgaben muß jeder OpenGL-Treiber mindestens sechs zusätzliche Schnittflächen anbieten können. Für unseren Spiegel reicht jedoch eine.
Eine Clippingplane wird mittels der Funktion glClipPlane(plane: TGLEnum; equation: PGLdouble), wobei die Fläche mittles eines array[0..3] of Double über ihre Gleichung Ax+By+Cz+D = 0 definiert wird. Aktiviert bzw. deaktiviert wird eine Schnittfläche mittels der Konstante GL_CLIP_PLANEi, wobei i für die Nummer der Schnittfläche steht und zwischen 0 und 5 liegt.
Das hört sich jetzt erstmal leicht trocken an, und dürfte ohne praktisches Beispiel auch nicht hängen bleiben. Deshalb hier zwei Screenshots aus dem Beispielprogramm zu diesem Tutorial, die verdeutlichen wozu die Schnittfläche benötigt wird. Der Spiegel liegt hier im Koordinatensystem am Ursprung (x:0,y:0,z:0) und "liegt" auf der Y-Achse.
Definiert wird dieser Spiegel deshalb als {{{1}}}. Das Minus vor der Eins gibt also quasi an, auf welcher Seite der Spiegelfläche geclippt werden soll. Würde der Spielerraum also auf der anderen Seite liegen, wäre das Vorzeichen positiv.
Programmiertechnische Umsetzung
Nachdem wir nun mit der Begriffserklärung abgeschlossen haben und jedem klar sein sollte, was ein Stencilpuffers macht und wozu eine Clipping Plane gut ist, widmen wir uns nun der Implementation eines Spiegels in unser Programm. Der Quelltext zu diesem Thema ist zwar relativ kurz, bietet aber dennoch einiges an Erklärungsspielraum.
Deshalb : Nicht einfach nur abtippen, sondern auch sorgfältig meine mehr oder weniger ausführlichen Erklärungen mitlesen.
Die Renderprozedur sowohl für die normale Szene als auch für den Spiegelraum befindet sich im Beispielprogramm in der Prozedur DrawScene_Stencil. Im folgenden Kapitel werden alle relevante Teile dieser Prozedur besprochen, unwichtige Grundlagen lasse ich jedoch weg. Jeder der sich mit Spiegelungen beschäftigen will, sollte sich mit den OpenGL-Grundlagen auskennen.
Definition der Schnittfläche
Zu allererst definieren wir die Schnittfläche an der das Spiegelbild abgeschnitten wird. Würden wir diesen Teil weglassen, dann würden spätestens beim Durchschreiten eines Spiegels (auch wenn das in einem Ego-Shooter nicht vorkommen sollte) hässlich falsche Spiegelbilder entstehen...und das wollen wir ja nicht, schliesslich soll unser Spiegel ja die reale Welt nachstellen. Wie die Schnittfläche definiert wird, haben wir ja oben bereits besprochen. Wichtig wäre hier vielleicht noch, dass als Datentyp unbedingt Double verwendet werden muß. Andere Datentypen (z.B. Single) führen zu unvorhersehbaren Effekten.
const ClipPlane : array[0..3] of Double = (0, 0, -1, 0);
Den Stencil-Puffer löschen
Wie bekannt müssen (oder sollten) die verwendeten OpenGL-Puffer vor jedem neuen Zeichendurchgang gelöscht werden. Dies geschieht ja bekannterweise mit der Funktion glClear() und als Parameter oderverknüpft die zu löschenden Puffer. Neben den Bits für den Farbpuffer (GL_COLOR_BUFFER_BIT) und dem Tiefenpuffer (GL_DEPTH_BUFFER_BIT) kommt nun ein neues Puffer-Bit dazu, nämlich GL_STENCIL_BUFFER_BIT, um den Stencilpuffer zu "löschen".
Löschen habe ich deshalb in Anführungszeichen gesetzt, weil der Stencilpuffer nicht einfach wie z.B. der Farbpuffer geleert wird, sondern durch glClear(GL_STENCIL_BUFFER_BIT) mit einem vorher definierten Wert gefüllt wird. Dieser Wert wird (am besten bei der OpenGL-Initialisierung) mittels glClearStencil(s:Integer) festgelegt. In unserem Falle übergeben wir als hier Parameter den Wert 0.
// Löschen des Stencil-Puffers zum Beginn der Szene glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT or GL_STENCIL_BUFFER_BIT);
Den Stencil-Puffer vorbereiten und die Maske erstellen
Kommen wir nun also zum schwierigsten Teil dieses Tutorials. Die folgenden Zeilen aktiveren den Stencilpuffer und erstellen die Maske unseres Spiegels in diesem Puffer. Leider lassen sich die jetzt kommenden Befehle und Funktionen nicht ganz so einfach beschreiben.
glColorMask(False, False, False, False);
Mit der Funktion glColorMask(R,G,B,A : ByteBool) teilen wir OpenGL mit, welche Farbkomponenten auf den Bildschirm gezeichnet werden. In unserem Falle deaktivieren wir alle Farbkomponenten, es wird also nichts auf den Bildschirm gezeichnet.
Dies ist insofern nötig, da wir jetzt erstmal unsere Maske in den Stencilpuffer zeichnen wollen, und dies kein Puffer ist in den man direkt hineinschreiben kann (wie das beim Farbpuffer der Fall ist). Wir müssen also den Weg über den Farbpuffer gehen.
glEnable(GL_STENCIL_TEST);
Zu diesem Befehl muss ich wohl kaum Worte verlieren. Kurz und schmerzlos : Er aktiviert den Stencil-Test und damit verbunden auch den Stencilpuffer.
glStencilFunc(GL_ALWAYS, 1, 1);
Mit diesem Befehl teilen wir OpenGL mit, welcher Test auf jeden gezeichneten Pixel angewendet werden soll. Der erste Parameter GL_ALWAYS gibt an, dass der Test immer erfolgreich ist. Der zweite Parameter gibt den Referenzwert für diesen Test an (den wir gleich noch brauchen werden). Wenn der Test z.B. GL_LESS wäre, dann würde das Pixelfragment den Test bestehen, wenn es kleiner als der im Referenzparameter angegebene Wert wäre.
Der letzte Parameter gibt die Maske an, mit der unsere Referenzparameter beim Gelingen des Tests verunded wird. Wenn der Test also gelingt, wird eine 1 in den Stencilpuffer geschrieben (Test gelungen->Stencil-Wert = Refernzwert UND Maskenwert -> 1 UND 1 = 1).
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
Nach obigem OpenGl-Vorschlaghammer kommt jetzt wieder etwas leichter verdauliche Kost. Mit der Funktion glStencilOp(fail, zfail, zpass: TGLEnum) teilen wir OpenGL mit, wie der Stencilwert verändert wird, wenn ein Fragment den Test besteht oder nicht besteht. Dabei unterscheidet OpenGL anhand der drei übergebenen Parameter drei verschiedene Szenarien :
- Fail
- Diese Funktion wird angewendet, wenn das Fragment den Stencil-Test nicht besteht
- zFail
- Wird angewendet, wenn das Fragment den Tiefentest nicht besteht
- zPass
- Wird angewendet, wenn das Fragment den Test besteht (Wichtigste Funktion!)
Die ersten zwei Parameter sind in unserem Falle relativ uninteressant, weshalb wir OpenGL über GL_KEEP mitteilen dass der Stencilpuffer in diesen beiden Fällen nicht verändert wird. Der dritte Parameter ist jedoch sehr wichtig : Er gibt an, was OpenGL mit dem Stencil-Puffer macht, wenn das Fragment den Test besteht. In unserem Fall soll der Stencil-Pufferwert über GL_REPLACE mit dem veränderten Masken- und Referenzwert (=1) ersetzt werden.
Wer die Hoffnung jetzt noch nicht verloren hat, der hats fast geschafft. Das Schwerste liegt bereits hinter uns!
glDisable(GL_DEPTH_TEST); glDisable(GL_TEXTURE_2D); DrawMirror;
Diese drei Zeilen sind ja schon fast selbsterklärend, aber aufgrund dessen was oben auf das Gehirn zukam auch eher zur Entspannung gedacht. Bevor wir mit DrawMirror unsere Spiegelfäche in den Stencil-Puffer zeichnen, deaktivieren wir den Tiefentest und das Texturemapping. Nun haben wir eine unsichtbare Maske unseres Spiegels im Stencilpuffer. Überall dort, wo unsere Spiegelfläche in den Stencilpuffer gezeichnet wurde, steht eine 1 (=Referenzwert und Maksenwert, da der Test bestanden wurde). Solange der Stenciltest jetzt also aktiv bleibt, wird nur dort gezeichnet wo unsere Spiegelfläche im Stencilpuffer eine 1 hinterlies.
glEnable(GL_TEXTURE_2D); glEnable(GL_DEPTH_TEST); glColorMask(True, True, True, True);
Da wir nun beginnen die Szene hinter dem Spiegel (also quasi das Spiegelbild) zu zeichnen, aktivieren wir sowohl das Texturemapping als auch den Tiefentest wieder. Und da wir natürlich auch was von unserem Spiegelbild sehen woll und dies in den Farbpuffer gelangen soll, teilen wir OpenGL mittels glColorMask() mit, dass wieder alle Farbkomponenten in den Farbpuffer gelangen sollen.
glStencilFunc(GL_EQUAL, 1, 1);
Mit dieser Zeile teilen wie OpenGL mit, das nur dort gezeichnet werden soll, wo sich im Stencilpuffer eine 1 befindet (siehe oben). Dies ist das "Geheimnis" hinter realistisch aussehenden Reflektionen im Stencilpuffer!
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
Nachdem wir unseren Stencilpuffer ja bereits mit einer unsichtbaren Spiegelmaske gefüllt haben, soll er von nun an nicht mehr verändert werden. In allen drei Testfällen wird der aktuelle Stencilwert deshalb also mittels GL_KEEP nicht mehr verändert.
glEnable(GL_CLIP_PLANE0); glClipPlane(GL_CLIP_PLANE0, @ClipPlane);
Bevor wir jetzt dazu übergehen unsere gespiegelte Szene zu zeichnen, aktivieren wir noch die Schnittfläche unseres Spiegels. Die Erklärung dazu gabs ja bereits zum Beginn dieses Tutorials.
Die gespiegelte Szene zeichnen
So. Nachdem die Stencilpuffer-Tortur überstanden ist und mindestens die Hälfte der Leser eines unnatürlichen Todes gestorben sind, darf ich die anderen beglückwünschen. Wir sind fast fertig!
Nachdem unser Stencilpuffer also inzwischen die unsichtbare Spiegelfläche enthält und er auch schon für das Zeichnen der gespiegelten Szene vorbereitet wurde, müssen wir die Szene nur noch gespiegelt zeichnen. Nichts leichter als dass, denn mit glScalef() kann man eine Szene ja bekannterweise auf einfachste Art und Weise an einer Achse spiegeln. In unserem Falle müssen wir die Szene an der Z-Achse spiegeln, also der Achse an der unser Spiegel spiegelt. Das geht dann kurz und schmerzlos :
glScalef(1,1,-1)
Wie gesagt...einfacher gehts nimmer. Wenn wir unsere Szene z.b. auf dem Fussboden spiegeln wollten, dann würe ein einfaches glScalef(1,-1,1) reichen. Dass sieht in der Praxis dann im Endergebnis so aus :
Anschliessend wird dann die Szene wie gewohnt gezeichnet. Wenn die Szene etwas komplexer ist, sollte man das Zeichnen in eine externe Prozedur auslagern, die man dann zweimal aufrufen muss. Nicht vergessen : Vor dem skalieren der Matrix sollte man diese mit glPushMatrix auf den Matrizenstack legen, und vor dem Zeichnen der normalen Szene wieder mit glPopMatrix wiederherstellen!
glDisable(GL_CLIP_PLANE0); glDisable(GL_STENCIL_TEST);
Nachdem wir unsere gespiegelte Szene gezeichnet haben, diese an der definierten Schnittfläche (wenn nötig) abgeschnitten wurde und aufgrund der Stencilmaske nur in die Spiegelfläche gezeichnet wurden, können sowohl die Schnittfläche als auch der Stenciltest wieder deaktiviert werden.
Den Spiegel zeichnen
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glColor4f(1,1,1,0.45); MirrorTex.Bind; DrawMirror; glDisable(GL_BLEND);
Nachdem wir nun unsere gespiegelte Szene hinter dem Spiegel gezeichnet haben, sollten wir noch unsere sichtbare Spiegelfläche zeichnen, um dem Spieler auch den Eindruck zu vermitteln, dass er vor einem Spiegel steht.Dazu benutze ich eine recht helle Glastextur, die ich mittels Alphablending über die gespiegelte Szene zeichne. Welchen Unterschied das macht, lässt sich auf folgenden beiden Screenshots (links ohne Spiegeltextur, rechts mit) sehr gut sehen :
Und die Moral von der Geschicht...
...ohne die normale Szene dann zu rendern gehts nicht! Also nicht vergessen nach dem rendern des Spiegels auch noch die normale Spielszene zu zeichnen!
Nachwort
So...das war also mein erstes Tutorial für die DGL.Für mich wars halb so schlimm, während es für einige von euch wohl doch einem Vorschlaghammer auf den Hinterkopf gleich kam. Das macht aber nix, denn wenn irgendjemand etwas nicht verstanden hat, ich bin ja auch als Mod im Forum tätig und antworte gerne auf eure Fragen!
Abschliessend sei noch zu sagen, dass diese Technik einen Nachteil hat : Der Raum hinter der Spiegelfläche ist tabu, da man sonst einen Teil des illusionären Spiegelraumes dahinter sehen würde.Dieser Nachteil lässt sich jedoch recht leicht mit einigen Sichtbarkeitstechniken umschiffen, und der Nutzung solcher Spiegelungen sollte damit keine Grenzen mehr gesetzt sein!
Hoffe das Tut hat euch gefallen und vor allem auch geholfen!
Euer
Sascha Willems (webmaster AT delphigl.de)
Nachtrag (Oktober 2005)
Das Tutorial hat bereits einige Jahre auf dem Buckel, und in einer so schnellen Industrie wie der Grafikkartenindustrie ist das von größerer Bedeutung. Die Technik Spiegelungen mittels des Stencilpuffers zu realisieren ist heute kaum noch von Relevanz. Heute benutzt man so gut wie immer Render-To-Texture (z.B. via Pixelpuffer oder EXT_FBO) um Spiegelungen aller Art zu realisieren. Dadurch hat man diverse Vorteile :
- Die Spiegelung kann u.a. über Verschiebung der Mip-Maps schärfer bzw. dumpfer gemacht werden, so lassen sich auch einfach Reflektionen auf dumpfen Materialien wie z.B. verrostetes Metall darstellen.
- Die Spiegelung liegt in einer Textur vor. Damit verbunden sind jede Menge Manipulationen möglich, sei es nun so was grundlegendes wie Multitexturing oder erweitertes Sachen wie Shader. Mit Shadern kann man dann recht einfach solche Sachen wie Verwirbelung oder Refraktion auf der Oberflächen (u.a. für Wasser genutzt) realisieren.
Von daher sollte man heutzutage auf die Spiegelungsmethode im Stencilpuffer verzichten und die Render-To-Texture Methode verwenden.
Sascha Willems (webmaster AT delphigl.de)
|
||
Vorhergehendes Tutorial: Tutorial_MultiTexturing |
Nächstes Tutorial: Tutorial_StereoSehen |
|
Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com. Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen. |