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 minisztuczkę 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!