1
DirectX ▪ Piel shader 2
Przechodzimy od teorii do praktyki - dzisiaj więc napiszemy nasz własny, pierwszy piksel shader. Mam nadzieję, że choć
pobieżnie się zapoznaliście się z dokumentacją i macie niejakie pojęcie jakie mamy dostępne instrukcje, modyfikatory i
rejestry. Ufam także, że lekcja poprzednia została porządnie przerobiona i raczej wszystko będzie jasne w tym artykule.
Oczywiście omówimy sobie wszystko dokładnie co i jak, no ale do pewnych rzeczy raczej już nie będziemy wracać. Tak
więc chyba wszystko w gotowości więc zaczynamy!
Dzisiejszy przykład jak już wiemy ma pokazać, w jaki sposób posługiwać się nowym wynalazkiem, jakim jest pixel shader.
Ale przykład nasz będzie się charakteryzował jeszcze jedną, dosyć istotną cechą - już na pełną skalę posłużymy się w nim
zarówno vertex jak i pixel shaderem - oznacza to, że będziemy mieć w zasadzie pełną kontrolę nad tym co się dzieje na
ekranie - i to zarówno nad szalejącymi wierzchołkami jak i poszczególnymi pikselami znajdującymi się na samej bryle,
zdefiniowanej przez te wierzchołki. Przyznam szczerze, że chciałem w tym przykładzie zasunąć już jakieś super wyglądające
mapowanie wypukłości, ale niestety - to nie ta lekcja. Dzisiaj pobawimy się najprymitywniej w zasadzie jak można, ale za to
poznamy tajemnice pixel shaders, które potem umożliwią nam zabawę na naprawdę wysokim poziomie.
W tej lekcji w zasadzie cały potrzebny kod jest wam doskonale znany - doszło tylko kilka instrukcji, które i tak nie będą dla
was stanowić żadnej tajemnicy. Opiszemy także szczegółowo pixel shader, który będzie naprawdę bardzo króciutki, ale
zobaczycie za to różnicę w ilości kodu, który trzeba by wklepać normalnie, aby osiągnąć ten sam efekt ;). Aby więc nie
przedłużać zacznijmy może od tego, co przedstawiał będzie przykład. Omówimy sobie dzisiaj przykład, który kiedyś już w
zasadzie omawialiśmy - multiteksturing jednoprzebiegowy. Czyli nałożymy sobie na nasz obiekt dwie tekstury, renderując ją
tylko raz no i oczywiście zmieszamy nasze tekstury w odpowiedni sposób aby osiągnąć określony efekt. Tak więc w naszym
przykładzie wystąpi bryła z wierzchołkami, które będą posiadały odpowiednią strukturę - będą mianowicie zawierały dwa
zestawy mapowania, dla każdej tekstury oddzielny. Oczywiście pociąga to za sobą odpowiednie zmiany w deklaratorze
vertex shadera oraz w typie wierzchołków. Ale o tym już mówiliśmy sobie także nie raz i nie dwa, a jeśli już nie pamiętacie
to zapraszam do lekcji, właśnie choćby o multiteksturingu. Oczywiście trzeba stworzyć i załadować tekstury (ktoś nie
potrafi? ;) no i co najważniejsze w tej lekcji załadować i skompilować program pixel shadera. I właśnie od tego sobie
zaczniemy opis dzisiejszego kodu:
hError = D3DXAssembleShaderFromFile( szBuffer, 0, NULL, &pCode, &pError );
if( NULL != pError )
{
plik.clear();
plik << (char*)( pError->GetBufferPointer() ) << endl;
plik.flush();
pError->Release();
return false;
}
g_pd3dDevice->CreatePixelShader( (DWORD*)pCode->GetBufferPointer(), &PixelShader );
pCode->Release();
I cóż tutaj widać takiego strasznego... Jak widać nasza ulubiona biblioteka
D3DX
nie próżnuje i tym razem dostarcza nam
odpowiednich funkcji do działania. Tak samo jak w przypadku vertex shadera tak i w tym mamy możliwość zarówno
wklepania kodu shadera bezpośrednio w kodzie C/C++ jako łańcucha znakowego (przykłady znajdziecie w SDK na każdym
kroku), ja jednak nie preferuję takich rozwiązań. My sobie napiszemy nasz shader w pliku tekstowym, załadujemy i
skompilujemy. Do tego właśnie służy funkcja
D3DXAssembleShaderFromFile()
, którą dostarcza nam biblioteka
D3DX
.
Co do parametrów, to w zasadzie już omawialiśmy sobie tę funkcję, ale może przypomnijmy sobie. Pierwszym parametrem
jest ścieżka do pliku, który zawiera kod naszego shadera. U nasz trochę ostatnio pokręciłem z układem projektów, więc
przekazujemy tu bufor znakowy, który zawiera w jakiś tam sposób tę ścieżkę wykombinowaną, ze zrozumieniem nie
powinniście mieć żadnych problemów. Drugi parametr to pewna kombinacja flag, na które zostanie zwrócona uwaga podczas
kompilacji shadera. Umożliwiają one głównie ułatwienia w debugowaniu i analizowaniu shadera pod względów w działaniu.
Flag tych nie jest wiele, więc ciekawscy mogą pogrzebać w opisach. My na razie się uczymy, więc nie będziemy nic
kombinować i ustawimy sobie to na zero. Pozostałe trzy parametry to wskaźniki na obiekty typu
D3DXBUFFER
. Są to nic
innego jak tylko bufory, do których Direct3D będzie wkładał odpowiednie informacje w czasie wywoływania tej funkcji.
Pierwszy bufor (u nas nie używany) może zawierać pewne informacje na temat stałych deklarowanych dla określonego
shadera. Jeśli wartość tę ustawić na
NULL
to parametr ten jest ignorowany. Nas na razie takie informacje nie interesują, więc
wiadomo skąd taka a nie inna wartość. Następny bufor, u nas nazwany jako pCode to miejsce, gdzie zostanie umieszczony
skompilowany, wykonywalny kod naszego shadera. Bufor ten potem zostanie użyty do stworzenia shadera, ponieważ samo
wywołanie omawianej funkcji niczego takiego oczywiście nie powoduje. Czwarty parametr to można by powiedzieć tak na
wszelki wypadek, ale jest to bardzo pożyteczny zarazem element. W razie jakiś błędów w kodzie shadera podczas jego
analizowania w tym buforze zostaną umieszczone informacje o błędach w postaci zwykłych łańcuchów znakowych
zawierające opis i numer linii na przykład co znacznie ułatwi ich poszukiwanie i eliminowanie. Jeśli podczas analizy kodu
nie wystąpią żadne błędy to parametr ten zostanie ustawiony na wartość
NULL
. Po wywołaniu tej funkcji, jeśli wszystko się
uda, w zmiennej pCode powinniśmy otrzymać to, co nas interesuje, czyli kod shadera. Ale na wszelki wypadek sprawdzamy
jeszcze zawartość zmienne pError, bo może się okazać, że bufor z błędami nie jest pusty. Jeśli taka sytuacja wystąpi to my
w naszym programie po prostu wrzucimy sobie zawartość tego bufora do pliku, żeby mieć wyraźnie i bez wątpliwości, że coś
2
DirectX ▪ Piel shader 2
jest nie tak. Taki sposób jak nietrudno zauważyć zapewnia dosyć prostą obronę przez ewentualnym wywaleniem się
programu, w sytuacji gdy spróbujemy tworzyć shader nie mając skompilowanego jego kodu.
A skoro już o tworzeniu shadera... czas aby wreszcie na scenę naszych dzisiejszych działań wkroczyło nasze cudne
urządzenie i zadziałało. No i działa - za pomocą metody
CreatePixelShader()
. Sama nazwa nie wymaga chyba komentarza
mam nadzieję, parametry zresztą też. Jako pierwszy podajemy urządzeniu kod shadera znajdujący się w odpowiednim
buforze a jako drugi identyfikator naszego shadera w postaci liczby typu
DWORD
. I od tej pory w zasadzie naszym
obiektem zainteresowania powinna być właśnie tylko ta liczba, bo dzięki niej będziemy mieć dostęp do naszego shadera i
będziemy go mogli używać dzięki niej. Jeśli wszystko się powiedzie oczywiście to dostaniemy do ręki naszą liczbę i
będziemy mogli aplikować nasz shader kiedy tylko nam się żywnie będzie podobało. A czy z dobrym skutkiem?
Jak widać, zupełnie nic strasznego się nie dzieje i jak na razie nie wygląda to tak źle. I faktycznie, jak wszystko w DirectX,
tylko straszy to na początku a potem jest już zupełnie miło i człowiekowi aż brakuje pomysłów do czego by to jeszcze można
było zmusić.
Ale wracając do naszego kodu - cóż shader skompilowaliśmy (przynajmniej taką mamy nadzieję), mamy jego identyfikator,
tekstury załóżmy, że także załadowaliśmy do pamięci, bryłę utworzyliśmy, ale zanim coś na ekran wyrzucimy trzeba jeszcze
parę rzeczy zrobić. A tak naprawdę te rzeczy zrobimy sobie dzisiaj znowu w mało elegancki sposób, bo wrzucimy je
bezpośrednio do funkcji renderującej.
g_pd3dDevice->SetTexture( 0, g_pTex1 );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_MINFILTER, D3DTEXF_LINEAR );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR );
g_pd3dDevice->SetTexture( 1, g_pTex2 );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_MINFILTER, D3DTEXF_LINEAR );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_MAGFILTER, D3DTEXF_LINEAR );
Chodzi właśnie o tekstury. Wprawdzie załadowaliśmy je do pamięci, ale urządzenie jeszcze nie wie, że akurat w przypadku
tej bryły ma się nimi posłużyć. Tak więc przed renderingiem naszej bryły musimy odpowiednio poustawiać poziomy tekstur,
aby urządzenie a co za tym idzie i sam pixel shader miał skąd pobierać dane. To nie stanowi problemu, bo potrafimy robić to
przecież znakomicie i mamy to w małym palcu. Ale tak dla przypomnienia wrzucam odpowiedni kawałek kodu.
Wspomniałem także na początku artykułu o tym, że będziemy się dzisiaj bawić w multiteksturing - no i słowa oczywiście
dotrzymuję. W tym momencie powinienem was odesłać do kodu lekcji o multiteksturingu jednoprzebiegowym, ale żeby było
całkiem jasne przytoczmy sobie ten kawałek tutaj. Oto co musieliśmy wpisać w naszym kodzie, aby tekstury nam się ładnie
nałożyły na siebie:
g_pd3dDevice->SetTexture( 0, g_pTex1 );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_TEXCOORDINDEX, 0 );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1 );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_MINFILTER, D3DTEXF_LINEAR );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR );
g_pd3dDevice->SetTexture( 1, g_pTex2 );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_TEXCOORDINDEX, 0 );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_MODULATE );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_TEXTURE );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_CURRENT );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_ALPHAOP, D3DTOP_DISABLE );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_ALPHAARG1, D3DTA_TEXTURE );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_ALPHAARG2, D3DTA_CURRENT );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_MINFILTER, D3DTEXF_LINEAR );
g_pd3dDevice->SetTextureStageState( 1, D3DTSS_MAGFILTER, D3DTEXF_LINEAR );
Także mamy więc ustawienie poziomów tekstur i sposobu filtracji. No ale oprócz tego mnóstwo wywołań funkcji
SetTextureStageState()
, która to wywoływała u urządzenia różne dziwne stany a te z kolei powodowały takie a nie inne
efekty na ekranie. No a teraz czas na nasz kod, który musimy wklepać, żeby osiągnąć ten same efekt:
g_pd3dDevice->SetPixelShader( PixelShader );
Uff!, czekałem na ten moment ;). Podoba się, prawda? Zamiast mnóstwa wywołań tej samej funkcji jedna linia! I cóż można
dodać - jest i przejrzyściej, mniej klepania kodu, pliki są mniejsze - same zalety. Oczywiście to tylko na pierwszy rzut oka.
Bo bez skompilowanego kodu naszego shadera nic nie zdziałamy tym wywołaniem a tylko możemy spowodować
zawieszenie się albo wywalenie naszego programu. Ale przy założeniu, że kod takowy posiadamy wygląda to po prostu
3
DirectX ▪ Piel shader 2
elegancko, czyż nie? A co zrobić, że kod takowy posiadać? Otóż bardzo niewiele, trzeba wykonać wszystkie powyższe
opisane przeze mnie czynności na jednym maleńkim pliku zawierającym kilka linii:
ps.1.1
; texture instructions
tex t0
tex t1
; arithmetic instructions
mul r0, t0, t1
Tak, tak - to kod naszego pierwszego pixel shadera - nie ogarnia was zdziwienie? Jeszcze raz proponuję przyjrzeć się temu
kawałowi z kodu multiteksturowania jednoprzebiegowego i tym kilku wyrazom, z których większość jest zaledwie
dwuliterowa! ;). Aż trudno uwierzyć, że taka "pchełka" potrafi to samo, co tamten kawałek a przy bardzo niewielkiej zmianie
potrafi o wiele więcej! No ale to się nazywa właśnie postęp technologii i to zarówno w programowaniu jak i sprzęcie - mi w
każdym razie kojarzy się jak porównanie lampy elektronowej i współczesnego tranzystora ;). No ale koniec może
zachwytów, bo wszystko pięknie działa a my jeszcze nie wiemy dlaczego, czas więc się dowiedzieć właśnie teraz.
ps.1.1
Pierwszą instrukcją shadera jest, bez wyjątku czy dla wierzchołków czy pikseli zawsze instrukcja wersji. Mówi ona, w jakiej
wersji shader jest napisany, co jest szczególnie ważne w przypadku pixel shaders, bo jak wiemy z lekcji poprzedniej w
zależności od wersji shadera te same instrukcje czy rejestry mają niekiedy różne działanie! Oczywiście im wyższa wersja tym
więcej różnych nowych rozkazów nam się pojawia. Ale ponieważ my dopiero zaczynamy więc wystartujemy może od wersji
1.1, która nie jest jeszcze aż tak skomplikowana, żeby nie dało jej się zrozumieć. Tak więc teraz, analizując kod naszego
shadera będziemy sprawdzali w dokumentacji jak działają instrukcje w wersji 1.1.
Jak wiemy także z poprzedniej lekcji pixel shader ma pewien określony porządek występowania instrukcji w kodzie.
Najpierw jest oczywiście instrukcja wersji, potem definicje stałych (tych nie znajdziemy w naszym programie), potem
instrukcje dotyczące tekstur a na końcu instrukcje arytmetyczne. Istnieje jeszcze instrukcja fazy, ale pojawia się ona w
wyższych wersjach shaderów, więc na razie nie ma potrzeby jej dokładnie omawiać, na pewno jeszcze na nią przyjdzie czas.
Skoro więc mieliśmy już instrukcje wersji a jak powiedziałem nie mamy w naszym kodzie deklaracji rejestrów pamięci
stałej, więc kolej na instrukcje tekstur:
; texture instructions
tex t0
tex t1
W naszym przykładzie jak już wałkujemy od samego początku, będziemy się bawić w multiteksturowanie. A skoro tak, to
niewątpliwie będziemy mieli do czynienia z teksturą i to nie jedną. W programie głównym naszej aplikacji załadowaliśmy i
zrobiliśmy co trzeba jeśli chodzi o tekstury. Czas więc aby dobrał się do nich nasz shader, bo do tego właśnie jest
przeznaczony - przerabianie danych pikseli to jego ulubione zajęcie. Pamiętamy jak to wszystko działało w przypadku vertex
shaders - tam było prosto. Figura miała x wierzchołków, z których o każdym wiedzieliśmy. Taki wierzchołek wpadał do
vertex shadera i tam był przetwarzany i w każdym miejscu wiedzieliśmy co się z nim dzieje. A co w przypadku tekstur?
Ogólnie mówiąc - też jest podobnie, tylko że chodzi o piksele, ale możemy się poczuć tutaj trochę zakręceni. Chodzi
oczywiście o piksele tekstury czy obiektu (kolory wierzchołków), ale wiemy, że w tym momencie nie możemy pominąć tak
ważnej sprawy jak rodzaj cieniowania zastosowanego dla obiektu jak i rodzaju filtrowania zastosowanego do tekstury.
Przecież w zależności od odpowiednich ustawień uzyskamy zupełnie inne efekty na ekranie! Tutaj powstaje więc pytanie w
jaki sposób pixel shader pobiera piksele z bryły i skąd wie, jakie mają być kolejne wartości. Tutaj trzeba trochę wyobraźni,
ale poradzimy sobie z tym jakoś razem. Wyobraźmy sobie, że nasz pixel shader przetwarza piksele bryły, która nie ma
nałożonych tekstur. Pobiera więc pixel, coś z nim robi i przekazuje na wyjście. Żeby shader miał jakiś sensowny czas
działania ilość tych pikseli jest w jakiś sposób ograniczona, ale czy ktoś jest mi w stanie powiedzieć ile pikseli ma bryła
zbudowana z określonych wierzchołków? Przyznam szczerze, że tak naprawdę to nie mam pojęcia, w jaki sposób shader to
robi. Pobierając kolor z bryły analizuje on kolory wierzchołków i na ich podstawie jest w stanie określić konkretny kolor,
który zostanie mu przekazany przez rejestr wejściowy. Czy w jakiś sposób bazuje on na położeniu bryły na ekranie, czy
przelicza jakieś współrzędne z płaskich na przestrzenne czy może stosuje jeszcze jakieś magiczne sztuczki - mnie nie
pytajcie. Ale jeszcze bardziej fascynującą rzeczą jest to, że my wcale nie musimy wiedzieć w jaki sposób się to dzieje!
Pamiętamy lekcję o teksturowaniu i o mieszaniu kolorów wierzchołków z kolorem nakładanej tekstury? Wtedy urządzeniu
ustawialiśmy odpowiedni sposób mieszania kolorów i w efekcie otrzymywaliśmy na przykład na ścianie teksturę zabarwioną
na czerwono (od koloru wierzchołków). I wcale nie przejmowaliśmy się tym, w jaki sposób się to dzieje, po prostu kolory się
mieszały i już.
I tak samo postąpimy w tym przypadku, bo i efekt działania pixel shadera będzie dokładnie taki sam jak tej prostej sztuczki
ze wspomnianej ze mnie lekcji. Nas po prostu będzie interesować to, żeby powiedzieć shaderowi skąd ma pobrać dane a
resztę załatwi sam. Ale w przeciwieństwie do tamtej metody będziemy mieć jedną zaletę. Otóż będziemy mieli w ręku
4
DirectX ▪ Piel shader 2
aktualnie pobrany z bryły czy tekstury kolor w jakimś rejestrze. W którym dokładnie miejscu licząc w pikselach na bryle to
nie będzie nas to obchodzić, bo nas nie interesują miejsca a wartości kolorów. My możemy sobie odpowiednio zareagować
na pojawienie się koloru, bo będziemy po prostu znali jego wartość.
Wracając zaś do naszego programu - widzimy najpierw komentarz (zasady są takie same, jak ze znanego niektórym
asemblera - rozpoczyna się po prostu średnikiem). Następnie są dwie instrukcje
tex
. Dokumentacja mówi, że powodują one
załadowanie koloru z zsamplowanej tekstury do rejestru tekstury - inaczej mówiąc pobierają kolor tekstury do rejestru
docelowego, którym jest w przypadku rejestr tekstury
tn
. Aby było jednak możliwe pobranie tego koloru, musi być
spełnionych kilka warunków. Po pierwsze - tekstura musi być przypisana do określonego poziomu tekstury (za to jak wiemy
odpowiedzialna jest funkcja
SetTexture()
). Po drugie - poziom taki ma ustawione pewne atrybuty powodujące określony
wygląd tekstury - zalicza się do nich sposób filtrowania, funkcje mieszania i tym podobne (za te atrybuty odpowiedzialny jest
szereg wywołań funkcji
SetTextureStageState()
). Wywołując instrukcję
tex
nakazujemy pobranie koloru piksela z tekstury,
która jest w odpowiedni sposób przekształcona dzięki atrybutom poziomu na którym się znajduje. Pozostaje jedno zasadnicze
pytanie - jeśli mamy kilka poziomów tekstur, to skąd instrukcja wie, z jakiego poziomu ma pobrać odpowiednią próbkę?
Odpowiedź na to pytanie znajdziemy w rejestrze docelowym - jak wiemy jego nazwa to
tn
, gdzie n jest numerem poziomu, z
jakiego należy pobrać próbkę. I tutaj od razu mała uwaga - działa to tak tylko w przypadku shaderów od wersji 1.0 do 1.3. W
przypadku wyższych już inne jest to działanie i numer rejestru niekoniecznie musi się zgadzać z numerem poziomu, z jakiego
jest pobierany kolor. Ponieważ my jednak mieścimy się w dolnej granicy wersji więc na razie poprzestaniemy na tej mniej
skomplikowanej wiedzy. Najpierw pobieramy do rejestru
t0
kolor z poziomu numer 0 a następnie do rejestru
t1
z poziomu
jak wskazuje nazwa rejestru.
; arithmetic instructions
mul r0, t0, t1
Mając jakieś kolory, możemy z nimi zrobić co nam żywnie się tylko podoba. Pamiętamy z opisu teoretycznego pixel shaders,
że rejestrem wyjściowym shadera jest rejestr oznaczony jako
r0
- jest to zwykły rejestr tymczasowy, uzbrojony jednak w tę
dodatkową funkcję. Widać tutaj istotną różnicę w stosunku do vertex shadera, gdzie rejestrów wyjściowych było tyle, żeby
objąć nimi wszystkie wartości wierzchołka. Pixel shader produkuje tylko jedną wartość - kolor, więc nie potrzeba w zasadzie
żadnych specjalnych rejestrów wyjściowych. Jasne jest więc, że jeśli potraktujemy rejestr
r0
jako rejestr docelowy jakiejś
instrukcji i będzie to ostatnią instrukcją shadera to wynik będzie jednocześnie kolorem wyjściowym. I taką właśnie mini-
sztuczkę sobie tutaj zastosujemy - potraktujemy
r0
jako rejestr wyjściowy instrukcji mnożenia dwóch rejestrów tekstur, które
zastosowaliśmy wcześniej. A co powstanie w wyniku mnożenia kolorów? - oczywiście - nowy kolor. W zależności od tego,
co będą zawierały nasze tekstury wynik tego mnożenia będzie różny - wszystko przecież zależy od kolorów, jakie będą
występować na teksturze. Przypatrzmy się naszym teksturom bliżej - jedna jest dosyć kolorowym przedstawicielem swojego
gatunku, druga to czarno-biała bitmapa (oczywiście w sensie występowania na niej ilości kolorów). I teraz - co powstanie
przez pomnożenie kolejnych pikseli obydwu tekstur? Kolor biały drugiej tekstury to będzie w rejestrze wejściowym czwórka
liczb
float
postaci (1.0f, 1.0f, 1.0f, 1.0f). Jeśli pomnożymy to przez wartości pikseli pierwszej tekstury to da się zauważyć,
że... nic się nie zmieni! To znaczy w wyniku dostaniemy dokładnie niezmienione wartości z tekstury tej bardziej kolorowej.
Tak więc kolor biały będzie jakby kolorem przeźroczystym. Idźmy teraz w drugą stronę. Kolor czarny z drugiej tekstury w
rejestrze będzie się przedstawiał jako (0.0f, 0.0f, 0.0f, 0.0f). Jeśli pomnożymy przez to kolory pierwszej tekstury to
otrzymamy co? No właśnie - same zera. Czyli kolor czarny będzie zupełnie nieprzeźroczysty w tym momencie. W wyniku
połączenia w taki sposób tych konkretnych tekstur dostaniemy to co widać na obrazku poniżej.
*
=
Można więc powiedzieć, że jest to dokładnie to, o co nam chodziło, prawda? No i tak też jest w istocie, chcieliśmy mieć efekt
multitekstury, to go otrzymaliśmy. A że odbyło się to w zasadzie za pomocą
jednej instrukcji asemblera
! więc tym bardziej
należy nam się chwała, bo nic nie ma ważniejszego w grafice 3D niż szybkość i minimalizacja kosztów wykonania.
Dostaliśmy dokładnie taki sam wynik, jak przy zastosowaniu funkcji mieszania z Direct3D. Oczywiście nie będzie tak w
każdym przypadku - tutaj specyfika jednej z tekstur pozwoliła oszczędzić czas na obliczeniach a osiągnięty efekt zupełnie
nas zadowala. Ale jeśli pragniemy mieć dokładnie takie działanie jak wynika z równania na mieszanie to nic nie stoi na
przeszkodzie, żeby takie w kodzie shadera zaimplementować przecież. Zestaw instrukcji jest aż nadto potężny do osiągnięcia
zamierzonego przez nas celu. Nie wspomnę już tutaj nawet o zastosowaniu wszelkiej maści modyfikatorów, co jednak w
przypadku takich prostych tekstur nie dałoby dobrych rezultatów - a wręcz przeciwnie, zepsułoby nam zupełnie efekt. Ale
zapewniam, że nie zapomnimy o nich i już niedługo będzie okazja je wykorzystać na pewno.
I to byłby w zasadzie koniec naszego shadera. Przyznam szczerze, że bardzo obawiałem się tych dwóch tutoriali, ale po
5
DirectX ▪ Piel shader 2
bliższym zaprzyjaźnieniu się z pixel shaders trzeba stwierdzić, że nie taki diabeł straszny. Mam nadzieję, że Wy też tak
uważacie? ;). Oczywiście przedstawiłem tutaj najmniejszy z możliwych scenariuszy jeśli dotyczy pixel shaders - to zalicza
się w zasadzie do podstaw już dzisiaj i bez takich umiejętności lepiej nie pokazujmy się światu. Nie zastosowałem żadnym
modyfikatorów i masek, ale o powodach już wspomniałem - nie dałoby to dobrego efektu. Oczywiście w kolejnych lekcjach
bez tego się na pewno nie obejdzie, więc przyjdzie jeszcze czas to omówić. A na tym zakończylibyśmy naszą lekcję, choć
dodam jeszcze kilka uwag na koniec.
• Przykład ten standardowo, jeśli chodzi o kod źródłowy działa na urządzeniu emulowanym w software, dlatego jest
tak wolno. Posiadam niestety na razie kartę GeForce 2MX, która nie posiada wsparcia sprzętowego ani dla vertex
ani pixel shaders, dlatego zabawę z pikselami będziemy kontynuowali na razie w ten sposób. Posiadacze kart od
GeForce3 w górę oraz odpowiednich typów kart ATI mogą sobie we wiadomym miejscu przestawić i sprawdzić jak
to wygląda na żywym sprzęcie. Oczywiście nie muszę wspominać, że jeśli ktoś chce sobie napisać shadery w wersji
już nawet 3.0 (DX SDK 9.0 już oferuje takie możliwości) to może sobie uruchomić emulację w software, bo kart,
które to obsługują jeszcze nie ma na świecie ;).
• Przy pisaniu shaderów, zwłaszcza pikselowych pamiętajmy zawsze o wersji shadera i sprawdzajmy jak działa dana
instrukcja czy rejestr w konkretnej wersji, bo możemy się nieźle nagłowić szukając w kodzie błędu, gdy Direct3D
nas o tym poinformuje podczas kompilacji.
• Pamiętajmy także o kolejności instrukcji, które muszą występować w określonym porządku w kodzie, ponieważ
inaczej shader oczywiście nam się zbuntuje i się po prostu albo nie skompiluje albo spowoduje błędy programu.
• Pamiętajmy także o sposobie traktowania tekstur - tak jak wspomniałem obejmujmy je raczej całościowo niż patrząc
na poszczególne piksele. Przeważnie chodzi nam o jakieś operacje, które dotyczą całej tekstury a nie pojedynczych
jej pikseli, ale w razie potrzeby można oczywiście odpowiednio zareagować.
Możliwości jakie dostaliśmy w zamian za wysiłek włożony w poznanie shaderów i to zarówno vertex jak i pixel będzie nam
się teraz zwracał w sposób nie dający się policzyć. Możemy od tej chwili zupełnie zapomnieć o normalnym, znanym od lat
przekształcaniu wierzchołków i tekstur. Od dzisiaj możemy osiągnąć każdy efekt, jaki tylko chcemy i to na o wiele niższym
poziomie a co za tym idzie mieć większą kontrolę niż dotychczas. W kolejnych lekcjach na pewno poznamy sztuczki, które
będą umożliwiały tworzenie o wiele bardziej fascynujących efektów niż dotychczas a wszystko to - za pomocą shaderów
oczywiście.
Jeszcze tak na zakończenie oczywiście tak aby zachować jakąś ustaloną konwencję i na dowód, że to naprawdę działa
oczywiście screen i otrzymany efekt. A z wami się żegnam na dzisiaj i do następnego, bardzo odlotowego tutorialu ;), bye!