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).

Podobne dokumenty