background image

1 

DirectX ▪ Vertex shader 2 

Witam w dzisiejszej, mam nadzieję bardzo ciekawej lekcji. Od teorii do praktyki czyli w sumie prawidłowo przejdziemy 
sobie dzisiaj na przykładzie najprostszego vertex shadera. Teorię mamy już mam nadzieję w małym palcu i doskonale 
rozumiemy o co w tym wszystkim chodzi, więc czas przystąpić do poważnego działania. Kod w większości oczywiście 
doskonale znamy, więc naprawdę szkoda czasu na powtarzanie tego samego w kółko. Tradycyjnie więc opiszemy tylko to, 
co będzie dla nas ważne. 

 

// A structure for our custom vertex type 
struct CUSTOMVERTEX 

  FLOAT x; 
  FLOAT y; 
  FLOAT z; 
  DWORD color; 
  FLOAT tx; 
  FLOAT ty; 
}; 
 
#define D3DFVF_CUSTOMVERTEX ( D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 ) 
 

To oczywiście wiemy doskonale czym jest, ale ponieważ ma to ogromne znaczenie dla naszych późniejszych działań, więc 
warto zaznaczyć. Deklarujemy strukturę, która będzie określała, jakie dane będziemy przekazywać w naszym wierzchołku. U 
nas będą to: współrzędne (konieczne oczywiście), kolor wierzchołków (wskazany) no i współrzędne tekstur (dla bajeru). 
Następnie dyrektywą 

#define

 składamy nasz typ z typów podstawowych. 

 

// vertex shader declarator 
DWORD dwDecl[] = 

  D3DVSD_STREAM(0), 
  D3DVSD_REG( D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), 
  D3DVSD_REG( D3DVSDE_DIFFUSE, D3DVSDT_D3DCOLOR ), 
  D3DVSD_REG( D3DVSDE_TEXCOORD0, D3DVSDT_FLOAT2 ), 
  D3DVSD_END() 
}; 
 

Powiedzieliśmy sobie w poprzednim tutorialu o deklaratorze vertex shadera, który określa powiązanie strumienia danych z 
rejestrami wejściowymi. Deklarator powinien oczywiście ściśle odpowiadać deklaracji naszego typu dla wierzchołków. 
Znaczy to, że vertex shader powinien pobierać ze strumienia wszystkie dane dla danego wierzchołka, które przychodzą 
taśmociągiem. Dane płyną jednym strumieniem, więc, aby rozróżnić poszczególne wierzchołki, koniecznym staje się 
odbieranie paczek ściśle odpowiadających rozmiarowi naszej struktury do wierzchołków. Tak naprawdę to deklarator 
omówiliśmy sobie już poprzednim razem, więc tylko małe przypomnienie. 

 

D3DVSD_STREAM(0) 
 

Makro, które wygeneruje taką liczbę 

DWORD

 i umieści ją w tablicy deklaratora, że shader analizując tę tablicę zobaczy: 

dane napływać będą strumieniem numer 0, więc ustaw wejścia, tak żeby stamtąd pobierać poszczególne paczki i wrzucaj 
odpowiednie wartości do odpowiednich rejestrów wejściowych. 

 

D3DVSD_REG( D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), 
D3DVSD_REG( D3DVSDE_DIFFUSE, D3DVSDT_D3DCOLOR ), 
D3DVSD_REG( D3DVSDE_TEXCOORD0, D3DVSDT_FLOAT2 ), 
 

Te trzy makra powiążą nam rejestry wejściowe z danymi płynącymi strumieniem. Zanalizujmy szczegółowiej. Kombinacja 
stałych 

D3DVSDE_POSITION

 i 

D3DVSDT_FLOAT3

 mówi tak: słuchaj shader, płynie sobie strumień. Ty się zaczaj i 

kiedy pojawi się paczka danych, pobierz z niej tyle bajtów, aby wystarczyło na trzy liczby float i skopiuj te bajty do rejestru 
numer 0. Ponieważ w naszej strukturze te pierwsze trzy liczby to 

float

 i są to współrzędne wierzchołka a rejestr 0 jest to 

rejestr odpowiedzialny za pozycję wierzchołków, więc właściwe dane we właściwe miejsce - ujmując krótko i treściwie. 
Następna para stałych 

D3DVSDE_DIFFUSE

 i 

D3DVSDT_D3DCOLOR

 instruuje shader w sposób następujący. Cztery 

bajty ze strumienia weź i umieść je w rejestrze numer 4. Patrzymy w naszą strukturę - faktycznie, 

DWORD

 czyli cztery bajty 

popłyną strumieniem i będą one oznaczać kolor wierzchołków, należy je więc umieścić w rejestrze odpowiedzialnym za 
kolor. Ostatnia para stałych z tej serii, czyli 

D3DVSDE_TEXCOORD0

 i 

D3DVSDT_FLOAT2

 to, jak łatwo się już 

domyśleć, umieszczenie bajtów odpowiedzialnych za współrzędne tekstury w rejestrze numer 6. Dokładny opis stałych i ich 
przypisanie do rejestrów znajdziecie w dokumentacji do makra 

D3DVSD_REG

.

 

 
D3DVSD_END() 
 

background image

2 

DirectX ▪ Vertex shader 2 

Ponieważ tyle bajtów, ile już zadeklarowaliśmy, w pełni zapełnia naszą paczkę, czas zakończyć nasz deklarator. To makro 
tworzy specjalną wartość (0xFFFFFFFF), która oznacza dla vertex shadera koniec deklaracji i po tym pozostaje nam już 
tylko zamknąć naszą tablicę. Wrzucania wierzchołków do bufora, inicjalizacji urządzeń i tym podobnych rzeczy nie 
wałkujemy zgodnie z umową i przystępujemy od razu do działania, czyli tworzymy nasz vertex shader.

 

 
bool CreateVertexShader() 

  LPD3DXBUFFER pCode; 
  LPD3DXBUFFER pError; 
  HRESULT hError; 
 
  hError = D3DXAssembleShaderFromFile( "shader.vsh", 0, NULL, &pCode, &pError ); 
  // plik << (char*)( pError->GetBufferPointer() ) << endl; 
  g_pd3dDevice->CreateVertexShader( dwDecl, (DWORD*)pCode->GetBufferPointer(), 
&VertexShader, 0 ); 
  pCode->Release(); 
 
  return true; 

 

Aby utworzyć vertex shader, posłużymy się jedną z metod urządzenia renderującego, widoczną we fragmencie kodu 
przedstawionym powyżej. Metoda 

CreateVertexShader()

 utworzy nam długo już oczekiwany obiekt, ale pod kilkoma 

ważnymi warunkami. Jak widać, liczbą parametrów przyjmowanych nie odbiega ona od standardu Direct3D ;-) i nie będzie 
nam łatwo. Pierwszym parametrem tej metody będzie tablica wartości 

DWORD

, która określa deklarator naszego shadera. 

Co to deklarator już doskonale wiemy i rozumiemy, wałkowaliśmy to mam nadzieję wystarczająco długo? Następnym 
parametrem jest wskaźnik do miejsca w pamięci, gdzie znajdują się wywołania instrukcji vertex shadera. Tłumacząc to na 
nasze, jest to miejsce w pamięci, gdzie znajduje się nasz skompilowany kod. Trzeci parametr to zmienna, która będzie 
oznaczać naszego shadera - nic prostszego, natomiast czwarty określa sposób działania naszego shadera - jak na razie jest 
dostępny tylko jeden, domyślny, więc wpiszmy tam zero. My natomiast zajmijmy się teraz kodem... 
Ponieważ już za momencik ujrzymy źródło naszego pierwszego shadera, musimy powiedzieć o jeszcze jednej rzeczy. 
Ponieważ mamy kod źródłowy, więc nieuniknione jest to, że będziemy musieli go kiedyś skompilować. Pytanie tylko - skąd 
wziąć kompilator??? Otóż odpowiedź jest bardzo prosta... kompilatora nie ma. Ktoś zapyta - no to co teraz? Ano, jak zwykle 
rozwiązanie jest bliżej niż się wydaje. Zawołajmy na pomoc nasz wspaniały pakiet i jego ogromnie pożyteczną bibliotekę 

D3DX

. I cóż takiego możemy tam znaleźć? Szperając w dokumentacji możemy się natknąć na kilka funkcji, które... 

kompilują nasz kod! Strasznie to wszystko pokręcone, ale nie martwmy się. Musimy popatrzeć na to z trochę wyższego 
poziomu abstrakcji. Tak naprawdę kompilator mamy. Nie będzie to jednak taki, jaki znamy z naszej codziennej praktyki. Dla 
nas kompilator to wbudowana w Direct3D funkcja, która pobierając nasz kod źródłowy będzie potrafiła sprawić, że pojawi 
się on gdzieś w pamięci jako coś użytecznego dla naszego shadera. Za pomocą biblioteki 

D3DX

 można kompilować nasz 

kod na dwa sposoby. W pierwszym trzeba gdzieś w pamięci umieścić nasze źródło, określić miejsce gdzie on jest i do dzieła. 
Proponuję przyjrzeć się bliżej funkcji 

D3DXAssembleShader()

. Ponieważ miotanie się z kodem i błąkanie się z nim po 

pamięci jest trochę niewygodne, możemy wykonać sobie inny manewr. Możemy sobie napisać naszego shadera w zupełnie 
oddzielnym pliku! Biblioteka daje nam do ręki funkcję, która nie chce miejsca w pamięci, gdzie znajduje się kod, ona chce 
mieć kod czarno na białym, wypisany w pliku. Funkcją tą jest, jak widzicie, 

D3DXAssembleShaderFromFile()

. Ona też 

powoduje kompilację naszego kodu, tylko tak, jak napisałem wcześniej, ma tę zaletę, że robi to bezpośrednio z pliku 
tekstowego. Dlaczego jest to zaleta? Załóżmy, że mamy ogromny projekt (tysiące plików) i mnóstwo różnych shaderów, 
których używamy dosyć gęsto. Jeśli napiszemy kod naszych shaderów bezpośrednio w kodzie źródłowym aplikacji, to 
będziemy musieli: 

•  po pierwsze błądzić w gąszczu plików i katalogów, jeśli jesteśmy bałaganiarzami a jesteśmy nimi na pewno ;-),  

•  po drugie, pliki źródłowe aplikacji mogą być ogromnie długie i szukanie po kodzie naszego interesującego kodu nie 

jest zbyt frapującym zajęciem.  

Popatrzmy teraz co będzie jeśli dla przykładu wszystkie nasze shadery zgromadzimy w oddzielnym katalogu. Znajdziemy je 
od razu, bo wiemy, gdzie są. Poprawek dokonamy nie szarpiąc się z tysiącami linii kodu (przypominam, że każdy shader to 
maksimum 128 instrukcji!). Po prostu żyć nie umierać. To rozwiązanie ma jednak pewną, dosyć istotną wadę. Plik z 
shaderem musi zostać dołączony jako jeden z plików rozprowadzanym wraz z aplikacją i niewątpliwie ktoś będzie mógł 
przeanalizować sobie nasz shader. Jeśli umieścimy kod naszego shadera w programie, to po jego skompilowaniu kod binarny 
jest oczywiście umieszczany w kodzie aplikacji, więc nie musimy dołączać żadnych plików do naszego programu 
wykonywalnego. Ponieważ my na razie się uczymy, napiszemy oddzielny plik i będziemy go kompilować funkcją 

D3DXAssembleShaderFromFile()

. Pobiera ona pięć argumentów. Pierwszym z nich jest ścieżka do naszego pliku z kodem 

shadera - chyba nie wymaga komentarza? Druga określa sposób kompilacji naszego dzisiejszego bohatera. Otóż wiadomo, że 
jak w każdym kodzie źródłowym nie uda się od razu napisać doskonałego programu bez pomyłek. Aplikacje kompiluje się w 
fazie powstawania w trybie debuggera, umożliwiającym śledzenie programu krok po kroku i wychwytywanie błędów. Ale 
chyba o posługiwaniu się debuggerem nie muszę Wam pisać? Aby nie być gorszym, kompilator shaderów też umożliwia 
wstawienie do kompilowanego kodu informacji, które mogą być pomocne przy debugowaniu. A jak go debugować to może 

background image

3 

DirectX ▪ Vertex shader 2 

później ;-). Trzy ostatnie parametry są typu 

LPD3DXBUFFER

. Typ ten jest, jak łatwo zapewne się domyśleć, rodzajem 

bufora, w którym można składować dane... ot, taki kawałek pamięci zarządzany przez obiekt. Obiekt ten ma metody 
pozwalające na odczyt danych i pobieranie rozmiaru bufora. Ale o tym to przy okazji. Wracając do kompilatora, pierwszy z 
trzech buforów zawierać powinien deklaracje stałych dla shadera (umieszczanych w rejestrach pamięci stałej). My jednak na 
razie odpuścimy go sobie i ustawimy go na 

NULL

 - w takim przypadku będzie po prostu ignorowany. Druga zmienna tego 

typu w kolejności określa bufor, gdzie znajdował się będzie poszukiwany przez nas długo i wytrwale kod binarny 
(skompilowany). Jak nietrudno zauważyć, ta sama zmienna posłuży nam przy wywołaniu metody do tworzenia shadera. Kod, 
który jest kompilowany, zostanie umieszczony w buforze w pamięci i stamtąd będzie pobierany w czasie wykonywania sie 
funkcji shadera. Jak doskonale wiemy, podczas kompilacji mogą wystąpić błędy składni - zwłaszcza, że jesteśmy jeszcze 
bardzo niedoświadczeni, jeśli chodzi o ten wynalazek. Ponieważ nasz kompilator działa w dosyć specyficzny sposób, więc 
nie mamy bezpośrednio widocznych rezultatów kompilacji. Ale wcale nie oznacza to, że stoimy na straconej pozycji. 
Ostatnim argumentem funkcji, która przeprowadza kompilację kodu shadera jest wskaźnik do bufora, który, jak sama nazwa 
wskazuje, będzie zawierał łańcuchy znaków (komunikaty), które zostaną nam dostarczone podczas kompilacji przez naszą 
funkcję. Załóżmy, że kod skompilował nam się bez błędów. Jego binarna postać, gotowa do wykorzystania znajduje się w 
buforze, którym zarządzamy dzięki obiektowi 

LPD3DXBUFFER

. Jak dobrać się do naszego kodu, aby móc przekazać go 

do funkcji tworzącej shader? Przyjrzyjmy się jej wywołaniu jeszcze raz: 

 

g_pd3dDevice->CreateVertexShader( dwDecl, (DWORD*)pCode->GetBufferPointer(), 
&VertexShader, 0 ); 
 

Wszelkie instrukcje wchodzące w skład funkcji shadera po kompilacji mają postać 4-bajtowych bloczków, podobnie jak 
wszelkie deklaracje w jego deklaratorze. Jako drugi parametr metody urządzenia tworzącej shader na podstawie 
skompilowanego kodu podawać się powinno wskaźnik do pamięci, w której znajdują się takie czterobajtowe bloczki. U nas 
pamięć ta jest obsługiwana poprzez obiekt typu 

LPD3DXBUFFER

 o nazwie pCode. Ale podanie adresu samego obiektu 

wcale nie oznacza, że dobierzemy się do obsługiwanej przez niego pamięci! Jak już nadmieniałem wcześniej, obiekt bufora 
obecny w bibliotece 

D3DX

 zawiera pewne metody, które pozwolą nam na dostęp do tej pamięci. I jak na dłoni widać, co to 

za metoda. Wynikiem działania metody 

GetBufferPointer()

 jest adres do początku pamięci (bufora), gdzie znajdują się 

obsługiwane przez niego dane, czyli w tym przypadku nasz kod. 
 
Ufff! Mam nadzieję, że wszystko jasne ;-). W zasadzie można by przystąpić do omawiania kodu, ale musimy powiedzieć 
sobie o jeszcze jednej, ważnej rzeczy. Będzie nią wypełnianie rejestrów pamięci stałej. Pisałem wcześniej, że w rejestrach 
tych będziemy mogli wstawić sobie cokolwiek, co będzie nam potrzebne do działania naszego shadera. Teraz właśnie 
powiem, co my wstawimy sobie u nas. To, że użyjemy nowego, specyficznego vertex shadera wcale nie zwalnia nas od 
przeprowadzenia transformacji przez macierze, a wręcz przeciwnie - nakłada na nas obowiązek przeprowadzenia tego 
wyjątkowo uważnie! Jakoś więc trzeba będzie przekazać naszemu shaderowi nasze macierze, które obliczymy bardzo dobrze 
znanymi nam funkcjami z biblioteki 

D3DX

. Jak już łatwo się domyśleć, przekazanie tych wartości odbędzie się za pomocą 

rejestrów pamięci stałej. Po kompilacji, shader będzie się odwoływał podczas wykorzystywania go (w funkcji Render()) do 
rejestrów pamięci stałej, które możemy zmieniać w trakcie działania naszego programu. Kompilacja wcale nie oznacza, że 
zostaną w tych rejestrach umieszczone wartości na stałe i już ich nie będzie można zmieniać. Będziemy mogli to robić za 
pomocą metody urządzenia zwanej 

SetVertexShaderConstant()

. Cóż takiego będzie robić ta metoda? Jako pierwszy 

parametr przyjmuje ona numer rejestru, od którego zapisujemy określoną wartość. Jaka to będzie wartość i od którego 
rejestru to już zależy tylko i wyłącznie od nas, tutaj panuje całkowita swoboda. Dlaczego napisałem "od" a nie "do"? Jak 
pisałem wcześniej, rejestry przechowują wektory po cztery liczby typu 

float

. Jako drugi argument nasza metoda przyjmuje 

adres pamięci, w której znajdują się dane, które mamy zamiar zapisać do rejestru, jako parametr trzeci przekazujemy zaś 
ilość stałych jakie mamy zamiar zapisać w naszych rejestrach. Wartość 1 oznaczać będzie jedną stałą, ale w postaci jednego 
wektora zawierającego cztery liczby 

float

! Zapamiętajmy to bardzo dokładnie, bo wiele razy będziemy z tego korzystać. Jeśli 

więc na przykład wywołamy naszą metodę w następujący sposób: 

 

g_pd3dDevice->SetVertexShaderConstant( 0, adres, 4 ); 
 

Oznaczać to będzie, że ładujemy dane od rejestru numer 0, z pamięci o adresie zawartym w zmiennej "adres" i stałych będzie 
cztery, czyli cztery wektory po cztery liczby typu 

float

. Jeśli teraz wywołalibyśmy naszą metodę kolejny raz i jako rejestr 

startowy podali 1, to oczywiste jest chyba, że nadpiszemy sobie wartości w rejestrze numer 1, które zostały zapisane 
poprzednim wywołaniem tej metody. Jeśli zapisujemy więcej niż jedną stałą (w postaci wektora liczb), to zostanie zapisane 
tyle kolejnych rejestrów w pamięci stałej, ile wektorów podajemy jako stałe. Tak więc jeśli podamy cztery, to zostaną 
zapisane rejestry o oznaczeniach 

c0

c1

c2

 i 

c3

. Aby nic nam się nie pokręciło, należy następne wywołanie metody 

SetVertexShaderConstant()

 zacząć od numeru rejestru numer 4 a nie od 1. Jeśli będziemy zapisywać po jednej stałej, nic 

takiego oczywiście nam nie grozi. Dlaczego o tym piszę? Otóż nader często zdarzy nam się zapisywać do rejestrów stałych 
macierze przekształceń (świata, widoku i rzutowania) i, aby sobie nie zaciemniać kodu oraz oszczędzać instrukcje asemblera, 
będziemy to robić za jednym zamachem. Tak też będzie w naszym przykładzie, co zobaczymy zaraz w kodzie naszego 
shadera. 
 
A co będzie jeśli w kodzie naszego shadera wystąpią błędy składniowe, uniemożliwiające kompilację? Wtedy, jeśli nasz 
program będzie miał postać z naszego przykładu, na pewno się wywali. To może i nawet lepiej bo od razu będziemy 
wiedzieć, że coś jest nie tak. A jeśli tak, to zmienna pError będzie obiektem, który będzie zarządzał buforem, w którym to z 

background image

4 

DirectX ▪ Vertex shader 2 

kolei znajdować się będą komunikaty o błędach, jak wspomniałem wyżej. O funkcji 

GetBufferPointer()

 już zdążyłem 

wspomnieć, więc wystarczy tylko popatrzeć teraz na zakomentowane wywołanie naszej funkcji w kodzie. W naszym 
przykładzie wrzucimy sobie wszystkie komunikaty do pliku. Wy, jeśli chcecie, możecie napisać sobie bardziej 
skomplikowaną obsługę błędów, możecie je na przykład wyświetlać na ekranie w postaci okienka a może nawet uda Wam 
się je wrzucić gdzieś w kompilator główny? (specjaliści od VC mają pole do popisu ;-). No ale w naszym kodzie na pewno 
nie ma błędów i bez obaw możemy przystąpić do naszej analizy. 
Nadszedł więc długo wyczekiwany moment, teraz ujrzycie najprostszy shader, który jeśli sądzić po efektach wizualnych... 
nie będzie robił w zasadzie nic szczególnego. Wszystko będzie wyglądać po staremu. Dlaczego tak? Chcę Wam pokazać 
ogólne idee pisania a na efekty na pewno przyjdzie czas. Ale żeby Waszej cierpliwości nie wystawiać już na dłuższą próbę, 
przystąpmy do działania. Oto nasze arcydzieło:

 

 
vs.1.0 
 
; c0 - c4 = world matrix 
; c4 - c8 = view matrix 
; c8 - c12 = projection matrix 
 
; ----------------------- 
; vertex transformations 
; ----------------------- 
 
m4x4 r0, v0, c0 ; world matrix 
m4x4 r1, r0, c4 ; view matrix 
m4x4 r2, r1, c8 ; projection matrix 
 
; ----------------------- 
; effects 
; ----------------------- 
 
; ----------------------- 
; output result to screen 
; ----------------------- 
 
mov oPos, r2 ; Emit the output 
mov oD0, v5 ; The constant color 
mov oT0, v7 ; Output texture coordinates 
 

I co powiecie? Strasznie wygląda? Nie da się ukryć, że jest to asembler. A że jest asembler, to też Ci, którzy kiedyś pisali w 
tym świetnym języku wiedzą, że średnik na początku linii oznacza komentarz. I tak naprawdę jeśli się przyjrzeć naszemu 
shaderowi i wyrzucić linie z komentarzem to zostanie raptem... 7 linijek. Jak na coś, co ma zrewolucjonizować całą grafikę 
3D, to trochę mało, prawda? ;-) A skoro mało, to nie ma na co czekać i przystępujemy od razu do omawiania:

 

 
vs.1.0 
 

Znamy już rozkazy, więc wiemy, że akurat ten służy do definiowania numeru wersji naszego shadera. Rozkaz ten musi być 
obecny w każdej funkcji i musi także występować jako pierwszy, przed wszystkimi innymi. Ponieważ jest to nasz pierwszy 
shader, więc pozwoliłem sobie nadać mu roboczą wersję 1.0.

 

 
; c0 - c3 = world matrix 
; c4 - c7 = view matrix 
; c8 - c11 = projection matrix 
 

Zapewne ktoś zapyta po co omawiam komentarz. Otóż dobry zwyczajem i to w każdym języku a szczególnie w asemblerze 
jest komentowanie kodu. Nie musimy spędzać potem godzin nad naszymi wypocinami i domyślać się, o co chodziło 
autorowi (czyli nam :-). W vertex shaderach przyda nam się to szczególnie mocno, ponieważ będziemy stosować tutaj wiele 
różnych podchwytliwych sztuczek, bez opisu których nie bylibyśmy w stanie się domyślić o co chodzi. Ale wracając do 
sprawy... Przed rozpoczęciem omawiania kodu napisałem o ustawianiu rejestrów pamięci stałej. Wprawdzie jeszcze nie było 
kodu, który to robi, ale będziemy wiedzieć po co i jak to robimy. Musimy posłużyć się tutaj trochę naszą wyobraźnią. 
Ponieważ, jak nadmieniłem, wcale nie jesteśmy zwolnieni z obowiązku transformowania naszych wierzchołków przez 
macierze przekształceń, więc gdzieś tam w programie my sobie nasze macierze utworzymy (jak zawsze z resztą). Tylko, że 
tym razem nie przekażemy ich urządzeniu, ale naszemu shaderowi, aby on sam mógł dokonać na nich przekształceń. I jak 
wiemy, macierze te przekażemy mu za pomocą metody 

SetVertexShaderConstant()

. Jak zobaczymy później, w naszym 

programie, którego omawianie będziemy na pewno kontynuować, macierze te policzymy sobie w tradycyjny sposób i 
przekażemy do shadera, jak pisałem wyżej. Tutaj, w komentarzu mamy napisane, jaka macierz znajduje się w jakich 

background image

5 

DirectX ▪ Vertex shader 2 

rejestrach. I tak mamy po kolei: cztery wiersze macierzy świata znajdują się jako cztery stałe w rejestrach od 

c0

 do 

c3

macierz widoku to stałe w rejestrach 

c4

 do 

c7

 no i macierz projekcji to 

c8

 do 

c11

.

 

 
m4x4 r0, v0, c0 ; world matrix 
m4x4 r1, r0, c4 ; view matrix 
m4x4 r2, r1, c8 ; projection matrix 
 

Wróćmy teraz na chwilę do naszego deklaratora. Tam przypisaliśmy rejestrom wejściowym odpowiednie dane ze strumienia. 
Powiedzieliśmy sobie, że współrzędne wierzchołków będą znajdować się w rejestrze wejściowym odpowiedzialnym za 
pozycję nazywanym 

v0

. Trąbiliśmy już także o przekształcaniu naszych wierzchołków. Pamiętamy na czym polegało takie 

działanie? Jeśli nie, to przypominam - mnożyliśmy współrzędne każdego wierzchołka przez trzy macierze, właśnie te, o 
których cały czas teraz mówimy. Cóż nam więc pozostało innego? Skoro będziemy mieć współrzędne wierzchołków w 
rejestrze 

v0

 a macierze w dobrze nam znanych już (z komentarza choćby), możemy przystąpić do mnożenia: 

 

m4x4 r0, v0, c0 ; world matrix 
 

Jak pamiętamy, instrukcja 

m4x4

 to makro, którego rozwinięcie znajdziecie w dokumentacji do SDK. Ja powiem tylko tyle, że 

służy ono do pomnożenia naszego wektora (czterech wartości typu 

float

) przez macierz 4x4. Wektor wejściowy podajemy 

jako argument nr 2. Macierz wejściowa to parametr nr 3. Wynik znajdzie się w rejestrze podanym jako argument pierwszy. I 
jak widzimy, wektorem wejściowym będzie rejestr 

v0

 czyli de facto współrzędne naszego wierzchołka. Macierzą, przez 

którą mnożymy - macierz znajdująca się w rejestrach od 

c0

 do 

c3

, czyli macierz świata. Wynik znajdzie się w rejestrze 

tymczasowym 

r0

. Dlaczego tam? Moglibyśmy w zasadzie umieścić go w dowolnym rejestrze oznaczonym jako r + numer. 

Wykorzystujemy po prostu pierwszy wolny. Ktoś może zadać pytanie czemu nie do rejestru wyjściowego 

oPos

. Jak 

pamiętacie, rejestr ten jest tylko do zapisu - nie można z niego odczytywać! Jeśli więc tam zapiszemy naszą wartość, to 
utknie tam już na zawsze i nie będziemy jej mogli użyć do dalszych działań. A przecież pomnożenie przez macierz świata 
współrzędnych naszego wierzchołka zupełnie nam nie wystarcza, prawda? Trzeba przepuścić to, co wyjdzie z działania 
Pozycja * macierz świata przez następne przekształcenia. Robimy więc to dalej, mając naszą tymczasową daną w rejestrze  
 

r0

:

 

m4x4 r1, r0, c4 ; view matrix 
 

czyli mnożymy wierzchołek przekształcony przez macierz świata (w 

r0

) przez macierz widoku, która, wedle naszej wiedzy 

wyniesionej z komentarza i programu, znajduje się w rejestrach 

c4

 do 

c7

. Wynik umieszczamy dla porządku w rejestrze 

r1

Wywołania ostatniej instrukcji już chyba nie muszę Wam tłumaczyć ? W każdym razie, ostateczny wynik znajdzie się na 
razie w rejestrze 

r2

. Znowu ktoś zapyta, dlaczego nie w 

oPos

? Przecież już przekształciliśmy sobie wierzchołek przez 

macierze i to w zasadzie koniec. Jeśli chce, oczywiście, może sobie umieścić rezultat w 

oPos

, nikt mu nie zabroni. My 

jesteśmy eleganccy, mamy nadmiar instrukcji, więc zrobimy to na samym końcu. 

 

mov oPos, r2 ; Emit the output. 
mov oD0, v5 ; The constant color. 
mov oT0, v7 ; Output texture coordinates 
 

Ponieważ na razie nie będziemy wymyślać żadnych bajeranckich efektów (to Wasze zadanie ;-), więc w zasadzie 
moglibyśmy wyświetlić nasze dane na ekranie, zobaczymy co z tego wyjdzie. Aby Direct3D mógł cokolwiek wyświetlić, 
należy oddać mu przekształcone przez verex shader wierzchołki oczywiście. Robimy to wrzucając nasze dane do rejestrów 
wyjściowych. Rejestr 

oPos

 powinien zawierać policzone pozycje wierzchołków. U nas, w programie shadera na razie 

znajdują się one w rejestrze 

r2

. Wrzucamy je więc do rejestru 

oPos

 intrukcją 

mov oPos, r2

. Z definicji deklaratora 

pobieramy ze strumienia również kolor wierzchołka. Jest on pobierany i umieszczany w rejestrze wejściowych oznaczonym 
jako 

v5

. Ponieważ nic nie będziemy robić na razie z kolorem wierzchołków, więc my po prostu przepiszemy go sobie z 

wejścia na wyjście, będzie on "przepuszczony" niejako przez shader bez czepiania się go zupełnie. Podobnie uczynimy także 
ze współrzędnymi tekstury. Ze strumienia trafią one do rejestru oznaczonego jako 

v7

 a stamtąd prosto na wyjście, do rejestru 

oT0

. I to w zasadzie koniec. Prawda, że było to proste? Jak uruchomić program, to w zasadzie nie widać żadnej różnicy w 

stosunku do tego, co robiliśmy do tej pory. I prawidłowo, bo tak jak sobie powiedzieliśmy, my z wierzchołkami nie robimy 
nic prócz tego, że przekształcamy je przez macierze. Ktoś spostrzegawczy oczywiście zaraz zawoła - przecież nasz sześcian 
się obraca! Więc coś jednak musi się dziać. I będzie miał całkowitą rację. Sześcian obraca się, a dlaczego?

 

 
void SetupFrame() 

  D3DXMATRIX mat; 
 
  D3DXMatrixRotationX( &matWorldX, timeGetTime()/1500.0f ); 
  D3DXMatrixRotationY( &matWorldY, timeGetTime()/1500.0f ); 
  mat = matWorldX * matWorldY; 
 
  g_pd3dDevice->SetVertexShaderConstant(0, D3DXMatrixTranspose(&mat, &mat), 4); 

background image

6 

DirectX ▪ Vertex shader 2 

  g_pd3dDevice->SetVertexShaderConstant(4, D3DXMatrixTranspose(&mat, &matView), 4); 
  g_pd3dDevice->SetVertexShaderConstant(8, D3DXMatrixTranspose(&mat, &matProj), 4); 

 

Cóż to za funkcja. Ano będzie ona wywoływana za każdym wywołaniem funkcji Render(). Będzie ona służyć do... no 
właściwie do czego? Jak nie wiemy, to omawiamy. Ponieważ, jak widać w przykładowym programie, sześcian się obraca, 
więc czymś to trzeba robić. W poprzednich przykładach obracaliśmy cały świat za pomocą zmiany macierzy świata. Nie 
inaczej będzie i w tym przypadku. Będziemy sobie obracać macierz świata o odpowiedni kąt. Kąt ten będzie się zmieniał 
wraz z czasem. Numer ten jest nam oczywiście doskonale znany i dosyć oczywisty, więc nie będziemy mu poświęcać zbyt 
wiele uwagi. Ciekawsze natomiast będzie ładowanie rejestrów pamięci stałej naszego shadera.

 

 
g_pd3dDevice->SetVertexShaderConstant(0, D3DXMatrixTranspose(&mat, &mat), 4); 
g_pd3dDevice->SetVertexShaderConstant(4, D3DXMatrixTranspose(&mat, &matView), 4); 
g_pd3dDevice->SetVertexShaderConstant(8, D3DXMatrixTranspose(&mat, &matProj), 4); 
 

Metodę już opisywałem i wiemy doskonale jak działa. Niepokój może budzić jedynie wywołanie groźnie wyglądającej 
funkcji z biblioteki 

D3DX

 - 

D3DXMatrixTranspose()

. Po cóż nam to takiego. Funkcja ta dokonuje transpozycji macierzy, 

czyli zamiany kolumn z wierszami, wiersze stają się kolumnami, kolumny wierszami. Dlaczego to robimy? Całe zamieszanie 
bierze się ze sposobu, w jaki przedstawiamy macierz, wektor i jak działa pewna instrukcja 

m4x4

 shadera. Chodzi o to, aby po 

przekazaniu shaderowi macierzy oraz wierzchołka znajdowały się one w odpowiedniej do siebie relacji jeśli chodzi o układ 
kolumn i wierszy w obu przypadkach. No i oczywiście o instrukcję shadera. A tak się składa, że omawiana instrukcja do 
mnożenia wierzchołka przez macierz wymaga, aby macierz, przez którą mnożymy wierzchołek, była przetransponowana 
ponieważ, tak jak przedstawiona jest w strukturach samego Direct3D, shaderowi po prostu nie odpowiada. Instrukcja musi 
mieć po prostu podaną macierz w inny sposób niż jesteśmy przyzwyczajeni i koniec, nic na to nie poradzimy. Dlatego przed 
przekazaniem shaderowi, musimy dokonać prostej operacji zamiany kolumn na wiersze i odwrotnie. Tutaj warto sobie 
zapamiętać, żeby dokładnie analizować działanie shaderowych instrukcji, zawłaszcza pod kątem sposobu wykonania operacji 
(w dokumentacji znajdziemy zawsze kawałek pseudokodu, który pokazuje jak się odbywa działanie na poszczególnych 
składowych). Unikniemy w ten sposób na pewno wielu frustracji i będziemy mieli jasność dlaczego tak a nie inaczej. 
Ale wracając co naszego kodu. Jak widzimy, przekazujemy do rejestrów pamięci stałej trzy macierze. Pierwszą jest 
zmieniona macierz świata (jest mnożona za każdym razem, tak aby cały świat się obracał) oraz macierze widoku i projekcji. 
Jak są wykorzystane w vertex shaderze to już wiemy. Funkcja 

D3DXMatrixTranspose()

 transponuje macierz podaną jako 

drugi parametr i umieszcza wynik w dwóch miejscach - pierwszy to pierwszy argument wywołania tej funkcji, drugim jest 
wartość zwracana przez samą funkcję. Dlatego też możemy użyć wywołania tej funkcji jako argument naszych metod 
urządzenia do ustawiania wartości rejestrom pamięci stałej. No i to w zasadzie tyle. Za każdym wywołaniem funkcji 
Render() wywołuje się funkcja SetupFrame() a co za tym idzie zmienia się macierz świata i nasz sześcian a w zasadzie 
wszystko co umieścimy na scenie będzie się obracać. Spójrzmy jeszcze tylko na wywołanie funkcji Render(). W zasadzie 
wszystko wygląda tak samo prócz jednej linijki.

 

 
g_pd3dDevice->SetVertexShader( VertexShader ); 
 

Co to oznacza, chyba już nie muszę nikomu tłumaczyć. Zastąpimy sobie standardowy taśmociąg geometrii naszym własnym, 
który właśnie zdefiniowaliśmy sobie w naszym vertex shaderze. To, że wygląda wszystko tak samo jak wcześniej jest 
zasługą tylko i wyłącznie kodu naszego shadera. Jeśli tylko spróbowalibyśmy w kodzie wpłynąć w jakiś "widoczny" sposób 
na nasze wierzchołki czy to zmieniając kolor, pozycję czy współrzędne tekstury uwidoczni się to natychmiast. I nie będziemy 
musieli zupełnie nic kombinować w kodzie naszego programu! Teraz tylko wszystko zależy od Waszej wyobraźni i chęci 
nauczenia się, że o zrozumieniu nie wspomnę. No i cóż, lekcja była ciężka, dosyć trudna trzeba przyznać, ale sami 
powiedzcie czyż nie ciekawa? Jak dla mnie, przy niej wszystkie poprzednie to się chowają. Oczywiście aby zobaczyć czy 
program przykładowy to aby ten sam i nie oszukuję - macie obrazek poniżej.