Direct3D Vertex shader 2

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.


Wyszukiwarka

Podobne podstrony:
Direct3D Vertex shader 1
Direct3D Vertex shader 1
Direct3D Pixel shader 2
Direct3D Pixel shader 1
Direct3D 11 Tessellation
Premier Press Beginning DirectX 9
Active Directory
5. Prensa, Hiszpański, Kultura, España en directo
Active Directory
Intermediate Short Stories with Questions, Driving Directions
Blender 3D Materiały Shadery Chrom
Directional Movement Index, giełda(3)
directx
Komunikacja rynkowa direct response marketing

więcej podobnych podstron