DirectX - Efekty

Transkrypt

DirectX - Efekty
1
DirectX ▪ Efekty
Zanim przystąpimy do dalszego zgłębiania tajemnic naszego ulubionego pakietu, nadszedł czas aby... ułatwić sobie nieco
pracę. Ktoś pewnie pomyśli, że bredzę, no bo co można sobie w naszych tutorialach ułatwiać? Ano jakby nie patrzeć to nasze
programiki są w zasadzie banalnie proste i ich napisanie nie powinno zająć nikomu więcej niż kilka minut a stosując zasadę
Ctrl+C i Ctrl+V to nawet szybciej. Lekcje lekcjami i tutaj można by dać sobie spokój, ale przecież nie po to się tego
wszystkiego uczymy, żeby na lekcjach się skończyło, nieprawdaż? Każdemu, nawet mnie marzy się jakiś super wypasiony
silnik, który zaćmi wszystko, co było do tej pory. Ale powiem szczerze, że pisząc w taki sposób, jak dotychczas nasze
przykłady to nie wróżę nikomu wielkiej przyszłości jako programista takich engine-ów.
Jeśli jeszcze nie zauważyliście, to pokażę wam podstawową wadę naszych przykładów. Załóżmy, że opanowaliśmy nasz kurs
w stopniu pozwalającym zacząć się bawić w poważne pisanie engine-a. Znamy wszystkie podstawowe i bardziej
zaawansowane sztuczki, mamy zespół zaangażowanych ludzi, koncepcje, rysunki, grafikę, muzykę itd... - prawie wszystko
gotowe. Do akcji wkraczamy my, czyli programiści. No i nagle się okazuje jak wszystko zaczyna nam się walić i jak trudno
to wszystko poskładać do kupy a pisanie przypomina prawdziwą męczarnię. Każdy jeden efekt, jaki przyjdzie odtworzyć
engine-owi trzeba żmudnie wpisać w kodzie, coś na pewno będzie się nie zgadzać w ustawieniach naszego urządzenia, będą
błędy w shaderach itp. Czeka nas na pewno ślęczenie nad kilometrami kodu źródłowego, szukanie linia po linii co jest nie tak
i czego zapomnieliśmy. Na pewno wiecie jak potrafi być to irytujące i jak ciężko czasem znaleźć oczywisty błąd zwłaszcza
korzystając na przykład z takiego API jak Direct3D, które silnie jest zależne od sprzętu.
Powracając zaś do początku naszego artykułu wspomniałem coś o ułatwianiu sobie pracy - czy więc można nam,
programistom czasu rzeczywistego jakoś ułatwić pisanie tego wrednego kodu?
Direct3D już nie raz nas zaskoczył i zapewne jeszcze nie raz zaskoczy, więc nie inaczej będzie i tym razem. Ale może o tym
za chwilę, wróćmy może do genezy samego zagadnienia. Po latach doświadczeń innych programistów "siedzących" w
przemyśle gier stwierdzono już dawno, że siedzenie nad kodem i szukanie linia po linii błędów może przyprawić o rozstrój
nerwowy, nie wspominając o tym, że mogło to być napisane w asemblerze na przykład. I zaczęto poszukiwać już wtedy jakiś
rozwiązań, które dawałyby większą kontrolę nad tym, co się dzieje w programie podczas jego wykonywania a jednocześnie
do minimum ograniczyły potrzebę grzebania w kodzie źródłowym.
Pomysł okazał się bardzo prosty a jednocześnie tak skuteczny, że dzisiaj stał się bardzo potężnym pomocnikiem w tworzeniu
skomplikowanych procedur przetwarzania danych. Rozwiązaniem tym okazał się... stary, poczciwy plik tekstowy.
A dokładniej mówiąc jego specjalna wersja, zwana popularnie skryptem. Nie będę tutaj może wchodził w dywagacje co to
jest skrypt, jak się powinno go pisać i inne egzystencjalne problemy. Popatrzmy na skrypt naszymi oczami i jak zwykle
pomyślmy, do czego mógłby się on nam przydać.
Skrypt z reguły zawiera przepis wykonania jakiejś czynności w postaci jakiegoś specyficznego języka. Można by
powiedzieć, że ilu programistów, tyle sposobów pisania skryptów. Są oczywiście gotowe, niezłe rozwiązania, ale na pewno
nie bylibyśmy programistami, gdybyśmy nie próbowali mieć naszego własnego. Skrypt ten jest analizowany i najczęściej
wkompilowywany w jakiś sposób do kodu źródłowego naszego projektu, aby mógł po prostu zostać wykonany. Sposobów,
w jaki działają skrypty jest na pewno setki, jeśli nie tysiące i nie będziemy na pewno analizowali tutaj wszystkich.
Zastanówmy się natomiast, do czego mogłyby nam się takie skrypty przydać w grafice 3D.
To, co najczęściej denerwuje przy analizowaniu kodu renderującego nasze sceny to bez wątpienia są ustawienia naszego
urządzenia renderującego. Jak wiemy z lekcji, które przeprowadziliśmy do dzisiaj stanów tych jest od groma i trochę jeszcze.
Co gorsza, z każdą nową kartą graficzną ich ilość się powiększa a z każdą nową wersją pakietu niektóre się wręcz zmieniają można dostać naprawdę rozstroju nerwowego.
Gdybyśmy więc napisali sobie taki skrypt, który byłby w stanie kontrolować stany naszego urządzenia, odpowiednio je
ustawiać bez zmian kodu źródłowego bezpośrednio operującego na urządzeniu byłoby wspaniale. Tam mielibyśmy tylko
wywołanie określonego skryptu zawierającego przepis wykonania i ustawienia odpowiednich stanów naszego urządzenia
renderującego i voila. W przypadku jakiegoś błędu w wyświetlaniu (złe nakładanie tekstur, filtrowanie, przeźroczystość nie
taka) to nie musielibyśmy zupełnie babrać się w kodzie źródłowym! Wystarczyłoby wziąć na warsztat skrypt i dokładnie go
przeanalizować. Jeśli jego język będzie bardziej ścisły i przejrzysty niż funkcje w kodzie źródłowym (a w większości
przypadków na pewno tak), to błąd znajdziemy o wiele wcześniej. Już nie wspomnę nawet o wygodzie w posługiwaniu się
krótkimi plikami skryptów, do których możemy mieć oddzielny edytor i które będą bardziej przejrzyste niż przerzucanie
tysięcy linii i szukanie bugów. I teraz pytanie podstawowe - czy w grafice 3D, w pętlach renderingu jest to możliwe?
Wspominałem, że Direct3D ma dla nas jeszcze nie jedną niespodziankę i tym razem na pewno się nie zawiedziemy. Dzisiaj
zaprezentuję państwu bardzo przydatną rzecz, czyli mowa będzie o tzw. Efektach.
Czym jest efekt? Tak w zasadzie to trudno odpowiedzieć - efekt jest po prostu... efektem, czymś, co widzimy na ekranie w
połączeniu z geometrią i odrobiną naszej wyobraźni, która pchnęła w to życie. Na efekt można patrzeć z naprawdę wielu
stron i trudno uznać, która tak naprawdę jest tutaj decydująca. Z naszego, programistycznego punktu widzenia jest to obiekt,
którego będziemy używali w naszym programie. Możemy na to także spojrzeć jako na plik tekstowy, który będzie rodzajem
skryptu, o którym mówiłem wyżej. Efekt możemy także traktować jako sposób wykonania czynności renderingu obiektu,
który zawiera w sobie wszystkie niezbędne ustawienia urządzenia, ale jak za chwilę się przekonamy, nie tylko.
Spróbujmy teraz omówić po kolei każdy aspekt efektu, o którym wspomniałem powyżej.
•
Jako obiekt - aby móc posługiwać się efektami w Direct3D będziemy potrzebować po raz kolejny pomocy naszej
niezastąpionej biblioteki narzędziowej D3DX . Jak w niej poszperać głębiej to można znaleźć bardzo interesujący,
acz dosyć prosty interfejs o nazwie ID3DXEffect . Posłuży on nam do wszystkich niezbędnych operacji na efekcie
2
DirectX ▪ Efekty
•
•
jako takim, pomoże załadować skrypt z pliku, skompilować i posługiwać się nim w naszym programie. W dalszej,
praktycznej części dokładnie poznamy co i jak.
Jako skrypt - w tym przypadku będziemy mieli do czynienia z plikiem tekstowym, zapisanym na pierwszy rzut oka
dosyć niezrozumiałymi poleceniami i nazwami. Ale jak się potem okaże wcale nie będzie to takie trudne a pozwoli
na taką manipulację naszą pętlą renderingu o jakiej nam się dotąd nie śniło.
No i ostatni punkt naszych rozważań - dosyć abstrakcyjny trzeba przyznać. Sposób wykonania będzie zależał tylko i
wyłącznie od nas. To co my sobie wymyślimy będziemy mogli zapisać w skrypcie efektu i będziemy mogli
dowolnie zmieniać sobie sposób naszego renderingu bez ponownej kompilacji kodu.
Nie będę tutaj omawiał szczegółowo ani interfejsu efektu, bo tak po prawdzie będziemy potrzebować kilku jego metod na
początek. Nie będziemy także analizowali całej składni skryptu zawierającego efekt, bo jest tego całe mnóstwo a poza tym
jest od tego SDK. Tak naprawdę składnia skryptu będzie w pewien sposób powiązana z tym, co do tej pory znamy doskonale
i wcale nie trzeba się będzie uczyć na pamięć nowych rzeczy, wystarczy trochę pogłówkować i będzie jak najbardziej w
porządku.
Uzbrojeni w wiedzę teoretyczną możemy więc przystąpić do lekcji jak najbardziej praktycznej aby na własnej skórze odczuć
wygodę i elastyczność w posługiwaniu się efektami w Direct3D. Oczywiście całego kodu nie ma sensu tutaj przytaczać bo
większość rzeczy możemy wyrecytować o północy wyrwani nagle ze snu ;P. Ale te ważniejsze i nowe rzeczy oczywiście
trzeba omówić:
Na samym początku oczywiście musimy mieć obiekt, najlepiej globalny, aby był widoczny w całym programie, który będzie
odpowiedzialny za efekt, jako obiekt.
LPD3DXEFFECT
D3DXHANDLE
g_lpEffect;
g_hTechnique;
Oczywiście LPD3DXEFFECT jest to obiekt odpowiedzialny za nasz efekt. Drugi parametr typu D3DXHANDLE może tutaj
budzić pewne niepokoje, ale jest to tzw. uchwyt - coś podobnego jak HANDLE w Windows. Posłuży nam on do posługiwania
pewnymi częściami skryptu efektu, o których powiem później, przy omawianiu kodu zawartego w skrypcie.
Wiemy już, że kod skryptu możemy sobie wpisać w zewnętrznym pliku tekstowym, który zostanie skompilowany i używany
jako kawałek naszego kodu. Naszym zamiarem jest przecież korzystanie z plików zewnętrznych aby nie błądzić po kodzie
źródłowym.
Aby wczytać i skompilować skrypt efektu posłużymy się jak w większości tego typu przypadków biblioteką D3DX i bardzo
użyteczną funkcją D3DXCreateEffectFromFile() , której działanie nietrudno przewidzieć.
HRESULT
LPD3DXBUFFER
hResult;
lpError;
hResult = D3DXCreateEffectFromFile( g_pd3dDevice, "effect.fx", NULL, NULL, 0, NULL,
&g_lpEffect, &lpError );
Jako drugi parametr funkcja ta przyjmuje oczywiście ścieżkę dostępu do pliku, który zawiera skrypt efektu. Parametr siódmy
to będzie wskaźnik na nasz obiekt interfejsu efektu, parametr nr 8 będzie zawierał łańcuch znakowy identyfikujący
ewentualne błędy składniowe w naszym skrypcie. Jeśli będziemy chcieli poznać co sknociliśmy w kodzie skryptu to
wystarczy wrzucić zawartość tego bufora do pliku tekstowego. Sądzę, że tutaj wielkiej filozofii raczej nie ma i że każdy
widzi o co chodzi. Aby jednak dalej brnąć w nasz kod musimy zrobić sobie w tym momencie małą odskocznię od kodu
źródłowego naszej aplikacji do kodu skryptu. Bez tego trochę się pogubimy i nie bardzo będzie wiadomo o co chodzi, więc
teraz w dużym skrócie powiemy sobie co i jak w skryptach efektów.
Skrypt efektów można podzielić na kilka głównych części, które wcale nie muszą jednak występować w konkretnym
przykładzie. Jednak bez jednej na pewno nie można się obejść - część ta nazywa się techniką (technique). Technika
stanowi główny blok skryptu efektu, który zawierać będzie inne, mniejsze elementy. Czym jest technika? Ano - zawiera ona
opis sposobu naszego renderingu - czyli wszelkie ustawienia urządzenia renderującego, które mają jakikolwiek wpływ na
wygląd końcowej sceny na ekranie. Wszystkie te ustawienia są zdefiniowane przez słowa kluczowe, które mają dane
znaczenie oraz w większości przypadków przez przypisywane do tych słów wartości parametrów urządzenia. Co do czego
okaże się zaraz przy bliższej analizie naszego przykładowego skryptu.
Aby móc jednak w zdefiniować jakąkolwiek technikę, która będzie działać na naszym urządzeniu po skompilowaniu skryptu
w bloku techniki musimy zdefiniować coś, co trzeba chyba nazwać z angielskiego pass (można by się upierać przy nazwie
przebieg, ale jakoś mi nie leży, więc zostańmy może przy passie). Tutaj od razu uwidoczni nam się cała fajność skryptów
efektów. Otóż wiemy, że do niektórych specjalnych, bardzo zaawansowanych efektów na ekranie często gęsto będzie
potrzeba wykonać nie jeden przebieg renderowania danego obiektu, ale kilka (przeważnie dwa lub trzy). Wprawdzie karty
dzisiaj mogą już naprawdę robić cuda, ale czasem nie ma siły i po prostu trzeba. Wyobraźmy sobie teraz, że musimy właśnie
zrobić coś takiego za pomocą kodu aplikacji. Potrzeba więc będzie dla każdego rodzaju efektu naklepać nowy kod - jeden dla
pojedynczego przebiegu, drugi dla dwóch itd. Można oczywiście kombinować różnymi pętlami itp. ale będzie to na pewno
strasznie upierdliwe i niewygodne - jeśli mieliśmy podobne problemy kiedykolwiek, to właśnie w takich momentach marzył
nam się język skryptowy. I proszę - oto mamy właśnie odpowiednie narzędzie w ręce.
W czym rzecz - otóż dla danej techniki zdefiniowanej w skrypcie efektów możemy sobie zdefiniować kilka przebiegów
3
DirectX ▪ Efekty
(passów). W każdym przebiegu możemy zmieniać ustawienia naszego urządzenia dowolnie, co daje przeogromne
możliwości konfiguracji i wprowadza niesamowitą elastyczność. Bez zmiany linijki kodu aplikacji będziemy w stanie za
pomocą skryptu wygenerować w zasadzie dowolną kombinację stanów urządzenia potrzebną w danej chwili.
W skrócie można napisać, że będzie to wyglądać tak:
Technika
{
Przebieg1
{
// ustawienia
}
Przebieg2
{
// ustawienia
}
}
Analizując skrypt efektu, Direct3D widząc taki a nie inny układ poleceń będzie wiedział od razu - ile przebiegów ma
wykonać urządzenie i co w każdym przebiegu ma się stać. Jak działa to w praktyce - zobaczymy na przykładzie już za
moment.
Mało tego - skoro dla jednej techniki możemy zdefiniować kilka przebiegów, to czemu nie dla efektu kilka technik? Sprawa
oczywista - możemy w jednym pliku zawrzeć kilka technik, każda na przykład po kilka przebiegów. Dla przykładu - jeśli
nasze urządzenie nie będzie wstanie obsłużyć jednej techniki, to napiszemy drugą tak, aby było wstanie ją wykonać i aby
otrzymać podobny efekt na ekranie. Prostym przykładem może być problem wielokrotnego nakładania tekstur. Jedno
urządzenie będzie potrafiło nałożyć cztery na raz (o dwóch to nawet nie wspominam) a inne nie. Dla jednego więc napiszemy
jedną technikę a dla drugiego inną. Potem tylko sobie wybierzemy odpowiednią i voila...
Mało wam? To proszę jeszcze - możemy w skrypcie efektów klepać shadery! Zarówno vertex-owe jak i pixel-owe.
Wystarczy odpowiednia definicja i w bloku techniki wystarczy tylko wywołać.
Jeśli nadal macie pewne wątpliwości, że ciężko będzie sterować takim skryptem, bo na przykład zajdzie potrzeba zmiany
niektórych parametrów w czasie działania programu to mam dla was radosną nowinkę :) - do skryptu możemy wysyłać dane!
Jeśli zajdzie na przykład potrzeba zmienić jakąś stałą, macierz czy liczbę - żaden problem.
To tyle na początek - wiemy mniej więcej co w skrypcie można a co nie więc czas ruszyć z kodem w dalszą drogę. Oto nasz
pierwszy, przykładowy skrypt:
/* this file is presenting effect of applying texture to object */
texture DiffuseTexture;
VertexShader VShader =
asm
{
vs.1.1
dcl_position
dcl_color
dcl_texcoord0
// calculate
dp4 r0.x,v0,
dp4 r0.y,v0,
dp4 r0.z,v0,
dp4 r0.w,v0,
v0
v5
v7
transformed pos to world
c0
c1
c2
c3
// and to final
dp4 oPos.x, r0,
dp4 oPos.y, r0,
dp4 oPos.z, r0,
dp4 oPos.w, r0,
pos
c4
c5
c6
c7
// store diffuse color
mov oD0, c1
mov oT0, v7
};
technique T0
{
pass P0
{
fvf = XYZ | Diffuse | Tex1;
4
DirectX ▪ Efekty
pixelshader = NULL;
vertexshader = (VShader);
// render states
Lighting = False;
CullMode = None;
// texture operations
texture[0] = (DiffuseTexture);
ColorOp[0] = Modulate;
ColorArg1[0] = Texture;
ColorArg2[0] = Current;
// sampler states
MinFilter[0] = Linear;
MagFilter[0] = Linear;
MipFilter[0] = Linear;
MipmapLodbias[0] = (fMipMapLodBias);
}
}
Jak łatwo się zorientować w skrypcie na dzisiaj opisaną mamy jedną techikę z jednym przebiegiem i dodatkowo vertex
shader. Ale może po kolei polećmy linia po linii.
/* this file is presenting effect of applying texture to object */
To jest jak łatwo się domyśleć nic innego jak tylko komentarz, które możemy sobie oczywiście w skrypcie wstawiać dowoli,
aby po paru dniach jeszcze pamiętać co mieliśmy na myśli pisząc coś w ten a nie inny sposób. Komentarze przypominają
kropka w kropkę te, znane z języka C/C++. Skrypt efektu obsługuje zarówno te w formie /**/ jak i jednoliniowe, zaczynające
się od symbolu //.
texture DiffuseTexture;
Następnie mamy coś, co przypomina nieco deklarację zmiennej w programie i takową też jest w istocie. Forma nadal jest
podobna do języka C/C++ - czyli najpierw typ zmiennej a następnie jej nazwa, którą będziemy posługiwali się w programie.
Ponieważ skrypt nie jest napisany w języku C więc czasem typy, które będziemy napotykać nie będą przypominać tych,
powszechnie znanych, chociaż te (float, int itp.) oczywiście także będą występować. Pocieszające są natomiast dwa fakty,
jeśli chodzi o zmienne w skrypcie efektów. Po pierwsze, zmienne podstawowe, że je tak nazwę będą się na pewno kojarzyć z
tym, z czym mamy do czynienia w grafice 3D na co dzień a konkretnie mówiąc do dyspozycji będzie nam dane to wszystko,
co jest dostępne przy programowaniu za pomocą języka HLSL - czyli czegoś, czego jeszcze nie znamy (ale poznać na pewno
nie omieszkamy ;). W przypadku typu texture chyba wiadomo o czym mowa - będzie to odpowiednik obiektu tekstury w
programie aplikacji, który skojarzymy sobie z ta zmienna w skrypcie - jak, dowiemy się o tym w dalszej części artykułu.
VertexShader VShader =
Następnie mamy deklarację typu VertexShader - czym to jest, także nie trudno się domyśleć. Do tej pory w naszych
programach używaliśmy, podobnie jak w przypadku tekstur określonego typu, który reprezentował interfejs danego obiektu.
Tak było w przypadku tekstur (typ LPDIRECT3DTEXTURE ) czy właśnie vertex shader-a
(LPDIRECT3DVERTEXBUFFER ). To, co powyżej jest odpowiednikiem tego ostatniego typu - deklarujemy nowy obiekt
vertex shadera o nazwie nie brzmiącej zbyt oryginalnie - VShader. Za pomcą tej nazwy będziemy się odnosić w składni
skryptu do konkretnego obiektu. Programując w języku C czy C++ mamy do dyspozycji możliwość inicjalizacji zmiennych nie inaczej jest w skrypcie efektów. Po zadeklarowaniu zmiennej możemy napisać po niej znak równości i przystąpić do jej
definiowania (nadawania wartości). Tak postąpimy w tym przypadku - po znaku równości zaczniemy deklarować nasz vertex
shader, czyli po prostu wklepiemy jego kod. Całego shadera oczywiście sensu nie będzie opisywać bo dla nas to już
przedszkole. Ważna tylko jest następna linia po deklaracji zmiennej:
asm
{
Mówi to programowi analizującemu skrypt efektu, że to, co nastąpi po nawiasie będzie napisane w asemblerze - a dokładniej
mówiąc w języku niskiego poziomu dla vertex shadera. Program kompilujący skrypt będzie wiedział co i jak, bo raz, ze
powiemy mu jakich instrukcji ma się spodziewać (VertexShader) a dwa, że w asemblerze. To, co nastąpi pomiędzy
nawiasami powinno być ni mniej ni więcej tylko dobrze nam znanym programem vertex shadera, którego używaliśmy już nie
raz. Jeśli w kodzie wystąpią jakieś błędy to oczywiście kompilator skryptu nie omieszka nas o tym poinformować. Kiedy
kończymy definiowanie shadera, to po ostatnim nawiasie musimy postawić średnik, bowiem zgodnie z zasadami
definiowania zmiennych w języku C/C++ (które obowiązują także w skryptach efektów). Co do vertex shadera, to chyba
5
DirectX ▪ Efekty
raczej jest wszystko jasne mam nadzieję. W sumie jak ktoś coś tam wie z C czy C++ to wielkich problemów nie powinno
być. My teraz przejdziemy może do ważniejszych rzeczy a mianowicie do tego, co stanowi o sile skryptów - czyli do technik
i przebiegów, bo bez tego skrypt nawet nie ma sensu. Więc patrząc w kod mamy:
technique T0
{
pass P0
{
// ...
}
}
Najpierw przyjrzyjmy się budowie ogólnej tego fragmentu skryptu. Jak wspominałem powyżej możemy mieć w nim
zdefiniowanych kilka technik a każda może mieć kilka przejść. Technika w efekcie zawsze jest zdefiniowana w postaci bloku
(para nawiasów {}), podobnie jak w języku C/C++. Ta para nawiasów poprzedzona jest słowem kluczowym technique ,
które jednoznacznie sugeruje, co zawierać będzie para nawiasów. Za tym słowem następuje nazwa naszej techniki, tutaj
wybieramy najprostszą jaka nam się narzuca - po co zresztą kombinować. Po identyfikatorze technique może wystąpić
jeszcze kilka innych czynników, ale dla dobra sprawy dzisiaj je sobie pominiemy. Mając zdefiniowaną technikę powinniśmy
umieścić w niej jakieś przebiegi. Jak ktoś uparty, to wcale przebiegów definiować nie musi, ale za efekty na ekranie nie ręczę
- wątpię czy w ogóle coś uda się zobaczyć.
Aby zdefiniować przebieg posłużymy się również parą nawiasów {}, jak w większości przypadków, z tym że poprzedzimy ją
słowem kluczowym pass . Po tym identyfikatorze następuje nazwa przebiegu, która jest podobnie jak w przypadku nazwy
techniki w zasadzie dowolna - u nas dla ułatwienia nie będziemy się zbytnio wysilać.
Aby zbytnio nie nadwyrężać waszych umysłów natłokiem wiadomości uprościmy nasz skrypt do minimum, żeby poznać
ogólną zasadę. Ręczę, że jak zrozumiecie o co tutaj chodzi to następne, najbardziej nawet skomplikowane techniki z wieloma
przejściami nie będą stanowić najmniejszego problemu.
Przy zwykłym renderingu, którego używaliśmy do tej pory wszystkie ustawienia urządzenia były dokonywane za pomocą
dostępnych metod interfejsu ID3Ddevice . Znamy na pewno takie funkcje SetTextureStageState() , SteTexture() ,
SetRenderState() . Dzieki skryptowi efektów będziemy niejako uwolnieni od używania tych metod w naszym programie
(dokładnie nam przecież o to chodzi, ponieważ nie chcemy klepać bezpośrednio kodu renderującego). Trzeba będzie więc w
jakiś sposób zastąpić te wywołania za pomocą instrukcji skryptu. A jak?
pixelshader = NULL;
vertexshader = (VShader);
Lighting = False;
CullMode = None;
// texture operations
texture[0] = (DiffuseTexture);
ColorOp[0] = Modulate;
ColorArg1[0] = Texture;
ColorArg2[0] = Current;
// sampler states
MinFilter[0] = Linear;
MagFilter[0] = Linear;
MipFilter[0] = Linear;
MipmapLodbias[0] = (fMipMapLodBias);
Przypatrzmy się kawałkowi kodu, który dostępny jest w passie o nazwie P0. Już na pierwszy rzut oka widać, że nie mamy
tutaj wywołań metod urządzenia. Są za to różne tajemnicze zmienne i jeszcze bardziej tajemnicze wartości - ale czy tak
naprawdę? Przypatrzmy się temu instrukcja po instrukcji:
pixelshader = NULL;
Jak łatwo się domyśleć, coś, co jest nazwane pixelshader dostanie wartość NULL , czyli w praktyce tego nie będzie. W
skrypcie efektów to, co jest po lewej stronie znaku "=" jest niejako zdefiniowane do wewnętrznego użytku kompilatora
skryptu i on wie co jest czym, my tylko musimy wiedzieć co do czego przypisać. Ustrojstwo o nazwie pixelshader nie
będzie niczym innym, tylko aktualnie używanym pixel shaderem, który możemy nadać urządzeniu. W naszych
dotychczasowych zabawach posługiwaliśmy się shaderami w nieco inny sposób. Pamiętacie na pewno jak to się robiło w
DirectX 8 - aby ustawić urządzeniu używanie skompilowanego pixel shadera wołaliśmy metodę SetPixelShader() z
indentyfikatorem odpowiedniego shadera w postaci liczby typu DWORD . W DirectX 9 sytuacja się zmieniła diametralnie i
dostaliśmy do ręki interfejsy shaderów w postaci obiektów typu COM aby było łatwej się nimi posługiwać (czy to faktycznie
coś ułatwiło to zupełnie inna sprawa ;). Ale do włączania/wyłączania shaderów nadal używaliśmy metody SetPixelShader()
, tyle że tym razem podawaliśmy wskaźnik interfejs odpowiedniego shadera. A jak jest w przypadku skryptu efektu? Nic
6
DirectX ▪ Efekty
prostszego - aby było dobrze widać zasadę działa pokaże dwie sytuacje - kiedy nie życzymy sobie shadera i kiedy mamy już
taki skompilowany.
Ustrojstwo o nazwie pixelshader reprezentuje jednostkę pixel shadera w urządzeniu renderującym. Jeśli przypiszemy
temu czemuś wartość NULL (jak w naszym przykładzie) to wiadomo, że nie życzymy sobie używania pixel shadera przez
urządzenie (bo co niby pusty pixel shader miały robić) - tak samo, jak w przypadku metody SetPixelShader() , kiedy
podaliśmy NULL to pixel shadera po prostu nie było.
W ten sam sposób lub podobny działają niemal wszystkie metody urządzenia, służące do zmiany jego stanu - nie wołamy już
żadnej z nich a po prostu odpowiedniemu ustrojstwu przypisujemy odpowiednią wartość - działamy tak jakby na niższym
poziomie. Wyobraźmy sobie, że te zmienne to tak naprawdę bezpośredni interfejs do urządzenia renderującego - po prostu
odpowiedniej komórce nadamy odpowiednią wartość aby odpowiednio ustawić procesor renderujący - to właśnie robią
okrężną drogą metody urządzenia o których wspominałem powyżej.
Możemy sobie nawet dla porządku pokazać jak to wyglądało w kodzie C/C++ i jaki jest tego odpowiednik w skrypcie efektu:
// skrypt efektu
pixelshader = NULL;
// DirectX 8
// parametr jako zmienna typu DWORD
lpD3Ddevice->SetPixelShader( 0 );
// DirectX 9
// parametr jako zmienna typu LPDIRECT3DPIXELSHADER9
lpD3Ddevice->SetPixelShader( NULL );
Czas na vertex shader. Pixeli nie używaliśmy, ale jak widzieliśmy, na początku skryptu zdefiniowaliśmy sobie vertex shader.
Jak więc użyć tego ustrojstwa? Ano, widzimy coś takiego:
vertexshader = (VShader);
Aby się odnieść do jakiejś zmiennej w naszym skrypcie wystarczy wziąć jej nazwę w nawiasy i przyisać do określonej
komórki, odpowiadającej za konkretny parametr urządzenia renderującego - w tym przypadku vertex shader. VShader jak
wiemy było zmienną typu shadera, którą od razu przy deklaracji zdefiniowaliśmy jednocześnie klepiąc jej kod w asemblerze
CPU. Odwołanie do niej w nawiasie powoduje niejako wyciągnięcie wartości tej zmiennej (w tym przypadku będzie to
skomplikowany kod shadera) i umieszczenie jej w odpowiedniej części naszego urządzenia renderującego. Gdybyśmy mieli
zdefiniowaną prostą liczbę jako int albo float , to aby dobrać się do jej wartości trzeba by było podać po prostu nazwę
zmiennej w nawiasach, tak samo jak tutaj.
W podobny sposób będziemy się odnosić także do innych zmiennych, co zaraz zobaczymy na innych przykładach.
Aby być w porządku oczywiście pokażmy porównanie do wywołań metod urządzenia:
// skrypt efektu
vertexshader = (VShader);
// DirectX 8
// parametr vshader jako zmienna typu DWORD
lpD3Ddevice->SetVertexShader( vshader );
// DirectX 9
// parametr vshader jako zmienna typu LPDIRECT3DVERTEXSHADER9
lpD3Ddevice->SetVertexShader( vshader );
Na końcowy wygląd naszej sceny bardzo duży wpływ mają zawsze ustawienia naszego urządzenia renderującego, których
dokonujemy za pomocą metody SetRenderState() . Co można ustawić za jej pomocą - praktycznie wszystko, co ma
znaczący wpływ na rendering - oświetlenie, blending, tryby pracy buforów itp... lista jest naprawdę sporych rozmiarów i
wystarczy popatrzeć do dokumentacji. Jak to ma się teraz przełożyć na nasz skrypt? Ano, wystarczy odpowiedniej komórce
naszego urządzenia przekazać odpowiednią wartość (bo tak naprawdę to samo robimy wołają powyższą metodę i gotowe).
Oczywiście kompilator skryptów ma już zakodowane pewne nazwy i wartości, które są dla niego sensowne a my tylko
musimy poznać naprawdę banalną regułę, jak się tym posługiwać. Załóżmy zatem, że pierwszą rzecz jaką robimy na naszej
scenie jest wyłączenie liczenia oświetlenia przez Direct3D, bo my do tego mamy swój własny vertex shader. Co byśmy
zrobili tradycyhną drogą? Ano:
lpD3Ddevice->SetRenderState( D3DRS_LIGHTING, FALSE );
"I " jak to mawiał pewien ganster ;) - "wszystko jasne" (a raczej ciemne, bo światła nie ma). Komórka urządzenia
odpowiedzialna za ustawienia dotyczące liczenia oświetlenia dostaje sygnał, że tym razem konkretny kawałek procesora nie
będzie wykorzystany. Jak do tego zabrać się w skrypcie? Po pierwsze musimy wiedzieć jak nazywa się komórka
odpowiedzialna za takie działanie dla kompilatora skryptu. Tutaj i w każdym podobnym przypadku zasada jest ta sama:
7
DirectX ▪ Efekty
D3DRS_LIGHTING - bierzemy tę nazwę, odcinamy część przed znakiem "_" (włącznie z nim) i mamy gotową nazwę :) czyli (D3DRS_)LIGHTING . W skrypcie wygląda to tak:
Lighting = False;
Ponieważ duże i małe literki w tym akurat przypadku nie mają znaczenia, więc aby nasz skrypt wyglądał bardziej elegancko
ja nadałem nazwie tej taką, a nie inną formę, wy możecie napisać jak chcecie, byle nazwa była zachowana. I skoro wiemy,
dokąd wpisać wartość to po prostu to robimy - przypisujemy po prostu tej komórce wartość False (tutaj także moja dowolna
interpretacja jeśli chodzi o wielkość liter). Prawda, że elegancko?
Przejdźmy teraz może do Cullingu (kolejności renderowania wierzchołków trójkątów). Dla przykładu zażyczymy sobie, że
nie chcemy mieć tej operacji przeprowadzanej - trójkąty będą więc widoczne zarówno z przodu jak i z tyłu.
CullMode = None;
I tak samo jak w poprzednim przypadku nie będzie trudno dowiedzieć się, co i gdzie wpisać. Pamiętamy doskonale, co trzeba
było zrobić, aby taki culling wyłączyć w kodzie C++, bezpośrednio w aplikacji, prawda?
lpD3Ddevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
Zgodnie z zasadą, którą podałem powyżej odcinamy przedrostek D3DRS_ i otrzymujemy nazwę ustrojstwa, do którego
przypisujemy wartość. W przeciwieństwie jednak do pierwszego przyadku nie mamy tutaj tak prostej wartości jak False, ale
twór o nazwie D3DCULL_NONE - a na co i w jaki sposób to zamienić chyba nie muszę tłumaczyć? :).
Kolejną rzeczą, na którą należy zwrócić uwagę są tekstury i wszelkie ustawienia, które się ich dotyczą. Nauczyliśmy się już
jakiś czas temu, że nasze niektóre przynajmniej karty graficzne potrafią renderować kilka tekstur na raz w jednym przebiegu
(passie). Dowiedzieliśmy się co to są poziomy tekstur i jak ich używać. Dla każdej tekstury użytej w naszym programie
możemy użyć innego zestawu właściwości - oznacza to, że różne poziomy mogą różne dziwne rzeczy robić z naszą teksturą,
co więcej także w pewien sposób poziomy te współpracują ze sobą. Wszystko, co robiliśmy z daną teksturą na danym
poziomie odbywało się za pomocą metod SetTexture() i SetTextureStageState(). Pierwsza z metod po prostu przypisywała
konkretnemu poziomowi konkretną teksturę, natomiast druga służyła do odpowiedniego pokierowania urządzeniem aby
osiągnąć zamierzone efekty. No więc, co i jak z tymi teksturami w skrypcie efektów. Właściwie metodę to znamy wystarczy poobcinać nazwy i gotowe... ale, ale - nie zapominajmy o poziomach tekstur. Powstaje problem, jak się do nich
odwoływać. A rozwiązanie jest jak zwykle banalnie proste i daleko szukać nie trzeba. Przypatrzmy się fragmentowi kodu:
//texture operations
texture[0] = (DiffuseTexture);
ColorOp[0] = Modulate;
ColorArg1[0] = Texture;
ColorArg2[0] = Current;
I już widać jak sobie poradzimy - w kodzie C++ jako jeden z argumentów metody dotyczącej operacji na teksturach
występował numer poziomu. Tutaj zaś mamy po prostu tablicę odpowiednich komórek, gdzie index tej tablicy to po prostu
numer poziomu. Jak chcemy na przykład na poziom 0 wstawić teksturę, to tak po prawdzie mamy tablicę ośmiu ustrojstw o
nazwie texture i pierwszemu elementowi w tej tablicy (o indeksie zero) przypisujemy co trzeba. I podobnie jak przypadku
przypisywania vertex shadera, tak tutaj odnosimy do zawartości obiektu o nazwie DiffuseTexture , który jest zadeklarowany
na początku jako typ Texture . A porównując sobie kod oryginalny (C++) i skrypt będzie to wyglądało następująco:
// skrypt
texture[0] = (DiffuseTexture);
// kod C++
lpD3Ddevice->SetTexture( 0, DiffuseTexture );
Reszta już jest banalnie prosta, bo nie sądzę nawet, żeby trzeba było coś wyjaśniać. Każda operacja na teksturze jest w
postaci tablicy, więc jeśli chcemy coś konkretnego umieścić na konkretnym poziomie to po prostu odnosimy się do
konkretnego indeksu. Nazwy tablic poszczególnych komórek urządzenia tworzymy w analogiczny sposób jak dotychczas po prostu odcinamy prefiks zakończony znakiem "_" i gotowe, dla elegancji możemy sobie w jakiś tam sposób ustalić sposób
pisowni, byle się zgadzały nazwy.
// sampler states
MinFilter[0] = Linear;
MagFilter[0] = Linear;
MipFilter[0] = Linear;
8
DirectX ▪ Efekty
Jeśli chodzi o stany samplera (odpowiedzialnego min. za filtrowanie, mipmapy itp.) to nie ma żadnej różnicy w stosunku do
samych tekstur - po prostu każdy poziom ma odpowiednik w tablicy a nazwą komórek i wartości jak zwykle obcinamy po
prostu przedrostek i ładnie wszystko działa.
Aby sobie to wszystko jednak ładnie uporządkować przedstawmy sobie może porównanie kodu w skrypcie i analogicznego
kodu, gdybyśmy naklepali to w C++.
// skrypt
pixelshader = NULL;
vertexshader = (VShader);
// render states
Lighting = False;
CullMode = None;
// texture operations
texture[0] = (DiffuseTexture);
ColorOp[0] = Modulate;
ColorArg1[0] = Texture;
ColorArg2[0] = Current;
// sampler states
MinFilter[0] = Linear;
MagFilter[0] = Linear;
MipFilter[0] = Linear;
// kod C++
lpD3DDevice->SetPixelShader( NULL );
lpD3DDevice->SetVertexShader( VShader );
// render states
lpD3DDevice->SetRenderState( D3DRS_LIGHTING, FALSE );
lpD3DDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
// texture operations
lpD3DDevice->SetTexture( 0, DiffuseTexture );
lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_MODULATE );
lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_CURRENT );
// sampler states
lpD3DDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR );
lpD3DDevice->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR );
lpD3DDevice->SetSamplerState( 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR );
Trzeba przyznać, że skrypt wygląda przy kodzie C++ nader elegancko, nie mówiąc już tym, że jest o wiele bardziej czytelny.
Ale żeby się zbytnio na zachwycać - wyobraźmy sobie teraz potencjalną sytuację. Nagle zachodzi potrzeba dopisania jeszcze
jednego przejścia (pass) w kodzie, ponieważ chcemy osiągnąć jakiś super wypasiony efekt. Najpierw idźmy drogą naturalną,
która narzuca się od razu. Otwieramy kod C++ i klepiemy co trzeba, warunkujemy dodatkowo albo duplikujemy kod
odpowiednio do sytuacji, robimy warunki kiedy używać jednego a kiedy drugiego itd. Zacznie na pewno nas to denerwować
w pewnym momencie, ponieważ zaczniemy odczuwać nieelastycznośc tego kodu - zaczniemy wprowadzać kombinacje i na
pewno to dobrze na kod nie wpłynie.
A załóżmy, że mamy do dyspozycji skrypt w postaci podobnej jak powyżej. Nie ruszając zupełnie (!) kodu C++ otwieramy
sobie pliczek efektu, dopisujemy w danej technice po prostu nową parę nawiasów i poprzedzamy ją kombinacją pass
"nazwa", wpisujemy pomiędzy nawiasami co trzeba i gotowe! Bez żadnych komplikacji, kombinacji - tak po prostu.
Kompilator kompilując skrypt będzie wiedział już co z tym zrobić. Czy nie wygodniej, prościej i przyjemniej?
Aby jednak to wszystko zadziałało to musimy jednak postarać się o to, aby skrypt efektów było odpowiednio obsługiwany w
kodzie C++, czas więc wrócić z chmur na ziemię, bo to jeszcze nie wszystko.
g_lpEffect->FindNextValidTechnique( NULL, &g_hTechnique );
g_lpEffect->SetTexture( "DiffuseTexture", g_pTexture );
Jeśli wszystko się powiodło przy kompilacji skryptu za pomocą odpowiednich funkcji powinniśmy dostać do ręki dziłający
poprawnie obiekt typu LPD3DXEFFECT o którym już wspomniałem wyżej. Obiekt ten posiada kilka bardzo interesujących
ale zarazem bardzo ważnych metod, które teraz właśnie wykorzystamy.
Po pierwsze - musimy się dowiedzieć, czy mamy w skrypcie zdefiniowaną odpowiednią technikę dla naszego urządzenia.
Tutaj objawi nam się jeszcze jedna, fantastyczna zaleta skryptów i efektów. Otóż za pomocą metody
FindNextValidTechnique() obiektu efektu możemy od razu na początku sprawdzić, czy nasze urządzenie będzie w stanie
dany, zdefinowany w skrypcie efekt wykonać. Zostaną więc sprawdzone możliwe do przeprowadzenia operacje na
teksturach, ilość możliwych do wykorzystania poziomów tekstur itd. W kodzie C++ musielibyśmy przed uruchomieniem
9
DirectX ▪ Efekty
renderingu sprawdzać poszczególne właściwości urządzenie (w skrócie CAPS) i odpowiednio reagować, wyłączać lub nie
poszczególne fragmenty. Za pomocą wspomnianej metody mamy to zrobione od ręki i nie musimy się o to martwić. Jeśli
dana technika nie może być obsłużona to po prostu nie zostanie wyciągnięta ze skryptu i tyle.
Na tym nie koniec - wspominałem przecież, że w skrypcie możemy mieć zdefiniowanych kilka technik. Jeśli nasz graficzny
engine zaprojektujemy w taki sposób, że dany efekt będzie możliwy do osiągnięcia na rózne sposoby w zależności od
możliwości sprzętu to tutaj mamy do tego idealną sytuację. Po prostu umieszczamy w skrypcie wszystkie możliwe
kombinacje parametrów dla hipotetycznych sytuacji (których nie będzie znowu aż tak wiele) i przy wołaniu
FindNextValidTechnique() dla naszego urządzenia po prostu zostanie dla niego wybrana technika jak najbardziej
optymalna (jeśli w skrypcie ustawimy je od najbardziej do najmniej optymalnych). Jeśli żadna z technik nie zostanie
zaakceptowana jako wykonywalna przez nasze urządzenie to danego efektu po prostu się nie da uzyskać.
Jako pierwszy parametr podajemy uchwyt szukanej w skrypcie techniki. Uchwyt ten to dobrze nam znana liczba z systemu
Windows w jednoznaczny sposób identyfikująca dany obiekt. Zadaniem tej metody jest jednak wyszukiwanie technik w
skrypcie, więc aby przeszukać wszystkie, które są tam zdefiniowane podajemy ten argument jako NULL. Jako drugi z
parametrów podajemy uchwyt, do którego przypisana zostanie odpowiednia wartość w przypadku zaakceptowania danej,
znalezionej techniki do możliwości urządzenia. Jeśli żadna technika nie będzie odpowiadać możliwościom urządzenia to
uchwyt pozostanie jako wartość NULL.
Po drugie - musimy do skryptu przesłać pewne wartości, ponieważ używamy ich w kodzie. Aby to zrobić musimy użyć, w
zależności od typu wartości różnych metod obiektu efektu. Na pewno w naszym skrypcie posługujemy się teksturą, którą
identyfikujemy nazwą DiffuseTexture a która jest typu Texture. Jest to odpowiednikiem typu LPDIRECTD3DTEXTURE w
kodzie C++. Aby przesłać obiekt takiego typu z kodu C++ do skryptu wygodnie będzie się posłużyć metodą SetTexture().
Jej działanie jest jednak z goła odmienne od znanej nam metody SetTexture() obiektu urządzenia renderującego. Jako
pierwszy parametr dostaje ona nazwę parametru w skrypcie, z którym będzie skojarzony obiekt przesyłany jako parametr
drugi z aplikacji. Brzmi skomplikowanie, ale tak naprawdę jest banalnie proste. W skrypcie mieliśmy zmienną typu Texture
o nazwie DiffuseTexture. Wiemy, że jest ona odpowiednikiem typu LPDIRECT3DTEXTURE w kodzie C++. Należy więc
skojarzyć zmienną z kodu aplikacji z tą, w skrypcie. I robimy to właśnie za pomocą takiego mechanizmu.
No i z przygotowań to by było w zasadzie tyle. Mamy skompilowany skrypt efektu, mamy znalezioną odpowiednią technikę
dla naszego urządzenia, przesłaliśmy do efektu wszystkie potrzebne zmienne (w naszym przypadku teksturę) no i teraz co?
Trzeba by coś narysować, nie?
g_lpEffect->Begin( &nPasses, 0 );
{
for( UINT i = 0; i < nPasses; i++ )
{
g_lpEffect->Pass( i );
g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 12 );
}
}
g_lpEffect-&>End();
Aby poprawnie rysować z wykorzystaniem efektów musimy jeszcze dowiedzieć się paru rzeczy. Wiadomo, że będziemy
używać jednej, konkretnej techniki. Technika może mieć dowolną liczbę przejść (pass-ów) - czas więc wyciągnąć to z niej.
Cały rendering z użyciem efektu będzie wyglądał mniej więcej tak, jak we fragmencie kodu powyżej. Trochę to przypomina
rednering całej sceny z użyciem metod BeginScene() i EndScene(). Kiedy szukaliśmy odpowiedniej techniki w naszym
efekcie i znaleźliśmy odpowiednią dla nas to została ona niejako uznana za bieżącą dla danego efektu. Wywołując metodę
Begin() z obiektu efektu dostaniemy to, czego chcemy. Jako pierwszy parametr tej metody pobierany jest wskaźnik na
zmienną typu UINT, który po zakończeniu działania tej metody będzie zawierał ilość przejść dla bieżącej, wybranej dla
urządzenia techniki. Drugi parametr to pewne flagi, które dzisiaj nie mają dla nas znaczenia, ale jeśli chcecie poznać
szczegóły to dokumentacja ma oczywiście co trzeba.
Mając liczbę przejść w efekcie i wybranej technice możemy przystąpić do rysowania. I tutaj widzimy kolejną ogromną zaletę
używania efektów - nie musimy w kodzie aplikacji znać ilości przejść! Tę liczbę bowiem dostaniemy wtedy, kiedy będzie
trzeba. Wystarczy tylko zmienić liczbę przejść w kodzie skryptu a nasz kod aplikacji zostaje bez zmian!
Następnie wywołujemy kolejną metodę efektu o nazwie Pass(). Parametrem tej metody jest nic innego, jak tylko numer
przejścia, które aktualnie jest w użyciu. Działanie metody jest bardzo proste - po analizie co trzeba ustawić urządzeniu w
aktualnym przebiegu jest to po prostu robione - czyli jakbyśmy odnieśli to do kodu aplikacji następuje seria wywołań metod
SetRenderState(), SetTexture(), SetTextureStageStage() i całej reszty. A po ustawieniu wszystkiego nie
pozostaje nam innego niż tylko narysować naszą bryłę a to przecież potrafimy już doskonale.
Używanie efektu (techniki) kończymy wywołując metodę End() obiektu efektu i nie ma tutaj nic nadzwyczajnego. Mam
nadzieję, że wszystko jasne?
No i to w zasadzie koniec naszych rozważań - mam nadzieję, że wszystko zrozumieliście a jak coś nie jest jasne to pomoże
dokumentacja no i nie bójcie się pytać na forum. Ze swej strony dodam tylko, że od teraz w większości tutoriali będziemy
właśnie używali technik i efektów - bo jak sami widzicie daje to o wiele większe możliwości i tak naprawdę to moglibyśmy
pisać jeden szkielet i tylko wymieniać zawartość pliku efektów.
No to teraz pobawcie się nową zabawką a ja pomyślę, coby tu następnego ;).