Programowanie Gier -> Wykład: Cienie

1. Wstęp

Na początek, word of caution: techniki którymi będziemy bawić się na tym wykładzie pozwalają na dynamiczne cienie — co znaczy że shadow caster i/lub shadow receiver i/lub light source poruszają się w czasie gry w nieprzewidywalny sposób. Bardzo często w praktycznych zastosowaniach można zrobić cienie dużo łatwiej — bardzo często np. oświetlenie i najbardziej zauważalna część shadow casterów są statyczne, i można dużo informacji przeliczyć przed rozpoczęciem gry. Albo light source porusza się wzdłuż ustalonej (krótkiej) trasy, i wtedy interpolowanie pomiędzy kilkoma przeliczonymi rozwiązaniami jest Ok. W skrócie, kiedy cienie nie muszą być dynamiczne, mamy nieco prostych i bardzo mało kosztowych technik:

  • Najstarsze, "baking" cieni na teksturach. W sumie to samo co light maps. (Multitexturing tutaj znącząco pomaga, pozwalając mieć inne mapowanie tekstury z cieniami/rozbłyskami od innych tekstur.) Można zrobić w blenderze/3ds, nie ma tutaj nic do roboty dla programistów.

  • W pewien sposób związane jest z tym Volume LightMapping ("baking" w teksturze 3d oświetlenia sceny), chociaż ono już pozwala na dynamicznych shadow receiverów. Pokażemy sobie skrótowo jak to działa na wykładzie o shaderach.

Generalnie, często sama "sugestia" cieni daje całkiem Ok efekty — wiele gier używa prostych "ciemnych plam" pod graczem (WoW chociażby), i zupełnie nie przejmuje się trudnymi technikami z tego wykładu.

2. Shadow maps

Linki:

Ponieważ jest tyle dobrych opracowań, zdecydowałem że nie będę się sam strasznie rozpisywał, i pierwsza część wykładu będzie w/g slajdów Kilgarda powyżej.

Bardzo krótko plan shadow maps:

  1. Wyrenderuj scenę z punktu widzenia źródła światła. Np. używając framebuffera z poprzedniego wykładu. Zrzuć wyrenderowaną zawartość depth buffera do tekstury.

    Pamiętaj o glPolygonOffset dla tekstury (nie tylko ze względu na precyzję floatów, ale także na to że pixele ekranu będą ją próbkować trochę inaczej, see rysunek).

    Potrzebujemy do tego tekstur które mają precyzję jak z-bufor: ARB_depth_texture.

  2. Renderując scenę z normalnej kamery, nałoż na nią shadow mapę. Trzeba ją zmapować w specjalny sposób, tak żeby dla punktu 3D w scenie nałożyć texel shadow mapy pod którym widoczny byłby ten punkt.

    Projective texturing: odrobina zabawy z macierzami i wiemy czego chcemy. Umiemy też "wepchnąć" to w glTexGen(.., GL_EYE_SPACE, ...) żeby OpenGL sam wszystko za nas zrobił.

    Projective texturing jest fajne także bez shadow map! Dobre do rzucania obrazu z projektora etc.

  3. Pozostaje faktyczny test. Mamy w każdym punkcie współrzędne tekstury (x, y, z), które są współrzędnymi w clip space światła. ("clip space" — przestrzeń 3d w której lądują punkty po modelview i projection, ale przed viewport. Widoczne elementy są w cube o zakresach -1..1.) (x, y) możemy użyć żeby dobrać się do texela shadow mapy. Texel zawiera głębokość w tym punkcie — a więc porównaj z aktualną głębokością. Czyli

    jestew w cieniu <=> z >= texture2D(x, y)

    (De facto mamy projective texturing, więc mamy homogeneous coordinates, więc mamy x/w, y/w, z/w).

    ARB_shadow pozwala nam nakazać teksturze na takie porównania.

    Co zrobić z 0-1 wynikiem takiego porównania?

    • nic? tekstura modulate będzie pomnożona przez kolor. Rezultat: cień będzie totalnie, absolutnie czarny — nierealistyczny widok.
    • można kombinować z multi-texturowaniem, np. pomnożyć go potem przez kolor i normalną teksturę.
    • można zrobić multi-pass, czyli wyrenderować scenę 2 razy z kamery, raz bez światła (tylko z odpowiednim ambient + słabymi światłami), potem dodać wersję ze światłem (tylko tam gdzie nie ma cienia). Można wspomóc się blending, można wspomóc się alpha test, naprawdę wiele możliwości.
    • wreszcie, mając shadery, można wykonać test tekstury na shaderze. I mamy wtedy dużo prościej i bardziej elastycznie niż przy kombinowaniu z multi-texture env_combine, i tylko 1 pass z kamery. Demo na wykładzie o shaderach.

Demo :it works, for both SpotLight and DirectionalLight. (For PointLight, a texture like for environment mapping will be needed, maybe 6 for cube, or maybe paraboloid. No demo, but this is all doable without problems.)

Demo: filtering LINEAR vs NEAREST ulepsza shadow mapę, ale ciągle cień jest twardy — aliasing. Wszystko dlatego że uśredniamy wartości depth, ale ciągle wynik testu jest 0/1-kowy. Stąd pomysł percentage closer filtering. Jak użyć PCF?

  • NVidia robi PCF pomiędzy 4 próbkami na hardware za darmo (specjalne tranzystory czy coś w tym stylu)
  • ATI ma coś takiego jak fetch4, szczerze mówiąc nie wiem jak tego użyć w OpenGL :(
  • można zrobić samemu w shaderze próbkowanie, żeby mieć przenośną technikę o najlepszej jakości. Bardzo prosty przykład. Demo pcf 4 i 16.

Istnieje mnóstwo rozszerzeń shadow maps, na które zapewne nie będziemy mieli czasu. Np. perspective shadow maps. Strona na wikipedii na dole ma trochę linków, także GPU Gems:

  http://http.developer.nvidia.com/GPUGems/gpugems_ch11.html
  http://http.developer.nvidia.com/GPUGems/gpugems_ch12.html
  http://http.developer.nvidia.com/GPUGems/gpugems_ch14.html
  http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter17.html
  http://http.developer.nvidia.com/GPUGems3/gpugems3_ch08.html
  http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html

Zalety/wady shadow map:
+ Wsparcie hardware jest od bardzo dawna.
+ Pracuje z dowolną geometrią którą GPU renderuje. Więc działa np. z teksturami alpha-test (druciana siatka ogrodzenia), także z geometią konstruowaną/transformowaną na shaderach.
+ Szybkie — render to texture, glTexGen, test głębokości, to wszystko jest bardzo tanie (no, render to texture kosztuje, ale FBO znacznie nam pomagają; poza tym nie trzeba uaktualniać zawsze, tylko kiedy shadow caster się zmienia).
- Używa tekstury, zawsze problemy z aliasing. Trzeba tweakować size, near, far projection żeby było Ok.
- glPolygonOffset po drodze, kolejna rzecz do tweakowania (chociaż można poradzić sobie bez niego, ale to już większa kombinacja).
+ Rozszerzenia pozwalają na (mniej lub bardziej oszukańcze) soft shadows poprzez inteligentne rozmywanie/próbkowanie tekstury.
- Inne drobnostki: back-projection jest denerwujące, niekiedy obcinanie przez near (bo far można uniknąć) też.
+ Self shadowing jest Ok o ile offset jest Ok.
- Texture leaking: cień "wycieka" spod elementów (bias pomaga, ale zazwyczaj tylko z jednej strony).

3. Shadow volumes

Links:

Podstawowy pomysł: stencil buffor może robić inne rzeczy w zależności od tego czy depth test przejdzie czy nie. Konstruując shadow volume i rasteryzując je z odpowiednimi testami na stencil możemy badać czy coś jest w cieniu. Demo: wizualizuj shadow volumes

Problem z z-pass: near plane obcina. Nie możemy pozbyć się near plane. Więc: z-fail: licz to samo, ale od tyłu zamiast od kamery.

Problem z z-fail: far plane obcina... Ale na szczęście możemy zrobić projection matrix z far plane w infinity. Nie, nie tracimy zbyt dużo precyzji, jest Ok. Dla orthographic można sobie poradzić NV_depth_clamp.

Ale kolejny problem z z-fail: wymaga light/dark caps. Więc jest trochę wolniejszy. Więc optymalizuj:

  • jeżeli obiekt nie wymaga z-fail, to rób z-pass (jeżeli jego bbox jest cały przed near plane).
  • jeżeli wymaga z-fail ale nie jest we frustum, to light cap nie jest potrzebne (dark cap nie jest tak kosztowne, a dla directional lights dark cap nie istnieje)

Ponieważ z-pass i z-fail obliczają to samo, więc możemy je swobodnie przeplatać, tzn. niektóre obiektów robić przez z-pass, niektóre z-fail. Rozpoznawanie czy wymaga z-fail/z-pass (cytując komentarze z własnego kodu :) ):

  For positional light:
  Calculate a pyramid between light position and near plane rectangle
  of the frustum. Assuming light point is positional and it does not
  lie on the near plane, this is simple: such pyramid has 4 side planes
  (created by two succeding near plane rectangle points and light pos),
  and 1 additional plane for near plane.

  Now, if for any such plane, SceneBox is outside, then ZFail is for sure
  not needed.

  For directional light, this is somewhat similar to positional lights,
  except that you have 4 planes (each one from a segment of near rectangle,
  extruded to infinity in both directions).

Demo że wykrywanie z-pass/z-fail działa na shadow_volume_test

Silhouette edge detection: arcy-ważna optymalizacja: extrude tylko silhouette edges. Żeby szybko obliczyć silhouette edge, trzymaj listę sąsiedztwo krawędzi obliczone zawczasu. To sprawia że model musi być 2-manifold. Demo działania, manifold/border edges.

Na koniec: same shadow volume kończą się teoretycznie w infinity. Czy możemy je wyrenderować w infinity? Tak (skoro mamy pespektywę siegającą do infity, to clipping nie jest problemem), w homogeneous coords.

Czy mamy dość stencil bits? Normalne operacje na stencil robią saturate. Wrapping pomagają minimalizować błędy ("tylko raz na 256 warstw cień jest zły"), ponadto pozwalają na rysowanie i front i back w jednym przebiegu, w dowolnej kolejności:

  • glStencilOpSeparate (in OpenGL >= 2.0) żeby obsługiwać back i front faces shadow volumes w 1 przebiegu.
  • EXT_stencil_wrap żeby mieć wrap, nie saturate, na ++ i -- na stencil.

W praktyce, każdy obecny GPU daje min 8 bitów na stencil (i zazwyczaj tylko 8 bitów...). To wystarczy, w patologicznych sytuacjach operacje "wrap" robią "damage control". Demo: wrap na 8 bitach, 512 parallel rects casting shadow

Inne pomysły na optymalizacje:

  • culling: nie można zrobić frustum culling, bo cień widać nawet kiedy nie widać obiektu. Ale można sobie poradzić: kiedy convex hull frustum + light source nie koliduje z obiektem, na pewno nie widać cienia. Convex hull możemy policzyć byle jak, zależy nam tylko na optymalizacji (np. samo zrobienie bounding boxa frustum + light pos już pomaga w pewnych szczególnych przypadkach).
  • Można też kombinować z culling bbox obiektu extruded od światła (http://http.developer.nvidia.com/GPUGems3/gpugems3_ch11.html)
  • można użyć wersji low-poly dla cienia.
  • można robić Silhouette edge detection tylko raz na kilka ramek.

Zalety/wady:
+ Wsparcie hardware jest od bardzo dawna.
+ Przede wszystkim, rozwiązujemy problem na każdym pixelu ekranu, więc unikamy zupełnie problemów shadow map z aliasingiem tekstur.
+ Self shadowing jest Ok.
+ Mniej ograniczeń na światła niż w shadow maps. PointLight jest bez problemu, nie ma też obcinania blisko światła.
- Bardzo trudno uogólnić na soft shadows. Są techniki, ale w praktyce multi-pass, a więc wolne. Praktyka: Shadow mapy (albo inne podejścia) są lepsze jeżeli chcemy oszukańcze soft shadows.
- Szybkość zależy od złożoności geometrii. Nie ma szans na cienie przez tekstury z alpha testem. Mogą być cienie z geometrii tworzonej/transformowanej przez shadery jeżeli same shadow volumes konstruujemy na shaderach, wiele prac o tym pisze.
- Jest narzut na pixel rate — mamy dużo dużych polygonów. Dlatego pomaga depth range i scissor.
- Modele muszą być 2-manifold. Chociaż można sobie radzić (np. http://http.developer.nvidia.com/GPUGems3/gpugems3_ch11.html), jest wtedy duży narzut czasowy.
- Multi-pass: najpierw narysuj bez światła, dodaj 1 światło gdzie nie ma cienia, dodaj 2 ... ogólnie, n+1 passów dla n świateł.

4. Do poczytania dla chętnych: Precalculated Radiance Transfer

Nie było na nie czasu na wykładzie. Kto jest zainteresowany innymi technikami robienia cieni, może poczytać sam o PRT i shadow fields. Nie są to techniki powszechnie używane w grach, mają trochę inne wady (np. obliczają cienie per-vertex więc wymagają mocno tesselated obiektów), ale też i zalety (miękkie cienie, symulując dokładne zachowanie fizyczne). Poniżej są linki do moich streszczeń na seminarium:

radiance_transfer/README

shadow_fields/README

MichalisWiki: II/ProgramowanieGier/WykladCienie (last edited 2009-12-17 21:37:42 by MichalisKambi)