Schatten

Autor: Jonas Schievink

Schatten in Plantex

In Plantex kann die Sonne als momentan einzige Lichtquelle dynamische Schatten werfen. Dafür verwenden wir sogenanntes "Variance Shadow Mapping", eine Abwandlung des klassischen "Shadow Mapping".

Shadow Mapping

Shadow Mapping ist eine einfache Technik um dynamische Schatten zu realisieren. Um Shadow Mapping umzusetzen ist ein zusätzlicher Renderpass notwendig, in dem alle Geometrie die Schatten werfen soll aus der Sicht der Lichtquelle in eine Shadow Map gerendert wird. Diese Shadow Map ist eine Textur, die Tiefenwerte für jeden Pixel enthält (Distanzen zur Lichtquelle).

Shadow Map aus der Sicht der Sonne

Beispiel einer Shadow Map in Plantex

Da Farbwerte ignoriert werden können und nur Tiefenwerte von Bedeutung sind, ist dieser zusätzliche Renderpass in der Regel simpler als das eigentliche Rendern der Welt. So simpel, dass unser Fragmentshader für das Shadow Mapping aller Objekte so aussah (die Tiefe schreibt OpenGL automatisch in den aktiven Tiefenpuffer):

#version 140

void main() {}

Wenn jetzt die Welt zur Anzeige auf dem Bildschirm gerendert wird, wird für jeden Pixel eine zusätzliche Berechnung ausgeführt: Es wird der Pixel in der Shadow Map bestimmt, der den aktuellen Pixel enthält. Dann wird die Distanz des gerenderten Pixels zur Lichtquelle mit der Distanz aus der Shadow Map verglichen. Ist die Distanz in der Shadow Map geringer, so befindet sich Geometrie vor dem gerenderten Pixel (also zwischen dem Pixel und der Lichtquelle) und der Pixel liegt im Schatten. Sind beide Distanzen (ungefähr) gleich, liegt der Pixel in direkter Linie zur Lichtquelle.

Probleme

Klassisches Shadow Mapping hat den Nachteil, dass keine "weichen" Schatten entstehen können: Ein Pixel ist entweder im Schatten oder im Licht einer Lichtquelle.

Außerdem kann ein einzelner Pixel auf der Shadow Map sehr große Bereiche der Szene enthalten (wenn ein sehr großer Bereich beschattet werden soll). Das kann, in Verbindung mit einer sich langsam bewegenden Lichtquelle, dazu führen dass die Schatten sehr stark flackern. In Plantex trifft genau dies zu, weshalb simples Shadow Mapping nicht sonderlich gut aussieht:

Percentage Closer Filtering (PCF)

Percentage Closer Filtering ist ein Verfahren, welches die Qualität der Schatten beim Shadow Mapping durch mehrere Abfragen verbessern soll. Da die Konstruktion der Shadow Map dabei unverändert bleibt, ist PCF relativ einfach auf eine bestehende Implementierung von Shadow Mapping aufzubauen.

Für jeden gerenderten Pixel wird jetzt nicht nur der dazugehörige Pixel in der Shadow Map geprüft, sondern ein NxN-Block von Pixeln aus der Shadow Map. Anhand der Anzahl der schattierten Pixel wird jetzt ein Schattierungsfaktor im Bereich von 0 (alle Pixel sind von der Lichtquelle sichtbar, also voll beleuchtet) bis 1 (alle Pixel liegen im Schatten) berechnet. PCF mit einem 1x1-Filter ist also äquivalent zum klassischen Shadow Mapping.

PCF ermöglicht also zu einem gewissen Grad "weiche" Schatten, denn der Schattierungsfaktor kann genauso viele Werte annehmen wie Pixel getestet werden (also 9 Schattierungsgrade im Bild unten).

Percentage Closer Filtering mit einer 3x3 Filterregion

Probleme

Da das Erstellen der Shadow Map (und das grundlegende Konzept) gleich geblieben ist, ist das Flackern des Schattens kaum besser geworden. Auch sind die 9 möglichen Schattierungsgrade nicht wirklich zufriedenstellend, weil man noch deutliche Übergänge zwischen verschiedenen Graden sehen kann.

Natürlich kann man die Filtergröße von 3x3 weiter erhöhen, aber dann machen sich sehr bald Performanceprobleme bemerkbar (da wir um einen einzigen Pixel zu beleuchten bei einem 5x5-Filter schon 25 Texturzugriffe benötigen), und auch bei einem 5x5-Filter sieht man die Übergänge zwischen den 25 Graden noch deutlich.

Obwohl Grafikkarten hardwarebeschleunigtes PCF unterstützen (bei Verwendung sog. "Shadow Samplers", in GLSL sampler2Dshadow), konnten wir davon keine Verwendung machen, da glium diese nicht unterstützt.

Variance Shadow Mapping (VSM)

Die Schattentechnik, die wir am Ende implementiert haben, nennt sich Variance Shadow Mapping. Hier wird ein anderer Ansatz verfolgt: Anstatt direkt zu testen ob ein Pixel im Schatten liegt, wird die Tschebyscheff-Ungleichung verwendet, um abzuschätzen, wie stark der Pixel schattiert ist.

Konstruktion der Shadow Map

Wie bisher speichern wir die Tiefe jedes Pixels in der Shadow Map. Zusätzlich wird aber auch die quadrierte Tiefe gespeichert (man benutzt also eine Shadow Map mit 2 Kanälen anstatt einem).

Beispiel-Fragmentshader:

out vec2 out_color;

void main() {
    float depth = gl_FragCoord.z;

    out_color = vec2(depth, depth * depth);
}

Die so erzeugte Shadow Map kann jetzt, anders als beim klassischen Shadow Mapping, nach belieben gefiltert werden um die Schatten weicher zu zeichnen. Wir benutzen dafür einen Gaußschen Weichzeichner, der genauso auch für Bloom beim Postprocessing eingesetzt wird.

Probabilistische Schattierung

Beim PCF möchten wir den Anteil von Pixeln in der Shadow Map wissen, dessen Tiefenwert kleiner als der des aktuellen Pixels ist (im entsprechenden PCF-Filterbereich, oben also ein 3x3-Block). Auch beim Variance Shadow Mapping werden wir diesen Anteil berechnen.

Angenommen die Tiefe des aktuell gerenderten Pixels (aus Sicht der Lichtquelle) ist t. Dann können wir Tschebyscheff's Ungleichung verwenden um eine obere Grenze für die Wahrscheinlichkeit, dass der Pixel im Schatten liegt, zu berechnen:

P(xt)σ2σ2+(tμ)2P(x \geq t) \leq {\sigma^2 \over \sigma^2 + (t - \mu)^2}

Den Erwartungswert μ=E(x)\mu = E(x) und die Varianz σ2\sigma^2 können wir aus den Werten in der Shadow Map berechnen (mit nur einem Texturzugriff!): Der Erwartungswert ist einfach die gefilterte Tiefe (gespeichert im ersten Kanal), die Varianz lässt sich mithilfe des Verschiebungssatzes auch wie folgt ausdrücken:

σ2=E(x2)E(x)2=M2M12\sigma^2 = E(x^2) - E(x)^2 = M_2 - M_1^2

Wobei M1M_1 und M2M_2 das erste und zweite Moment sind, die als erster und zweiter Kanal in der Shadow Map gespeichert sind.

Im Shader wird die Wahrscheinlichkeit also wie folgt berechnet (zusammengefasst - es sind noch einige Spezialfälle zu beachten):

float lightCoverage(vec2 moments, float fragDepth) {
    vec2 moments = texture(shadow_map, lightCoords.xy).xy;
    float E_x2 = moments.y;
    float Ex_2 = moments.x * moments.x;
    float variance = E_x2 - Ex_2;
    float mD = moments.x - fragDepth;
    float mD_2 = mD * mD;
    float p = variance / (variance + mD_2);
    return p;
}

Probleme

Da Variant Shadow Mapping nur eine Annäherung ist, kann es zu Artefakten kommen wenn sich Schatten überlagern. In Plantex ist das vor allem dann sichtbar, wenn Schatten von Pflanzen und Terrain auf eine Wand fällt (etwa in eine Höhle oder Schlucht):

VSM Light Bleeding

"Light Bleeding"

Diese "Light Bleeding"-Artefakte sind leider bei VSM inhärent und können nicht ohne weiteres umgangen werden. Eine andere Schattierungstechnik wie Cascaded Shadow Mapping könnte sie vermeiden und gleichzeitig die Vorteile von VSM beibehalten.

results matching ""

    No results matching ""