background image

1 

DirectX ▪ Vertex shader 1 

No cóż. Przykłady sypią się jak ulęgałki z drzewa, więc nie ma na co czekać. Pędzimy do przodu niczym rakieta i dziś 
bierzemy się za nową, zupełnie odlotową i kosmiczną rzecz, czyli vertex shader! Może wielu z Was powie, że powinienem 
najpierw napisać coś o mapowaniu środowiskowym (ang. environment mapping) czy mapowaniu wypukłości (ang. bump 
mapping
), ale doszedłem do wniosku, że jednak lepiej będzie najpierw powiedzieć coś o vertex shaderze a dopiero potem 
brać się za kolejne techniki. 
Czymże zatem jest ta rzecz o kosmicznie brzmiącej nazwie? Microsoft tłumaczy, że vertex shader kontroluje ładowanie i 
przetwarzanie wierzchołków. Ale co to znaczy? Jeśli czytaliście dokumentację do Direct3D, pewnie nie raz spotkaliście się z 
terminem "rendering pipeline". Cóż to jest takiego? Mi kojarzy się to z taśmociągiem, po którym jadą wierzchołki - z 
pamięci, w której są przechowywane, na ekran. Na tym taśmociągu są jednak przystanki, w których wierzchołki te 
przechodzą pewne przekształcenia, aby w końcowej fazie można ujrzeć na ekranie coś fajnego. Teraz przetłumaczę kawałek 
SDK, ale myślę, że nikt się nie obrazi ;-). Tworzymy nasz świat jako zbiór wierzchołków. Świat ten zawiera definicje 
wielkości obiektów, ich wzajemne położenie w przestrzeni oraz położenie obserwatora. Direct3D przekształca ten opis na 
zbiór pikseli na ekranie. Ten pierwszy etap - przekształcania opisu świata na płaski obraz na ekranie - nazywa się (tutaj 
załóżmy dla uproszczenia, że "pipeline" to nie rurociąg a raczej taśmociąg ;-) taśmociągiem geometrii, nazywanym również 
taśmociągiem przekształceń. Po przejściu przez ten taśmociąg, dane pikseli są użyte do przeprowadzenia takich operacji jak 
multiteksturowanie (ang. multitexturing) czy mieszanie (ang. blending). Są przeprowadzane różne operacje wykorzystujące 
rozmaite bufory, ale będzie jeszcze okazja to omówić. My natomiast teraz zajmiemy się bardziej szczegółowo taśmociągiem 
geometrii. 

 

 
W Direct3D mamy dwa rodzaje taśmociągów geometrii. Jeden, dobrze nam już znany, to taśmociąg o określonej z góry 
kolejności (ang. fixed) dokonywania przekształceń. Wielu z Was zna doskonale pokazany powyżej rysunek. Pokazuje on ten 
taśmociąg oraz kolejne przekształcenia dokonywane na jadących w nim wierzchołkach. Direct3D określa obiekty i 
obserwatora w świecie, przeprowadza rzutowanie na ekran oraz dokonuje obcinania wychodzących poza obszar widoku 
wierzchołków. Na tym taśmociągu dokonywane są też obliczenia dotyczące oświetlenia, aby móc określić kolor oraz ilość 
odbijanego światła przez wierzchołki. Mówiliśmy już sobie o tym, ale niezbyt dokładnie, więc teraz dowiemy się, co się 
dzieje po kolei z naszymi wierzchołkami, które my każemy przetwarzać naszemu Direct3D. Na samym początku wrzucamy 
na taśmociąg nasze wierzchołki. Po chwilowej jeździe, wierzchołki te napotykają na trzy podstawowe przekształcenia, które 
my już sobie omawialiśmy - są to przekształcenia świata, widoku i rzutowania. Następnie wierzchołki dojeżdżają do 
przystanku zwanego "obcinanie", który odrzuca wierzchołki niewidoczne (nie mieszczące się na ekranie) i po jego minięciu, 
wierzchołki trafiają prosto do rasteryzera - czyli urządzenia, które spowoduje, że pojawią się one na ekranie. Teraz w 
szczegółach: kiedy wrzucamy wierzchołki na taśmę, są one umieszczone w swoim własnym, lokalnym układzie 
współrzędnych, mają własną orientację i położenie. Dane te nazywane są współrzędnymi modelu a jego położenie i 
orientacja nazywana jest przestrzenią modelu. Na pierwszym przystanku na taśmie wszystkie dane wierzchołków są 
przekształcane ze swojego układu współrzędnych, na układ używany przez wszystkie obiekty na scenie. Proces tego 
przekształcania nazywany jest przekształceniem świata (ang. world transformation). Każdy wierzchołek od teraz nie używa 
już swojego własnego układu współrzędnych czy orientacji - wszystko jest umieszczone przestrzeni wspólnego świata i 
wszystkie wierzchołki mają współrzędne pasujące do tego ogólnego świata. Po tym przekształceniu wierzchołki udają się w 
dalszą podróż po taśmie. Napotykają na swojej drodze kolejny przystanek, jakim jest przekształcenie widoku. Tutaj nasz 
świat jest orientowany (ustawiany) względem kamery obecnej na scenie. Ponieważ mamy punkt widzenia, więc wszelkie 
nasze wierzchołki są przemieszczane i obracane wokół widoku kamery i są przenoszone niejako z przestrzeni świata do 
przestrzeni kamery. Po obejrzeniu sobie naszych wierzchołków przez kamerę, jedziemy dalej po taśmie no i trafiamy w 
bardzo nieprzyjemne miejsce. Na tym przystanku, pomimo usilnych protestów naszych kochanych milusińskich, zostają one 
stłoczone z trzech wymiarów do dwóch, czyli do płaskiego, zupełnie nieciekawego świata. Obiekty zostają przeskalowane ze 
względu na swoją odległość od obserwatora, aby osiągnąć w ostatecznym rozrachunku złudzenie głębi. Bliższe obiekty 
wydają się być większe, leżące dalej są mniejsze. To się nazywa rzutowanie a całe zamieszanie to przekształcenie projekcji 
(ang. projection transformation). Na ostatnim przystanku wszystkie wierzchołki, które niestety nie miały szczęścia znaleźć 
się w polu widzenia naszej kamery, są usuwane i rasteryzer nie liczy dla nich kolorów czy cieniowania. Nie ma sensu 

background image

2 

DirectX ▪ Vertex shader 1 

poświęcać czasu dla czegoś, co i tak nie będzie widoczne. Ten proces nazywa się obcinaniem (ang. clipping). Po fazie 
obcinania, pozostałe wierzchołki są skalowane według parametrów widoku i są przekształcane na współrzędne na ekranie. W 
efekcie, kiedy scena jest malowana przez rasteryzer, na ekranie otrzymujemy tylko widoczne wierzchołki. 
 
DirectX 8.0 wprowadza nam zupełnie nową jakość, jeśli chodzi o taśmociąg geometrii. Od tej wersji będziemy mieli o wiele 
większą kontrolę nad tym, co się dzieje z naszymi wierzchołkami. Będziemy mogli ustalać sobie, jakie będziemy mieć 
przystanki i w jakiej kolejności. Poniższy rysunek, zresztą też doskonale Wam znany zapewne, pokazuje istotę takiej filozofii 
działania. Nas w tym momencie interesuje tylko pierwsza część tego rysunku... 

 

 
tam gdzie mamy taśmociąg geometrii. Jak widać, obok typowych operacji, jakie są przeprowadzane na wierzchołkach, mamy 
tam taki mały, niewinnie wyglądający prostokącik noszący dumnie nazwę "vertex shader". Co to takiego w zasadzie jest? W 
DirectX 8 proceduralnie (czyli my możemy o tym decydować) określane mogą być wszystkie operacje, jakie mają miejsce na 
taśmociągu geometrii i oświetlenia a także na taśmociągu, na którym dokonywane jest mieszanie pikseli. Takie podejście do 
sprawy, kiedy w sposób programowalny możemy określić zachowanie się naszego urządzenia ma oczywiście wiele zalet. 
 
Po pierwsze - umożliwia bardziej ogólną składnię programu do określania zwykle przeprowadzanych operacji. Model o 
ustalonej kolejności przetwarzania musi definiować modele, flagi oraz inne rzeczy dla rosnącej ciągle liczby operacji, które 
muszą być wykonane. Co gorsze, wraz z rosnącą mocą naszego sprzętu - więcej kolorów, tekstur, strumieni wierzchołków i 
całej reszty, operacje, które muszą zostać pomnożone przez przyrost danych, stają się coraz bardziej skomplikowane. W 
przeciwieństwie do tego, model programowalny umożliwia przeprowadzanie prostych operacji, takich jak pobieranie 
kolorów czy tekstur w bardziej bezpośredni sposób. Nie musimy się już przedzierać przez wszystkie możliwe tryby pracy 
urządzenia, aby znaleźć ten właściwy. My musimy się tylko dowiedzieć, jak działa nasz sprzęt i zażądać od niego, aby 
przeprowadził on zadany przez nas algorytm. Co możemy robić za pomocą takiego programowalnego przetwarzania? Oto 
kilka dobrze znanych nam rzeczy: 

•  podstawowe przekształcenia geometryczne,  

•  proste oświetlanie obiektów,  

•  mieszanie wierzchołków (co to jest, dowiemy się kiedyś),  

•  morfing wierzchołków (pamiętacie delfina z przykładów?),  

•  przekształcenia tekstur,  

•  generowanie tekstur,  

•  mapowanie środowiskowe (ang. environment mapping).  

Po drugie - programowalne podejście umożliwia łatwą implementację nowych operacji (naszych chorych pomysłów). 
Programiści często podczas pracy dowiadują się, że muszą zrobić coś, ale konkretne API nie posiada akurat tej rzeczy. I 
największy ból to ten, że to nie brak możliwości urządzenia, ale właśnie ograniczenia posiadanego przez programistę API 
uniemożliwiają mu realizację jego zamiarów. Ogólnie rzecz biorąc, programowalne operacje są o wiele prostsze niż próby 
ich przeprowadzenia z wykorzystaniem ustalonego taśmociągu. To, co będziemy mogli robić już w niedalekiej przyszłości, 
to: 

background image

3 

DirectX ▪ Vertex shader 1 

•  animacja postaci żywych,  

•  oświetlenie anizotropowe - teraz możemy robić tylko za pomocą tekstur,  

•  realistyczne modelowanie skóry, różnych rozciągliwych powłok,  

•  światła, które mogą wnikać pod powierzchnię,  

•  geometria proceduralna (np. mięśnie poruszające się pod skórą),  

•  modyfikowanie siatki na podstawie mapy bitowej (ang. displace).  

Po trzecie - skalowalność i ewolucja. Jak widać, sprzęt na przestrzeni ostatnich kilku lat, rozwija się bardzo gwałtownie i 
takie programowalne podejście pozwala dostosować posiadane API do możliwości sprzętu. Nowe właściwości i cechy mogą 
być dodane na rosnącą ciągle ilość sposobów poprzez: 

•  dodawanie nowych instrukcji,  

•  dodawanie nowych wejść dla danych,  

•  dodawanie nowych właściwości dla ustalonego trybu przetwarzania jak i programowalnego.  

Po czwarte - podejście proceduralne oferuje programistom coś bliższego skóry. Wiadomo, że bardziej znają się na 
programowaniu niż na sprzęcie. API, które w pełni zaspokoi potrzeby programistów, powinno móc przenieść funkcjonalność 
sprzętu na dostępny kod. 
 
Po piąte - podejście proceduralne to krok w stronę renderingu fotorealistycznego. Przez wiele lat stosowano programowalne 
shadery w takim sposobie renderingu. Ogólnie mówiąc, ten sposób nie jest ograniczony przez wydajność sprzętu, więc takie 
programowalne podejście staje się na dziś celem ostatecznym, jeśli chodzi o techniki renderingu. 
 
Po szóste i ostatnie - podejście proceduralne umożliwia bezpośrednie przeniesienie kodu na sprzęt. Większość obecnego 
dzisiaj sprzętu 3D może być w jakiś określony sposób programowana, jeśli chodzi o przekształcanie wierzchołków. 
Możliwość programowania urządzenia za pomocą API umożliwia przeniesienie aplikacji bezpośrednio na sprzęt. Umożliwia 
nam zarządzanie zasobami sprzętu według wymagań aplikacji. Za pomocą ograniczonego zbioru rejestrów lub instrukcji 
może zostać to wykonane. Natomiast trudniej jest zrobić funkcję o określonym przebiegu, która mogłaby wykonywać 
wszystkie operacje niezależnie. Jeśli włączymy sobie do pakietu zbyt wiele funkcji wymagających zasobów shadera, mogą 
one przestać działać i będą powodować różne dziwne zachowania. Model programowalnego API jest kontynuacją tradycji 
DirectX-a, która miała na celu eliminowanie problemów poprzez umożliwienie programiście zwrócenie się bezpośrednio do 
sprzętu i powodując zniesienie takich ograniczeń. 
 
Jeśli włączymy nasz vertex shader, standardowy moduł do przeprowadzania transformacji i oświetlenia w Direct3D zostaje 
przez niego zastąpiony na taśmociągu geometrii. W efekcie standardowe informacje (ustawienia) odnośnie transformacji i 
oświetlenia są ignorowane przez Direct3D. Kiedy nasz shader wyłączymy i funkcja o ustalonej kolejności jest na powrót 
włączona, wszystkie aktualne ustawienia są oczywiście przywracane. Na wyjściu vertex shader musi wystawiać współrzędne 
wierzchołków w jednorodnym układzie obcinania. Mogą być oczywiście generowane dodatkowe dane takie, jak: 
współrzędne teksturowania, kolory, współczynniki mgły i podobne. Taśmociąg geometrii zawierający nasz shader powinien 
wykonywać następujące zadania: 

•  przetwarzanie prymitywów,  

•  obcinanie ze względu na ustalone punkty widzenia i płaszczyzny obcinania,  

•  skalowanie widoku,  

•  jednorodne dzielenie,  

•  obcinanie tylnych powierzchni i widoku,  

•  ustawienia trójkątów,  

•  rasteryzacja.  

Programowalna geometria jest jednym z trybów Direct3D API. Jeśli jest włączona, zastępuje częściowo taśmociąg po którym 
jadą wierzchołki. Kiedy jest wyłączona, API ma normalną kontrolę nad tym co się dzieje z danymi tak, jak miało to miejsce 
w DirectX 6.0 i 7.0. Wykonywanie vertex shaderów nie powoduje zmian wewnątrz samego Direct3D a żaden stan z wnętrza 
Direct3D nie jest dostępny dla shadera. 

background image

4 

DirectX ▪ Vertex shader 1 

 

 
No dobra. Wszystko to brzmi tak strasznie naukowo i okropnie. Jak wielu się słusznie domyśla, jest żywcem wręcz zerżnięte 
z dokumentacji do SDK. A o co tak naprawdę tutaj chodzi? Popatrzmy chwilę na kolejny rysunek. Mamy coś jakby procesor 
(ALU - ang. Arithmetic Logic Unit), który posiada pewne wejście i wyjście. Cały bajer polega na tym, że ten procesor 
możemy teraz programować. Dawniej wrzucaliśmy do tego procesorka odpowiednie dane i on sam już nam się z tym 
załatwiał. Miał jakiś ustalony z góry program i wykonywał grzecznie po kolei zaprogramowaną z góry sekwencję rozkazów. 
Teraz dostaliśmy do ręki możliwość zmiany tego programu. Manipulując odpowiednio rozkazami i danymi, będziemy mogli 
osiągać trochę bardziej "rozrywkowe" efekty niż do tej pory, co najlepiej sobie obejrzeć w NVEffectBrowserze panów z 
dobrzej nam znanej firmy. No ale wracając do sedna sprawy. Skoro mamy procesor, to muszą być rejestry i rozkazy, prawda? 
Tak też jest w istocie. Procesor posiada cały zestaw bardzo pożytecznych rozkazów, które nie raz będzie okazja omówić. 
Zawierają się w nich wszelkie operacje arytmetyczne oraz kilka przydatnych operacji znanych z grafiki 3D w "klasycznym" 
wykonaniu (dla przykładu iloczyn skalarny). Mamy rozkazy, więc czas na rejestry. Procesor geometrii ma ich kilka 
zestawów zgromadzonych w pewne grupy. Jak widać na rysunku, są to rejestry wejściowe, tymczasowe, rejestry zawierające 
pewne stałe oraz rejestr adresu. Obecny jest także tak zwany wektor wyjściowy, złożony z komórek, które przechowują 
konkretne dane dla wierzchołków, które idą sobie dalej po przekształceniu przez nasz procesorek. Każdy vertex shader 
definiuje funkcję, która jest aplikowana każdemu wierzchołkowi po kolei na scenie. Nie ma takiej sytuacji, że wierzchołki są 
przetwarzane równolegle. Po prostu jeden wierzchołek wchodzi, jest obrabiany a następnie sobie wychodzi. Vertex shader 
nie dokonuje operacji projekcji i ustawiania wierzchołków w widoku. Obcinanie wierzchołków i składanie ich w bryły też 
nie jest przez niego dokonywane. Wszystko dzieje się już po samym fakcie zadziałania shadera. 
 
Jak już wspominałem, w Direct3D 8 mamy dwa rodzaje taśmociągu geometrii. Jeden o ustalonej z góry kolejności operacji, 
który ma taką samą funkcjonalność jak ten, obecny w DirectX 7.0, który zawiera transformacje, oświetlenie, mieszanie 
wierzchołków oraz generację współrzędnych mapowania. W przeciwieństwie do vertex shadera, gdzie operacje wykonywane 
na wierzchołkach są definiowane w jego obrębie, przetwarzanie wierzchołków w ustalonej kolejności jest kontrolowane 
poprzez stan urządzenia renderującego za pomocą metod jego obiektu. Ustawiają one oświetlenie, transformacje i wszystko 
co potrzebne. Wektor wejściowy dla przetwarzania o stałej kolejności przekształceń ma z góry ustaloną składnię. Dlatego też 
deklaracje wierzchołków określają je za pomocą współrzędnych, koloru, normalnej i tak dalej. Dane wyjściowe dla 
wierzchołków, które są przetwarzane przez funkcję o ustalonej kolejności przetwarzania, zawsze zawierają na wyjściu 
współrzędne, kolor, wszystkie współrzędne teksturowania, które są wymagane przez aktualny stan urządzenia. 
Programowalny vertex shader ma funkcję przetwarzającą, zdefiniowaną jako tablicę instrukcji, która to tablica jest 
aplikowana każdemu wierzchołkowi podczas przetwarzania. Zależność pomiędzy tymi danymi, które przychodzą z aplikacji 
do rejestrów wejściowych vertex shadera, jest zdefiniowana poprzez tzw. "deklarację shadera", ale dane te nie mają jakiegoś 
ściśle określonego formatu. Zinterpretowanie danych nadchodzących należy tylko i wyłącznie do instrukcji zawartych w 
vertex shaderze. Dane wyjściowe są wpisywane rejestrów wyjściowych także poprzez instrukcje zawarte w shaderze. 
 
"Deklaracja vertex shadera" - definiuje zewnętrzny interfejs shadera, który będzie połączony z nadchodzącymi danymi. Taka 
deklaracja zawiera między innymi: 

•  połączenie strumienia danych do rejestrów wejściowych shadera. Informacja ta definiuje typ i rejestr wejściowy dla 

każdego elementu zawartego w strumieniu danych. Typ określa po prostu rodzaj danych - czy jest to liczba 
całkowita, zmiennoprzecinkowa czy może wektor oraz, co za tym idzie, ich rozmiar. Wszystkie elementy 
strumienia, które są mniejsze od czterech (mają mniej niż cztery elementy - np. współrzędne to trzy wartości) są 
uzupełniane do czterech przez wartości 0 i 1.  

background image

5 

DirectX ▪ Vertex shader 1 

•  łączy wejściowe rejestry vertex shadera z danymi niejawnymi pochodzącymi od takiego wewnętrznego urządzonka, 

zwanego teselatorem prymitywów. Urządzonko to powoduje podział większych figur na pojedyncze trójkąty 
strawne dla shadera. To umożliwia kontrolę ładowania danych wierzchołków, które nie pochodzą bezpośrednio z 
bufora wierzchołków, ale tworzonych na przykład podczas teselacji (podziału) prymitywów.  

•  deklaracja ładuje pewne wartości do stałej pamięci, kiedy shader jest ustawiany jako bieżący. Każdy element 

ładowany do takiej stałej pamięci zawiera wartości zapisywane do jednego lub wielu, ciągłych (występujących po 
sobie) stałych rejestrów o wielkości 4 słów (

DWORD

) każdy.  

Wróćmy na chwilkę do naszego obrazka z pseudo procesorkiem. Widzimy tam zestaw rejestrów wejściowych, które są 
wiązane za pomocą deklaratora z danymi wejściowymi, Są także rejestry tymczasowe, które będą służyć do przechowywania 
jakiś naszych tymczasowych obliczeń oraz dokonywania operacji, które mogą być przeprowadzane tylko na rejestrach. 
Widzimy równie rejestr adresowy (nas na razie nie interesuje, ale wspomnimy o nim później) oraz rejestry pamięci stałej, o 
których była mowa przed momentem. Na wyjściu, które niełatwo przegapić, otrzymujemy dane, które wykorzystamy do 
naszych niecnych celów. Powiedziałem trochę o deklaratorze, dowiedzmy się więc jak to działa w praktyce. 
 
Jak napisałem wyżej, deklarator łączy napływający strumień danych z rejestrami wejściowymi. Co to oznacza? Załóżmy, że 
płynie sobie strumyczek bajtów, które niosą sobie tylko wiadomą informację. Deklarator umożliwi shaderowi określenie, 
które spośród tych danych posłużą mu do poszczególnych obliczeń. Przyjrzyjmy się może deklaratorowi, ktorego my 
użyjemy w naszym pierwszym vertex shaderze:

 

// first vertex shader 
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() 
}; 

Jak nietrudno zauważyć, deklarator to tablica wartości typu 

DWORD

 (0xFFFFFFFF). Wartości te są w jakiś tam sposób 

przydatne shaderowi, który na ich podstawie będzie potrafił przypisać określonemu rejestrowi wejściowemu, którąś z danych 
w nadchodzącym strumieniu. Jak też nietrudno zauważyć, w tablicy tej nie mamy wartości (przynajmniej na razie), ale 
wywołania pewnych, tajemniczych makr, zwanych makrami deklaratora. Cóż będą oznaczać poszczególne z nich?

 

D3DVSD_STREAM(0) 

Domyśleć się jest bardzo łatwo. Po prostu będzie to numer strumienia (jako argument makra), z którego vertex shader będzie 
pobierał dane na swoje potrzeby i przetwarzał je zgodnie ze swoim programem. Makro to weźmie numer naszego strumienia, 
zmiesza to z czymś i umieści w tablicy deklaratora jako pewną liczbę. Shader widząc taką liczbę, będzie wiedział, że to 
nakazuje mu pobierać dane ze strumienia o numerze 0.

 

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

Te makra mówią shaderowi, do którego rejestru wejściowego ma pobrać dane lecące strumieniem oraz ile tych danych ma 
być. W tym przypadku będziemy mieć trzy różne wartości. Gdybyśmy popatrzyli na strumień danych pędzący po ścieżkach 
naszej karty, widzielibyśmy tylko ciąg bajtów. Shader w zasadzie widzi tak samo, dla niego jest to tylko strumień nic nie 
znaczących bajtów. Ale dzięki tym makrom dowie się on teraz, jak z tego chaosu wyłowić, coś co się nada do dalszych 
obliczeń. Nie powiedzieliśmy sobie jeszcze o znaczeniu rejestrów, ale o nich może za chwilę dokładniej. My musimy 
podeprzeć się tutaj naszą wyobraźnią, aby jakoś wybrnąć, ale obiecuję, że już za moment wszystko będzie jasne. 
Przedstawione powyżej pierwsze makro mówi shaderowi tak: słuchaj, weź 3 wartości typu 

float

 (stała 

D3DVSDT_FLOAT3

) z nadchodzącego strumienia i umieść je w rejestrze, w którym powinny być współrzędne 

wierzchołków (stała 

D3DVSDE_POSITION

). Następne makro sugeruje shaderowi, aby wziął cztery bajty ( 

D3DVSDT_D3DCOLOR

) i umieścił je w rejestrze, w którym przechowuje kolor wierzchołka. Na samym końcu widzimy 

makro, które w ciągu bajtów znajduje współrzędne mapowania tekstury na poziomie 0 (

D3DVSDE_TEXCOORD0

), które 

będą potrzebne shderowi. Wszystkie te konstrukcje, dzięki makrom, zostaną zastąpione liczbami 

DWORD

 (bo taką mamy 

tablicę!) i tam też zmagazynowane. Shader dostając taką tablicę, sobie tylko znanymi sposobami będzie potrafił 
zinterpretować odpowiednio znajdujące się tam informacje a nasze wierzchołki zaczną się zachowywać w nader przedziwne 
sposoby. No ale to już będzie zależało już tylko i wyłącznie od naszej wyobraźni. 
 

Funkcja.

 

Powiedzieliśmy sobie trochę o deklaratorze, więc trzeba coś napomknąć o funkcji vertex shadera. Co to znowu jest takiego. 
Pamiętacie rozważania o taśmociągach geometrii? Na taśmociągu o ustalonej kolejności przetwarzania wszystkie operacje 
były z góry zaprogramowane i były wykonywane w określonej kolejności. Nasz taśmociąg, z programowanym 
przetwarzaniem wierzchołków będzie musiał poradzić sobie sam, bez pomocy funkcji, która jest stosowana w standardowych 
przypadkach. Ale to nic złego a może nawet lepiej! My napiszemy sobie własną, o wiele, wiele lepszą, która pozwoli nam 
rozwinąć naszą wyobraźnię w sposób dotychczas niespotykany! Ale zanim zajmiemy się tworzeniem funkcji shadera, 
musimy powiedzieć sobie sporo o instrukcjach i rejestrach. 
 

background image

6 

DirectX ▪ Vertex shader 1 

Rejestry.

 

Tak w zasadzie trudno powiedzieć czym tak naprawdę jest vertex shader. Oglądając obrazki i czytając ten artykuł, możemy 
odnieść dwojakie wrażenie. Z jednej strony jest to pewien program (jeśli myślimy o jego użyciu). Deklarujemy wierzchołki, 
uruchamiamy urządzenia, robimy całą potrzebną inicjalizację, uruchamiamy nasz vertex shader i działamy. Z drugiej strony 
myślimy o nim jako o procesorze, który można zaprogramować w określony sposób, Teorię tę potwierdza między innymi ten 
fragment, w którym omówimy sobie rejestry. Rejestry nierozerwalnie kojarzą się nam z kostką krzemu, która będzie 
wykonywać co tylko nam się zachce. Przestawmy więc nasze myślenie na właśnie takie to może łatwiej nam to do głowy 
wejdzie. Jak zwykle też zachęcam do spojrzenia na nasz słynny już rysuneczek przedstawiający ideę vertex shadera. Rejestry 
wejściowe już wiemy do czego służą (przynajmniej tak połowicznie). Po pierwsze - odbierają ze strumienia dane, w sposób 
ustalony w deklaratorze, czyli do określonego rejestru trafia określona część strumienia. Co się potem z takimi danymi 
dzieje? Gdybyśmy mieli taśmociąg z ustaloną kolejnością, dane zostałyby pobrane z tych rejestrów, wykonane zostałyby na 
nich pewne operacje, które są kontrolowane przez stan urządzenia renderującego i zostałyby wyrzucone jako wektor 
wyjściowy, gotowe do dalszego przetwarzania. My nie będziemy korzystać z ustalonego trybu przetwarzania, więc... musimy 
napisać sobie własną funkcję przetwarzania. Aby jednak to zrobić, trzeba coś zrobić z danymi wejściowymi i gdzieś podziać 
dane wyjściowe. Do tego celu użyjemy właśnie rejestrów. Mamy cztery rodzaje rejestrów - wejściowe, wyjściowe, 
tymczasowe i rejestry pamięci stałej. Istnieje jeszcze rejestr adresowy, ale o nim na końcu. Rejestry wejściowe służą do 
pochwycenia ze strumienia odpowiedniej ilości bajtów i dane będą potem przekształcone za pomocą naszej funkcji. Tak w 
zasadzie to rejestrami wejściowymi nazywane są także rejestry tymczasowe, rejestry pamięci stałej oraz rejestr adresowy i 
żeby sobie niepotrzebnie nie mieszać w głowie pozostańmy przy takiej terminologii. Czym tak w ogóle jest rejestr? Mieścił 
on będzie zawsze cztery liczby typu 

float

, dlatego, aby możliwe było przechwycenie i przechowanie odpowiedniej ilości 

danych. Macierze używane w Direct3D do przekształceń będą miały rozmiar 4x4, kolor będziemy podawać jako 4 składowe 
(

RGBA

), współrzędne wierzchołków zawierać będą trzy składowe (xyz) oraz wartość do przeliczania na współrzędne 

jednorodne, właściwie prawie wszystkie dane będą w takim właśnie formacie. Jeśli jakaś dana będzie miała mniej 
składowych (na przykład współrzędne tekstury), to po wpisaniu do rejestru pozostałe składowe zostaną dopełnione wartością 
0.0 lub 1.0, ale tym to już nie musimy się przejmować. W zależności od tego, do czego służą rejestry, mają one odpowiednie 
oznaczenia. Rejestry pobierające dane ze strumienia zaczynają się od "v", rejestry tymczasowe to "r", rejestry pamięci stałej 
to "c". Jedynym rejestrem nie będącym wektorem czterech wartości 

float

 jest rejestr adresowy oznaczony jako "a", który 

zawiera jedną wartość całkowitą. Direct3D ma także pewne ograniczenie, dotyczące udziału rejestrów w poszczególnych 
rozkazach. W zależności od rodzaju rejestru, w jednej instrukcji może wystąpić od jednego do kilku rejestrów. Na przykład 
rejestry oznaczone jak "vn" (gdzie n to kolejna liczba), mogą wystąpić w instrukcji tylko raz. Nie jest więc dopuszczalna 
instrukcja na przykład 

add r0, v0, v1

, ponieważ mamy tu już dwa rejestry "vn". Na pierwszy ogień omówmy sobie 

rejestry oznaczone jako "v". Kolejne rejestry będą oznaczone jako "v" + kolejna liczba, czyli v0, v1, v2. Rejestrów tych 
mamy 16 i każdy z nich jest tylko do odczytu, zapamiętajmy sobie dobrze - tych rejestrów nie można zapisywać! Każdy z 
nich ma predefiniowane przeznaczenie i poniższa tabela zawiera to zestawienie: 

opis rejestr

pozycja wierzchołka 

v0 

waga mieszana 

v1 

indeksy mieszania 

v2 

normalna 

v3 

wielkość punktu 

v4 

kolor wierzchołka (diffuse)

v5 

kolor odbicia (specular) 

v6 

współrzędne tekstury 0 

v7 

współrzędne tekstury 1 

v8 

współrzędne tekstury 2 

v9 

współrzędne tekstury 3 

v10 

współrzędne tekstury 4 

v11 

współrzędne tekstury 5 

v12 

współrzędne tekstury 6 

v13 

współrzędne tekstury 7 

v14 

pozycja wierzchołka 2 

v15 

normalna 2 

v16 

 

Teraz czas na rejestry tymczasowe, oznaczone jako "r" + liczba. Rejestrów tych jest 12 i można je w każdej chwili zapisywać 
i odczytywać. Mogą one wystąpić jako wszystkie trzy argumenty instrukcji. Nie mają one jakiegoś specjalnego 
przeznaczenia, służą do przechowywania wyników operacji i przekazywania ich do następnych. 

background image

7 

DirectX ▪ Vertex shader 1 

 
Rejestry pamięci stałej, oznaczone jako "cn". Mamy maksymalnie 96 czteroelementowych (liczby 

float

 jak wiemy) rejestrów 

pamięci stałej, w których możemy zawrzeć dosłownie wszystko co nam się podoba. Nie jest to jednak prawda na każdej 
karcie, musimy sprawdzić strukturę 

D3DCAPS8

 w celu zbadania ile ich tak naprawdę mamy do dyspozycji, ale na pewno i 

tak nam to wystarczy do przechowywania potrzebnych nam danych. Co takiego będziemy mogli przechowywać w tych 
rejestrach? Poszczególne wiersze macierzy przekształceń, jakieś wartości potrzebne do obliczeń (na przykład liczbę PI), 
dobrym pomysłem jest zapisać sobie rozwinięcie sinusa i cosinusa (z szeregu Taylora), ponieważ w asemblerze nie ma 
rozkazów do obliczania funkcji trygonometrycznych, czy jakiekolwiek, inne niezbędne do działania naszego shadera 
wartości. Kiedy piszemy funkcję vertex shadera, rejestry stałe są tylko do odczytu i mogą wystąpić tylko raz w instrukcji. Ale 
wcześniej musimy mieć przecież możliwość zapisu do tych rejestrów pewnych, potrzebnych nam wartości. Jak to robić, 
powiem przy okazji omawiania naszego programu, a właściwie jego elementów dotyczących vertex shadera.  
Rejestr adresowy, oznaczony jako "a" jest jednym, malutkim biednym rejestrem przechowującym pewną wartość, która 
oznacza przesunięcie. Używany on jest do odczytu rejestrów pamięci stałej - "cn". Zmieniając zawartość rejestru a można 
"przesuwać" się po pamięci stałej i odczytywać jakieś określone miejsca z rejestrów pamięci stałej. Jeśli zajdzie konieczność 
użycia czegoś takiego, to Wam opiszę przy omawianiu programu shadera, na razie nie musimy zawracać sobie tym głowy. 
 
Omówiliśmy sobie rejestry wejściowe, czas więc na rejestry wyjściowe. Jak sama nazwa wskazuje, będą one przechowywać 
wartości przeznaczone do wyrzucenia na ekran. Te rejestry są inaczej nazywane wejściami rasteryzera, ponieważ stamtąd już 
mają naprawdę niedaleko na ekran. Wygenerowane przez nasz shader dane są zapisywane do zbioru rejestrów wyjściowych, 
które mają oczywiście atrybut "tylko do zapisu", nie można z nich czytać danych, no bo i po co, lepiej żeby nas nie kusiło. 
Rejestry wyjściowe mają nieco inną koncepcję nazewnictwa. Zaczynają się na "o", zapewne od "output" (czyli wyjście), 
potem następuje wielka litera oznaczająca co dany rejestr przechowuje a następnie, jeśli jest to możliwe, numer rejestru 
danego typu. W wersji 8.0 Direct3D mamy do dyspozycji co następuje: 

• 

oDn

 - są to rejestry, które są przeznaczone do przesłania do nich koloru wierzchołków. Załóżmy dla przykładu, że 

piszemy nowe, super oświetlenie. Będzie tam na pewno trzeba obliczać kolory wierzchołków na podstawie pewnych 
danych. Te właśnie obliczone kolory będziemy umieszczać w rejestrach oDn, a zwłaszcza 

oD0

, bo z drugiego to nie 

wiem, czy kiedykolwiek zdarzy się nam korzystać.  

• 

oFog

 - rejestr odpowiedzialny za mgłę. Wpisywać do niego będziemy współczynnik mgły, na podstawie którego 

będzie ona obliczana i tworzona mgła tablicowana. Jest to rejestr, który przechowuje tylko jedną wartość typu 

float

.  

• 

oPos

 - rejestr, który będzie zawierał pozycję wierzchołka po przetworzeniu go przez shader. Jeśli na przykład 

napiszemy shadera, który zrobi to samo co standardowa funkcja o ustalonej kolejności przetwarzania, czyli tylko 
pomnoży go przez macierze przekształceń, to wynik ma się znaleźć tutaj, w innym przypadku po prostu nic nie 
zobaczymy na ekranie.  

• 

oPts

 - rejestr przechowuje rozmiar punku stawianego na ekranie (wierzchołka), podobnie jak w przypadku rejestru 

oFog

 zawiera on tylko pojedynczą wartość.  

• 

oTn

 - rejestry przechowujące poszczególne pary współrzędnych tekstur. Są to cztery rejestry, każdy podejrzewam 

przechowujący po dwie pary, ale za to ręki uciąć sobie nie dam. Najlepiej sprawdzić na własnej skórze.  

Jak widzimy, rejestrów wyjściowych jest bardzo niewiele, ale to powinno nas tylko cieszyć. Odpowiednia manipulacja 
nadchodzącymi danymi, jakiś niebanalny pomysł na shader i zobaczycie, że naprawdę będzie można robić cudeńka. 
Wystarczy ściągnąć sobie ze strony NVidii NVEffectBrowser i pooglądać co ludzie potrafią wymyślać. I jak tak sobie myślę 
to największym problemem wcale nie jest "jak?", tylko "co?". No ale to tylko taka moja filozofia. 
 

Instrukcje. 

Wracając zaś do rzeczywistości - mamy już omówione deklaratory i rejestry, więc przyszedł czas na instrukcje. Właściwie 
powinienem Was odesłać do dokumentacji, no ale skoro już zaczęliśmy, to brnijmy w to dalej, aż do samego końca, kto wie 
co z tego potem będzie? ;-) Program vertex shadera może zawierać nie więcej niż 128 instrukcji. Dlaczego takie 
ograniczenie? Nie wiem szczerze mówiąc, najprawdopodobniej jest to ograniczone pojemnością jakiejś pamięci, ale może 
ktoś rozbierał kartę i wie coś na ten temat, to się podzieli. Mogę za to zapewnić, że nawet relatywnie tak mała liczba 
zapewnia nam spore możliwości i na razie nie ma się co stresować, no chyba tylko tym, że nie wystarczy nam wyobraźni, 
aby tyle możliwości wykorzystać. Instrukcje mogą przyjmować maksymalnie 3 argumenty do operacji, choć zależy to 
oczywiście od rozkazu. Dokładny opis wszystkich instrukcji znajdziecie oczywiście w dokumentacji, ja napiszę tylko, co tak 
orientacyjnie mogą one robić a resztę doczytacie sobie sami, albo jeśli chcecie, to zrobimy jakiś reference online po polsku z 
opisem co do czego. Jeśli tak - czekam na odzew. Instrukcje dostępne w asemblerze możemy podzielić na takie trzy kategorie 
- instrukcje ogólne, definiowanie wersji i stałych oraz bardzo pomocne makra. Zaczniemy od instrukcji ogólnego 
przeznaczenia: 

• 

add

 - nic prostszego, dodanie dwóch argumentów,  

• 

dp3

 - iloczyn skalarny wektorów złożonych z trzech wartości,  

• 

dp4

 - to samo, ale dla wektorów posiadających cztery współrzędne,  

• 

dst

 - oblicza odległość wektorów,  

• 

expp

 - podnoszenie do potęgi,  

background image

8 

DirectX ▪ Vertex shader 1 

• 

lit

 - instrukcja pomocna przy obliczaniu oświetlenia,  

• 

loqp

 - obliczanie logarytmu,  

• 

mad

 - pomnożenie i dodanie  

• 

max

 - maksymalna wartość,  

• 

min

 - minimalna wartość,  

• 

mov

 - przesłanie wartości,  

• 

mul

 - pomnożenie wartości,  

• 

rcp

 - rozkaz przydatny przy przeliczaniu na współrzędne jednorodne,  

• 

rsq

 - to samo, tylko jeszcze obliczany jest pierwiastek kwadratowy,  

• 

sqe

 - rozkazy do porównywania, ustaw jeśli większy lub równy,  

• 

slt

 - ustaw, jeśli mniejszy,  

• 

sub

 - odejmowanie.  

Na każdym operandzie źródłowym może być dokonana zamiana jego składowych (potem wytłumaczę) oraz negacja podczas 
odczytu. Zapis do rejestrów wynikowych może zawierać maskowanie poszczególnych składowych, tak że tylko określone 
składowe wektora mogą zostać zmienione. Nie można dokonywać zamiany i negacji składowych wektorów podczas zapisu. 
Ale o tym wszystkim powiem przy okazji omawiania programu. 
 
Następną grupą instrukcji jest bardzo mały zbiorek zawierający tylko dwie. Grupa ta służy do definiowania stałych (na 
przykład dla rejestrów pamięci stałej) oraz opisu wersji kodu shadera, którego będziemy używać. Instrukcja dotycząca wersji 
jest wymagana na początku kodu każdego shadera natomiast instrukcje definiujące stałe muszą być wywoływane po 
instrukcji dotyczącej wersji, ale przed wszelkimi innymi. Może też oczywiście ich nie być w ogóle, jeśli nie potrzebujemy 
żadnych stałych. Mamy więc: 

• 

def

 - instrukcja definiująca stałą,  

• 

vs

 - instrukcja określająca wersję naszego shadera - musi ona być obecna w kodzie każdego shadera i musi być 

wywoływana jako pierwsza.  

Ostatnią rzeczą, o jakiej musimy się dowiedzieć, zanim przystąpimy do pisania shadera, są makra. Makra, podobnie jak w 
każdym języku, są złożone najczęściej z kilku prostszych instrukcji i są bardzo pożyteczne. Nie inaczej jest w naszym 
przypadku. Bardzo ważną rzeczą, o której trzeba wspomnieć, jest ilość instrukcji, które zostaną wykonane po wywołaniu 
makra. W celu zagwarantowania tego, że nie przekroczymy regulaminowego rozmiaru 128 instrukcji, Direct3D gwarantuje 
nam, że makra nie rozwiną się w więcej instrukcji, niż tyle, ile jest wymienione w ich szczegółowym opisie w dokumentacji. 
Jeśli coś będzie się Wam nie zgadzać i przekroczycie dozwolony rozmiar, to poszukajcie winy być może właśnie w makrach. 
A cóż możemy znaleźdź wśród naszych milusińskich? Spójrzmy: 

• 

exp

 - makro liczy potęgę liczby 2 z dużą dokładnością,  

• 

frc

 - zwraca część ułamkową argumentu wejściowego,  

• 

log

 - liczy logarytm przy podstawie 2,  

• 

m3x2

 - mnożenie macierzy 3x2,  

• 

m3x3

 - mnożenie macierzy 3x3,  

• 

m3x4

 - to samo dla macierzy 3x4,  

• 

m4x3

 - chyba nie muszę tłumaczyć :-),  

• 

m4x4

 - będziemy to na pewno często używać.  

No i pozostały nam już do omówienia jeszcze tak zwane modyfikatory. Są to rzeczy o których wspominałem już wyżej. 
Pamiętacie jak pisałem o zamianie składowych wektora, maskowaniu wartości przy zapisie do rejestru wyjściowego i negacji 
wektora wejściowego? No więc proszę, oto szczegóły. Zacznijmy od negacji, bo jest najprostsza. 

• 

-r

 - no i tyle wystarczy, aby wszystko odwrócić, czyli zmienić wartości składowych na znak przeciwny, niż mają. 

Plus staje się minusem, minus plusem. Można już zacząć kombinować z naszą sceną. Poniżej przykład: 

 

mov r0, -r1

 

 

• 

r.{x}{y}{z}{w}

 - maskowanie składowych. Wystarczy zaznaczyć, które chcemy mieć zmienione i gotowe. 

Przykład? Proszę bardzo (zapisanie tylko składowej x i w w rejestrze r0, składowa y pozostaje bez zmian): 

background image

9 

DirectX ▪ Vertex shader 1 

 

mov r0.xw, r1

 

 

• 

r.[xyzw][xywz][xywz][xywz]

 - teraz postaram się wytłumaczyć ten straszliwie wyglądający stwór. Otóż 

wspominałem o zamienianiu składowych wektora. Jak to działa? Proszę, oto przykład (i pierwszy sprawdzian tego, 
czy rozumiemy co to jest vertex shader ;-). Mamy rozkaz: 

 

mov r1, r2

 

czyli przesłanie zawartości rejestru tymczasowego r2 do rejestru r1. Po wykonaniu takiej instrukcji oba rejestry będą 
zawierać taką samą wartość. Cóż można by zrobić, żeby jednak sobie to trochę urozmaicić? Ano wykorzystajmy zamianę 
składowych i zróbmy tak:

 

mov r1, r2.xzyw 

 

wygląda pięknie, tylko co to nam zrobi? Popatrzmy na poniższy schemat a wiele nam się powinno wyjaśnić. 

 

 
Jak widzimy, podczas przesyłania wektora z r2 do r1 następuje niejako zamiana składowej y ze składową z, tylko, że ten 
zmieniony wektor znajduje się już w rejestrze r1. Jeśli będziecie mieć jakieś bardziej skomplikowane bryły niż tylko 
sześcian, to być może osiągniecie dzięki temu jakieś fajne efekty. Tutaj ilustruję Wam tylko na czym polega podmianka 
współrzędnych w wektorze. 
 
No i cóż. Omówiliśmy sobie praktycznie całą teorię, która będzie nam niezbędna do zaprogramowania najprostszego vertex 
shadera! Teraz możemy przystąpić do próby stworzenia naszego pierwszego shadera. Czy coś nam z tego wyjdzie? 
Zobaczymy już w następnej lekcji...