background image

1 

DirectX ▪ Dot3 

Witam w kolejnym tutorialu. Przechodząc od teorii do praktyki nowoczesnego mapowania wypukłości, które dzisiaj 
całkowicie rewolucjonizuje grafikę 3D czasu rzeczywistego na prostym przykładzie pokażemy sobie jak zrealizować tę 
technikę za pomocą pakietu Direct3D. W zasadzie wszystko wiemy z lekcji teoretycznej - co nam potrzeba, jak to policzyć i 
jak zastosować. Pozostanie nam tylko omówienie odpowiednich kawałków kodu źródłowego. Żeby więc niepotrzebnie nie 
komplikować i nie zanudzać na wstępie połączymy teorię z praktyką. Po pierwsze - jak już wiemy z lekcji teoretycznej o tej 
technice będziemy dzisiaj zmuszeni zmienić naszą już dosyć starą i zardzewiałą strukturę wierzchołka. Na potrzeby 
mapowania wypukłości niestety to co mieliśmy do tej pory to jest stanowczo za mało i trzeba co nieco dodać wierzchołkowi 
aby coś jeszcze wycisnąć z naszego sprzętu. Wprawdzie dodawanie danych do wierzchołków nie jest mile widziane przez 
nasze karty graficzne - bo one to tylko jedno by chciały - jak najmniej bajtów. No ale wysiłek włożony w przetworzenie tych 
kilku dodatkowych bajtów na wierzchołek na pewno by nam wybaczyły, gdyby wiedziały ile danych musiałyby przetworzyć 
aby otrzymać podobną geometrię za pomocą małych, ale jakże licznych wierzchołków ;). Przypatrzmy się więc, czym 
obciążymy dzisiaj nasz wysłużony sprzęt:  
 

struct SVertex 

    float        x, y, z;        // position 
    float        nx, ny, nz;     // normal 
    DWORD        color;          // diffuse color 
    float        u1, v1;         // firts texture channel 
    float        u2, v2;         // second texture channel 
    float        ux, uy, uz;     // U vector 
    float        vx, vy, vz;     // V vector 
    float        uvx, uvy, uvz;  // UxV vector 
}; 
 
#define FVF_VERTEX ( D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX5 | \ 
                        D3DFVF_TEXCOORDSIZE2( 0 ) | D3DFVF_TEXCOORDSIZE2( 1 ) | \ 
                        D3DFVF_TEXCOORDSIZE3( 2 ) | D3DFVF_TEXCOORDSIZE3( 3 ) | \ 
                        D3DFVF_TEXCOORDSIZE3( 4 ) ) 

Na początku oczywiście wiadomo - pozycja, normalna wierzchołka, kolor diffuse oraz dwa zestawy współrzędnych tekstur. 
Ten zestaw w zasadzie doskonale znamy i omawiać go nie muszę, no może troszkę o teksturach tutaj. Pierwszy zestaw 
posłuży do przechowania współrzędnych mapy normalnych (to już chyba wiemy czym jest), drugi natomiast będzie 
przechowywał ewentualne tekstury właściwe obiektowi, jeśli będzie takowe posiadał. Dlaczego w takiej kolejności okaże się 
już niezadługo. Nowością natomiast, zgodnie zresztą z teorią są trzy wektory złożone z trzech liczb float (wektor w 
przestrzeni oczywiście). Te trzy wektory posłużą nam jak wiemy do przechowywania macierzy obrotu, która przekształci 
wektor światła padający na obiekt do przestrzeni tekstury zawierającej normalne. Taka a nie inna konstrukcja wierzchołka 
pozwoli nam na przekazanie tych macierzy dla każdego wierzchołka bezpośrednio do Vertex Shadera, co jest dla nas bardzo 
ważne, bo jak wiemy dzięki Shaderowi nie będziemy musieli się dobierać do buforów wierzchołków tylko zrobi to za nas 
urządzenie. Nieco dziwnie tym razem może wyglądać dla nas definicja typu wierzchołka, a szczególnie konstrukcje 

D3DFVF_TEXCOORDSIZEX( Y )

. Tak naprawdę jednak jest to bardzo proste. Już nie jeden raz wspominaliśmy o tym jak 

wspaniałe możliwości daje nam do ręki Direct3D w zakresie definiowania formatu wierzchołka. Nie dosyć, że możemy sobie 
w miarę swobodnie wybierać co w tym wierzchołku ma się znaleźć to niektóre z tych składowych możemy jeszcze sobie 
dodatkowo regulować. Szczególnie dotyczy się to współrzędnych mapowania tekstury. Jak wiemy standardowo zestaw 
współrzędnych zawiera dwie dane dla tradycyjnych tekstur. Ponieważ jednak współrzędne te zostały niejako zaprojektowane 
nie tylko do przechowywania danych tradycyjnych tekstur więc mamy tutaj pewne możliwości manewru. Otóż możemy 
zrobić następującą rzecz - każdy zestaw współrzędnych tekstur może zawierać od jednej do czterech danych. Dla 
tradycyjnych tekstur jak wiemy są to dwie U i V albo X i Y albo S i T - jak kto woli. Wiemy także, że w Direct3D możemy 
mieć aż osiem zestawów współrzędnych mapowania - na każdy poziom tekstury po jednym. Popatrzmy teraz na naszą 
strukturę wierzchołka. Widać, że na pewno będziemy używać dwóch poziomów tekstur - zerowego dla mapy normalnych i 
pierwszego dla tekstury definiującej wygląd obiektu. Każdy z tych poziomów będzie zawierał po dwie dane. Ponieważ 
dzisiaj namieszamy sporo w naszym formacie wierzchołków więc trzeba także trochę to uporządkować i powiedzieć 
urządzeniu co i ile będzie na każdym poziomie tekstur. Aby tak zrobić użyjemy właśnie konstrukcji 

D3DFVF_TEXCOORDSIZEX( Y )

, która mówi urządzeniu ile danych "X" będzie ważnych na poziomie "Y". Tak więc aby 

urządzenie wiedziało, że na poziomie zero używamy dwóch danych (bo tyle potrzeba dla tekstury) przekażemy mu - 

D3DFVF_TEXCOORDSIZE2( 0 )

, analogicznie w przypadku poziomu 1 - 

D3DFVF_TEXCOORDSIZE2( 1 )

. Następnych 

poziomów mapowania użyjemy natomiast do przechowywania macierzy - bo dla tekstur nie będą one już potrzebne w 
naszym przypadku. Dla macierzy obrotu wystarczą nam trzy wektory definiujące obroty wokół poszczególnych osi - więc 
ostatecznie trzy po trzy. Zabieramy więc naszemu urządzeniu poziomy 2, 3 i 4 i mówimy, że na każdym z nich są ważne trzy 
dane: 

D3DFVF_TEXCOORDSIZE3( 2 ) 
D3DFVF_TEXCOORDSIZE3( 3 ) 
D3DFVF_TEXCOORDSIZE3( 4 ) 

 

background image

2 

DirectX ▪ Dot3 

Po zdefiniowaniu struktury wierzchołka następuje załadowanie wszystkich potrzebnych danych - a więc tekstur i modelu. Tej 
procedury nie będę omawiał bo nie ma to najmniejszego sensu - wszyscy potrafimy to robić już przecież doskonale. 
Następnie jak wiemy, po załadowaniu naszego modelu ze standardowego pliku *.x musimy dokonać klonowania obiektu 
siatki, ponieważ używamy dosyć specyficznej struktury wierzchołka. Robimy to poniższym kawałkiem kodu: 

// convert mesh to appropriate vertex format 
hResult = g_lpMesh->CloneMeshFVF( 0, FVF_VERTEX, g_lpD3DDevice, lpCloneMesh ); 
if( D3D_OK != hResult ) 

    return false; 

 
g_lpMesh = lpCloneMesh; 

Teraz nasz załadowany model ma na pewno taki format jak potrzeba. To klonowanie jednak pociągnęło za sobą kolejną 
rzecz, o której trzeba pamiętać. Otóż model w pliku *.x na pewno nie zawierał wszystkich potrzebnych nam danych. W 
naszym prostym przykładzie problem taki zaistnieje przy współrzędnych tekstury. Model załadowany z pliku *.x będzie 
posiadał współrzędne mapowania, ale tylko dla poziomu zerowego, który u nas będzie odpowiedzialny za mapę normalnych. 
Tak się dla nas jednak szczęśliwie składa, że zarówno mapa normalnych jak i mapa definiująca końcowy wygląd obiektu 
(diffuse) będą nakładane w ten sam sposób, więc wystarczy skopiować dane współrzędnych mapowania z poziomu 0 na 
poziom 1 i będzie w porządku. Ale jeśli będziecie kiedyś stosować różne mapowanie normalnych i właściwych tekstur 
obiektu to musicie oczywiście rozwiązać to w jakiś inny sposób - będzie nim chyba tylko własne ładowanie modeli z 
odpowiednim formatem. W naszym przykładzie sprawa na szczęście jest bardzo prosta. Aby skopiować dane mapowania 
wystarczy więc zalockować bufor wierzchołków i zrobić co trzeba: 

// lock mesh vertex buffer 
hResult = lpVB->Lock( 0, 0, (void**) &pVertices, 0 ); 
 
if( D3D_OK != hResult ) 

    return false; 

 
// copy uv mapping to second layer 
for( DWORD i = 0; i < dwVertNum; i++ ) 

    pVertices[i].u2 = pVertices[i].u1; 
    pVertices[i].v2 = pVertices[i].v1; 
    pVertices[i].ux = pVertices[i].uy = pVertices[i].uz = 0.0f; 
    pVertices[i].vx = pVertices[i].vy = pVertices[i].vz = 0.0f; 
    pVertices[i].uvx = pVertices[i].uvx = pVertices[i].uvx = 0.0f; 

Jak widać, przy okazji także zerujemy nasze wektory odpowiedzialne za obroty, które właśnie teraz, zgodnie z lekcją 
teoretyczną wyliczymy. Jednak, jak wspomniałem w tejże lekcji obliczeń tych będzie dosyć sporo, dlatego też pozwoliłem 
przygotować sobie mały aparat matematyczny do tego celu. Funkcje, które tutaj wykorzystam będą dla was dostępne w 
postaci kodu, więc będzie sobie można je wykorzystać, skopiować czy zawzorować się (tak jak ja to zrobiłem), aby dla 
własnych potrzeb to zaadoptować. My wiemy skąd się wezmą nasze dane, więc tylko pokażmy jak skorzystać z naszego 
narzędzia: 

// get data using appropriate tools 
std::vector<SVertexFix>    g_vertices; 
std::vector<int>           g_indices; 
 
// store vertex data 
for( int i = 0; i < dwVertNum; i++ ) 

    SVertexFix  vertex; 
 
    // copy texture coordinates 
    pVertices[i].u2 = pVertices[i].u1; 
    pVertices[i].v2 = pVertices[i].v1; 
 
    // prepare data for mesh correction 

background image

3 

DirectX ▪ Dot3 

    vertex.vectPos = SVector3f( pVertices[i].x, pVertices[i].y, pVertices[i].z ); 
    vertex.vectUV = SVector2f( pVertices[i].u1, pVertices[i].v1 ); 
    g_vertices.push_back( vertex ); 

 
// store index data 
for( i = 0; i < dwIndicesNum; i++ ) 

    int    nIndex; 
 
    switch( D3DIBDesc.Format ) 
    { 
        case D3DFMT_INDEX16: 
        { 
            nIndex = ((WORD*) pIndices)[i]; 
            g_indices.push_back( nIndex ); 
        } 
        break; 
 

 

 

 

        case D3DFMT_INDEX32: 
        { 
            nIndex = ((DWORD*) pIndices)[i]; 
            g_indices.push_back( nIndex ); 
        } 
        break; 
    } 

 
// make calculations 
Dot3Mesh( g_vertices, g_indices ); 
 
// restore tangent space 
for( i = 0; i < dwVertNum; i++ ) 

    pVertices[i].ux = g_vertices[i].vectTangent.x; 
    pVertices[i].uy = g_vertices[i].vectTangent.y; 
    pVertices[i].uz = g_vertices[i].vectTangent.z; 
 
    pVertices[i].vx = g_vertices[i].vectBinormal.x; 
    pVertices[i].vy = g_vertices[i].vectBinormal.y; 
    pVertices[i].vz = g_vertices[i].vectBinormal.z; 
 
    pVertices[i].uvx = g_vertices[i].vectNormal.x; 
    pVertices[i].uvy = g_vertices[i].vectNormal.y; 
    pVertices[i].uvz = g_vertices[i].vectNormal.z; 

 
// unlock mesh vertex buffer 
hResult = lpVB->Unlock(); 
 
if( D3D_OK != hResult ) 

    return false; 

 
// unlock mesh index buffer 
hResult = lpIB->Unlock(); 
if( D3D_OK != hResult ) 

    return false; 

Postępując zgodnie z algorytmem podanym w części teoretycznej, dla każdego wierzchołka wyliczamy trzy wektory 
definiujące przestrzeń tekstury i umieszczamy je w kolejnych współrzędnych na danych poziomach tekstur. Wektor 
definiujący obrót wokół osi x na poziomie nr 2, y na poziomie nr 3 i z na nr 4. Po zakończeniu wyliczania wektorów 
oczywiście należy odblokować zalockowane wcześniej bufory wierzchołków i indeksów. Metoda podana tutaj w zasadzie 

background image

4 

DirectX ▪ Dot3 

niczym się nie różni od tej z części teoretycznej. Nieścisłości mogą wynikać z zastosowanej konstrukcji kodu, ale chodzi 
dokładnie o to samo - na podstawie ułożenia tekstury normalnej na obiekcie definiujemy po prostu układ współrzędnych, 
który niejako obrazuje naturalne ułożenie tekstury - osie X i Z leżą w płaszczyźnie tekstury a kierunki osi wskazują na 
rosnące wartości współrzędnych mapowania natomiast oś Y jest skierowana prostopadle do tekstury na danym trójkącie - to 
tak dla krótkiego przypomnienia. Do kompletu danych brakuje nam jeszcze dwóch, bardzo ważnych elementów naszej 
układanki. Będą to oczywiście Pixel jak i Vertex Shader. Wprawdzie omawiany dzisiaj efekt dałoby się przy pewnych 
założeniach co do możliwości sprzętu osiągnąć także bez Pixel Shadera, no ale my, aby nabierać dobrych przyzwyczajeń już 
teraz zawsze będziemy się nimi posługiwać, aby nabierać wprawy, która potem na pewno się przyda. No i zakładając, że 
wszystko nam się poprawnie załadowało, shadery są poprawnie napisane, urządzenia zainicjalizowane i wszystko dosłownie 
"gra i buczy" możemy przystąpić do renderingu. Aby dokładnie sobie zobrazować kolejne fazy tworzenia naszego efektu 
nasza przykładowa aplikacja została skonstruowana w ten sposób, że umożliwia po prostu pokazywanie kolejnych etapów 
powstawania wypukłego obiektu. Wszystko, co ważne rozegra się dla nas tak naprawdę w sferze shaderów, bo to one będą 
decydowały co się będzie działo z danymi a szczególną rolę w naszym przykładzie odegra niewątpliwie Vertex Shader:  

vs.1.1 
 
dcl_position     v0 
dcl_normal       v3 
dcl_color        v5 
dcl_texcoord0    v7 
dcl_texcoord1    v8 
dcl_texcoord2    v9 
dcl_texcoord3    v10 
dcl_texcoord4    v11 
 
; c0-c3 - world transform matrix 
; c4-c7 - view*proj transform matrix 
; c8 - simple light 
 
def c8, 0.0f, 0.0f,-1.0f, 0.0f 
def c9, 0.0f, 0.25f, 0.5f, 1.0f 
 
; transform position and normal into camera-space 
m4x4 r0, v0, c0    ; vertex in the world space 
m3x3 r1, v3, c0    ; normal in the world space 
mov r1.w, c9.z 
 
; move u to world space 
dp3 r2.x, v9, c0 
dp3 r2.y, v9, c1 
dp3 r2.z, v9, c2 
 
; move v to world space 
dp3 r3.x, v10, c0 
dp3 r3.y, v10, c1 
dp3 r3.z, v10, c2 
 
; move v to world space 
dp3 r4.x, v11, c0 
dp3 r4.y, v11, c1 
dp3 r4.z, v11, c2 
 
; move light to texture space 
dp3 r1.x, r2.xyz, c8 
dp3 r1.y, r3.xyz, c8 
dp3 r1.z, r4.xyz, c8 
 
; normalize light in texture space 
dp3 r1.w, r1, r1 
rsq r1.w, r1.w 
mul r1, r1, r1.w 
 
; output 
mad oD0, r1, c9.z, c9.z 
mov oD0.w, c9.w 

background image

5 

DirectX ▪ Dot3 

m4x4 oPos, r0, c4 
mov oT0, v7 
mov oT1, v8 

Na samym początku oczywiście numer wersji shadera (od wersji DX9.0 najniższa możliwa to 1.1) a następnie deklaracja 
używanego formatu wierzchołka: 

dcl_position     v0 
dcl_normal       v3 
dcl_color        v5 
dcl_texcoord0    v7 
dcl_texcoord1    v8 
dcl_texcoord2    v9 
dcl_texcoord3    v10 
dcl_texcoord4    v11 

Jak widać tradycyjnie już pozycja wierzchołka będzie wchodzić do shadera poprzez rejestr 

v0

, normalna poprzez 

v3

 itd. 

Widać także, że pomimo użycia różnej ilości ważnych danych dla poszczególnych poziomów tekstur dla shadera nie ma to 
żadnego znaczenia - on i tak domyślnie pobiera z każdego poziomu wszystkie cztery dostępne dane - a co my z tym w 
shaderze zrobimy i czy będzie to poprawne to już nasze zmartwienie. Oczywiście do shadera przekazujemy także macierze 
przekształceń poprzez rejstry pamięci stałej. Jak zwykle będą to: macierz świata oraz pomnożone przez siebie dla 
oszczędności miejsca macierze widoku i projekcji. W shaderze także zdefiniujemy sobie stałą, która będzie nam mówiła o 
położeniu światła (w naszym przykładzie będzie ono stałe) oraz drugą stałą zawierającą kilka potrzebnych liczb. 

; transform position and normal into camera-space 
m4x4 r0, v0, c0                 ; vertex in the world space 
m3x3 r1, v3, c0                 ; normal in the world space 
mov r1.w, c9.z 

Pierwsze co robimy to mnożymy współrzędne zarówno wierzchołków jak i normalnych aby przenieść je w świecie. To już 
standard w naszych shaderach i to działanie jest chyba najzupełniej zrozumiałe - ono po prostu umożliwia nam animację 
naszego obiektu, jeśli znajduje się on w ruchu lub umożliwia odpowiednie ułożenie w świecie. 

; move u to world space 
dp3 r2.x, v9, c0 
dp3 r2.y, v9, c1 
dp3 r2.z, v9, c2 
 
; move v to world space 
dp3 r3.x, v10, c0 
dp3 r3.y, v10, c1 
dp3 r3.z, v10, c2 
 
; move v to world space 
dp3 r4.x, v11, c0 
dp3 r4.y, v11, c1 
dp3 r4.z, v11, c2 

W wektorach wejściowych shadera 

v9

v10

 i 

v11 

umieściliśmy to, co nas najbardziej dzisiaj interesuje - czyli układy 

współrzędnych określające jak zmienia się mapa normalnych w danym wierzchołku. Te układy współrzędnych posłużą do 
tego, aby przenieść do nich padający na dany wierzchołek wektor światła co będzie w ostateczności stanowiło przeniesienie 
wektora światła do przestrzeni tekstury normalnej - co jest naszym ostaecznym celem. Ponieważ wierzchołki naszego obiektu 
mnożyliśmy przez macierz świata a wiemy, że istnieje możliwość poruszania się naszego obiektu po scenie więc trzeba 
zrobić to samo z wektorami lokalnych układów w wierzchołkach. Jeśli obiekt się będzie na przykład obracał to wiadomo, że 
tekstury też się obracają (no chyba, że zależy nam na jakimś efekcie bardzo specjalnym). Załóżmy jednak, że mapa 
wypukłości nie porusza się po obiekcie a jest niejako "statyczna" - to znaczy jeśli obiekt się porusza to mapa normalnych 
także. Dziwnie w sumie by wyglądała ziemia z wędrującymi Himalajami po powierzchni ;P. Jeśli obiekt się obróci to na 
pewno zmieni kąt padania światła na wierzchołek a co za tym idzie i ułożenie lokalnego układu współrzędnych tekstury w 
stosunku do światła. Tak więc cały nasz lokalny układ w postaci trzech osi obracamy wraz z obiektem. Dziwić was może 
tutaj inny sposób mnożenia wektorów przez macierz świata (umieszczona w stałych 

c0

-

c3

). Ale zapewniam was, że jest to 

dokładnie to samo co użycie makra 

m3x3

 i jeśli chcecie to możecie sobie to zastąpić. Dla mnie to jest jak na razie bardziej 

przejrzyste i lepiej widać przynajmniej na początku o co chodzi.  

background image

6 

DirectX ▪ Dot3 

; move light to texture space 
dp3 r1.x, r2.xyz, c8 
dp3 r1.y, r3.xyz, c8 
dp3 r1.z, r4.xyz, c8 
 
; normalize light in texture space 
dp3 r1.w, r1, r1 
rsq r1.w, r1.w 
mul r1, r1, r1.w 

No i teraz następuje to, o czym zaczęliśmy mówić jeszcze w części teoretycznej - czyli przenosimy wektor światła do 
przestrzeni tekstury. Wystarczy, podobnie jak w przypadku obracania wektorów lokalnego układu współrzędnych tekstury 
przez macierz świata, tak samo wektor światła mnożymy przez lokalną macierz tekstury. Z tym, że nasza macierz tekstury 
jest przekształcona przez macierz świata i znajduje się w tymczasowych rejestrach 

r2

r3

 oraz 

r4

. Po pomnożeniu wektora 

światła przez te trzy wektory (definiującą macierz obrotu) mamy wektor światła w przestrzeni tekstury - więc prawie sukces. 
Prawie, ponieważ jak to w przypadku większości operacji na wektorach należy jeszcze wynikowy wektor znormalizować aby 
uniknąć kłopotów w przyszłości. Robimy więc to za pomocą naszej ulubionej sztuczki, którą także mieliśmy okazję już nie 
raz omawiać. 

; output 
mad oD0, r1, c9.z, c9.z 
mov oD0.w, c9.w 
m4x4 oPos, r0, c4 
mov oT0, v7 
mov oT1, v8 

No i na zakończenie należy uzupełnić rejestry wyjściowe, ponieważ inaczej oczywiście nie otrzymamy oczekiwanych 
efektów na ekranie. Co może zaskakiwać na samym początku to zawartość rejestru 

oD0

, czyli tego, który przechowuje w 

normalnym trybie kolor diffuse wierzchołków. Tutaj natomiast na pewno nie będzie on posiadał tej wartości, która w naszym 
przypadku po prostu zostanie zgubiona. I powinniśmy sobie przy okazji powiedzieć, że tak naprawdę to czas już powoli się 
odzwyczajać od koloru wierzchołków, którego nie powinno się używać do definiowania wyglądu bryły - o tym powinna 
decydować tekstura. Ale jak ktoś się uprze to trzeba by przekazać ten kolor po prostu jako wektor pamięci stałej i coś w tym 
względzie kombinować. Ale wracając do naszego przykładu - w rejestrze 

oD0

 znajdzie natomiast pewna kombinacja. 

Znormalizowany wektor światła przeniesiony do układu tekstury znajduje się w rejestrze tymczasowym 

r1

. Znormalizowany 

wektor może posiadać wartości od -1 do 1. My jednak będziemy potrzebowali tych wartości w zakresie od 0 do 1 - 

oD0

 

przecież zawiera kolory właśnie w takim zakresie. Żeby dokonać takiej konwersji wystarczy podzielić wartości z rejestru 

r1

 

przez 2 (pomnożyć przez 0.5) i dodać do tego także 0.5. Robimy to za pomocą instrukcji 

mad

, która najpierw mnoży rejestr 

r1

 przez 0.5 a następnie dodaje tę samą wartość do niego po wymnożeniu i umieszcza wynik w rejestrze wyjściowym 

oD0

Aby uzupełnić cały rejestr do części 

w

 rejestru wkładamy cokolwiek, nie będzie to miało znaczenia. Rejestry pozostałe 

uzupełniamy jak zwykle - wyjściową pozycję mnożymy przez macierz widoku i projekcji a rejestry tekstur po prostu 
przepisujemy z wyjścia, ponieważ akurat współrzędnych tekstur nie zmieniamy. No i w zasadzie jesteśmy już na samym 
końcu, ale jednocześnie jeszcze bardzo, bardzo daleko. Aby nam się jaśniej zrobiło po tych wszystkich kombinacjach teraz 
zilustrujemy sobie poszczególne etapy. Pierwszą i najważniejszą rzeczą było przeniesienie wektora światła do przestrzeni 
tekstury. Jak pamiętamy, umieściliśmy sobie ten wektor jako składowe koloru diffuse. Zobaczmy więc, jak to będzie 
wyglądało na bryle. W tym celu wyłączymy sobie wszystkie tekstury i inne kombinacje: 

case eDiffuse: 

    // shaders settings 
    g_lpD3DDevice->SetPixelShader( NULL ); 
    g_lpD3DDevice->SetVertexShader( g_lpVertexShader ); 
 
    // texture settings - pass diffuse to final stage 
    g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 ); 
    g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_DIFFUSE ); 
 
    g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_SELECTARG1 ); 
    g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_CURRENT ); 

break; 

Jak widać z podanego kawałka kodu nie mamy używanego tutaj Pixel Shadera. To powoduje, że znaczenia nabierają 
ustawienia poszczególnych poziomów tekstur. Jak pamiętamy na poziomie nr 0 mamy umieszczoną teksturę normalnych, 
natomiast na poziomie nr 1 teksturę właściwą obiektu. Na poziomie zerowym widać z ustawień, że wyjściowym kolorem 

background image

7 

DirectX ▪ Dot3 

będzie kolor diffuse a pamiętamy z shadera, że ten kolor oznacza nic innego, jak tylko przeniesiony wektor światła do układu 
tekstury. Z ustawień poziomu nr 1 wynika, że kolor wyjściowy będzie po prostu przeniesiony z poziomu 0, czyli nic nie 
zmieniamy. Czyli w końcowym efekcie dostaniemy kolor diffuse, który wychodzi z shadera. A wygląda to mniej więcej tak: 

 

Tak więc w każdym wierzchołku wektor światła jest obrócony do przestrzeni tekstury zgodnie z naszym życzeniem, resztę 
załatwia sposób wygładzania (Gourauda). Aby się jeszcze upewnić, czy tekstury są poprawnie umieszczone na poziomach i 
aby zobaczyć czy się poprawnie nakładają 

case eNormalMap: 

    // shaders settings 
    g_lpD3DDevice->SetPixelShader( NULL ); 
    g_lpD3DDevice->SetVertexShader( g_lpVertexShader ); 
 
    // texture settings - pass normal map to final stage 
    g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG2 ); 
    g_lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_TEXTURE ); 
 
    g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_SELECTARG1 ); 
    g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_CURRENT ); 

break; 

Aby zobaczyć nałożoną teksturę normalnych to trzeba z poziomu nr 0 jako wyjściowy kolor "wypuścić" kolor tekstury (nie 
mieszając go z kolorem diffuse wierzchołków ani z żadnym innym) a na poziomie nr 1 po prostu przepuścić to, co 
przychodzi z poziomu nr 0. A tak to wygląda na ekranie: 

 

case eDiffuseMap: 

background image

8 

DirectX ▪ Dot3 


    // shaders settings 
    g_lpD3DDevice->SetPixelShader( NULL ); 
    g_lpD3DDevice->SetVertexShader( g_lpVertexShader ); 
 
    // texture settings - pass diffuse map to final stage 
    g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_SELECTARG2 ); 
    g_lpD3DDevice->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_TEXTURE ); 

break; 

A teksturę właściwą obiektu można zobaczyć jeszcze prościej - po prostu z poziomu nr 1 wypuszczamy kolor tekstury, bez 
względu na to, co mieliśmy na poziomie nr 0. I na ekranie otrzymujemy: 

 

No i skoro wiemy, że wektor światła mamy przeniesiony, tekstury zarówno normalna jak właściwa dla obiektu nakładają się 
poprawnie nadszedł czas aby zobaczyć w końcu coś wypukłego na ekranie. Ale aby to zobaczyć do pracy musimy tym razem 
zaprzęgnąć do pracy Pixel Shader: 

ps.1.1 
 
; texture instructions 
tex    t0 
tex    t1 
 
; arithmetic instructions 
dp3_sat r1, v0_bx2, t0_bx2 
mul r0, r1, t1 

Można by rzecz, że zbyt okazały to on nie jest, ale jak już mieliśmy się okazję przekonać cała moc tkwi w jego rozkazach, bo 
za ich pomocą można robić naprawdę cuda. No ale popatrzmy, co będzie się tutaj działo. Na sam początek oczywiście 
tradycyjnie wersja, która podobnie jak w przypadku wierzchołków poprawna może być 1.1 (najniższa). Następnie 
deklarujemy rejestry wejściowe - w naszym przypadku rejestry tekstur, które będą wprowadzały do shadera dane tekstur z 
poziomów nr 0 i nr 1. Następnie mamy dosyć skomplikowany twór, który jest kombinacją kilku możliwych do zastosowania 
technik w Pixel Shaderze. Instrukcja jest oczywista - skoro wykorzystujemy technikę dot3 na kolorach, więc zastosowanie tej 
instrukcji właśnie tutaj jest jak najbardziej na miejscu. Jako parametry tej instrukcji mamy: - rejestr tymczasowy 

r1

, w 

którym umieszczamy rezultat operacji. - rejestr wejściowy 

v0

, w który umieszczony jest kolor diffuse, pochodzący z 

obliczeń w Vertex Shaderze. - rejestr tekstur 

t0

, w którym będą umieszczane kolejne wartości pochodzące z tekstury na 

poziomie nr 0, czyli mapy normalnych. Zanim jednak operacja dot3 zacznie działać w Pixel Shaderze należy odpowiednio 
przygotować dla niej dane. Jak widzimy na każdy z rejestrów wejściowych przed dokonaniem operacji jest nałożony 
modyfikator, który jak wiemy powoduje zmianę danych wchodzących do operacji. W obydwu przypadkach modyfikator jest 
ten sam - 

bx2

. Powoduje on rozkład wartości w rejestrze z przedziału ( 01 ) na przedział (-1, 1 ), a dokładniej mówiąc 

odejmuje od każdej wartości w rejestrze wartość 0.5 i mnoży otrzymany wynik przez 2. Pamiętamy również, że odwrotnej 
operacji dokonywaliśmy w Vertex Shaderze, przed wpisaniem znormalizowanego wektora światła do rejestru 

oD0

. Dlaczego 

więc takie kombinacje? Otóż wszystko przez operację dot3 a dokładniej mówiąc przez rozkaz 

dp3

. Aby otrzymać poprawne 

wyniki musimy mieć dane kolorów w zakresie (-11 ) a kolory wchodzące do Pixel Shadera jak wiemy są w zakresie ( 01 ). 
W Vertex Shaderze musieliśmy zamienić na zakres ( 01 ), ponieważ tak pracuje rejestr 

oD0

. Tutaj po prostu robimy 

background image

9 

DirectX ▪ Dot3 

odwrotnie, czyli wracamy do początku. Mając kolor diffuse i teksturę w zakresie (-11 ) robimy to, na co czekaliśmy tyle 
czasu - czyli dokonujemy operacji dot3 na kolorach. Da nam to w wyniku oczywiście jakiś kolor wyjściowy, który tak 
naprawdę będzie wektorem. Ale tak jak mówiliśmy od początku części teoretycznej - w mapie normalnych mamy zapisany 
wektor mówiący jaka w danym pikselu bryły jest normalna do powierzchni. Kolor diffuse przechowuje wektor światła w tej 
samej przestrzeni co mapa normalnych. I tak jak w prostym Vertex Shaderze liczącym oświetlenie wyliczaliśmy jego 
natężenie poprzez operację dot3 pomiędzy wektorem światła a normalną tak samo czynimy tutaj - tylko po wielu 
kombinacjach. Ale za to nie na poziomie wierzchołków a pikseli. Po dokonaniu operacji również mamy modyfikator, tylko 
trochę inny. Dokonuje on obcięcia wyjściowej wartości do przedziału ( 01 ). W wyniku dostaniemy poziom koloru od 
czarnego do białego, bez żadnych niespodzianek. 
Ale to jeszcze nie koniec, ponieważ jak pamiętamy o dyspozycji mamy dwa poziomy tekstur. Jeden już wykorzystaliśmy, 
ponieważ dokonaliśmy operacji dot3 pomiędzy pikselami a kolorem diffuse wierzchołków, który zawiera odpowiednio 
przekształcony wektor światła. Po tej operacji będziemy mieli już wypukły obiekt - zobaczymy to zaraz na przykładowych 
rysunkach. Aby pokryć wypukły obiekt końcową teksturą wystarczy pomnożyć rezultat poprzedniej operacji (mamy już ten 
rezultat w przedziale ( 01 ) ) przez drugi rejestr tekstur (wtedy je zmieszamy), umieścić rezultat w rejestrze wyjściowym 
Pixel Shadera 

r0

 i to w zasadzie wszystko. Żeby jednak jeszcze się nie zgubić zobaczmy jak to będzie wyglądało: 

case eBump: 
case eFinal: 

    // shaders settings 
    g_lpD3DDevice->SetPixelShader( g_lpPixelShader ); 
    g_lpD3DDevice->SetVertexShader( g_lpVertexShader ); 

break; 

Oczywiście aby zobaczyć końcowy efekt musimy włączyć używanie Pixel Shadera. Inne ustawienia urządzenia w tym 
ustawienia poziomów tekstur co do typów operacji i argumentów w zasadzie nie mają znaczenia, bo o wszystkim decyduje 
tutaj Pixel Shader. Aby pokazać efekt zarówno samego obiektu wypukłego jak i z nałożoną teksturą właściwą posłużę się 
pewną sztuczką z teksturami. Aby za dużo nie mieszać i posługiwać się tylko jednym Pixel Shaderem, w przypadku 
pokazywania tylko obiektu wypukłego zamiast tekstur końcowych mieszam obiekt wypukły w teksturą dokładnie białą, co 
zupełnie nie zmienia rezultatu mnożenia. Wtedy widać ładnie na ekranie tylko obiekt wypukły, o właśnie tak:  

 

No a końcowy obiekt wypukły z nałożoną teksturą finalną wygląda... hmm... trochę chyba mniej efektownie no bo tekstura 
końcowa "zagłusza" nieco tę wypukłość. Jest bardzo kolorowa no i efekt nie jest taki, jakiego można by oczekiwać. Można 
by to regulować nieco natężeniem wypukłości lub próbować nieco głębszej mapy wypukłości no ale to już jak komu potrzeba 
i się podoba. No ale chyba coś tam widać na końcowym ekranie:  

background image

10 

DirectX ▪ Dot3 

 

I to byłoby wszystko, jeśli chodzi o mapowanie wypukłości z użyciem operacji dot3. Nie muszę chyba mówić jak ogromną 
oszczędność w ilości użytych wierzchołków daje ta metoda - nadrabiamy czasami nawet tysiące wierzchołków za pomocą 
jednej, nawet niewielkiej tekstury! Jeśli mamy zdolnego grafika, który wie jak wykorzystać moc map normalnych to 
naprawdę efekty mogą być oszałamiające. Mam nadzieję, że zrozumiałe jest dla was większość przykładu, jeśli nie wszystko 
;P. Jeśli macie jakieś wątpliwości to oczywiście nie wahajcie się pytać a postaram się wszystko wyjaśnić. W przykładowej 
aplikacji możecie się pobawić klawiszami zgodnie z opisem aby zobaczyć wszystkie omawiane przeze mnie fazy i zobaczyć 
wszystkie pokazywane dzisiaj obrazki.