Tekstury, czyli trochę więcej życia na naszej scenie.

Nasze poprzednie dwa boxy były piękne i idealne w swojej prostocie, jednak nas to nie zadowala. W końcu chyba żadna z gier w jakie się gra i jakie będą jeszcze grane nie składała się z boxów i interpolowanych kolorów! W dwóch kolejnych lekcjach zapoznamy się z dwoma filarami grafiki 3D – teksturami i siatkami 3D. Przy okazji pobawimy się trochę w rozbudowanie naszych biednych shaderów.

Tak więc – czym jest tekstura? Dla nas jest to obrazek, a dla naszego głupiego komputera poprostu tablica bajtów, które oznaczają kolory poszczególnych teksli, czyli elementów tekstury (texture element – analogicznie do pixel – picture element).

image

Oto przykładowa tekstura. Zaczniemy więc od wczytania tego pliku graficznego w naszym programie:

   1: IDirect3DTexture9* pTexture;
   2: // a nastepnie, po zainicjalizowaniu urzadzenia
   3: D3DXCreateTextureFromFile(pDev, "tex.jpg", &pTexture);

Na razie jest prosto – mamy interface IDirect3DTexture9, dzięki któremu możemy odwoływać się do danych naszej tekstury, oraz może robić to urządzenie D3D. Sam obrazek wczytujemy z dysku funkcją biblioteki D3DX, której parametrów nie trzeba tłumaczyć. Teraz, gdybyśmy używali starego, dobrego Fixed Pipeline, wywołalibyśmy metodę SetTexture gdzieś przed renderingiem i życie wydawałoby się proste. Ale, że używamy shaderów, musimy przekazać naszemu shaderowi, że mamy wczytaną teksturę i chcielibyśmy cośtam z nią kombinować.

   1: pEffect->SetTexture("tex0", pTexture);

Oto wywołanie, które mówi naszemu efektowi żeby – no właśnie, co? Dlatego na tym etapie, powinniśmy wreszcie otworzyć nasz z lekka obsypany kurzem plik shadera i troszkę się nim zająć.

   1: float4x4 worldViewProj;
   2:  
   3: texture tex0;
   4: sampler2D sampler0 = sampler_state
   5: {
   6:     texture = tex0;
   7:     MinFilter = Linear;
   8:     MagFilter = Linear;
   9:     MipFilter = Linear;
  10: };
  11:  
  12: void vsmain(float3 pos : POSITION,
  13:             float4 color : COLOR,
  14:             out float4 oPos : POSITION,
  15:             out float4 oColor : COLOR)
  16: {
  17:     oPos = mul(float4(pos,1), worldViewProj);
  18:     oColor = color;
  19: }
  20:  
  21: float4 psmain(float4 color : COLOR) : COLOR
  22: {
  23:     return color;
  24: }
  25:  
  26: technique std
  27: {
  28:     pass
  29:     {
  30:         VertexShader = compile vs_2_0 vsmain();
  31:         PixelShader = compile ps_2_0 psmain();
  32:     }
  33: }

Właściwie tutaj nie zmieniło się narazie wiele, prócz dodania magicznego wynalazku:

   1: texture tex0;
   2: sampler2D sampler0 = sampler_state
   3: {
   4:     texture = tex0;
   5:     MinFilter = Linear;
   6:     MagFilter = Linear;
   7:     MipFilter = Linear;
   8: };

Identyfikator tex0 zapewne już rozpoznajemy – jest to ten sam obiekt, pod który ustawiliśmy teksturę w naszym programie (wywołaniem pEffect->SetTexture, kawałek wyżej). Cóż to natomiast sampler2D? Zanim opowiemy o tym typie języka HLSL, będziemy musieli zarysować kontekst w stosunku do sprzętu. W naszej karcie grafiki mianowicie, istnieje coś takiego jak Sampler. Możemy wyobrazić sobie Samplery jako układy (faktycznie nie są to oczywiście fizycznie istniejące układy na płytce, a jedynie pseudorejestry shadera), które na żądanie programisty pobierają z pamięci GPU „kawałki” tekstur, dokonują ich odczytu zgodnie ze stanami, które można im ustawić, oraz zwracają odczytane teksle jako wektor kolorów. Dodatkowo, wymaganiem kart graficznych starszych niż GeForce 6/7 i odpowiadająca im seria Radeonów, są tekstury o rozmiarach potęgi liczby 2 (128, 256, 64…). Pomimo, że większość nowych kart wspiera tekstury o innych rozmiarachm warto się trzymać tego ograniczenia, gdyż obliczenia na liczbach będących potęgami dwójki są szybsze (przesuwanie bitów).

Konstrukcja widoczna w kodzie powyżej, natomiast, powoduje utworzenie obiektu o typie sampler2D (samplować, czyli odczytywać, będziemy teksturę dwuwymiarową). Konstrukcja semantyczna sampler_state, jest natomiast sposobem „powiedzenia” naszemu obiektowi samplera o tym, jaką teksturę będzie samplował (opcja texture – tutaj tex0); w bloku tym możemy również definiować różnorodne stany samplera. W tym przykładzie, zdefiniowaliśmy trzy stany filtrowania tekstur. Filtrowanie mianowicie zasadza się na tym, iż czasami musimy naszą teksturę powiększyć lub pomniejszyć przez perspektywę, bo jest za duża, lub z dowolnego innego powodu. W takim przypadku brak filtrowania znacząco wpłynąłby na pogorszenie jakości generowanego obrazu. Stany MinFilter oraz MagFilter odpowiadają sposobowi filtrowania tekstur dla ich powiększania (MAGnification) oraz pomniejszania (MINimization) – tutaj ustawiamy, na interpolację liniową. Co to natomiast za stan MipFilter? Jest to mianowicie stan filtrowania takiego tworu, jakim są Mipmapy (mip – z łaciny „multum in parvo”, co oznacza „wiele w jednym” + mapy – tekstury).

Wyobraźmy sobie sytuację: stoimy na trawniku, świeci słoneczko, wesoło śpiewają ptaszki, życie jest proste i piękne. Patrzymy pod nogi i widzimy dokładnie każdą trawkę, kamyczek i wszystkie szczegóły powierzchni. Patrzymy kawałek dalej i powoli przestajemy odróżniać od siebie poszczególne kawałki trawki, nie widzimy kamyczków, bo są za daleko i trawnik przypomina nam poprostu kobierec o różnych odcieniach zieleni.

Teraz wyobraźmy sobie naszą scenę 3D: stoimy na trawniku, w tym przypadku przybliżonym jako powtórzona wielokrotnie tekstura trawy. Świeci directional light na skyboxie, wesoło kwili Beep(..) i jesteśmy zadowoleni. Patrzymy pod nogi – na teksturze widzimy poszczególne kawałki trawy, kamyczki. Patrzymy dalej – nadal widzimy trawę i wszyskie kamyczki; patrzymy naprawdę daleko – nadal widzimy wszystkie szczegóły! Jako że takie zachowanie nie jest zgodne z rzeczywistością, mądre głowy wieki temu wymyśliły proste rozwiązanie. Mianowicie, dlaczego by nie zmniejszać szczegółowości tekstury wraz ze wzrostem odległości od obserwatora? W dodatku tak, by to nie on się o to martwił, a sampler. No i właśnie na tym zasadza się cała idea Mipmap, gdyż szybciej jest użyć mniejszej tekstury niż pomniejszać już istniejącą. Mipmapy to ciąg tekstur, z których poziom 0 ma pełną rozdzielczość tekstury, a każdy następny jest o potęgę liczby 2 mniejszy od poprzedniego:

image

Przy czym tekstury nie muszą być kwadratowe, są zazwyczaj autogenerowane, a ostatnim poziomem w łańcuchu jest poziom o rozmiarze 64×64.

No, tośmy sobie pogadali, a tutaj czekają tekstury na wyświetlenie!

No, jednak jeszcze pogadamy. Mianowicie, wspominałem wcześniej, że tekstura to po prostu dwuwymiarowa tablica bitów (na razie takimi się zajmiemy). W takim razie, skąd karta grafiki ma wiedzieć w którym miejscu ma odczytać (zsamplować) teksturę tak, aby uzyskać ten jej teksel o który nam chodzi? Wie to, ponieważ to my, programiści jej to mówimy! Aby nam to ułatwić, każdy teksel adresowany jest przez dwie współrzędne – koordynaty tekstury. Koordynaty możemy przedstawić jako układ współrzędnych:

image

Teraz możemy zobaczyć kod:

   1: texture tex0;
   2: sampler2D sampler0 = sampler_state
   3: {
   4:     texture = tex0;
   5:     MinFilter = Linear;
   6:     MagFilter = Linear;
   7:     MipFilter = Linear;
   8: };
   9:  
  10: void vsmain(float3 pos : POSITION,
  11:             float2 texc : TEXCOORD0,
  12:             out float4 oPos : POSITION,
  13:             out float2 oTexc : TEXCOORD0)
  14: {
  15:     oPos = mul(float4(pos,1), worldViewProj);
  16:     oTexc = texc;
  17: }
  18:  
  19: float4 psmain(float2 texc : TEXCOORD0) : COLOR
  20: {
  21:     return tex2D(sampler0, texc);
  22: }

Jak widać, zrezygnowaliśmy z koloru – jest nam niepotrzebny. Zamiast niego, przekazujemy do Vertex (a potem Pixel) Shadera koordynaty tekstur. W PS-ie zaś wywołujemy funkcję HLSL o nazwie tex2D – robi ona prostą rzecz – bierze sampler podany w pierwszym argumencie, podaje mu koordynaty tekstur podane w drugim argumencie oraz zwraca wynik, w postaci wektora 4-elementowego (float4), w którym przechowywane są wartości koloru dla poszczególnych kanałów – A,R,G,B.

Skoro teraz zamiat koloru przekazujemy koordynaty tekstur, musimy również odpowiednio zmienić definicje naszych wierzchołków oraz FVF:

   1: struct OurVertex
   2: {
   3:     float x, y, z; // pozycja
   4:     float u,v; // koordynat tekstury
   5: };
   6:  
   7: const DWORD OURVERT_FVF = D3DFVF_XYZ | D3DFVF_TEX1;
   8:  
   9: // Pozniej:
  10:  
  11: OurVertex verts[] =
  12: {
  13:     { -1.0f,-1.0f,-1.0f, 1, 1 }, 
  14:     { -1.0f, 1.0f,-1.0f, 1, 0 }, 
  15:     {  1.0f,-1.0f,-1.0f, 0, 1 }, 
  16:     {  1.0f, 1.0f,-1.0f, 0, 0 }, 
  17:     {  1.0f,-1.0f, 1.0f, 1, 1 }, 
  18:     {  1.0f, 1.0f, 1.0f, 1, 0 }, 
  19:     { -1.0f,-1.0f, 1.0f, 0, 1 }, 
  20:     { -1.0f, 1.0f, 1.0f, 0, 0 }, 
  21:     { -1.0f,-1.0f,-1.0f, 1, 1 }, 
  22:     { -1.0f, 1.0f,-1.0f, 1, 0 },
  23: };

Jako ciekawe zadanie domowe proponuję pobawienie się koordynatami tekstur, zarówno w Shaderze jak i w programie. Szczególnie inspirujące może być wykroczenie poza zakres 0..1 – ale niech będzie to eksperymentem własnym dociekliwego czytelnika. :)

Wynik działania naszego programu (do pobrania TUTAJ, wraz ze źródłami) jest dokładnie taki, jakiego się spodziewaliśmy:

image

Reklamy

Jedna odpowiedź to “Tekstury, czyli trochę więcej życia na naszej scenie.”

  1. Świetna sprawa ten Twój kurs! Szukałem w tej lekcji czegoś o filtrowaniu mipmap w samplerze. No i bach! Od razu mam informację MipFilter = Linear. Rewelacja! Bardzo pożyteczną pracę wykonałeś pisząc ten kurs.

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj / Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj / Zmień )

Zdjęcie na Google+

Komentujesz korzystając z konta Google+. Wyloguj / Zmień )

Connecting to %s

 
%d blogerów lubi to: