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

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

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

Czym będziemy się zajmować?

Poznamy metody próbkowania i filtrowania (wygładzania) tesktury. Dowiemy się jak teksturować sferę i trójkąt oraz jak uzyskiwać wypukłości na płaskich powierchniach przy pomocy techniki zwanej mapowaniem wektorów normalnych.

Wektor 2d

$ vec2 $ to element przestrzeni dwuwymiarowej, będzie służył głównie do obliczeń związanych z wyznaczaniem punktów na teksturze.

1
2
3
4
5
6
7
struct vec2 {
  typedef const vec2& i; // input ref
  float u, v;
 
  vec2(float u, float v) : u(u), v(v) {}
  vec2() { u = v = 0.0f; }
};

Podstawowe operacje dla $ vec2 $:

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// długość wektora 2d
inline float length(vec2::i w)
{  return sqrt(w.u * w.u + w.v * w.v);  }
 
///// operacje arytmetyczne dla wektora 2d
 
inline vec2 operator+ (vec2::i a, vec2::i b)
{  return vec2(a.u + b.u, a.v + b.v);  }
 
inline vec2 operator- (vec2::i a, vec2::i b)
{  return vec2(a.u - b.u, a.v - b.v);  }
 
inline vec2 operator* (vec2::i a, float t)
{  return vec2(a.u * t, a.v * t);  }

Tekstura

Reprezentacja tekstur w kodzie programu:

1
2
3
4
5
6
7
8
9
// mapa kolorów
struct colorMap {
  color* map; // tablica kolorów
  nat xRes, yRes, size; // wymiary tekstury
  // map[x + y*xRes] = teksel o współrzędnych (x,y)
 
  // konstruktory...
  // próbkowanie tekstury...
}

1
2
3
4
5
6
7
8
9
10
// mapa wektorów
struct vectorMap {
  vec3* map; // tablica wektorow
  nat xRes, yRes, size; // wymiary tekstury
  // map[x + y*xRes] = teksel o współrzędnych (x,y)
 
 
  // konstruktory...
  // próbkowanie tekstury...
}

Teksel (ang. texel) to element tekstury (ang. texture element), podobnie jak piksel to element obrazu (ang. pixel ~ picture element).

Wczytywanie tekstury z pliku jest związane z użyciem bibliotek do wczytywania obrazów, więc zostaje pominięte. W dołączonym do artykułu programie wczytywanie realizują funkcje $ loadColorMap $ oraz $ loadVectorMap $, które można podglądnąć w pełnym kodzie programu.

Filtrowanie tekstury

Bbędziemy posługiwali się współrzędnymi $ (u, v) \in [0,1]^2 $. Tekstury mają jednak postać krat tekseli o współrzędnych $ (x, y) $, gdzie $ x \in \{0, 1, ..., n-1\} $ oraz $ y \in \{0, 1, ..., m-1\} $. Naturalnie należy przeskalować współrzędne z kwadratu $ [0,1]^2 $ na prostokąt $ [0,n-1] \times [0,m-1] $ wykonując mnożenie współrzędnych $ (u \cdot (n-1), v \cdot (m-1)) $. Jak łatwo się domyślić, nieczęsto w wyniku takiego przeskalowania otrzymamy współrzędne będące liczbami całkowitymi, a więc na ogół nie będziemy trafiali w teksele naszej tekstury. Co robić?

Poznamy dwie metody radzenia sobie z tym problemem - próbkowanie punktowe oraz filtrowanie dwuliniowe. Poniższa ilustracja prezentuje efekty stosowania tych metod.

Próbkowanie punktowe

O urokach próbkowania punktowego można się przekonać grając w stare gry 3d takie jak Doom czy Quake lub jeśli to możliwe wyłączając filtrowanie w jakiś nowszych grach.

Zaokrąglamy współrzędne do części całkowitej otrzymując w ten sposób współrzędne konkretnego teksela. To proste rozwiązanie ma jednak bardzo poważną wadę. Jeśli zbliżymy się do obiektu na dostatecznie krótki dystans widoczne bedą jednokolorowe kwadraty. Ponadto tekstura może wyglądać jakby była zrobiona z przypadkowych ziarenek, które co gorsze lekko migotają gdy obiekt lub obserwator jest w ruchu (efekt szczególnie widoczny w przypadku tekstur mających charakter szachownicy).

Ilustracja próbkowania punktowego.

Analogia próbkowania punktowego tekstury do funkcji jednej zmiennej.

Poniżej implementacja. W wierszach nr 3-4 obliczamy współrzędne teksela zgodnie z powyższym opisem, następnie w wierszu nr 5 obliczamy współrzędne tekstela w tablicy i dzielimy modulo rozmiar tekstury na wypadek wyskoczenia poza rozmiar tekstury (może się to zdarzyć z powodu ograniczonej precyzji liczb w reprezentacji zmiennopozycyjnej).

1
2
3
4
5
6
// próbkowanie punktowe
color operator() (float u, float v) const {
  nat x = nt(u * fl(xRes -1));
  nat y = nt(v * fl(yRes -1));
  return map[ (x + y*(xRes -1) % size ];
}

Filtrowanie dwuliniowe

Pierwsze karty graficzne 3d (zwane akceleratorami, np. voodoo 3dfx) polegały właśnie na sprzętowej realizacji filtrowania dwuliniowego.

Filtrowanie 2-liniowe (ang. bilinear filtering) polega na zastosowaniu interpolacji dwuliniowej w celu wyznaczenia wartości pośredniej pomiędzy tekstelami. Rozwiązanie to eliminuje efekt ziarenek powodując, że tekstura jest gładka. Natomiast obserwując obiekt z bliskiej odległości zamiast jednokolorowych kwadratów będziemy widzieli rozmyte plamy, co też jest niekoniecznie przyjemne, ale przy teksturze w dostatecznie wysokiej rozdzielczości daje rezultaty lepsze niż próbkowanie punktowe.

A co to jest interpolacja? Rozważmy pewną funkcję $ f(x) $ i przypuśćmy, że znamy jej wartości w punktach $ x_0 $ oraz $ x_1 $, ale nie znamy wzoru na funkcję $ f(x) $. Tak więc znamy $ f(x_0) $ oraz $ f(x_1) $, ale chcielibyśmy umieć też liczyć wartości tej funkcji w punktach $ x_0 \leq x \leq x_1 $.
Możemy przyjąć proste przybliżenie funkcji f(x) wielomianem stopnia pierwszego (czyli prostą). To własnie jest interpolacja liniowa.

$$ g(x) = f(x_0) + (f(x_1)-f(x_0)) \cdot \frac{x - x_0}{x_1 - x_0} $$

Ilustracja interpolacji liniowej funkcji jednej zmiennej.

Interpolacja dwuliniowa jest uogólnieniem interpolacji liniowej dla funkcji dwóch zmiennych. A więc znamy wartości pewnej funkcji $ f(x,y) $ w czterach punktach $ (x_0,y_0) $, $ (x_1,y_0) $, $ (x_0,y_1) $, $ (x_1,y_1) $. Chcemy obliczać $ f(x, y) $ dla $ x_0 \leq x \leq x_1 $ oraz $ y_0 \leq y \leq y_1 $. Jeśli ustalimy wartość zemiennej $ y $ w funkcji $ f(x,y) $ to staje się ona funkcją jednej zmiennej, zmiennej $ x $. Podstawiając za $ y $ kolejno wartości $ y_0 $ oraz $ y_1 $ możemy użyć interpolacji liniowej względem zmiennej $ x $ obliczając:

$$ g'(x) = f(x_0, y_0) + (f(x_1, y_0)-f(x_0, y_0)) \cdot \frac{x - x_0}{x_1 - x_0} $$

$$ g''(x) = f(x_0, y_1) + (f(x_1, y_1)-f(x_0, y_1)) \cdot \frac{x - x_0}{x_1 - x_0} $$

Do powyższych dwóch wartości możemy zastosować interpolację liniową wględem zmiennej $ y $:
$$ g(x, y) = g'(x) + (g''(x)-g'(x)) \cdot \frac{y - y_0}{y_1 - y_0} $$


Jak to się ma do naszej tekstury? Jeśli z mapowania na współrzędne tekstury i mnożeniu przez jej rozmiar otrzymamy punkt $ (x, y) $, to wartość w tym punkcie obliczamy stosując wyżej omówioną interpolację dwuliniową do punktów $ (|x|, |y|) $, $ (|x| + 1, |y|) $, $ (|x|, |y| + 1) $, $ (|x| + 1, |y| + 1) $, gdzie $ |.| $ to część całkowita z liczby.

Ilustracja filtrowania dwuliniowego.

Implementacja:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// próbkowanie z filtrowaniem dwuliniowym
color operator() (float u, float v) const {
  // x = u * fl(xRes -1)
  // y = v * fl(yRes -1)
  nat ui = nt(u * fl(xRes -1)); // x0
  nat vi = nt(v * fl(yRes -1)); // y0
  nat a = (  x0    +  y0    * (xRes -1) ) % size; // (x0,y0)
  nat b = ( (x0+1) +  y0    * (xRes -1) ) % size; // (x1,y0)
  nat c = (  x0    + (y0+1) * (xRes -1) ) % size; // (x0,y1)
  nat d = ( (x0+1) + (y0+1) * (xRes -1) ) % size; // (x1,y1)
  // g'(x) = f(x0,y0) + ( f(x1,y0) - f(x0,y0) ) * ((x-x0) / (x1-x0))
  color ab = map[a] + (map[b]-map[a]) * ( u*fl(xRes -1) - fl(x0) );
  // g''(x) = f(x0,y1) + ( f(x1,y1) - f(x0,y1) ) * ((x-x0) / (x1-x0))
  color cd = map[c] + (map[d]-map[c]) * ( u*fl(xRes -1) - fl(x0) );
  // g(x,y) = g'(x) + (g''(x) - g'(x)) * ((y-y0) / (y1-y0))
  return ab + (cd - ab) * ( v*fl(yRes -1) - fl(y0) );
}

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

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com