Tutorials

Hier finden sie Tutorials zum Thema Spieleentwicklung.


Converting Ms3d-animations to DirectX Datum: 12.5.2010 Autor: Christoph P. Schwierigkeitsgrad: Mittel
Einführung in DirectX Shader Datum: 15.5.2010 Autor: Christoph P. Schwierigkeitsgrad: Einfach
Dual-Paraboloid-Shadow-Mapping Datum: 23.5.2010 Autor: Christoph P. Schwierigkeitsgrad: Mittel
Einführung in Milkshape 3D Datum: 17.6.2010 Autor: Christoph P. Schwierigkeitsgrad: Einfach

Dual Paraboloid Shadow Mapping

Vorwort

In diesem Tutorial wird anhand eines DirectX-Effekts und Shader Model 3.0 die Thematik erklärt. Wissen über die Implementierung mithilfe von Render-Targets, setzen von Effektvariablen und Transformationen von Objekten sind Vorraussetzung für das Verständnis des Beispiels. Das Beispiel lässt sich ohne Schwierigkeiten auch auf andere Shader-Sprachen übertragen.

Einführung

Shadow Mapping ist eine Technik, um Schatten in Computergrafiken darzustellen. Bei diesem Verfahren wird eine Textur mit Tiefenwerten (Shadow Map) aus der Sicht der Lichtquelle gerendert. Beim Zeichnen der Szene werden dann die Tiefenwerte der Pixel mit den Tiefenwerten in der Shadow Map verglichen. Dazu müssen die Pixel in das Koordinatensystem der Lichtquelle transformiert werden. Sind die ausgelesenen Werte niedriger, befinden sich die Pixel im Schatten.
Diese Technik in Verwendung mit nur einer Shadow Map, macht aber nur bei einem Scheinwerfer (Spotlight) Sinn. Möchte man ein, in mehrere Richtungen scheinendes Licht, zB. ein Punktlicht realisieren, reicht diese Methode nicht mehr aus. Die naheliegendste Lösung ist die Verwendung einer Shadow Map für jede Richtung, in die das Licht strahlt. Bei einem Punktlicht wären das sechs Shadow Maps. Diese Methode nennt sich übrigens Cubical Shadow Mapping, hat qualitativ zwar keine Einbußen, ist aber sehr unperformant, da man sechs Shadow Maps rendern muß.
Eine weitere Technik, um dieses Problem in den Griff zu bekommen, nennt sich Spherical Shadow Mapping. Diese Technik verwendet zwar, wie das traditionelle Shadow Mapping, nur eine Shadow Map und ist dadurch, was die Performance betrifft, annähernd so schnell, qualitativ gesehen aber problematisch, da die Abtastrate (Sample Rate) ungleichmäßig ist und die Pixel dadurch teilweise verzerrt dargestellt werden. So wurde weiter geforscht und man stieß auf die Dual Paraboloid - Technik, zum ersten mal beschrieben 1998, in der Publikation "View-independent Environment Maps" von Wolfgang Heidrich und Hans-Peter Seidel.
Hierbei sei erwähnt, daß das bis jetzt beschriebene Problem des Shadow Mappings in fast gleicher Form beim Environment Mapping zu finden ist, nur das dort Farbwerte der Umgebung anstatt Tiefenwerte in den Maps verwendet werden. Dazu möchte ich nun auch ein Bild zeigen, daß eine Paraboloid Map darstellt, wie sie beim Dual Paraboloid Environment Mapping verwendet wird. Der Sinn dieses Bildes aber ist, euch einen Einblick in die Paraboloid-Projektion zu geben, denn das ist mit einer Paraboloid Shadow Map, wie wir später sehen werden, nur schwer möglich, wir wollen aber beim Thema Dual Paraboloid Shadow Mapping bleiben.

Paraboloid Environment Mapping
Paraboloid Environment Mapping

Paraboloid Maps liefern eine gute Performance und sind qualitativ höherwertig als Spherical Maps. Zunächst mal ein Bild eines elliptischen Paraboloids, ähnlich wie wir es verwenden werden:

Elliptisches Paraboloid
Elliptisches Paraboloid

Eine vorteilhafte Eigenschaft von Paraboloiden ist, daß auftreffende Strahlen, die vom Brennpunkt kommen, parallel zur Richtung des Paraboloids reflektiert werden, was die Berechnung der Projektion erleichtert. Auch dazu ein Bild:

Parallele Paraboloid Reflektion
Paraboloid Reflektion

Beim Paraboloid Mapping wird der Raum um die Lichtquelle in zwei Hemisphären, also Paraboloide geteilt. Es gibt einen vordere und eine hintere Hemisphäre, die, wie im Bild, mit den offenen Seiten voran, zusammengesetzt werden und einen geschlossenen Raum bilden.

Matematischen Herleitung

Zuerst wird ein Paraboloid gewählt, daß für diesen Zweck geeignet ist:

Paraboloid Funktion

Jeder Vertex der gerendert wird muß auf die Paraboloid-Oberfläche projiziert werden. Als Erklärung ein Bild:

Paraboloid Projektion
Paraboloid Projektion

Es müssen die x- und y- Koordinate des Projektionspunkts gefunden werden. Über den Normalenvektor am Projektionspunkt können diese Koordinaten berechnet werden. Dazu braucht man zunächst die Definition des Normalenvektors an der Oberfläche. Dieser wird per Kreuzprodukt der Tangenten ermittelt. Die Tangenten erhaltet man über die partielle Ableitung der Funktion, in Bezug auf x und y.

Paraboloid-Normalenvektor

Der Richtungsvektor des einfallenden Projektionsstrahls entspricht der normalisierten Vertexposition im Paraboloid-Koordinatensystem. Addiert man nun den Reflektionsvektor zu diesem Richtungsvektor, erhält man einen Vektor, der der Normalen am Projektionspunkt entspricht, nur die Länge ist unterschiedlich. Da der Reflektionsvektor, ja immer in Richtung Z-Achse verläuft, braucht zum z-Wert des einfallenden Vektors nur 1 dazuaddiert werden.

Stellt man nun eine Beziehung zu obiger Ableitung her, so stellt man fest, daß eine Division des addierten Vektors durch seinen z-Wert, die x- und y- Koordinate des gesuchten Projektionspunkts berechnet.

Normalenvektor am Projektionspunkt

Implementierung

a.) Erstellung der Paraboloid Shadow Maps:

Im den ersten beiden Render-Durchgängen müssen die Paraboloid Shadow Maps erstellt werden. Für die Maps habe ich eine Auflösung von 1024 x 1024 gewählt, das kann aber nach Belieben gewählt werden. Umso höher die Auflösung, desto besser die Schattenqualität, aber umso schlechter die Performance. Für das Texturformat ist hier das R32F-Floating-Point-Format zu empfehlen.

Zuerst einmal muß das Render-Target für die vordere Shadow Map gesetzt werden. Als nächstes muß die World-View-Matrix für den Paraboloid-Raum erstellt werden und als Parameter für den Effekt gesetzt werden. Die World-Matrix entspricht einer gewöhnliche Transformation in den absoluten Raum und die View-Matrix ist die Matrix, die es uns ermöglicht die Szene aus der Sicht des Lichtes zu rendern. Die Position dieser Matrix ist die Lichtposition und die Orientierung kann frei gewählt werden. Ich habe die Orientierung in Richtung der zu beleuchtenden Objekte gewählt. Eine Projektions-Matrix wird nicht benötigt, da wir die Projektion manuell durchführen.

Nun können wir die Shader implementieren. Wie so oft schaut es im Shader um einiges einfach aus:

zunächst der Vertex-Shader:

Als Erstes werden die Vertizes in den Paraboloid-Raum transformiert und durch w dividiert, um homogene Koordinaten zu erhalten. Die Bedeutung von g_fDir wird später erklärt. Nun muß der Vertex auf das Paraboloid projiziert werden. Wie wir wissen entspricht die normalisierte Vertexposition im Paraboloid-Raum dem Richtungsvektor des einfallenden Projektionsstrahls. Deswegen normalisieren wir nun die Vertexposition. Die z-Koordinate speichern wir für den Pixel-Shader.
Jetzt der entscheidende Teil: wir addieren den Reflektionsvektor (0/0/1) zu dem soeben berechneten Projektions-Richtungsvektor und wir erhalten den Vektor, der der Normalen am Projektionspunkt entspricht. Diesen dividieren wir durch seinen z-Wert und wir bekommen die gesuchte x- und y-Koordinate. Im Beispiel habe ich beide Schritte in einem erledigt, lasst euch dadurch nicht verwirren. Die gefundenen Koordinaten entsprechen den Texturkoordinaten in der Shadow Map. Für den anschließenden Z-Buffer-Test müssen wir den z-Wert des Vertex noch korrekt berechnen, indem wir ihn mithilfe der fernen und nahen Clipping-Ebene zwischen null und eins bringen. Diesen speichern wir dann auch für den Pixel-Shader. Der w-Wert wird neutralisiert.

Zur Erklärung von g_fDir: um sich das Schreiben von zwei, fast identischen Shadern, zu ersparen, wird die z-Koordinate des Vertex für die hintere Hemisphäre einfach invertiert. Außerdem muß noch das Back-Face-Culling deaktiviert werden um dies zu ermöglichen, denn durch die Invertierung der z-Koordinate wird die Reihenfolge der Dreiecks-Vertizes umgedreht.

Der Pixel-Shader:

Hier brauchen nur mehr die Pixel, die sich vor der Clipping-Ebene der jeweiligen Hemisphäre befinden, entfernt werden und die Tiefe der Pixel in die Shadow Map geschrieben werden.

Nun rendern wir die Szene, einmal mit g_fDir = 1, für die vordere Hemisphäre und einmal mit g_fDir = -1, für die hintere Hemisphäre und die Shadow Maps sind erstellt. Das Ergebnis sollte dann prinzipiell so aussehen, wie das folgende Bild:

Dual Paraboloid Shadow Map
Dual Paraboloid Shadow Map

b.) Rendern der endgültigen Szene:

Als Erstes müssen das Render-Target für die Szene und einige Parameter für den Effekt gesetzt werden. Und zwar sind das folgende:

Im Vertex-Shader befindet sich nichts was für das Paraboloid Mapping spezifisch wäre, deswegen ist hier eine Erklärung nicht notwendig.

Nun zum Pixel-Shader:

Auch wenn es im unten dargestellten Pixel-Shader nicht den Eindruck macht, es befindet sich auch hier, nichts darin, was unbedingt erklärt werden müßte, denn die Berechnung der Texturkoordinaten ist die gleiche, wie wir sie beim Erstellen der Shadow Maps angewandt haben, nur daß hier für die Berechnung beider Hemisphären vorgesorgt ist und jenachdem in welcher Hemisphäre sich der Vertex dann befindet, die Koordinaten für eines der beiden Paraboloide berechnet und die Werte zwischen null und eins gebracht werden, um die Shadow Maps richtig auslesen zu können. Der letze Teil im Pixel-Shader ist gleich wie beim traditionellen Shadow Mapping: es wird der Tiefenwert der Pixel verglichen. Ist er höher als der Wert in der Shadow Map, liegt der Pixel im Schatten und er bekommt nur die Texturfarbe und das Umgebungslicht ab. Andernfalls wird er richtig beleuchtet und bekommt alles wovon ein Pixel träumt. :)

Um den Effekt zu vollenden, die Kompilierung der Shader:

Nun muß nur mehr das Rendering der Szene vollzogen werden und das war's!

Fazit zum Dual Paraboloid Shadow Mapping (auch für Dual Paraboloid Environment Mapping gültig):

Wie oben bereits erwähnt haben Paraboloid Maps eine gute Performance und sind qualitativ höherwertig als Spherical Maps, allerdings gibt es auch Nachteile gegenüber dem Cubical Mapping, die hauptsächlich mit der Verzerrung des Paraboloids zusammenhängen. Ein Effekt der auftreten kann, ist, wenn man beim Zeichnen der Szene die Vertexpositionen bereits im Vertex-Shader in den Paraboloid-Raum transformiert, denn dann würde durch die Interpolation im Rasterizer eine Verzerrung auftreten. Da wir die Position in unserem Beispiel, aber erst im Pixel-Shader transformiert haben, fällt dieses Problem weg. Das andere, etwas schwerer in den Griff zu bekommende Problem, sind Verzerrungen, die vor allem bei der Clipping-Ebene der beiden Hemisphären auftreten und durch Risse sichtbar werden. Dieser Effekt ist allerdings stark abhängig von der Auflösung der Geometrie. Ist sie hoch genug, wird der Effekt kleiner. Außerdem kann man mit geschickten Setzen dieser Ebene ebenfalls diesem Effekt entgegenwirken. Man sollte, wenn möglich, Kollisionen der Geometrie mit dieser Ebene vermeiden.

Wie wir nun gesehen haben, sind Paraboloid Maps, trotz kleiner Nachteile, sehr sinnvoll. Da, durch immer schneller werdende Grafikkarten, die Auflösung der Geometrie aber immer höher wird, sollte dem Paraboloid Mapping in Zukunft nichts im Wege stehen.

Das komplette Beispiel könnt ihr hier, als DirectX-Effektdatei downloaden.


Nach oben

Copyright 2015 (c) by Christoph P.
Impressum    AGB