Seminarium z Grafiki - Deep Shadow Maps i Variance Shadow Maps

Wstęp

Co to są shadow maps - render to shadow map, project over scene, compare

Problemy: aliasing.

Percentage Closer Filtering

Warto wspomnieć, bo PCF to popularny hack. Nasze dwie prace omawiają jak sobie z tym radzić dużo lepiej.

Problemy PCF:

http://developer.amd.com/media/gpu_assets/Isidoro-ShadowMapping.pdf ma na początku kilka wartościowych uwag o implementacji PCF na nowych shaderach.

Proste przykładowe implementacje w GLSL: https://vrmlengine.svn.sourceforge.net/svnroot/vrmlengine/trunk/kambi_vrml_game_engine/src/vrml/opengl/glsl/shadow_map_common.fs

Deep Shadow Maps

http://graphics.stanford.edu/papers/deepshadows/


Motywacja: niepokonany problem dla pcf to że mamy zapisane w teksturze głębokości. A co gdyby zapisywać coś więcej, tak żeby zasłanianie nie było 0-1-kowe? Moglibyśmy wtedy generować dobre shadow mapy dla:


Pomysł: niech każdy pixel shadow mapy pamięta funkcję f(z), która maleje od 1 (w f(0)) do czegoś pomiędzy 0 a 1 (w f(+nieskończoność)). f(z) = jak duży cień jest rzucany na punkt o głębokości z.

Notka: relation alpha do naszej funkcji.


Sampling, czyli tworzenie shadow mapy:

1. Dla każdego pixela, puść kilka (np. 2x2, 4x4 wydają się sensowne) promieni.

1.1. Dla 1 promienia, zbadaj przecięcia z normalnymi (płaskimi powierzchniami). Powstaje funkcja schodkowa. Funkcja spada do 0 jeżeli jest na końcu obiekt opaque, lub zostaje na czymś > 0 jeżeli są co najwyżej obiekty półprzezroczyste po drodze.

1.2. Dla 1 promienia, sampluj (próbkuj) też obiekty wolumetryczne. W teorii obiekty wolumetryczne dają gładki spadek jasności (cień), w praktyce próbkujemy kilka(-naście, -dziesiąt) miejsc po drodze czyli mamy gęstą łamaną.

1.3. Dla 1 promienia, połącz dwie łamane (mnożąć głębokości w punktach).

1.4. Dla 1 pixela, połącz funkcje jego promieni uśredniając je (no, de facto uśredniając z wagami, czyli właściwie filtrujemy). Dopiero w tym momencie uwzględniamy drobne obiekty opaque (włosy etc.).

2. Kompresja. Kluczowy element algorytmu! Bez niego mielibyśmy okrutnie długie funkcje, i uśrednianie ich byłoby długie.

Prosty acz skuteczny algorytm zachłannej kompresji. Rygorystyczna miara błędu, która jednak daje bardzo prosty algorytm dobrej kompresji --- cool.

3. Lookup: patrzymy na pixel shadow mapy, odczytujemy jasność... koniec. De facto: możemy w tym miejscu brać kilka punktów shadow mapy, i odpowiednio filtrować ich jasności.


Interesujące szczegóły

(Tylko niektóre, i to skrótowo, bo w tym miejscu będę się spieszył żeby zrobić przerwę i przejść do drugiej części o VSM :)

Mipmapping: a jakże, można: po prostu uśredniaj (sumuj z wagami) 4 funkcje do jednej, i kompresuj od nowa.

Motion blur: pomysł o tyle ciekawy że w sumie niezależny od deep shadow maps: jeżeli shadow caster się rusza, to apply random move in time dla każdego promienia. (Dla shadow map w OpenGLu, możnaby np. zrobić transformację shaderem która nieznacznie go przesuwa wzdłuż wektora ruchu. To zakłada że mamy multi-sampling i nasze próbki mozna uśredniać --- np. możnaby to wepchnąć w VSM, o ile dobrze myślę.)

Variance Shadow Maps

http://www.punkuser.net/vsm/


Idea: zapiszmy w pixelu shadow mapy coś co można sensownie interpolować, czyli będziemy mieli miękkie cienie (bez pomocy pcf) automatycznie dzięki filtrowaniu w hardware (bilinear, mipmapy, anisotropic.. wszystko co daje hardware).

Czyli w sumie zaczynamy podobnie jak deep shadow maps. Ale będziemy zapisywać coś innego, co może być łatwo zapisane w teksturze i co GPU umie interpolować (czytaj: coś czego uśrednianie ma sens).

Konkretnie: zapiszemy 2 floaty w każdym pixelu (szczegóły "wpychania" w teksturę później).


Pomysł: na każdym pixelu, popatrz jakie głębokości widać.

To samo powiedziane inaczej: dla każdego pixela, mamy zmienną losową X, która generuje głębokości które widać na tym pixelu. Czyli mamy jej rozkład prawdopodobieństwa: określa jakie jest prawdopodobieństwo że na głębokości X coś jest. (Formalnie, rozkład dla zmiennej z continuum podaje prawdopodobieństwo że wynik wpadnie w odpowiedni przedział.)

Narysuj śmieci + przykładowy rozkład, to jest proste :)


Bawimy się w nazywanie tego tak ładnie (zmienna losowa, rozkład), żeby teraz użyć pewnych znanych faktów:

Znając obiekty na scenie widoczne przez dany pixel, możemy zapisać E[X], czyli wartość oczekiwaną, czyli średnią głębokość.

Jak ją znajdujemy OpenGLem? Na początek, na 0 poziomie tekstury po prostu to jest *ta* głębokość. A potem? Filtrowanie w GPU robi za nas całe uśrednianie, na najróżniejsze sposoby! Czyli filtrowanie w GPU samo liczy za nas wartość oczekiwaną.

Możemy rozważać także E(x^2). Czyli średnia z kwadratów głębokości.

Wiemy że wariancja = E[X^2] - (E[X])^2. Ok, czyli znając powyższe dwie liczby znamy też wariancję.


MAGIA: nierówność Czebyszewa, one-tailed version:
Znając wariancję i średnią, mamy wzorek
dla t > średniej, P(x >= t) <= cośtam co zależy od wariancji i wart.oczek.

Na chwilę zapomnijmy o tym jak to się ma do naszego zadania, i zauważmy że zupełnie intuicyjnie wzorek ma sens: jeżeli znamy tylko wariancję i wart.oczek., to wyobrażamy sobie rozkład praw. jako górkę ze środkiem w średniej, i rzeczywiście:

Wracamy do naszego problemu: niech

  jasność(punktu na głębokości t) =
    if t < średniej then
      return 1 (nie w cieniu) else
      return P(X >= t)

Przy czym oblicz P(X >= t) ze wzorku powyżej, zakładając że to jest równość! Zaraz zobaczymy czemu.

Narysuj wykres jasności --- płasko = 1 dla małych głębokości, po prawej od średniej opadająca górka. Wygląda sensownie, jak funkcja głębokość => jasność.

Czemu P(X >= t) ma sens? P(X >= t) = 1 - P(X < t) = 1 - szansa że coś zasłania obiekt na głębokości t = 1 - cień = jasność. Czyli cool, dokładnie to co chcemy.

Dlaczego założenie że to jest równość ma sens? Przerachuj dla dwóch głębokości, jaki jest cień na dalszym obiekcie? Dokładnie taki jak trzeba! A przypadek dokładnie 2 głębokości na jednym pixelu jest typowy.


Implementacja:

1. Zapis do tekstury:

Najprościej: Renderuj do tekstury, zapisz (depth, depth^2) dla tego pixela w buforze. Czyli poziom 0 tekstury de facto ma tylko 1 próbkę... Ale i tak wszystko będzie działać.

Opcjonalnie: włącz multi-sampling, żeby nawet poziom 0 był przefiltrowany.

Opcjonalnie: przetwórz shadow mapę shaderem (renderuj do FBO (texture) naszą tex z shaderem wygładzającym), choćby prostym |1 2 1|2 4 2|1 2 1|.

Zrób mipmapy (glGenerateMipmap)! Opcjonalne, ale trywialne, między innymi po to jedliśmy tą żabę.

2. Odczyt z tekstury i porównanie:

Bajecznie proste, w/g nierówności Czebyszewa, patrz funkcja jasność(t) powyżej.

Show example shader on page 16 of presentation.


Jaki rodzaj tekstury?

Będziemy wymagać stosunkowo dobrej precyzji tych floatów

Pages 25-29 of presentation.

Podsumowanie: najlepszy jest format float-32 jeżeli są filtrowalne na GPU, else float-16.


Problem: Marnujemy 2 komponenty tekstury

Solution: koduj 1 depth na 2 komponentach. Koduj tak żeby interpolacja liniowa ciągle miała sens! Np. 1 komponent = wynik całkowity z dzielenia przez 64, 2 komponent = reszta z tego dzielenia.

Page 32 z presentation.
Pomaga dla fp 16, dla 32 niepotrzebne w/g ich rezultatów.


Problem: Numerical stability

Wzór na wariancję powoduje odejmowanie dwóch dużych liczb -> unstable. Zwłaszcza na fp 16.

Potem używamy tego w mianowniku, i przy małym t-średnia (czyli wtedy kiedy na shadow receiver nie ma cienia, ale shadow receiver jest ujęty w shadow mapie) mamy problem.

Solution: variance := max(variance, epsilon).
Epsilon generalnie może być stałe, byleby coś małego, w/g pracy. Więc to nie jest koszmar, tzn. w praktyce nie trzeba dawać tego do kontroli autorom.


Problem: light bleeding

Rachunkowo, pokazaliśmy że mamy równość kiedy każdy shadow receiver też jest shadow casterem, i tylko 2 obiekty prostopadłe do promieni światła wpływają na daną informację (z, z^2).

Problem: Czasami jednak widać że liczymy górne ograniczenie na P(X >= t), tzn. coś jaśniejszego niż należy. Przy dużej wariancji, kiedy mamy > 2 obiekty wpływające na daną próbkę shadow mapy (co może się zdarzyć łatwo na mipmapach), możemy mieć coś jaśniejszego.

Solution 1: Layered Variance Shadow Maps. Trochę kombinujemy z głębokością, żeby uzyskać lepsze przybliżenie. Zapisujemy też odpowiednio więcej do tekstur.

See http://www.punkuser.net/lvsm/lvsm_web.pdf

Solution 2: prostsze i brutalne: przeskaluj obliczone P(X >= t), tak żeby wartości < epsilon były równe 0. Przeskaluj też zakres (epsilon, 1) -> (0, 1) żeby używać pełnego zakresu. W rezultacie sprawiamy że całe cienie są ciemniejsze, ale pozbywamy się problemu.

A epsilon może być konfigurowalny per shadow-receiver, czyli można go używać tylko dla obiektów na których widać artefakty.

See http://http.developer.nvidia.com/GPUGems3/gpugems3_ch08.html, "8.4.3 Light Bleeding"


Problem: shadow receiver musi być shadow casterem

Kiedy shadow receiver nie rzuca cienia, mamy uśrednianie z max głębokością (na którą wyczyściliśmy z buffer). Czyli średnie z są szybko ściągane w stronę +nieskończoności, i szybko rzeczy przestają być w cieniu. See torus + plane screenshots from http://developer.download.nvidia.com/presentations/2008/GDC/GDC08_SoftShadowMapping.pdf

Solution: brak?, tzn. po prostu don't do this. Niech zawsze shadow receiver będzie też shadow casterem.

Others

Podobne podejścia które zapisują funkcję głębokości w shadow mapach w inny sposób:
Convolution Shadow Maps
Exponential Shadow Maps

Świetny paper z overview metod shadow map: http://developer.download.nvidia.com/presentations/2008/GDC/GDC08_SoftShadowMapping.pdf

I jeszcze drugi: http://developer.amd.com/media/gpu_assets/Isidoro-ShadowMapping.pdf

MichalisWiki: II/SemGrafDeepVarianceShadowMaps (last edited 2010-07-03 21:52:33 by MichalisKambi)