Ładne shadowmapy

28.05.2009 02:04 in devblog, grafika 3D

W końcu udało mi się zaimplementować ładnie wyglądające cienie dla światła słonecznego. Użyłem dwóch technik: Variance Shadow Mapping w połączeniu z warstowymi mapami (zwanymi często Cascade albo Parallel-Split). Efekt jest moim zdaniem znakomity. Kilka uwag implementacyjnych:

vsm1.jpg
  • nasz frustum widzenia musimy podzielić na kilka mniejszych. Ja zdecydowałem się na podział (mniej więcej) logarytmiczny -- przy widoku z pierwszej osoby b. ważne są szczegółowe cienie będące bardzo blisko, więc pierwsza warstwa musi obejmować możliwie niewielki obszar.
  • najprostszym sposobem na wyznaczenie kolejnych macierzy dla cienia jest kolejno:
    • wyznaczenie frustuma w world space
    • obliczenie jego AABB
    • stworzenie macierzy LookAt od pozycji światła do środka boxa
    • przetransformowanie boxa przez powyższą macierz
    • stworzenie macierzy rzutowania obejmującą przetransformowany box
  • używamy dynamic branching aby liczyć zacienienie jedynie dla jednego lub dwóch obszarów (interpolując między nimi)
  • VSM: piękna technika i jak na swoje efekty dosyć tania. Wszystko, co jest nam potrzebne to zapis do tekstury koloru w formacie float2. Zapisujemy liniową głębię (w przypadku światła słonecznego to po prostu współrzędna .z przetransformowanego wierzchołka) oraz jej kwadrat. Następnie teksturę rozmywamy kilka razy (to jest ogromna zaleta VSM, ponieważ zwyczajnych SM nie możemy sobie od tak blurować). Potem obliczanie zacienienia możemy wykonać taką funkcją:
    float VSM(sampler2D texture, float4 coords)
    {
      float dist_to_light = coords.z;
      float2 moments = tex2D(texture, (coords.xy * 0.5 + 0.5)).ra;
      const float vsm_epsilon = 0.00001f;
    	
      float lit_factor = (dist_to_light <= moments.x);
    
      float E_x2 = moments.y;
      float Ex_2 = moments.x * moments.x;
      float variance = min(max(E_x2 - Ex_2, 0) + vsm_epsilon, 1);
      float m_d = (moments.x - dist_to_light);
      float p_max = variance / (variance + m_d * m_d);
    
      return saturate(max(lit_factor, p_max) * 2 - 1);
    }
    
  • cienie uzyskane za pomocą VSM są naprawdę "miękkie" i wyglądają zdecydowanie lepiej niż PCF. W niektórych miejscach wyglądają nawet bardziej na użycie SSAO niż zwykłych map cieni: ;)
    vsm2.jpg
  • niestety, nie ma róży bez kolców -- VSM ma jedną wadę, a mianowicie light bleeding. W dużej mierze eliminuje go przeskalowanie i saturate na zwracanej wartości, lecz pod pewnymi kątami nadal widać artefakty.
  • na najdalszej warstwie cienia nie warto już robić VSM - zwykły PCF 2x2 załatwia sprawę.

Dodatkowo, nauczyłem się, że nie warto polegać na cgGLSetManageTextureParameters. Przy sporym żonglowaniu rendertargetami CG gubiła się i "zapominała" odpinać tekstury, przez co oczywiście nie można było do nich renderować.

Na zakończenie chciałbym podziękować Adamowi Sawickiemu za udostępnienie źródeł swojego silnika. Lektura jego kodu związanego z macierzami cieni była bardzo pouczająca. Bardzo pomocna była też wskazówka udzielona przez Krzysztofa Kluczka na forum Warsztatu -- zawierała ona trick z skalowaniem wyniku zacieniowania.

namespace enum

11.05.2009 18:43 in c++, devblog

Ta sztuczka pewnie jest znana większości z Was, ale myślę, że i tak warto o niej wspomnieć:

namespace LightTypes
{
  enum ELightTypes
  {
    Point,
    Spot,
    Sun
  };
};
typedef LightTypes::ELightTypes LightType;

Co dzięki takiej definicji enuma zyskujemy?

  • poszczególne wyliczenia nie wchodzą w konflikty i nie zaśmiecają globalnej przestrzeni nazw
  • możemy liczyć na podpowiedzi ze strony naszego IDE, gdy napiszemy LightTypes::
  • jesteśmy zgodni ze standardem C++ (MSVC++ dopuszcza na przykład enum w klasie, do którego odwołujemy się NazwaKlasy::NazwaEnuma, ale jednocześnie dostajemy "brzydki" warning o użyciu rozszerzenia C++)

Trzeba tylko pamiętać, że chociaż typem zmiennej będzie LightType, to jej wartością może być np. LightTypes::Spot (nie mylić z LightType::Spot). Dlaczego? Patrz trzeci punkt powyżej.

Jako ciekawostka, w C++0x ma pojawić się twór określany mianem enum class, rozwiązujący problem z zakresami ważności.

Mutujące zmienne

10.05.2009 15:27 in c++, devblog

Słowo kluczowe C++ na niedzielę: mutable. Działanie:

struct A
{
  mutable int a;
  void f(int v) { a = v; }
  int g() const { int _a = a; a += 2; return _a; }
};

Jaki jest pożytek z mutable? Możemy wywoływać funkcje const na obiekcie klasy i zmieniać jej wewnętrzne dane. Chory pomysł? Nie, jeżeli nasze zmienne mają charakter pomocniczy (np. służą do debugowania/profilowania) albo po prostu korzystamy z czyjegoś kodu.

W PhysX na przykład jest sobie klasa NxUserStream, która udostępnia operacje odczytu/zapisu do strumienia. Operacje odczytu są właśnie const. Implementacja z SDK korzysta z pliku (FILE*) i operacje odczytu są realizowane za pomocą fread. Oczywiście stan klasy się nie zmienia - ona posiada jedynie wskaźnik do uchwytu. Jednak chcąc zrealizować strumień korzystający z danych w pamięci potrzebne nam będzie na przykład pole określające bieżącą pozycję w strumieniu. I o tym, że jego zawartość może się zmieniać, projektanci PhysX już nie pomyśleli - ale na szczęście standard C++ tak. :)