Własny silnik graficzny. Część III: teksturowanie.

30.11.2010 - Robert Kraus
TrudnośćTrudnośćTrudnośćTrudność

Mapowanie wypukłości

Istnieje kilka technik mapowania wypukłości, my zajmiemy sie jedną z nich - mapowaniem wektorów normalnych (ang. normal mapping, dot3 bump mapping). Metoda ta polega na zakrzywianiu lub zastępowaniu wektora normalnego w różnych punktach powierzchni obiektu tak, aby uzyskać efekt wgłębień i wypukłości na gładkiej powierzchni. Wykorzystuje się do tego również tekstury w postaci bitmap z tą różnicą, że teksele zawierają teraz inną informację. W każdym tekselu zakodowany jest wektor normalny (czyli wektor prostopadły do powierzchni obiektu).

Na poniższym obrazku widać efekt zastosowania metody mapowania wypukłości. W rzeczywistości powierzchnia sfery po prawej stronie jest tak samo gładka jak sfery po lewej stronie, jednak zakrzywione wektory normalne dają złudzenie nierówności... niestety to złudzenie kończy się na brzegu, gdzie nasza sfera pokazuje nam jaka jest naprawdę, czyli idealnie okrągła.

Źródło obrazka z wyrenderowanymi sferami oraz informacja o prawach autorskich: link.

Na poniższej ilustracji sytuacja (A) przedstawia płaską powierzchnię z wektorami normalnymi w różnych miejscach, skoro powierzchnia jest płaska wszystkie te wektory mają ten sam kierunek. Sytuacja (B) przedstawia pofałdowaną powierzchnię. Pofałdowanie powoduje, że wektory normalne mają różne kierunki. Sytuacja (C) przedstawia płaską powierzchnię, w której prawdziwe wektory normalne zostały zastąpione wektorami z sytuacji (B), w efekcie czego obserwator ma wrażenie, że widzi powierzchnię z sytuacji (B). Dlaczego tak się dzieje? Plastyczność powierzchni postrzegamy dzięki światłu, które pada na powierzchnię, a następnie odbija się w kierunku naszych oczu. Przy czym ilość odbitego światła silnie zależy od cosinusa kąta między wektorem normalnym a kierunkiem padania światła (nazwijmy go $ \alpha $), który zależy od kierunku wektora normalnego oraz jego punktu zaczepienia. Sytuacja (D) pokazuje, że jeśli pominiemy informację o zaczepieniu wektora normalnego umieszczając wektor normalny z pofałdowanej powierzchni na płaskiej powierzchni, to wbrew pozorom informacja o pofałdowaniu powierzchni będzie ciągle duża (ponieważ kąt $ \alpha $ na ogół ulegnie tylko niewielkiej zmianie). Należy jednak pamiętać, że jest to rodzaj przybliżenia, a więc działa tym lepiej im mniejsze są różnice w kątach $ \alpha $ oraz im mniejsze są różnice w odległościach punktów na rzeczywistej powierzchni od punktów na płaskiej powierzchni. Dlatego w ten sposób można dobrze symulować drobne wklęsłości i wypukłości. Zdecydowanie gorzej działa przy wysokich wypukłościach i głębokich wklęsłościach takich jakie są widoczne w sytuacji (E). Dlaczego więc stosować tą metodę, skoro wydaje się być narzędziem tak bardzo ograniczonym? Dokładny model geometryczny może być bardzo kosztowny pamięciowo, więc upraszczamy go do modelu, który dobrze oddaje globalny kształt obiektu stosując do symulowania drobnych zmian geometrycznych mapę wektorów normalnych. Płyną z tego dwie korzyści, duża oszczędność pamięci jak i możliwość zmiany w czasie rzeczywistym charakteru powierzchni wynikającego z drobnych perturbacji geometrycznych (wystarczy zaaplikować do powierzchni inną mapę wektorów normalnych, zamiast zmieniać całą konstrukcję modelu geometrycznego).

Kodowanie i dekodowanie wektorów

Chcielibyśmy przechowywać w teksturze wektory, którymi zastępowalibyśmy prawdziwe wektory normalne na teksturowanej powierzchni. Wektory te możemy reprezentować jako trójki liczb rzeczywistych $ v = (v_x, v_y, v_z) $, gdzie $ v_x \in [-1, 1] $, $ v_y \in (0, 1] $, $ v_z \in [-1, 1] $ (innymi słowy wektor $ v \in [-1, 1] \times (0, 1] \times [-1, 1] $). Pojedynczy tekstel $ t $ w teksturze reprezentowanej przez 24-bitową bitmapę ma postać $ (t_{r}, t_{g}, t_{b}) \in \{0, 1, ..., 255\}^3 $. Widać tutaj wadę tego rozwiązania, wszystkich wektorów jest nieskończenie wiele, natomiast przy użyciu 24-bitowej bitmapy jesteśmy w stanie zakodować skończenie wiele wektorów. Jednak liczba wektorów jaką możemy zakodować wynosi $ 256^3 = 16777216 $, co w praktyce jest zadowalające.

Sposób kodowania:

$$ (v_x, v_y, v_z) \rightarrow ( \frac{v_x + 1}{2} \cdot 255, \frac{v_z + 1}{2} \cdot 255, v_y \cdot 255) $$


Sposób dekodowania:
$$ (\frac{t_{r}}{255} \cdot 2 - 1, \frac{t_{b}}{255}, \frac{t_{g}}{255} \cdot 2 - 1) \leftarrow (t_{r}, t_{g}, t_{b}) $$

Poniżej fragmenty map wektorów normalnych użytych w programie dołączonym do artykułu. Dla tekstur zawierających wektory zakodowane w wyżej opisany sposób charakterystyczne są niebiesko-fioletowe barwy.

Efekt zastosowania mapy po lewej stronie (cegły na powierchni sfery).

Efekt zastosowania mapy po prawiej stronie (zakrzywione odbicia od lustra).

Podstawianie wektora normalnego

Wyobraźmy sobie punkt $ p = (p_x, p_y, p_z) $ na powierzchni trójkąta oraz wektor normalny $ N = (n_x, n_y, n_z) $ do tej powierzchni w punkcie $ p $. Tak jak wcześniej obliczamy współrzędne $ (u, v) $ punktu $ p $ w przestrzeni tekstury (czyli punkt $ p' $), jednak tym razem pozyskana informacja jest zbyt mała, aby skorzystać z tekstury ponieważ nie wiemy jak zorientować odczytany z tekstury wektor na powierzchni obiektu (tak jakby możemy go dowolnie obracać wokół prawdziwego wektora normalnego).



Mając punkt $ p' $ obliczamy punkty $ x' = (u + \epsilon, v) $ oraz $ z' = (u, v + \epsilon) $, gdzie $ \epsilon \textgreater 0 $. Następnie obliczany współrzędne barycentryczne punktów $ x' $ i $ z' $ względem trójkąta $ A'B'C' $, wyznaczając przy ich pomocy punkty $ x $ i $ z $. Ostatecznie obliczamy wektory $ N_x = x - p $ i $ N_z = z -p $.



Niech $ v = \left[ \begin{array}{c} v_x \\ v_y \\ v_z \\ \end{array} \right] $ będzie wektorem odkodowanym z tekstury. Problem w tym, że jego współrzędne są w innym układzie współrzędnych niż punkt $ p $ i wektor normalny $ N $. wektor $ v $ można przedstawić jako kombinację liniową wektorów $ E_x = \left[ \begin{array}{c} 1 \\ 0 \\ 0 \\ \end{array} \right] $, $ E_y = \left[ \begin{array}{c} 0 \\ 1 \\ 0 \\ \end{array} \right] $, $ E_z = \left[ \begin{array}{c} 0 \\ 0 \\ 1 \\ \end{array} \right] $, czyli $  v = E_x \cdot v_x + E_y \cdot v_y + E_z \cdot v_z  $. Wyobraźmy sobie, że obserwujemy świat z punktu $ p $, a nasze ciało ustawione jest tak, że wektor $ N $ wskazuje nam kierunek w górę, wektor $ N_z $ wskazuje nam kierunek na przód, a wektor $ N_x $ wskazuje nam kierunek w bok. Możemy sobie wyobrazić, że punkt $ p $ jest teraz środkiem układu współrzędnych (punktem $ (0,0,0) $), a wektory $ E_x $, $ E_y $, $ E_z $ są wektorami bazowymi generującymi naszą przestrzeń, w takiej przestrzeni wyrażony jest nasz wektor $ v $. Jeśli spojrzymy na całą sytuację z początku globalnego układu współrzędnych, w którym umiejscowiony jest nasz trójkąt, punkt $ p $ i wektor $ N $, to wektory $ E_x $, $ E_y $, $ E_z $ staną się wektorami kolejno $ N_x $, $ N $, $ N_z $, zatem wektor $ v $ w globalnym układzie współrzędnych (nazwijmy go $ N' $) będzię kombinacją $  N_x \cdot v_x + N \cdot v_y + N_z \cdot v_z $. Teraz zamiast prawdziewgo wektora normalnego $ N $ podczas obliczania oświetlenia używamy wektora $ N' $. Podstawienia wektora dokonamy używając wywołania funkcji $ N' = uChangeSpace(v, N_x, N, N_z) $.

1
2
3
4
5
6
7
8
9
10
// przeniesienie wektora V do przestrzeni zbudowanej na wektorach Ex, Ey, Ez
// oraz jego normalizacja
inline vec3 uChangeSpace(vec3::i V, vec3::i Ex, vec3::i Ey, vec3::i Ez) {
  return vec3(
    V.x*Ex.x + V.y*Ey.x + V.z*Ez.x,
    V.x*Ex.y + V.y*Ey.y + V.z*Ez.y,
    V.x*Ex.z + V.y*Ey.z + V.z*Ez.z,
    1.0f
  );
}

Część I    Część II    Część III    Część IV   

5
Twoja ocena: Brak Ocena: 5 (2 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com