Direct3D Pixel shader 1

background image

1

DirectX ▪ Piel shader 1

Nowa zabawka, jaką poznaliśmy jakiś czas temu, czyli vertex shader dał nam do ręki wspaniale możliwości jeśli chodzi o
obróbkę wierzchołków przez nasza kartę i osiąganie wydumanych efektów dotyczących przekształcania geometrii czy
świateł. Ale panom od konstruowania kart było mało, bo nie poprzestali na tym. Nie wystarczyło już to, ze mamy
praktycznie kontrolę nad każdym przekształcanym wierzchołkiem - oni zapragnęli czegoś więcej - chcieli położyć łapę na
każdy piksel na ekranie. Długo myśleli, a ponieważ w międzyczasie inżynierowie od krzemu także się nie obijali, więc
dzisiaj mamy zupełnie nowy, niezwykle fascynujący temat do omówienia - czyli Pixel Shader!
Ci, co uważnie czytał lekcje poświecone vertex shaderom wiedza jak działa taki wynalazek. Można go rozumieć i
rozpatrywać niejako na dwa rożne sposoby. Pierwszy to taki, ze traktujemy nasz shader jako kawałek procesora, czy jakiś
oddzielony blok funkcjonalny takowego, który dostając jakieś dane przekształca je w charakterystyczny dla siebie sposób
używając dostępnych dla siebie rozkazów. Drugi sposób to taki, ze nie interesowaliśmy się tym kawałkiem sprzętu a
patrzyliśmy na program i sposób w jaki podane mu dane przetwarzał. Widząc poszczególne rozkazy i sposób ich
wykorzystania potrafiliśmy przewidzieć mniej więcej co program powinien robić. To, co odnosiło się do vertex shaderów
oczywiście w mniejszym, lub większym stopniu będzie także adekwatne dla ich pikselowych odpowiedników. Różnice
oczywiście będą - począwszy od konstrukcji fizycznej fragmentów układowych, poprzez potrzebne do działania shadera
dane, rozkazy, kończąc na ilości przetwarzanych danych.

Jak my, programiści będziemy patrzeć na piksel shadery? Otóż dla nas najważniejsze nie jest oczywiście badanie jak
fizycznie działa układ, który realizuje instrukcje pixel shadera. Ponieważ my bardzo lubimy kombinować, wymyślać i
tworzyć, więc skupimy się głównie na programowaniu. Będziemy więc patrzeć na shadery głównie przez pryzmat instrukcji,
rejestrów wejściowych i tym podobnych rzeczy. Omówimy więc sobie jak wygląda niejako interfejs shadera - czyli jakie
posiada rejestry wejściowe i wyjściowe oraz jakie są instrukcje, które pozwolą nam tymi danymi operować. Dowiemy się
oczywiście jak napisać takiego shadera, jak skompilować (bo będzie to dla nas oczywiście program) no i w końcu jak tego
wszystkiego użyć naszym programie, żeby zobaczyć na ekranie cos ładnego.

Podobnie jak w przypadku vertex shaderów rozpatrzymy sobie naszego bohatera dzisiejszego z dwóch punktów widzenia. Po
pierwsze - jego zasada działania jako układu, czyli rejestry wejścia, wyjścia, pamięci stałej i tym podobne. Co się dzieje ze
zmiennymi, jak są one przetwarzane i tak dalej. Z drugiej spojrzymy sobie na shadera jako na program - dowiemy się jaka ma
konstrukcje, jak go skompilować i co znaczą poszczególne instrukcje - wszystko odbędzie się podobnie jak w przypadku
vertex shadera.

Zanim zaczniemy jednak omawiać szczegóły konstrukcyjne i poszczególne elementy potrzebne w programowaniu shaderów
należy wspomnieć o dosyć uciążliwej wadzie (zalecie?) pixel shaderów. Otóż chodzi o ich wersje. Oczywiście jak każda
rzecz w komputerowym świecie tak i shadery się rozwijają, ale ponieważ grafika jest chyba jedną z najbardziej
dynamicznych dziedzin w znacznym stopniu odbija się to na shaderach właśnie. Otóż nie dosyć, że różnych bajerów naszym
bohaterom przybywa z każdą wersją, to jeszcze niektóre mechanizmy, sposób ich działania zupełnie się zmienia, co może
doprowadzić do białej gorączki. Na początku więc od razu uczulam na małe zamieszanie z wersjami, o którym na pewno
usłyszymy dzisiaj nie raz.

Zacznijmy może od strony układowej i zobaczmy co tam w środku siedzi. Poniżej znajdujemy rysunek, przedstawiający w
skrócie to, co będziemy dzisiaj analizować. I znowu największym kawałkiem naszego rysunku jest... ALU - czyli, jak
zapewne wszyscy się znakomicie domyślają jakiś kawałek procesora. Nie inaczej i w tym przypadku - wszystko co trafi do
pixel shadera poprzez rejestry wejściowe zostaje poddane obróbce i wypuszczone na zewnątrz. Ten element rysunku akurat
powinien nas najmniej ciekawić, ponieważ zaglądać do niego nie będziemy a nas bardziej szczegółowo interesuje jego
otoczenie.


Procesor nie byłby procesorem, gdyby nie jeden z jego podstawowych elementów, jakim są rejestry. Przechowuje on w nich

background image

2

DirectX ▪ Piel shader 1

wszystkie dane podczas jakichkolwiek operacji na danych, tam je przyjmuje i w nich umieszcza wyniki - analogiczna
sytuacja ma oczywiście miejsce i u nas. Ponieważ nasz procesor jest procesorem graficznym, więc ma trochę pokręconą
budowę w stosunku do znanych nam poprzedników. Ale na tym nie koniec, pixel shader okazuje się nader wrednym tworem
i zrozumienie zasad działania, jakimi się rządzi przyprawi nas zapewne o mały ból głowy.


Jeśli przypomnieć sobie budowę vertex shadera i pomyśleć o niej cieplej to jeszcze da się ją zrozumieć. Natomiast z rysunku
powyżej wynika coś dziwnego. Otóż pixel shader nie ma jednego ale jakby dwa jednoczesne potoki przetwarzania danych -
są one nazywane potokami równoległymi. Lewa strona tego rysunku odpowiada za tzw. kanał danych wektorowych - tutaj są
wpuszczane dane dotyczące kolorów a dokładniej mówiąc składowe RGB. Prawy kanał tak naprawdę operuje tylko na jednej
danej - kanale alfa koloru, czyli umożliwia nam mieszanie z przezroczystością. Żeby ułatwić sobie nieco operowanie tymi
nazwami przyjęło się nazywać lewy kanał kanałem koloru a prawy kanałem alfa. Jak widzimy, na samym początku mamy
wspólny blok, który służy do skopiowania danych z wejścia. Dzieje się tak, ponieważ na przykład wszystkie modyfikatory, o
których się na pewno dowiemy będą operowały na kopiach danych a nie na oryginalnej zawartości rejestrów wejściowych,
żeby za dużo nie namieszać na scenie. Następnie następuje modyfikowanie skopiowanych danych wejściowych. Co i jak
dokładnie wyjaśni nam się na pewno przy opisie modyfikatorów. Jak wiemy modyfikacje są już dokonywane na danych
skopiowanych, w rejestrach wejściowych pozostały dane nie zmienione. I tutaj każdy kanał już operuje sobie swoimi
danymi, w zależności od instrukcji i modyfikatora. Następnie zostaje wykonana jakaś instrukcja a następnie, jeśli jest to
potrzebne jeszcze wynik może zostać także zmodyfikowany. Żeby jeszcze weselej było na sam koniec możemy dokonać tzw.
maskowania składowych, czyli jeśli sobie zażyczymy, że tylko niektóre ze składowych koloru nas interesują to tylko takie
sobie weźmiemy. Zresztą na pewno jeszcze powiemy sobie o tym przy omawianiu wszelkiej maści identyfikatorów. Po
przejściu przez wszystkie fazy obróbki nasz kolor ze sceny zostaje umieszczony w rejestrze wyjściowym i tak naprawdę
zamiast oryginalnego koloru bryły jaki powinien być w danym miejscu zostaje na nią nałożone to, co powstało po
przepuszczeniu tego przez pixel shader. Zasada więc bardzo podobna do działania vertex shadera tylko tutaj działanie nie na
wierzchołkach ale pikselach malowanych na ścianach bryły a dodatkowo mamy rozdział na kolor i jego przezroczystość - to
tak, aby nam się lepiej i wygodniej manipulowało wszystkimi danymi.

Kolejna sprawa o której musimy sobie koniecznie powiedzieć jest mechanizm adresowania tekstur przy korzystaniu z
shadera. Najprościej mówiąc jest to mechanizm, który będzie umożliwiał pobieranie z tekstur nakładanych na daną bryłę
odpowiednich danych o kolorach bazując na przychodzących do shadera współrzędnych mapowania tekstur.
Zazwyczaj współrzędne mapowania tekstury będą dostarczane do shadera jako część formatu wierzchołka (odpowiednie
pola) lub mogą być określane na podstawie tego, z jakiego rodzaju tekstura mamy tak naprawdę do czynienia (możemy mieć
przecież tekstury do efektów specjalnych, jak dla przykładu mapy środowiska).
Po raz pierwszy w tym miejscu dają boleśnie też o sobie znać różnice w budowach poszczególnych wersji shaderów, o
których wspominałem powyżej. Ponieważ w naszym artykule skupimy się na wersjach od 1.0 do 1.4 więc w takim zakresie
będziemy sobie omawiali wszystkie zmiany. Zaczniemy właśnie od mechanizmu adresowania tekstur.

Ale zanim zaczniemy musimy sobie jeszcze powiedzieć o jednym ważnym terminie, jaki będzie nam potrzebny przy
omawianiu mechanizmu adresowania tekstur. Jest nim Samplowanie tekstury. Niektórym nazwa może się kojarzyć
(niekoniecznie poprawnie), ale dla porządku:

background image

3

DirectX ▪ Piel shader 1

Samplowaniem będziemy nazywać pobieranie odpowiednich danych o kolorze z tekstury, ale opierając się na czterech
ważnych informacjach:

• instrukcji adresowania użytej w pixel shaderze,

• współrzędnych mapowania,

• aktualnie ustawionej teksturze na danym poziomie tekstury,

• różnych atrybutów obróbki tekstury ustawionych dla danego poziomu.

Jasno z tego więc wynika, że aby mieć możliwość samplowania tekstury musi ona być przypisana do jakiegoś konkretnego
poziomu tekstury, z którego możemy pobrać dodatkowe informacje o sposobie przedstawiania tekstury na bryle. Inaczej
samplowanie możemy nazywać na przykład przeszukiwaniem tekstury.


Przypatrzmy się temu rysunkowi powyżej. Pokazuje on drogę, jaką mogą przebyć współrzędne mapowania w shaderach od
wersji od 1.0 do 1.3, zanim zostaną użyte do uzyskania koloru tekstury aplikowanej naszej bryle. Jak widać, dróg tych jest
kilka a każda z nich charakteryzuje się inną specyfiką:

• (kolor czerwony) W takim przypadku rejestry tekstury są ładowane bezpośrednio współrzędnymi mapowania a

shader posługując się nimi pobiera z określonego miejsca na teksturze odpowiedni kolor bez żadnych dodatkowych
obliczeń.

• (kolor niebieski) Współrzędne mapowania są przekazywane do tzw. samplera tekstury, który bazując na kilku

wspomnianych przeze mnie wyżej danych pobierze odpowiedni kolor z tekstury i umieści jego wartości w rejestrach
tekstury. Tekstura oczywiście będzie ustawiona na jakimś konkretnym poziomie i dane o samplingu są pobierane z
tegoż poziomu. Numer współrzędnych mapowania (wierzchołek ma ich aż osiem par, podobnie jest z ilością
dostępnych poziomów tekstur) zawsze koresponduje z numerem docelowego rejestru zastosowanego w instrukcji
shadera.

• Pewne instrukcje adresowe tekstur w konkretnych wersjach pixel shadera przeprowadzają różne transformacje na

wejściowych współrzędnych teksturowania aby stworzyć nowe współrzędne. Następnie mogą one być użyte do
samplowania tekstury (kolor zielony) lub być bezpośrednio przekazywane jako dane dla rejestrów tekstury (kolor
żółty). O tym, jakie są to instrukcje i co dokładnie one robią z tymi współrzędnymi możecie się dowiedzieć czytając
dokumentację w DX SDK. Jeśli będzie taka możliwość na pewno sobie o tym powiemy...

Dla wersji 1.4 rejestry tekstur maja trochę inne znaczenie niż w poprzednich wersjach. Zawierają one współrzędne tekstury
więc mamy tutaj niejako podobieństwo do sposobu oznaczonego w poprzednich wersjach kolorem czerwonym. Są to rejestry
tylko do odczytu (używane jako rejestry wejściowe dla instrukcji adresowych) i nie można na nich przeprowadzać operacji
arytmetycznych. Fakt, że posiadamy współrzędne tekstury w rejestrach oznacza tylko tyle, że teraz zbiór współrzędnych
mapowania i numer poziomu tekstury wcale nie musza się zgadzać dla wersji 1.4 pixel shadera. Numer poziomu, z którego
dokonywane jest samplowanie tekstury określany jest przez docelowy numer rejestru, ale zbiór współrzędnych tekstury jest
określany przez nowy rejestr wejściowy

t#

. Bloczek z rysunku powyżej nazwany "zmiana współrzędnych" nie jest obecny w

shaderze w wersji 1.4, ponieważ modyfikacja współrzędnych tekstury przed samplowaniem tekstury jest osiągana w prosty
sposób przez użycie arytmetycznych instrukcji poprzedzonych tzw. zależnym odczytem.
Często bowiem zdarza się, że pożytecznie jest wykonać jakieś operacje na współrzędnych mapowania przed samplowaniem
tekstury - jest to właśnie ten zależny odczyt. Termin "zależny" oznacza ni mniej ni więcej, że dane tekstury, które dostaniemy
w wyniku samplowania będą zależeć od pewnych operacji, które dokonamy wcześniej w pixel shaderze. Dla wersji 1.0-1.3
shadera odczyt zależny także jest możliwy, ale jest bardzo ograniczony i sprowadza się tylko do zastosowania instrukcji
adresowej używającej jako parametru wejściowego rezultatu poprzednio zastosowanej instrukcji adresowej. Dla wersji 1.4
shadera odczyt zależny ma o wiele większe możliwości, ponieważ współrzędne mapowania mogę pochodzić nie tylko z
poprzedniej instrukcji adresowej, ale także z prawie dowolnej instrukcji arytmetycznej - tak, że tutaj można naprawdę sporo
namieszać. O szczegółach takiego zależnego odczytu możecie dowiedzieć się także trochę więcej z dokumentacji.

background image

4

DirectX ▪ Piel shader 1


Zanim przystąpimy do omawiania rejestrów, instrukcji i modyfikatorów jeszcze parę słów:

O problemie ograniczeń w ilości zastosowanych w programie shadera instrukcji wiemy już z opisu vertex shaders. Musimy
uważać, aby nie przekroczyć pewnej ustalonej ilości instrukcji przypadających na jeden program, bo inaczej po prostu
niewiele nadziałamy. Dla pixel shaders "policzalnymi" instrukcjami są tylko instrukcje arytmetyczne i adresowe, nie liczą się
instrukcje definicji, wersji i fazy - dobrze jest o tym pamiętać. Dodatkowo, żeby "umilić" nam fakt nudnego pisania shaderów
konstruktorzy postarali się o to, żeby w każdej wersji były jakieś wyjątki i w jednej na przykład instrukcja się nie liczy, choć
należy do tych "policzalnych", w niektórych liczy się jako jedna, w niektórych jako dwie itd... szczegóły w dokumentacji -
ale zabawa naprawdę przednia w zapamiętywanie ;)) a i okazja do poćwiczenia pamięci nie najgorsza.

Jeśli chodzi o sam program to należy od razu przyjąć do wiadomości, że zostaliśmy zobligowani do ścisłego przestrzegania
pewnego harmonogramu pisania i jeśli nie będziemy się trzymać tego, to program na po prostu nie odpali. Oczywiście nie
obędzie się bez zamieszania z wersjami, więc żeby nie przedłużać:

Dla wersji od 1.0 do 1.3 program powinien wyglądać następująco:

• instrukcja wersji,

• instrukcje definicji stałych,

• instrukcje adresowe tekstur,

• instrukcje arytmetyczne.

Pixel shader w wersji 1.4 dokłada nową cegiełkę i daje możliwość stosowanie instrukcji fazy, która umożliwia w prosty
sposób zwiększenie limitu możliwych do wykorzystania instrukcji arytmetycznych i adresowania, jeśli przestaniemy się
mieścić w naszych zapędach. Program zatem będzie mógł mieć dwie fazy, z których każda może posiadać sześć instrukcji
adresowych a zaraz po nich osiem instrukcji arytmetycznych. Program więc zatem wyglądał będzie następująco:

• instrukcja wersji,

• instrukcje definicji stałych,

• instrukcje adresowe tekstur (6),

• instrukcje arytmetyczne (8),

• instrukcja fazy,

• instrukcje adresowe tekstur (6),

• instrukcje arytmetyczne (8),

Zastosowanie instrukcji ma także swój efekt uboczny, w postaci nie ustawionego elementu kanału alfa przy przejściu do
nowej fazy w rejestrach tymczasowych. Więcej o problemie w opisie instrukcji fazy w DX SDK.

Wiemy już, jak zbudowany jest pixel shader jeśli chodzi o kanały, którymi płyną dane. Wiemy, że są niejako dwa niezależne
od siebie - jeden dla składowych kolorów a drugi dla składowych alfa koloru. Pociąga to za sobą dosyć kuszącą perspektywę
możliwości wykonywania dwóch niezależnych instrukcji arytmetycznych na obu kanałach jednocześnie - są one przecież od
siebie niezależne! Taką możliwość będziemy nazywali parowaniem instrukcji a robienie tego będzie zupełnie banalne.
Wyobraźmy sobie, dwie, na razie zupełnie hipotetyczne instrukcje:

mul r0.rgb, t0, v0

oraz

add r1.a, r1, c2


Jedna z nich przedstawia operację na kanale koloru a druga na kanale alfa - na razie musimy przyjąć to na wiarę, ale bardziej
spostrzegawczy na pewno się domyślają dlaczego. Parowanie instrukcji to nic innego tylko proste dodanie ich w kodzie
shadera:

mul r0.rgb, t0, v0
+ add r1.a, r1, c2

Po prostu przed następną instrukcją dodajemy znaczek plus i gotowe. Główną rolę w takim łączeniu ogrywał będzie
modyfikator selekcji kanałów, ale o nim dowiemy się znacznie później. Ale sama możliwość parowania instrukcji
przedstawia się nader interesująco i maniacy optymalizacji już mogą zacierać ręce.

Ostatnią rzeczą, o której sobie powiemy przed przystąpieniem do omawiania rejestrów i całej reszty będzie kolejność
wykonywania operacji. Otóż instrukcje shadera mogą być modyfikowane przez tzw. modyfikatory, których jest całe
mnóstwo. O rodzajach wszystkich i ich działaniu oczywiście powiemy sobie nieco dokładniej, ale dla naszych programów
znaczenie będzie miała kolejność ich wykonywania na naszych instrukcjach. Rezultat instrukcji zależy właśnie od kolejności
w jakiej modyfikatory zostaną zaaplikowane wynikowi lub wartości wejściowej instrukcji. I tak w kolejności wykonywane są
od najważniejszych:

• selektory rejestrów wejściowych oraz modyfikatory rejestrów wejściowych,

background image

5

DirectX ▪ Piel shader 1

• instrukcje,

• modyfikatory instrukcji,

• selektory rejestrów wyjściowych (docelowych).

Hm... wiemy zatem już co nieco o budowie samego pixel shadera, wiemy jak posługiwać się teksturami oraz jak ma
wyglądać program, nie obeszło się także bez paru sztuczek. Czas więc przystąpić do tego, na co czekamy już od bardzo
dawna - czyli zagłębimy się w morze rejestrów, instrukcji ,modyfikatorów i selektorów. Oczywiście nie omówimy
wszystkich dokładnie, bo chyba artykuł by się wam nie załadował do przeglądarki ;). Powiemy pokrótce co, jak i na co a
szczegóły jak przyjdzie pora znajdziecie i co najważniejsze zrozumiecie bez problemu. Do dzieła więc!

Rejestry.


Zacznijmy może od podobieństw pomiędzy vertex a pixel shaderem a są nimi rejestry pamięci stałej, ale niestety - na samej
nazwie podobieństwa te się kończą. Pierwsza sprawa to ich ilość - tutaj mamy takich rejestrów tylko 8. Jak będziemy mieli
okazje się przekonać później, w testowych programach ta ilość powinna nam zupełnie do szczęścia wystarczyć - operacji na
teksturach znowu nie będzie tak wiele a efekty będą głównie zależeć od naszej wyobraźni ;). No ale wracając do sprawy -
rejestry pamięci stałej służą do tego, aby przechować w nich jakieś potrzebne nam dane. Na przykład jeśli zapragniemy
przekazać do shadera jakieś stale liczbowe, które maja nam pomoc w osiągnięciu kosmicznego, nowego efektu, do tego celu
mogą nam posłużyć właśnie rejestry pamięci stałej. Po prostu umieszczamy w nich kolejne liczby, których będziemy używać
i voila. Jeden rejestr pamięci stałej może przechowywać jedna liczbę z zakresu od -1 do 1. Ma on tez inne ograniczenia, ale w
tej chwili są one dla nas mniej istotne i może o nich kiedy indziej.
Rejestry pamięci stałej będziemy oznaczać w programach shaderów literą

c#

(od constant zapewne) i kolejnym numerem

rejestru, poczynając od

0

a kończąc na

7

. Czy dane nam będzie wykorzystać zawsze wszystkie osiem, zobaczymy.


Następnym zestawem rejestrów wejściowych są znane nam także z vertex shaderów rejestry tymczasowe, służące do
przechowywania wartości tymczasowych podczas przeprowadzania obliczeń. Jeden taki rejestr zawiera cztery liczby typu

float

a ilość takich rejestrów waha się od dwóch do sześciu, w zależności od obsługiwanej przez nasza kartę wersji pixel

shaders. Rejestry tymczasowe będziemy oczywiście oznaczać analogicznie do vertex shaderów literami

r#

i kolejnym

numerem, poczynając od rejestru pierwszego, czyli

0

. Należy w tym miejscu dodatkowo zaznaczyć, że w przypadku pixel

shader rejestr

r0

służy także jako

wyjście

pixel shadera, czyli w nim są umieszczane wartości końcowe obliczonych wartości

poszczególnych pikseli.

Na tych typach rejestrów podobieństwa pomiędzy pikselami a wierzchołkami się kończą. Bądź co bądź jedno z drugim ma
niewiele wspólnego, jeśli chodzi o dane go opisujące, więc nie obędzie się tez to oczywiście bez wpływu na pixel shader. Bez
wątpienia podstawowe znaczenie dla działania pixel shadera będą miały dwa ważne czynniki decydujące o wyglądzie pikseli
na scenie - kolor wierzchołków i tekstury nałożone na wielokąty. Dzięki shaderowi będziemy mieli do dyspozycji narzędzie,
które pozwoli wpływać nam na jedno i drugie w taki sposób, dzięki któremu uzyskamy naprawdę fascynujące efekty i pełną
kontrolę nad tym, co się dzieje z pikselami na ekranie. Ale żeby wiedzieć jak, dowiedzmy się najpierw o pozostałych
rejestrach.

Rejestry tekstury - są odpowiedzialne za przechowywanie danych tekstur. Dokładniej mówiąc będą ładowane
współrzędnymi teksturowania które następnie będą używane do pobrania z określonego miejsca określonego koloru naszej
mapy lub kolorami pochodzącymi z samplowania tekstury. Jak wspomniałem przy omawianiu mechanizmu adresowania,
dane są pobierane z tekstury skojarzonej z odpowiednim poziomem teksturowania. Wiemy, że zarówno poziomów tekstur
jak i zestawów współrzędnych mapowania w wierzchołku możemy mieć aż osiem. W przypadku rejestrów tekstur mamy
znowu doczynienia z zamieszaniem wśród wersji. W dokumentacji do SDK znajdziemy informacje, że dla wersji od 1.1 do
1.3 pierwszy zestaw współrzędnych tekstury w strukturze wierzchołka jest powiązany z pierwszym poziomem tekstury (o
numerze 0). Oznacza to, że tym przypadku istnieje tzw. relacja jeden do jeden jeśli chodzi o poziomy tekstur i kolejność
deklarowania zestawów współrzędnych. Po prostu pierwszemu zadeklerarowanemu zestawowi współrzędnych odpowiada
poziom pierwszy, drugiemu drugi itd.

W przypadku wersji shadera 1.4 takiej zależności już nie ma i kolejność deklaracji zestawów współrzędnych nie determinuje
do którego poziomu odnoszą się dane współrzędne tekstury używane przez shader do pobrania sobie koloru - poziom ten
można ustawić. W zależności od wersji także zachowanie rejestrów jest różne - dla wersji 1.0 pixel shadera rejestry tekstur są
tylko do odczytu, od 1.1 do 1.3 można je traktować tak samo jak rejestry tymczasowe dla działań arytmetycznych, natomiast
w wersji 1.4 rejestry te zawierają współrzędne teksturowania ale można ich używać również jako źródłowych rejestrów dla
operacji służących adresowaniu tekstur - co do czego jest używane na pewno wyjdzie w przykładowych programach. Na
razie musimy wiedzieć tylko tyle, że w rejestrze tym znajdziemy współrzędne tekstury lub jej dane (kolor). Rejestry tekstury
w programach będziemy oznaczali literka

t#

i kolejnym numerem poczynając od

0

. Oczywiście w najnowszych wersjach

shaderów ilości będą się zmieniać zdecydowanie na korzyść (czyt. ilość), ale co i ile to dokładnie w dokumentacji.

Rejestry koloru - jak sama nazwa wskazuje są używane do przechowywania danych o kolorach pikseli. Dane takie są
pobierane ze struktury wierzchołka (jeśli on takowe zawiera, bo jeśli ich nie ma to trudno zgadywać co tam ma być. Używa
się ich głównie kiedy mamy zamiar zrobić jakiś efekt z udziałem koloru wierzchołków (np. zmieszać go z kolorem tekstury,

background image

6

DirectX ▪ Piel shader 1

albo pozmieniać go zgodnie z naszym widzimisie. W rejestrze tym przechowujemy kolory jako liczby

float

(cztery, dla

każdej składowej). I ponownie w tym przypadku mamy zamieszanie jeśli chodzi o wersje shadera. Chodzi o wersję 1.4, w
której konstruktorzy uparli się, że rejestry koloru są dostępne dopiero w drugiej fazie. Z ważniejszych rzeczy należy dodać
jeszcze, że pixel shader ma dostęp do rejestrów koloru tylko do odczytu, nie można zawartości tych rejestrów zmieniać
bezpośrednio. Zawartość tych rejestrów jest ustalana na podstawie przeglądania danych wierzchołków, ale jest to robione z o
wiele mniejsza precyzja niż przeglądanie danych tekstury - zapewne dlatego, że przeważnie kolory wierzchołków nie
zmieniają się gwałtownie na przekroju całej bryły. Dane wejściowe dla rejestrów koloru są skalowane do przedziału od 0 do
1, ponieważ taki zakres jest poprawny dla działania pixel shadera jeśli chodzi o kwestie kolorów - o tym musimy pamiętać
przy operacjach w programie shadera. Znana nam z vertex shadera literka

v#

i kolejne numery są odpowiedzialne za

przechowywanie wartości kolorów - czyli tak właśnie będziemy oznaczać rejestry koloru. Przyjęte jest w zwyczaju, że do
rejestru

v0

wczytuje się wartość

diffuse

ze struktury wierzchołka a do

v1

wartość

specular

. My lubimy od razu nabierać

dobrych przyzwyczajeń, więc o tym pamiętajmy już od teraz, bo zapewne spotkamy wiele przykładowych programów, które
właśnie tak będą działać.

Ograniczenia wejścia.

Wiemy, że rejestry służą jednostce ALU shadera do pobierania i udostępniania danych - czyli ogólnie mówiąc do wymiany z
resztą całego elektronicznego śmiecia na zewnątrz shadera. Wydawać by się mogło, że mamy tutaj pewną dowolność, tak
jednak niestety nie jest. Otóż każdy typ rejestrów ma ściśle określone przeznaczenie i z tym są powiązane pewne restrykcje
jeśli chodzi o użycie ich w poszczególnych instrukcjach. Wielu z rejestrów nie wolno używać w instrukcjach danego typu, w
wielu występują ograniczenia jeśli chodzi o ich ilość w jednej instrukcji. W dokumentacji SDK jest tabelka, która obrazuje
dokładnie co i jak. Przy pisaniu shadera należy więc mieć tę tabelkę zawsze pod ręką i pamiętać o tych rzeczach... inaczej
będzie nam się waliło przy kompilacji i długo będziemy szukać co jest grane. Należy także pamiętać, że ograniczenia w ilości
rejestrów wejściowych nie wpływają w żaden sposób na rejestry wyjściowe - liczby nie są ze sobą w żaden sposób
powiązane. Zresztą - rejestr wyjściowy przeważnie jest tylko jeden, więc wielkiego problemu tutaj nie ma.

Odczyt/Zapis.

Nie może się oczywiście obejść także bez obostrzeń typu "tylko do odczytu". Modyfikacje pewnych rejestrów wejściowych
dla przykładu podczas pisania shadera mogłyby destabilizować jego pracę, postanowiono więc, że możliwość modyfikacji
części z nich zostanie zablokowana, co usprawni pracę shadera (choć nie wiadomo czy nam także ;). Oczywiście rejestry
"tylko do odczytu" mogą być użyte tylko jako rejestry wejściowe, ponieważ nie da się im zmienić wartości podczas działania
programu, ale to jest chyba oczywiste. Oczywiście i tutaj nie może się obejść bez zamieszania z wersjami, ale tabelka
dostępna w SDK powinna wyjaśnić wam wszystko. Proponuje sobie dwie powyższe tabele wydrukować, powiększyć i
powiesić dla przykładu na ścianie nad monitorem... po tysiącu shaderów żadne ściągi nie będą wam już oczywiście
potrzebne, ale na początku warto mieć zawsze pod ręką ;). W dokumentacji znajdujemy jeszcze dwie ważne uwagi co do
typów odczytu:

• dla wersji 1.0 rejestry tekstur maja możliwość zapisu i odczytu przy instrukcjach adresowania tekstur, ale są tylko

do odczytu dla instrukcji arytmetycznych.

• Dla wersji 1.4 rejestry tekstur są tylko do odczytu jeśli chodzi o instrukcje adresowania tekstur oraz należy

zapamiętać, że nie można z nich ani czytać a tym bardziej do nich zapisywać korzystając z instrukcji
arytmetycznych.

Zakresy.

Rejestry jak wiadomo powszechnie są miejscem przechowywania pewnych wartości liczbowych. Ponieważ większości
rejestry kojarzą się z takimi znanymi nam z procesorów jako układów tutaj musimy zwrócić uwagę na pewną, ważną rzecz.
Otóż rejestry shadera przypominają te powyższe tylko z nazwy - wprawdzie maja one określoną pojemność jeśli chodzi o
ilość bitów przypadających na wartość przechowywaną w rejestrze, ale bity te nie zawsze będą określały to samo. Tabela
dostępna w SDK ma za zadanie uzmysłowić użytkownikom jakimi wartościami powinny być ładowane poszczególne rejestry
pixel shadera aby działał on zgodnie ze swoim przeznaczeniem i umożliwiał otrzymanie oczekiwanych wyników. Jak widać
w tabelce cześć takich wartości rożni się oczywiście w zależności od wersji (jakże mogłoby być inaczej :), a cześć z nich
możemy odczytać bezpośrednio z właściwości karty. Należy także zaznaczyć, że wczesne karty mające możliwości
przetwarzania pixel shaders w trybie hardware używały specyficznego, ograniczonego trybu (jeśli chodzi o precyzję)
przechowywania części wartości ułamkowych (na ośmiu bitach tylko), więc należy niekiedy to mieć na uwadze przy
projektowaniu shadera.

Na tym zakończylibyśmy opis ważnej niewątpliwie części shadera jaką są rejestry. A skoro już mamy jak przekazywać i
odbierać dane więc podczas ich przepływu przez pixel shader możemy zacząć cos z nimi robić, czyli zacząć je przekształcać.
Zanim jednak do tego przystąpimy musimy poznać dwie kolejne, ważne sprawy - czyli instrukcje i modyfikatory. Zaczniemy
może od instrukcji, ponieważ z takimi mieliśmy już do czynienia przy omawianiu vertex shadera.

Instrukcje.

background image

7

DirectX ▪ Piel shader 1

Instrukcje możemy podzielić w przypadku pixel shaders na kilka rodzajów, w zależności od rodzaju operacji, jakie będą one
przeprowadzać. Żeby było śmieszniej w zależności od wersji shadera będziemy mieli rożne typy tych instrukcji - zamieszania
będzie co niemiara, ale miejmy nadzieje, że jakoś to wszystko obejmiemy. Oczywiście nie będę tutaj omawiał każdej
instrukcji z osobna na poziomie typów argumentów i jakie rejestry ona może odczytywać i modyfikować - to znajdziecie w
dokumentacji. Ale nakreślimy ogólnie, po co, gdzie i jak.

Typy instrukcji:
Ogólnie biorąc instrukcje możemy podzielić na:

• Instrukcja wersji (znamy ją już z vertex shadera), która definiuje wersję shadera. Musi być ona analogicznie jak w

przypadku vertex shadera pierwszą instrukcja w programie i wystąpić może tylko raz na samym początku - określa
ona według reguł której wersji sprawdzany będzie kod shadera.

• Instrukcje definiujące stale w programie shadera. Instrukcje te musza wystąpić po instrukcji wersji ale przed

wszystkimi instrukcjami arytmetycznymi czy adresowymi.

• Tak zwane instrukcje fazy. Powodują one rozbicie kodu shadera na dwie fazy (tylko w wersji 1.4). Każda z takich

faz ma swoja liczbę instrukcji arytmetycznych i adresowych i jest ona ograniczona.

• Instrukcje arytmetyczne - najbliższe naszemu sercu. Zawierają oczywiście zwykłe operacje matematyczne jak

dodawanie, odejmowanie czy mnożenie, ale także bardziej specyficzne jak na przykład obliczanie iloczynu
skalarnego wektorów - jednym słowem wszystko co potrzebne w obliczeniach grafiki 3D.

• Instrukcje adresowania - nazwa może trochę tajemnicza ale instrukcje te służą do manipulacji współrzędnymi

teksturowania, które są powiązane z określonym poziomem tekstur.

Drugą sprawą występującą niejako przy okazji są tak zwane modyfikatory. Służą one jak sama nazwa wskazuje modyfikacji -
ktoś zapyta "modyfikacji czego?". W dokumentacji mamy napisane, że instrukcji oraz rejestrów wejściowych i wyjściowych.
Oczywiście zamieszania mamy ciąg dalszy, ponieważ wersja 1.4 shadera umożliwia dodatkowo modyfikacje rejestrów
tekstur - są to tak zwane modyfikatory rejestrów tekstur. Ale o nich dalej.

Modyfikatory instrukcji - służą do zmiany sposobu działania instrukcji a dokładniej mówiąc do zmiany wartości
wyjściowej po jej zadziałaniu. Po zaaplikowaniu takiego modyfikatora a nie wiedząc dokładnie jak on działa możemy się
nieźle zdziwić obserwując wartości wyjściowe shadera - tutaj więc potrzebna jest szczególna rozwaga przy używaniu takich
wynalazków. Modyfikacja instrukcji polega na tym, że instrukcja najpierw oblicza wartość wyjściową, ale przed zapisaniem
jej do rejestru wyjściowego jest modyfikowana przez zaaplikowany modyfikator. Mogą one być używane tylko do instrukcji
arytmetycznych - używanie ich na rejestrach tekstur nie jest możliwe.
A jak zaaplikować modyfikator do instrukcji? Nic bardziej prostszego - po prostu po nazwie instrukcji należy dopisać
przyrostek definiujący dany modyfikator. W tabeli w SDK znajdziecie oczywiście listę wszystkich modyfikatorów
arytmetycznych dostępnych łącznie z wersja shadera. Najfajniejszą jednak z tego wszystkiego rzeczą jest możliwość łączenia
modyfikatorów! Otóż wystarczy po instrukcji dołączać kolejne modyfikatory na tej samej zasadzie do instrukcji a ona będzie
modyfikowana przez kolejne z nich. Jak przykład rozpatrzmy następującą instrukcje:

add_x2_sat dest, src0, src1

add

to instrukcja dodania do siebie dwóch wartości z rejestrów wejściowych

src0

i

src1

. W wyniku przeprowadzonej

operacji rezultat dodawania powinien się znaleźć w rejestrze

dest

, jednak nie stanie się to tak od razu. Popatrzmy dokładniej

co mamy za instrukcje - nie jest to przecież add bo ten stwór wygląda tak:

add_x2_sat

. To jest właśnie bajer ze składaniem

modyfikatorów - aplikujemy instrukcji add kilka pod rząd, tak że rezultat zostanie mocno przekształcony. Najpierw widzimy
cos takiego jak

_x2

- jak przeczytacie sobie w dokumentacji dokładnie to dowiecie się, że jest to po prostu pomnożenie przez

liczbę 2. Następnie widzimy

_sat

- czyli jak wynika z dokumentacji obcięcie wyniku do przedziału od 0.0 do 1.0. Kolejność

działań takich modyfikatorów jest taka, jak w zapisie instrukcji - czyli najpierw wyliczenie sumy argumentów, potem
pomnożenie wyniku przez dwa a na koniec obcięcie tego do przedziału 0.0 - 1.0. Mam nadzieje, że nie jest to zbyt
skomplikowane?

Modyfikatory rejestrów wejściowych - jak sama nazwa wskazuje. Są to modyfikatory, które powodują zmianę wartości
odczytanej z rejestru wejściowego przed przekazaniem jej do instrukcji. Jednocześnie zawartość samego rejestru pozostaje
niezmieniona. Jak widać takie modyfikatory mogą się przydać na przykład do przygotowania danych w rejestrach
wejściowych przed wykonaniem określonej instrukcji - jeśli dane nie spełniają jakiegoś warunku a instrukcja tego wymaga to
można takim danych aplikować modyfikator i po krzyku. Podobnie jak w przypadku modyfikatorów instrukcji nie mogą te
modyfikatory być używane w innych instrukcjach niż arytmetyczne - jedynym wyjątkiem od tej reguły jest modyfikator o
nazwie "signed scale". Po szczegóły odeślę was może do dokumentacji, ponieważ szczegółowe opisywanie wszystkich
instrukcji zajęłoby nam zbyt dużo czasu a nie jest ono nam do szczęścia koniecznie potrzebne. W przypadku tych
modyfikatorów istnieje także kilka ograniczeń, które trzeba wziąć konieczne pod uwagę:

• Modyfikator negacji nie może być użyty w połączeniu z modyfikatorem odwracania a w przypadku kombinacji z

innymi modyfikatorami jest wykonywany na końcu,

background image

8

DirectX ▪ Piel shader 1

• Modyfikator odwracania nie może być łączony z innymi,

• Modyfikatory mogą być łączone z selektorami (co to już za moment),

• Modyfikatory rejestrów źródłowych nie powinny być używane dla rejestrów pamięci stałej, ponieważ może to

spowodować nieokreślone ich zachowanie. Dla wersji 1.4 wywołanie modyfikatora na rejestrze stałym spowoduje
błąd już podczas sprawdzania shadera.

Selektory rejestrów wejściowych - ten typ modyfikatorów służy do wstawiania wartości z jednego kanału do innych. Cóż to
znaczy? Jak mieliśmy okazje się już dowiedzieć rejestry pixel shadera przechowują przeważnie cztery liczby

float

. Każdą z

tych liczb możemy sobie wyobrazić, że trzymana jest tak jakby w oddzielnym kawałku tego shadera - nazwijmy go sobie dla
naszych rozważań właśnie kanałem. Jeśli na przykład wyobrazimy sobie rejestr koloru to mam on cztery kanały a w każdym
przetrzymuje jedną składową koloru jako liczbę

float

.


Jak działają takie selektory? Otóż nic bardziej banalnego - po prostu jeśli wywołamy określony selektor (dla określonego
kanału) to jego zawartość zostanie przepisana do innych kanałów tego rejestru, dla którego został wywołany. Możemy więc
do wszystkich kanałów przepisać zawartość kanału zawierającego składową r, g, b czy alfa. Należy sobie w tym miejscu
zapamiętać, że selektor jest aplikowany danemu rejestrowi przed modyfikatorem rejestru wejściowego co z kolei implikuje
także pierwszeństwo przed instrukcją. Selektor aplikuje się bardzo prosto - wystarczy do nazwy rejestru po kropce dodać
odpowiednia literkę. Zestawów takich literek może być dwa - albo

rgba

(żeby się kojarzyło z kolorem) albo

xyzw

- zestawy

te są zamienne i powodują takie samo działanie. Kiedy shader spotyka selektor w swoim kodzie podczas odczytu rejestru
źródłowego zamiast odczytanych wartości z każdego kanału do instrukcji trafiają pozmieniane wartości - w każdy kanale jest
to samo - zawartość samego rejestru wejściowego się nie zmienia - modyfikatory po prostu w tym przypadku modyfikują
wartości już odczytane. Przypatrzmy się przykładowemu modyfikatorowi rejestru wejściowego:

mul r0, r0, r1.r

Rejestrami wejściowymi w tym przypadku są

r0

i

r1

. Jak widać w przypadku

r1

stosujemy na nim modyfikator, który

spowoduje, że wartości w kanale

r

(czerwonym) zostaną powielone na wszystkie inne kanały tego rejestru, ale stanie się to

po jego odczytaniu, czyli tak naprawdę rejestr

r1

pozostanie niezmieniony. Za to do instrukcji mnożenia trafi przekształcona

wartość z rejestru

r1

.


Maska zapisu rejestrów wyjściowych - brzmi skomplikowanie, ale nie jest takie w istocie. Podczas wykonywania
większości instrukcji pixel shadera dane są umieszczane w jakimś rejestrze wyjściowym. Jak wiemy większość z tych,
mogących zostać zapisanych przez instrukcje jest zbudowana z czterech kanałów.
Ten typ modyfikatora pozwala na kontrolę, które kanały w rejestrze wyjściowym instrukcji będą mogły być zapisane a które
pozostaną nie zmienione. Format tego modyfikatora może być łatwy do odgadnięcia. Otóż, aby oznaczyć które kanały mogą
być zapisane a które nie, wystarczy po nazwie rejestru wyjściowego dać kropkę i wpisać symboliczne oznaczenia kanałów.
Tymi oznaczeniami są r, g, b, a (lub analogicznie do modyfikatora poprzedniego x, y, z, w. Nie obyło się tutaj oczywiście
bez burzy jeśli chodzi o wersje shaderów. Dla shaderów z przedziału 1.0 do 1.3 mamy niestety dosyć ubogie możliwości jak
widać z tabelki dostępnej w dokumentacji jeśli chodzi o maskowanie i możemy sobie tylko zablokować niejako zapis kanału
odpowiedzialnego za wartość alfa (modyfikator

.rgb

). Natomiast w wersji 1.4 możemy stosować już dowolne kombinacje

poszczególnych kanałów, z tym że istnieje tutaj pewne ograniczenie co do pisania kodu. Poszczególne pola musza być
podane w kolejności, w jakiej występują w rejestrze wyjściowym - jeśli dla przykładu chcielibyśmy sobie zamaskować kanał
"niebieski" i umożliwić zapis tylko do pozostałych musimy skonstruować modyfikator w sposób:

.rga

- nie dopuszczalny

jest na przykład zapis ".gar" czy ".rag" - po prostu nie wolno nam w takim modyfikatorze przestawiać liter - kolejność
kanałów musi zostać zachowana. Maskowanie zapisu rejestrów wyjściowych jest dostępne tylko dla instrukcji
arytmetycznych - żadnych innych. Tutaj także istnieją oczywiście wyjątki, ponieważ dla shaderów 1.4 niektóre instrukcje
adresowania mogą takiego maskowania dokonywać. Ale po wszystkie szczegóły polecam udać się do dokumentacji, bo
gdybyśmy tak zaczęli opisywać wszystkie możliwe sytuacje to by nam wyszła niezła księga.

Nie podanie żadnego modyfikatora po nazwie rejestru wyjściowego jest równoznaczne, z maska .rgba - oczywiście można
się tego domyśleć po działaniu shadera bez zastosowania modyfikatora. Ale warto czasem o tym pamiętać, jak przyjdzie nam
się zmierzyć z jakimiś jeszcze bliżej nieokreślonymi kosmicznymi problemami.

No i cóż na koniec mogę powiedzieć - wygląda to dosyć skomplikowanie i jak popatrzeć nawet do dokumentacji to jest tego
dosyć sporo. Na pewno pisanie pixel shaders będzie wymagało o wiele więcej uwagi niż tych, dotyczących wierzchołków,
ale też efekty będą o wiele bardziej fascynujące. Mając wiedzę o każdym pikselu na ekranie będziemy mogli z nim zrobić
praktycznie wszystko a wszystkie dotychczas poznane techniki używające na przykład mieszania tekstur będziemy mieli do
dyspozycji na poziomie kodu programu. Tak więc zamiast definiować jakieś tam stany i posługiwać się właściwościami
poziomów tekstur my w kodzie zrobimy sobie z teksturą czy kolorem bryły dosłownie wszystko. Takie efekty jak cienie,
oświetlenie liczone dla pojedynczych pikseli, efekty przezroczystości, mgły, wypukłości - to wszystko czeka na nas w
zupełnie nowym wymiarze - wymiarze pixel shadera. Czeka nas zatem naprawdę fascynująca zabawa, bo uzbrojeni w w
kartę dowolnie programowalną możemy w zasadzie już tylko jedno - wymyślać i korzystać dowoli z dobrodziejstw
shaderów.
W momencie, kiedy powstaje ten artykuł są zapewne już na rynku zapewne karty obsługujące shadery w wersjach powyżej

background image

9

DirectX ▪ Piel shader 1

2.0. Jeśli wierzyć buńczucznym zapowiedziom konstruktorów będziemy mieli do dyspozycji możliwość sterowania kodem
shadera (instrukcje w stylu if-else) na przykład, na pewno się zwiększy ilość dostępnych instrukcji, zarówno jeśli chodzi o
różnorodność działania jaki i możliwości budowy większych programów, powstaną zapewne języki wysokiego poziomu jeśli
chodzi o shadery i pewnie znowu odejdziemy od asemblera. Ale jedno jest pewne - dobrze wiedzieć co siedzi na samym
spodzie, żeby w razie czego mieć od czego zacząć przy szukaniu błędów. Do czasu następnej lekcji polecam zasiąść
chwileczkę nad dokumentacją i zapoznać się choć pobieżnie z instrukcjami i modyfikatorami, żeby potem nie szukać i nie
kombinować niepotrzebnie, bo może się okazać, że efekt który usiłujemy mozolnie osiągnąć kombinując instrukcjami można
zrobić dosłownie w jednej linijce za pomocą modyfikatorów.
A już w następnej lekcji poznamy praktykę - czyli napiszemy nasz pierwszy pixel shader i spróbujemy wycisnąć z tego jakiś
niebanalny efekt - czy z dobrym skutkiem przekonamy się już niedługo ;).Do zobaczenia.


Wyszukiwarka

Podobne podstrony:
Direct3D Pixel shader 2
Direct3D Vertex shader 1
Direct3D Vertex shader 2
Direct3D Vertex 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