Tutorial StencilSpiegel

Aus DGL Wiki
Version vom 11. September 2005, 21:58 Uhr von Flash (Diskussion | Beiträge) (Der Stencil-Puffer: Cockpitbilder sauber ausgerichtet)

Wechseln zu: Navigation, Suche

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 Stencil-Puffers.
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 Stencil-Puffer 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 Stencil-Puffer das 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 Stencil-Puffers ist je nach Hardware unterschiedlich.Kyro-Karten liefern einen 4-Bittigen Puffer, während nVidia mal wieder etwas schlauer waren, und einen 8-Bit Stencil-Puffer zusammen mit einem 24-Bit Tiefenpuffer zu einem DWORD kombinieren.Dadurch kann der Stencil-Puffer auf nVidia-Karten "ohne Geschwindidkeitsverlust" genutzt werden.Das stimmt zwar, aber bei dieser Aussage haben die Marketingexperten die zeitaufwendige doppelte Berechnung der Geometrie natürlich aussen vor gelassen.

Ein praktisches Anwendungsbeispiel für die Nutzung des Stencil-Puffers zur Begrenzung der zu überzeichnenden Fläche wäre z.B. die Cockpitansicht einer Flugsimulation.Zuerst wird der Stencil-Puffer aktiviert, und dann wird das Cockpit in diesen Stencil-Puffer "hineingezeichnet" und quasi als Ausschlussmaske benutzt.Überall dort wo das Cockpit im Stencil-Puffer einen Pixel hinterlassen hat wird später im Farb-Puffer nicht gezeichnet.Dadurch muss das Cockpit also nur einmal gezeichnet werden.Ich hoffe die untere Bildreihe verdeutlicht dies :

Tutorial Stencil cockpit01.jpg
Tutorial Stencil cockpit02.jpg
Tutorial Stencil cockpit03.jpg
Tutorial Stencil cockpit04.jpg
Cockpit-Textur (oder 3D-Modell)
(wird in den Stencil Puffer 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 Stencil-Puffers, was unbedingt notwendig ist, und zum anderen funktioniert die Sache mit dem Spiegel genau umgekehrt.
Während der Stencil-Puffer im Cockpit-Beispiel dazu genutzt wird, den Teil an dem sich das Cockpit befindet vor dem Überschreiben durch andere Farbwerte zu bewahren, wird der Stencil-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 von 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.

Vorlage:center

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., das 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 mal zwei Screenshots aus dem Beispielprogramm zu diesem Tutorial, die verdeutlichen wozu die Schnittfläche benötigt ist.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.

Ohne Clipping
Mit Clipping

Programmiertechnische Umsetzung

Nachdem wir nun mit der Begriffserklärung abgeschlossen haben und jedem klar sein sollte, was ein Stencil-Puffer 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 Grundlage lasse ich jedoch extra weg.Jeder der sich mit Spiegelungen beschäfigen 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, das als Datentyp unbedingt Double verwendet werden muß.Andere Datentype (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 Stencil-Puffer zu "löschen".

Löschen habe ich deshalb in Anführungszeichen gesetzt, weil der Stencil-Puffer 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 Stencil-Puffer und erstellen die Maske unseres Spiegels in diesem Puffer.Leider lassen sich die jetzt kommenden Befehle und Funktionen nicht ganz so einfach beschreiben, aber ich gebe mein bestes!

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 Befel muss ich wohl kaum Worte verlieren.Kurz und schmerzlos : Er aktviviert den Stencil-Test und damit verbunden auch den Stencil-Puffer.

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, das 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 Stencil-Puffer 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, 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 das der Stencil-Puffer in diesen beiden Fällen nicht verändert wird. Der dritte Parameter ist jedoch sehr wichtig.Er gibt ja 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 verundeten 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 das 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 das, 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 also nimmer.Wenn wir unsere Szene z.b. auf dem Fussboden spiegeln wollten, dann würe ein einfaches glScalef(1,-1,1) reichen.Das sieht in der Praxis dann im Endergebnis so aus :

Vorlage:center

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 das 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 :

Vorlage:center


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, das 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

Son of Satan (alias Sascha Willems)


Vorhergehendes Tutorial:
Tutorial_Bumpmaps_mit_Blender
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.