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 ;).