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

Dodaj komentarz