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
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:
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.
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,
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,
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.
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,
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
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.