Seminarium z Grafiki - Deep Shadow Maps i Variance Shadow Maps
PomocSpisTreści
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.
- PCF zwykle,
- PCF bilinear: użyj tych samych wag i punktów co do normalnego bilinear filtering. Czyli 2x2 punkty, wybrane odpowiednio; można łączyć ze "zwykłym" naturalnie albo robić większe filtry obejmujące więcej niż 4 punkty.
- irregular (strzelaj losowo próbkami naokoło).
Problemy PCF:
- to wszystko zaczyna być wolne dla wielu próbek... Ultimately, chcielibyśmy moc jakoś precalculate takie rzeczy. Tak jak normalne tekstury mają mipmapy. Pomijając już fakt że normalne tekstury maja to wszystko (bilinear, mipmapping, anisotropic filtering) wmontowane w hardware. Shadery może są szybkie ale nie bez powodu GL 3.x etc. pozostawia filtrowanie tekstur wmontowane w hardware zamiast przerzucać to na shadery (tzn., filtrowanie tekstur jest ciągle szybsze, pomijając już "wygodniejsze dla programisty", niż reimplementowanie tego w shaderach.)
- Pojawiają się problemy z biasem dla PCF. Porównujemy jedną głębokość (naszego punktu) z próbkami głębokości naokoło --- czyli bias/scale powinny być większe żeby nie rzucać cienia na samego siebie.
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:
- obiektów tak drobnych że normalnie zmuszałyby nas do bardzo dużej rozdzielczości (np. włosy)
- obiekty półprzezroczyste płaskie (szyby etc.)
obiekty półprzezroczyste 3D (volumetric, tłumaczę (być może źle ) jako wolumetryczne) --- (dym, gaz etc.), generalnie wyrażane jako funkcja 3D -> gęstość (ograniczone do jakiejś zamkniętej bryły 3D).
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
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:
- całość powinna być odwr. proporc. do odległości t od średniej
- całość powinna być wprost proporc. do wariancji
dla t bliskiego średniej prawdopodobieństwo zbliża się do 1, bo wiemy że t > średniej
- Właściwie, na intuicję z górką, powinno zbliżać się do 1/2, ale w praktyce funkcja nie musi być górką, więc ograniczenie jest słabsze. To akurat dobrze dla nas, bo będzie ciągła z płaską funkcją = 1 (za chwilę zobaczymy dlaczego to jest istotne).
reszta pewnie jest łatwa :), trzeba tylko przerachować, patrz wikipedia etc.
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
- tekstury 8bitów/kanał raczej odpadają,
- tekstury depth nie mają interpolacji ani fsaa w praktyce,
- tekstury na 16-bitowych floatach są Ok.
- Tekstury na 32-bitowych floatacj byłyby cool, ale autorzy w 2006 piszą że GPU nie umieją ich interpolować (chociaż można zaimpl chociaż bilinear, i/lub obliczanie mipmap, samemu na shaderach) ani robić fsaa. W innej pracy z 2008 sugerują że już można, czyli fp 32 są optymalne.
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