Mapy cienia
Transkrypt
Mapy cienia
Mapy światła i cienia. W tym zadaniu dany jest projekt, renderujący scenę przedstawiającą pokój z bujającą się lampą zawieszoną pod sufitem. Pierwszym zadaniem będzie rzutowanie perspektywiczne tekstury światła (białe koło z rozmytym brzegiem na czarnym tle) na całą scenę, zgodnie z aktualnym położeniem lampy. Drugim zadaniem będzie dodanie cieni przy pomocy map cienia. Wykorzystanie mapy cienia w scenie przebiega analogicznie jak wykorzystanie mapy światła. Dlatego druga część sprowadza się głównie do odpowiedniego zainicjowania parametrów tekstury oraz wyrenderowania jej zawartości (scena widziana z pozycji lampy) i skopiowaniu do obiektu mapy cienia. Bez map światła i cienia Z mapami światła i cienia Do uzupełnienia są następujące funkcje w pliku lightmap.cpp: część 1: enable_texgen(), disable_texgen(), render_begin(), render_end() część 2 (dodatkowo): shadow_init(), shadow_begin(), shadow_end(), render_begin(), render_end() W funkcji display() (której nie należy modyfikować) w pliku main.cpp wywoływane są w odpowiedniej kolejności funkcje związane z renderowaniem. Mimo, że jest ona dość nieczytelna trzeba wspomnieć, że realizacja cieni odbywa się w trzech przebiegach: A) Renderowana jest cała scena z pozycji światła (mapa cienia) B) Renderowana jest cała scena z ustawionym przyciemnionym oświetleniem C) Renderowana jest cała scena z użyciem mapy światła i mapy cienia oraz jasnym oświetleniem. Włączone jest mieszanie z funkcją maximum. Mapy cienia zwracają kolor czarny dla obiektów w cieniu. Powyższy zabieg sprawia, że obiekty w cieniu nie są zupełnie czarne tylko przyciemnione. 1. Generowanie współrzędnych tekstury (funkcje enable_texgen() oraz disable_texgen()) Perspektywiczne rzutowanie tekstur bardzo przypomina w realizacji rzutowanie wierzchołków na ekran. Mamy daną macierz przekształcenia do układu projektora (w tym wypadku lampy) i macierz rzutowania perspektywicznego. Przekształcamy wierzchołki sceny przez obie macierze i wykonujemy dzielenie perspektywiczne. W przypadku tekstur musimy jeszcze przesunąć i przeskalować wynik, ponieważ zakres współrzędnych tekstury jest inny niż zakres współrzędnych okna widoku (dla okna widoku każda ze współrzędnych, włącznie z głębokością, jest z zakresu [-1,1]; dla tekstury każda ze współrzędnych, włącznie z głębokością w przypadku map cienia, jest z zakresu [0,1]). Przy rzutowaniu perspektywicznym potrzebne są cztery współrzędne tekstury: S, T, R i Q. Dzielenie perspektywiczne zostanie przeprowadzone automatycznie, jeśli tylko dana będzie wartość czwartej współrzędnej. Na początek włączymy generowanie współrzędnych przez OpenGL tak, aby za współrzędne tekstury przyjmowane były współrzędne wierzchołków w układzie sceny (świata). Realizuje to poniższy ciąg instrukcji: matrix44 identity = IdentityMatrix44(); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGenfv(GL_S, GL_EYE_PLANE, (float*)&(identity[0].x)); glTexGenfv(GL_T, GL_EYE_PLANE, (float*)&(identity[1].x)); glTexGenfv(GL_R, GL_EYE_PLANE, (float*)&(identity[2].x)); glTexGenfv(GL_Q, GL_EYE_PLANE, (float*)&(identity[3].x)); glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T); glEnable(GL_TEXTURE_GEN_R); glEnable(GL_TEXTURE_GEN_Q); Komentarz do powyższego kodu Mimo, że tryb generowania współrzędnych tekstury jest ustawiony na GL_EYE_LINEAR, co może sugerować układ kamery, otrzymane współrzędne będą wyrażały położenie wierzchołków w układzie sceny (świata). Dzieje się tak, ponieważ równania GL_EYE_PLANE (macierz identyczności w tym przypadku) są mnożone z prawej strony przez odwrotność macierzy GL_MODELVIEW ustawionej w trakcie wywoływania powyższych instrukcji. Rezultatem będzie przejście z układu kamery do układu sceny. Zamiast GL_EYE_LINEAR, można byłoby wybrać tryb GL_OBJECT_LINEAR; wtedy otrzymalibyśmy współrzędne w układzie lokalnym obiektu, ale dla każdego obiektu inna byłaby macierz rzutowania do układu lampy. Trzeba jeszcze ustalić przekształcenie dla współrzędnych tekstury z układu sceny do układu lampy: matrix44 matTexture = ScaleMatrix44(0.5f, 0.5f, 0.5f) * TranslateMatrix44(1.0f,1.0f,1.0f) * matProjection * matModelView; glMatrixMode(GL_TEXTURE); glLoadMatrixf((float*)(&matTexture[0][0])); glMatrixMode(GL_MODELVIEW); Macierze matModelView oraz matProjection są zmiennymi statycznymi w przestrzeni nazw LightMap w pliku lightmap.cpp. Ustalane są poprawnie w innej części programu i przechowują macierze przekształcenia z układu sceny do układu lampy i rzutowania perspektywicznego związanego z lampą. W programie kąt pola widzenia zakodowany w macierzy rzutowania perspektywicznego można zmieniać poruszając myszą z wciśniętym środkowym przyciskiem. Iloczyn ScaleMatrix44(0.5f, 0.5f, 0.5f) * 3 TranslateMatrix44(1.0f,1.0f,1.0f) odpowiada za liniowe odwzorowanie kostki [ − 1,1] w kostkę [ 0,1] 3 , i jest niezbędny dla poprawnego przejścia do współrzędnych tekstury. Cały kod opisany powyżej powinien znaleźć się w funkcji enable_texgen(). Umieszczenie go w osobnej funkcji jest o tyle ważne, że będziemy ten sam kod wywoływać zarówno dla mapy światła jak i mapy cienia. Trzeba jeszcze wypełnić funkcję disable_texgen(). Jej zadanie do przywrócić stan sprzed enable_texgen(): wyłączyć generowanie współrzędnych tekstury dla S, T, R i Q; przywrócić poprzednią wartość macierzy przekształcenia współrzędnych tekstury (można przyjąć, że domyślnie jest to macierz identyczności); zostawić tryb macierzy OpenGL ustawiony na GL_MODELVIEW (co jest w pozostałym kodzie przyjmowane za domyślne ustawienie podczas rozmieszczania obiektów w scenie). 2. Włączenie teksturowania (funkcje render_begin() i render_end()) Zaczynamy od samej tekstury oświetlenia, ale potem dodamy do niej teksturę cienia, dlatego warto od razu przygotować się na multiteksturowanie. Obiekty tekstur są już utworzone – ich identyfikatory pamiętane są w zmiennych statycznych tex_light (mapa światła) i tex_shadow (mapa cienia) w przestrzeni nazw LightMap w pliku lightmap.cpp. Dla każdej z tekstur w funkcji render_begin() trzeba wykonać kolejno: • Ustalić aktywny etap multiteksturowania (którego dotyczyć będą wszystkie kolejne instrukcje związane z teksturowaniem): glActiveTexture(GL_TEXTURE0); (glActiveTexture(GL_TEXTURE1); dla mapy cienia). • Ustalić aktywny obiekt tekstury: glBindTexture(GL_TEXTURE_2D, tex_light); (glBindTexture(GL_TEXTURE_2D, tex_shadow); dla mapy cienia) • Włączyć teksturowanie: glEnable(GL_TEXTURE_2D); • Włączyć generowanie współrzędnych tekstury: enable_texgen(); • Ustalić operację mieszania koloru tekstury: glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); funkcji render_end() trzeba cofnąć zmiany dla każdej z tekstur: ustalić aktywny W etap teksturowania, wyłączyć generowanie współrzędnych tekstury, wyłączyć teksturowanie. 3. Projekcja wsteczna (poprawka do funkcji render_begin() i render_end()) Istnieje pewna różnica pomiędzy rzutowaniem geometrii na ekran i rzutowaniem geometrii w celu wygenerowania współrzędnych tekstury. W pierwszym przypadku aktywnych jest sześć płaszczyzn obcinania geometrii. W drugim przypadku nie. Dlatego mapa światła i cienia na powyższym obrazie jest rzutowana zarówno na geometrię poniżej jak i powyżej lampy (jaśniejsze koło na suficie; w środku widać plamki cieni). Obiekty pod lampą, które nie mieszczą się w ściętej piramidzie jej widoku, przy ustawionym zawijaniu rzutowanych tekstur do brzegu, otrzymują z tekstury sensowne wartości kolorów/głębokości. Z obiektami po drugiej stronie lampy trzeba radzić sobie inaczej. Rozwiązaniem jest włączenie na czas rzutowania perspektywicznego tekstur dodatkowej płaszczyzny obcinania geometrii. Płaszczyzna zdefiniowana jest przez czwórkę wartości rzeczywistych ( x p , y p , z p , w p ) , gdzie ( x p , y p , z p ) to wektor prostopadły do płaszczyzny, skierowany w stronę półprzestrzeni, która nie zostanie odrzucona, zaś w p jest tak dobrane, że dla dowolnego punktu ( x, y, z ) na płaszczyźnie zachodzi x p x + y p y + z p z + w p = 0 . W programie równanie płaszczyzny zapisane jest w zmiennej statycznej clipplane_light w przestrzeni nazw LightMap w pliku lightmap.cpp. Ustalane jest poprawnie w innej części programu. Aby włączyć płaszczyznę obcinania trzeba wkleić kod: double cp[4] = {clipplane_light.x, clipplane_light.y, clipplane_light.z, clipplane_light.w}; glClipPlane(GL_CLIP_PLANE0, cp); glEnable(GL_CLIP_PLANE0); Wyłączenie obcinania realizuje kod: glDisable(GL_CLIP_PLANE0); 4. Inicjowanie mapy cienia (funkcja shadow_init()) Mapy cienia są specjalnym rozszerzeniem OpenGL. Są to tekstury, które zachowują się jak bufor głębokości. Inicjując obiekt tekstury dla mapy cienia musimy: • zdefiniować wymiary i ustalić odpowiedni format tekstury, który mówi, że będzie to mapa cienia glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, size_shadow, size_shadow, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, 0); W powyższej instrukcji: 1. parametr, to stała, która mówi, że mamy do czynienia z teksturą dwuwymiarową (w najnowszych kartach graficznych istnieje rozszerzenie pozwalające korzystać również z map sześciennych cienia). 2. parametr to poziom mip i powinien być ustawiony na zero. 3. parametr to format mapy cienia: 24 bitowy bufor głębokości, (często jest wymagane, aby format był taki sam jak dla głównego bufora głębokości). 4. i 5. parametr to wymiary mapy cienia: stała size_shadow jest zdefiniowana w pliku lightmap.cpp (implementacje OpenGL dla dobrych kart graficznych nie wymagają aby były to potęgi dwójki, dla starszych kart może to być wymagane). Z powodu faktu, że będziemy kopiować bufor głębokości związany z oknem widoku do mapy cienia, wymiary nie mogą być większe niż wymiary okna widoku (domyślnie 1024x1024). 6. parametr to szerokość brzegu i powinien być równy zero. 7. i 8. parametr związane są z formatem i dla 24-bitowego bufora głębokości powinny być równe: GL_DEPTH_COMPONENT i GL_UNSIGNED_INT. 9. parametr równy zero mówi, że nie definiujemy na razie zawartości mapy cienia. • ustalić w jaki sposób będzie liczony kolor uzyskany w wyniku próbkowania mapy cienia glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE); Pierwsza z tych instrukcji mówi, że próbkowanie mapy cienia będzie polegało na porównaniu wartości trzeciej współrzędnej tekstury (współrzędne tekstury oznaczane są S, T, R i Q), z głębokością zapisaną w mapie cienia. Druga instrukcja mówi, że test zwróci jeden, gdy współrzędna będzie mniejsza lub równa odczytanej głębokości i zero w przeciwnym razie. Trzecia instrukcja mówi, że wynik testu zostanie zapisany we wszystkich czterech składowych koloru zwróconego w wyniku próbkowania tekstury. Komentarz na temat filtrowania map cienia Map cienia nie można filtrować w taki sam sposób jak zwykłych tekstur. Oznaczałoby to interpolację liniową zapisanej głębokości, co prowadzi do nieprawidłowych wyników i dlatego nie jest realizowane w żadnej implementacji OpenGL. Jednym z rozwiązań, wspomaganym często sprzętowo, jest filtrowanie PCF (Percentage Closer Filtering), które polega na wykonaniu testu bufora głębokości w kilku punktach położonych blisko siebie i zwróceniu wartości średniej wyniku. Inne rozwiązanie problemu filtrowania zapewniają wariacyjne mapy cienia (Variational Shadow Maps), ale są to rozwiązania dość złożone, gdyż wymagają użycia dwóch tekstur, z których jedna pamięta głębokość, a druga kwadrat głębokości. Ostatecznie w programie ustawione jest filtrowanie liniowe, co pozwala mieć nadzieję, że tam gdzie PCF jest wspomagane sprzętowo pewna prosta jego forma zostanie użyta. 5. Renderowanie zawartości mapy cienia (funkcje shadow_begin() i shadow_end()) Ten etap jest prawie identyczny jak renderowanie sceny do pojedynczej ściany sześciennej mapy środowiska. Trzeba w funkcji shadow_begin() odczytać i zachować wielkość okna widoku, ustawić wielkość okna widoku na rozmiary mapy cienia (size_shadow x size_shadow), ustawić odpowiednie wartości macierzy GL_PROJECTION i GL_MODELVIEW. W shadow_end() należy skopiować bufor głębokości do mapy cienia, oraz przywrócić rozmiary okna widoku, macierze GL_PROJECTION oraz GL_MODELVIEW sprzed wywołania shadow_begin(). W funkcji display() w pliku main.cpp między wywołaniami shadow_begin() i shadow_end() renderowana jest cała scena. Odczytanie wymiarów okna widoku (viewport jest zmienną statyczną w przestrzeni nazw LightMap w pliku lightmap.cpp): Ustawienie glGetIntegerv(GL_VIEWPORT, viewport); wymiarów okna widoku na wymiary w x h : glViewport(0,0,w, h); Skopiowanie bufora głębokości do mapy cienia: glBindTexture(GL_TEXTURE_2D, tex_shadow); glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, 0, 0, size_shadow, size_shadow, 0); Poprawnie wyznaczone macierze GL_PROJECTION i GL_MODELVIEW dla lampy przechowywane są w zmiennych statycznych matProjection i matModelView w przestrzeni nazw LightMap w pliku lightmap.cpp. Warto jeszcze podczas renderowania mapy cienia odsunąć całą geometrię od kamery (czyli lampy w tej sytuacji), tak, żeby błędy numeryczne podczas przekształceń nie spowodowały potem, że obiekt rzuca cień na samego siebie. Tak więc ustawienie macierzy GL_MODELVIEW przed renderowaniem mapy cienia powinno wyglądać następujaco: glLoadIdentity(); glTranslatef(.0f,.0f,-0.1f); glMultMatrixf((float*)(&matModelView[0][0])); 6. Zakończenie (funkcje render_begin() i render_end()) Jeśli mapa cienia jest poprawnie inicjowana (shadow_init()) oraz głębokości obiektów widzianych z pozycji lampy są w niej zapisywane (shadow_begin() i shadow_end()), to dodanie cienia do mapy światła wymaga tylko uzupełnienia funkcji render_begin() i render_end() o operacje analogiczne jak dla mapy światła powtórzone dla mapy cienia (patrz punkt 3.). Mapy cienia są jedną z możliwych metod uzyskania cieni dla dynamicznej sceny. Inną popularną metodą są bryły cienia. Dla brył cienia uzyskuje się dużą dokładność wyznaczenia krawędzi cienia, podczas gdy dla map cienia może pojawić się aliasing jego krawędzi (jego wielkość zależy tutaj od rozdzielczości mapy cienia oraz kąta między kierunkiem światła i kierunkiem patrzenia) i błędy określenia czy obiekt jest w cieniu dla płaszczyzn prawie równoległych do kierunku światła. Z kolei mapy cienia można wykorzystać w połączeniu z teksturami zawierającymi maskę przezroczystości (np. prostokąty reprezentujące liście na drzewie z nałożoną teksturą posiadającą odpowiednią maskę przezroczystości), zaś bryły cienia operują wyłącznie na krawędziach geometrii sceny. Istnieje wiele odmian map cienia, które poprawiają ich precyzję (Trapezoidal Shadow Maps, Perspective Shadow Maps) oraz zapewniają lepsze filtrowanie i rozmycie(Variational Shadow Maps). Osobnym problemem, ale również dość dobrze już opracowanym, jest liczenie realistycznych cieni dla świateł powierzchniowych oraz światła otaczającego dochodzącego ze wszystkich kierunków (Ambient Occlusion).