1 DirectX �% Env. Cube Skoro wiemy już wszystko o mapowaniu sześciennym od strony teoretycznej czas przystąpić do zastosowania naszych wiadomości w praktyce. Napiszemy sobie dzisiaj prosty w miarę przykładzik, który ostatecznie udowodni nasze teoretyczne rozważania a co ważniejsze nauczy nas tworzenia nowego, fascynującego efektu. Nie ma na co czekać więc od razu przystępujemy do analizy ważniejszych części kodu. // vertex shader declarator for cube enviroment mapping DWORD dwDecl[] = { D3DVSD_STREAM (0 ), D3DVSD_REG( D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), D3DVSD_REG( D3DVSDE_NORMAL, D3DVSDT_FLOAT3 ), D3DVSD_REG( D3DVSDE_DIFFUSE, D3DVSDT_D3DCOLOR ), D3DVSD_REG( D3DVSDE_TEXCOORD0, D3DVSDT_FLOAT2 ), D3DVSD_REG( D3DVSDE_TEXCOORD1, D3DVSDT_FLOAT2 ), D3DVSD_END(), }; // our custom vertex format... struct CUSTOMVERTEX { FLOAT x, y, z; // untransformed, 3D position for the vertex FLOAT nx, ny, nz; // vertex normal DWORD color; // vertex color FLOAT tx; // first texture map coord FLOAT ty; // first texture map coord FLOAT tx1; // second texture map coord FLOAT ty1; // second texture map coord }; // ...and apropriate vertex type #define D3DFVF_CUSTOMVERTEX ( D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX2 ) Tak w zasadzie to już nie muszę wam tego tłumaczyć, prawda? Na samym początku definiujemy deklarator wejściowy naszego vertex shadera, który posłuży nam do policzenia współrzędnych mapowania dla tekstury sześciennej nakładanej na obiekt. Podobnie jak w przypadku przykładu z mapowaniem sferycznym tutaj nasz obiekt także będzie posiadał jakąś własną teksturę, reprezentującą jego powierzchnię, zaś druga tekstura - sześcienna będzie reprezentować odblaski. Dlatego oczywiście taki a nie inny zestaw danych w strukturze wierzchołków - współrzędne, normalne (niezbędne tym razem), kolor wierzchołków oraz dwa zestawy współrzędnych teksturowania. Co do czego mam nadzieję, że potraficie wyrecytować obudzeni o północy po ciężkim dniu kopania rowu ;-). Inicjalizacji i ładowania danych dla obiektów na scenie z plików też nie bardzo mi się chce omawiać (zapraszam do odpowiedniej lekcji), a zwrócę uwagę tylko na jedną rzecz, która może kogoś zastanowić, choć tak naprawdę już o niej wspominaliśmy w przykładzie mapowania sferycznego. pTorusMesh->CloneMeshFVF( 0L, D3DFVF_CUSTOMVERTEX, pD3DDevice, &pTorusMeshClone ); pTorusMesh = pTorusMeshClone; pTorusMesh->GetVertexBuffer( &pTorusVB ); pTorusMesh->GetIndexBuffer( &pTorusIB ) Na naszej przykładowej scenie jeden z obiektów będzie odbijał całe otoczenie, reszta natomiast będzie się w nim odbijać. Te odbijane obiekty wystarczy, że będziemy rysować za pomocą prostej metody obiektu wczytanego z pliku *.x - DrawSubset(). Natomiast obiekt, który nas najbardziej interesuje musi oczywiście posiadać swój własny typ wierzchołków, którego nie jesteśmy w stanie załadować z pliku, będzie to oczywiście ten, który zdefiniowaliśmy sobie na początku. Sposób powinniśmy także już znać z poprzedniego przykładu, ale dla pewności szybkie przypomnienie. Obiekt typu LPD3DXMESH posiada metodę CloneMeshFVF() która tworzy a w zasadzie kopiuje do nowego obiektu takiego samego typu wszystkie dane wierzchołków z oryginalnego obiektu zachowując jednak przy tym narzucony przez nas format wierzchołków. O dane, których nie ma w oryginalnym obiekcie oczywiście musimy zatroszczyć się już sami (jakoś je wyliczyć, w zależności od tego, co nam będzie potrzebne). Po wywołaniu tej metody robimy prostą zamianę obiektów, tak, że nasz oryginalny pokazuje teraz ten podmieniony, żeby nam się lepiej po prostu pracowało i nie było zbyt dużego zamieszania ze zmiennymi w programie. Ponieważ mając własny format wierzchołków nie będziemy korzystać z metody DrawSubset() więc koniecznym stanie się również pozyskanie bufora wierzchołków i bufora indeksów do wierzchołków ponieważ w takiej to właśnie formie są przechowywane wierzchołki i ściany w pliku *.x. 2 DirectX �% Env. Cube pD3DDevice->SetVertexShaderConstant( 0, D3DXMatrixTranspose( &matWorld, &matWorld ), 4 ); pD3DDevice->SetVertexShaderConstant( 4, D3DXMatrixTranspose( &matViewProj, &matViewProj ), 4 ); pD3DDevice->SetVertexShaderConstant( 8, &vectPos, 1 ); pD3DDevice->SetVertexShaderConstant( 9, &vect, 1 ); Mając już załadowane odpowiednie modele na scenę i tekstury, skopiowane i ustawione odpowiednie właściwości wierzchołków możemy przystąpić do ustawienia macierzy, które będą nam pomocne przy renderingu sceny - a więc macierzy widoku, projekcji i świata. Nie będzie we fragmencie programu za to odpowiedzialnemu wielkiej filozofii i raczej wszystko powinniśmy wiedzieć. Ustalmy sobie ino, jaka macierz będzie się znajdować w jakich rejestrach pamięci stałej. Jak widać na powyższym kawałeczku kodu: " macierz świata - rejestry c0 - c3 " połączone macierze widoku i projekcji - rejestry c4 - c7 " wektor reprezentujący położenie naszej kamery w świecie - c8 " zestaw kilku stałych, które nam mogą się przydać w shaderze - c9 Tak uzbrojeni w dane w zasadzie możemy przystąpić do zagłębienia się w sam shader. Jak nadmieniłem w części bardziej teoretycznej shader posłuży nam do dwóch rzeczy - po pierwsze tradycyjnego już przeliczenia punktów przez macierze w celu otrzymania pożądanego przez nas widoku sceny a po drugie obliczenia współrzędnych mapowania sześciennej tekstury reprezentującej nasze zrenderowane środowisko. Napisałem również w części teoretycznej o sposobie obliczania takich współrzędnych. W przeciwieństwie do shadera zastosowanego w przypadku mapowania sferycznego ten nasz dzisiejszy w cudowny sposób zamiast się skomplikować to się uprości a da nam w zamian o wiele lepsze efekty niż ten poprzedni :-). Jak wiemy, wystarczy dla potrzeb naszej mapy sześciennej, aby poprawnie ją zmapować na obiekt policzyć tylko współrzędne wektora odbitego (refleksu) ze znanego nam już zapewne doskonale wzoru: Nie trzeba potem już w żadne specjalny sposób już przekształcać i wykorzystywać jego współrzędnych, poza normalizacją wektora, bo jak łatwo się domyśleć jego poszczególne współrzędne nie mogą posiadać wartości większej niż 1, bo inaczej nasza tekstura nam by się nie ułożyła odpowiednio na bryle. No ale wszystko wyjdzie w praniu - zatem przyjrzyjmy się bliżej temu czemuś, co ma nam zaradzić na wszystkie nasze bolączki: ; Transform position and normal into camera-space m4x4 r0, v0, c0 ; vertex in the world space m3x3 r1, v3, c0 ; normal in the world space Jak zawsze na sam początek wszystkie potrzebne transformacje. Współrzędne wierzchołka (przypominam nieustannie, że standardowo w v0) mnożymy przez zestaw wektorów zawartych w rejestrach c0-c4 (macierz świata) i wynik umieszczamy w rejestrze tymczasowym r0. W tym momencie mamy już przekształcony wierzchołek przez macierz świata. To samo dotyczy współrzędnej odpowiedzialnej za normalną wierzchołka - ponieważ w znacznym stopniu obliczanie mapowania bazuje na normalnej, więc i ją musimy przenieść do świata w którym operujemy, żeby same wierzchołki nie czuły się osamotnione - normalna po przekształceniu (w v3 na wejściu) powędruje do rejestru r1. ; Compute normalized view vector add r2, r0,-c8 ; from eye to world vertex rsq r2.w, r2.w ; normalize vertex mul r2, r2, r2.w ; normalized eye vector Następnie do obliczenia wektora refleksu potrzebować będziemy wektora łączącego oko kamery z wierzchołkiem we współrzędnych świata. Współrzędne wierzchołka przed momentem obliczyliśmy i mamy je w rejestrze r0, natomiast jak wspominaliśmy wcześniej położenie naszej kamery jest przekazane do vertex shadera poprzez rejestr c8. Aby policzyć wektor patrzenia wystarczy po prostu odjąć od położenia wierzchołka w świecie - r0 położenie kamery - c8. Wynik w naszym programie umieścimy sobie dla przykładu w rejestrze tymczasowym r2. Aby wszystko przebiegało zgodnie z naszymi oczekiwaniami należy jeszcze do tego wszystkiego obliczony przed chwilą wektor znormalizować. A jak to robić za pomocą asemblera shadera przecież także doskonale już wiemy! Poznany przed tygodniem sposób szybkiej normalizacji stosować będziemy w zasadzie bez przerwy, krótko więc przypomnijmy. Rozkazem rsq obliczamy odwrotny pierwiastek z tego co znajduje się pod pierwiastkiem, a tam mamy nasz wektor pomnożony przez samego siebie, zgodnie z wszelkimi wzorami wygląda to tak: 3 DirectX �% Env. Cube Mając tę wartość (długość wektora w mianowniku) wystarczy podzielić nasz wektor przez tę długość (pomnożyć przez wynik operacji rsq) i otrzymamy wektor znormalizowany. Mam nadzieję, że to już wam się utrwali i już nie będziemy tego wałkować n-ty raz mogąc poświęcać czas bardziej wyrafinowanym sztuczkom. Po znormalizowaniu wektora nadal przechowujemy jego wartość w rejestrze r2 (no bo po co mamy sobie w kodzie i w rejestrach bałaganić). ; renormalize normal dp3 r1.w, r1, r1 rsq r1.w, r1.w mul r1, r1, r1.w Dokładnie ten sam manewr co przed chwilą robimy z naszą przeliczoną normalną. Najpierw mnożymy wektor przez siebie, potem pierwiastek z tego w mianowniku i mnożenie przez wektor - no ale przecież miałem się nie powtarzać. Uczulam tylko na operacje na wektorach, które akurat w tym przypadku są przeprowadzane na wektorach znormalizowanych - w ten sposób unikamy niedokładności i zakłóceń. Oczywiście nie wszędzie tak będzie, ale akurat w tym przypadku nie są ważne długości wektorów a raczej ich kierunki a z tymi lepiej mieć do czynienia jako ze znormalizowanymi. ; calculate reflection vector dp3 r4, r2, r1 add r4, r4, r4 mul r1, r1, r4 add oT0, r2,-r1 mov oT0.w, c9.z Mając przygotowane odpowiednie wektory - patrzenia i normalny, oba znormalizowane - o długości jeden i mając gotowy przepis na wektor refleksu możemy przystąpić do jego obliczenia. Nie pozostaje nam nic innego, jak tylko po kolei wykonywać operacje potrzebne do jego obliczenia. W shaderze do mapowania sferycznego już też to robiliśmy więc teraz tylko skrócony opis: dp3 r4, r2, r1 Najpierw potrzebujemy iloczynu skalarnego wyżej wymienionych wektorów, które przypomnijmy mamy w rejestrach r1 (normalna) i r2 (patrzenia). Wynik operacji iloczynu umieszczamy w rejestrze r4. add r4, r4, r4 Następnie musimy tę wartość pomnożyć przez dwa, więc aby było szybciej dodamy sobie po prostu ten sam wektor do siebie, co da nam dokładnie ten sam efekt a będzie szybciej niż mnożenie. mul r1, r1, r4 Mając wartość 2*(V*N) mnożymy przez nią wektor normalny zgodnie z ogólnym wzorem. Ponieważ wektor normalny nie będzie nam już potrzebny w swojej oryginalnej postaci, więc zastąpi sobie jego wartości w rejestrze r1. add oT0, r2,-r1 Kończąc, za jednym zamachem wykonujemy dwie operacje. Po pierwsze do wektora patrzenia odejmujemy wartość 2*(V*N)*N otrzymując tym samym szukaną wartość wektora refleksu. Po drugie, jak wiemy z lekcji teoretycznej, współrzędne x i y wektora refleksu stanowią dla nas w tym przypadku po prostu współrzędne mapowania dla tekstury sześciennej! Dlatego też wynikowym rejestrem operacji dodawania będzie oT0, czyli rejestr reprezentujący współrzędne mapowania tekstury dla pierwszego poziomu tekstur na obiekcie (drugim będzie poziom zawierający właściwe tekstury obiektu). ; Project position m4x4 oPos, r0, c4 mov oT1, v7 4 DirectX �% Env. Cube mov oD0, v5 No i to w zasadzie byłby koniec - wektor refleksu i współrzędne mapowania obliczone, więc teraz tylko pozostaje dopełnić formalność. Aby zobaczyć we właściwej perspektywie naszą bryłę należy pomnożyć jej wierzchołki przez macierz widoku i projekcji - u nas jedną, powstałą z połączenia powyższych i umieszczoną w rejestrach od c4 do c7. Jako wynik operacji otrzymamy wyjściowe wartość z shadera, które bezpośrednio umieszczamy w rejestrze odpowiedzialny za pozycję wierzchołka oPos. mov oT1, v7 Drugi zestaw współrzędnych tekstur pozostanie bez zmian, więc nie pozostaje nam nic innego jak tylko przepisanie go z wejścia - v7 na wyjście, czyli do rejestru za to odpowiedzialnego oT1. Musi to być zrobione ponieważ nasza aplikacja będzie oczekiwać od shadera w tym rejestrze danych a jeśli ich nie dostarczymy to będzie jedna wielka kiszka. mov oD0, v5 No i to samo będzie oczywiście w przypadku koloru diffuse wierzchołków. Wbijmy sobie tutaj niejako przy okazji do głowy raz na zawsze, że wszystkie rejestry wyjściowe shadera, których wykorzystanie zadeklarujemy definiując nasz własny format wierzchołków muszą zostać jawnie wypełnione w kodzie shadera - oszczędzi nam to sporo frustracji z dochodzeniem, co jest nie tak i nauczy naprawdę dobrych nawyków. A wracając do sprawy to... już koniec naszego shaderka. Jak widać jest naprawdę banalnie prosty a jak spojrzeć z perspektywy czasu na moje pierwsze zmagania z shaderami i próby ich zrozumienia to aż śmiech bierze i wstyd jak można było myśleć, że to jakiś hardcore, którego nie da się zrozumieć ;-). A teraz to niemal każdy, jaki tego sobie zażyczymy napiszemy naprawdę bez żadnego trudu! Dobra, koniec samouwielbienia, czas wracać do ciężkiej pracy bo to jeszcze nie koniec, a w zasadzie jesteśmy prawie na samym początku. Pomimo iż paraliśmy się z vertex shaderem, którego sama nazwa może przerażać to jednak dzisiaj był on jedną z łatwiejszych części aplikacji - teraz czas na nowe, bardziej wciągające rzeczy, czyli o tym jak wyrenderować środowisko, stworzyć mapę sześcienną i otrzymać to co chcemy - refleksy na obiekcie. Obiekty łącznie z teksturami załóżmy, że mamy już na scenie załadowane, scena ustawiona, wszystko się obraca jak należy. Co zrobić, żeby się to wszystko odbijało? Ano idea jest prosta - Aby w naszym obiekcie odbijało się wszystko musimy wyrenderować na mapę środowiska wszystko co go otacza prócz jego samego. Funkcja renderująca więc trochę nam się skomplikuje, bo będzie trochę kombinacji - no ale zobaczmy. Przyjrzyjmy się więc trochę dokładniej naszej funkcji renderującej, w której znajdziemy... zadziwiająco mało (czyżby ot było aż tak proste???;) void Render() { RenderScene( TRUE ); pD3DDevice->Present( NULL, NULL, NULL, NULL ); } Mamy tu tylko dwie funkcje, z których jedną znamy przecież doskonale - metoda Present() naszego urządzenia renderującego powoduje przerzucenie tego, co mamy w buforach na ekran, czyli w rzeczywistości tak naprawdę tutaj wszystko rysujemy. Druga funkcja to nasz tajemniczy wymysł, o którym właśnie teraz dowiemy się wszystkiego i całą tajemnica powinna nam się wyjaśnić. Przypatrzmy się więc naszej tajemniczej funkcji: void RenderScene( BOOL bRender ) { ... pD3DDevice->BeginScene(); { ... // draw sky box ... // draw rotating torus ... if( bRender ) // if not need don't render torus { RenderToCube(); // render teapot } ... } pD3DDevice->EndScene(); 5 DirectX �% Env. Cube } Wiem, nie jest dokładnie to, co w przykładzie i jak słusznie zauważyliście jest to pewnego rodzaju pseudo-kod. Ponieważ prawdziwy kod naszej funkcji zajmuje mnóstwo linii i tylko by nam tutaj zaciemnił, więc my sobie ją przedstawimy w sposób taki, który przedstawi na idee naszej działalności i cele a nie samo działanie na konkretnych obiektach (to jest banalne i każdy z nas potrafi to doskonale). Jak widać na początku po wywołaniach metod urządzenia renderującego jest to właściwy kod naszej funkcji do rysowania prymitywów, więc spodziewamy się tutaj mnóstwa rożnych ustawień dla urządzenia i konkretnych obiektów. I tak w zasadzie będzie, choć dadzą się też zauważyć pewne różnice. Po pierwsze nasza funkcja rysująca po raz pierwszy przyjmuje jakiś parametr typu BOOL (prawda, lub fałsz). Po co nam on będzie potrzebny okaże się już za moment a my może skupmy się na tym, co się dzieje na samym początku. Scena stoi, wszystko przygotowane, czas więc ruszyć wszystko z miejsca. Nie czekając więc na nic mamy od razu na początku wyczyszczenie wszystkich buforów, (metoda Clear() urządzenia), następnie wywołanie pary BeginScene() i EndScene(). Pomiędzy nimi robimy to co zwykle, czyli ustawiamy dla naszych modeli odpowiednie macierze przekształceń, jeśli są im potrzebne no i rysujemy poszczególne obiekty na scenie. I tak nam wszystko się pięknie odbywa do pewnego momentu. Otóż w pewnym momencie nasz program nagle spotyka warunek: if( bRender ) // if not need don't render torus { RenderToCube(); // render teapot } Ponieważ za pierwszym wywołaniem naszej funkcji jak prosto to sprawdzić jej parametr bRender, jego wartość wynosi TRUE, więc program posłusznie przystąpi do wykonania warunku, czyli wywoła kolejna funkcję o jeszcze bardziej fascynującej nazwie czyli RenderToCube(). W tym momencie musimy sobie przerwać analizę naszej dotychczasowej funkcji RenderScene() trzeba wskoczyć do następnej, żeby się dowiedzieć, co w ogóle jest tutaj grane. Zobaczmy więc: void RenderToCube() { // render scene to cube map LPDIRECT3DSURFACE8 pBackBuffer; LPDIRECT3DSURFACE8 pZBuffer; D3DXMATRIX matProjSave; D3DXMATRIX matViewSave; pD3DDevice->GetTransform( D3DTS_VIEW, &matViewSave ); pD3DDevice->GetTransform( D3DTS_PROJECTION, &matProjSave ); D3DXMATRIX matProj; D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/2, 1.0f, 1.0f, 1500.0f ); pD3DDevice->SetTransform( D3DTS_PROJECTION, &matProj ); D3DXMATRIX matViewDir; pD3DDevice->GetTransform( D3DTS_VIEW, &matViewDir ); matViewDir._41 = 0.0f; matViewDir._42 = 0.0f; matViewDir._43 = 0.0f; pD3DDevice->GetRenderTarget( &pBackBuffer ); pD3DDevice->GetDepthStencilSurface( &pZBuffer ); for( int i = 0; i < 6; i++ ) { D3DXMATRIX matView; matView = GetCubeMapViewMatrix( (D3DCUBEMAP_FACES) i ); D3DXMatrixMultiply( &matView, &matViewDir, &matView ); pD3DDevice->SetTransform( D3DTS_VIEW, &matView ); pCubeMap->GetCubeMapSurface( (D3DCUBEMAP_FACES) i, 0, &pCubeSurface ); pD3DDevice->SetRenderTarget( pCubeSurface, pZBufferSurface ); DXRELEASE( pCubeSurface ); // Render the scene (except for the teapot) RenderScene( FALSE ); } pD3DDevice->SetRenderTarget( pBackBuffer, pZBuffer ); DXRELEASE( pBackBuffer ); 6 DirectX �% Env. Cube DXRELEASE( pZBuffer ); pD3DDevice->SetTransform( D3DTS_VIEW, &matViewSave ); pD3DDevice->SetTransform( D3DTS_PROJECTION, &matProjSave ); } Na pierwszy rzut oka wygląda to dosyć groznie, no ale nie z takimi już żeśmy sobie radzili, dlatego bądzmy dobrej myśli ;). Jak sama nazwa wskazuje i wszelkie znaki na niebie i ziemi ta funkcja posłuży nam do wyrenderowania mapy środowiska, która potem nałożymy na obiekt - a jak? Już analizujemy. LPDIRECT3DSURFACE8 pBackBuffer; LPDIRECT3DSURFACE8 pZBuffer; D3DXMATRIX matProjSave; D3DXMATRIX matViewSave; Ponieważ trochę będziemy kombinować na rożnych macierzach i buforach, więc dla bezpieczeństwa i żeby niczego po drodze sobie nie zgubić będziemy potrzebować kilku obiektów, w których będziemy mogli przechować jakieś nasze tymczasowe wartości. Jak się okazało po analizie przykładów potrzebować będziemy co następuje - bufora z, bufora tylnego, do którego zwykle renderowana jest scena oraz macierzy projekcji i widoku. Podczas tworzenia sceny z widokiem mapy środowiska zmieniać się nam będą głównie macierze, dlatego ich zawartość będzie musiała zostać zachowana. Do tego potrzeba będzie buforów, do których będziemy renderować, żeby z nich pobierać gotowe obrazy. Mając gdzie składować dane, możemy iść dalej. pD3DDevice->GetTransform( D3DTS_VIEW, &matViewSave ); pD3DDevice->GetTransform( D3DTS_PROJECTION, &matProjSave ); No i od razu wykorzystamy sobie nasze zmienne. Nie zapominajmy w tym momencie skąd przyszliśmy - byliśmy w jakimś miejscu funkcji renderującej, która miała w danym momencie ustawione konkretne macierze widoku, projekcji i świata w urządzeniu. Ponieważ my zaraz te macierze zmienimy, dokonamy pewnych operacji i wyjdziemy z tej funkcji, więc po powrocie do funkcji renderującej macierze musza być takie same jak przed wejściem tutaj. Zapisujemy więc ich stan... Metoda GetTransform() robi dokładnie odwrotnie niż doskonale nam znana metoda SetTransform(), choć parametry pobiera dokładnie te same. Jako pierwszy parametr pobiera ona typ przekształcenia jakie ma zostać zapisane, w drugim parametrze, jakim jest macierz reprezentująca to przekształcenie. Zapisujemy więc aktualnie ustawione w urządzeniu macierze widoku D3DTS_VIEW oraz projekcji - D3DTS_PROJECTION. D3DXMATRIX matProj; D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI/2, 1.0f, 1.0f, 1500.0f ); pD3DDevice->SetTransform( D3DTS_PROJECTION, &matProj ); Macierze zachowaliśmy, czujemy pewien komfort, czas więc przystąpić do działań, żeby nie tracić darmo cennego przecież czasu - przystąpmy do renderingu naszej mapy środowiska. Aby to zrobić będziemy potrzebować dwóch rzeczy - odpowiednio musimy ustawić nasze macierze widoku i projekcji - trąbimy przecież o tym już ładną chwile. Na pierwszy ogień pójdzie więc macierz projekcji, bo z nią jest łatwiej. Mapę świata generujemy dla obiektu wiemy w jaki sposób. Stajemy w samym jego środku, który jest względny w stosunku do świata, w jakim obiekt się znajduje i patrząc stamtąd renderujemy nasz świat. Nie inaczej jest w tym przypadku. Ponieważ nasz świat jest dosyć duży i żeby można było zobaczyć wszystko co trzeba musimy odpowiednio dobrać kąt i zasięg widzenia. W naszym przypadku sprawa jest dosyć prosta - nasz obiekt stoi na środku sceny i do każdej ściany reprezentującej środowisko mam taka sama odległość. Wystarczy więc raz ustawić macierz projekcji i będziemy mieli spokój. Oczywiście jest ona ustawiana w taki sposób, żeby z miejsca pobytu obiektu widać było to, co ma się w nim odbić (czyli nie koniecznie wszystko), choć tak jest akurat w naszym przypadku. Gdyby obiekt się na przykład poruszał po scenie, pewnie w niektórych momentach należałoby niektóre wartości, zwłaszcza zasięgu zweryfikować na bieżąco, no ale to już zależy od konkretnych przypadków. Dla potrzeb naszej nauki tyle zupełnie nam wystarczy. D3DXMATRIX matViewDir; pD3DDevice->GetTransform( D3DTS_VIEW, &matViewDir ); matViewDir._41 = 0.0f; matViewDir._42 = 0.0f; matViewDir._43 = 0.0f; Kogoś mogą naprawdę te linie bardzo zastanowić - po jaka znowu cholerę nam kolejna kopia naszej macierzy widoku? Przecież jedna już mamy i na razie z niej nie skorzystaliśmy a tu już z następna mieszamy? Otóż, do działań mapa środowiska będziemy musieli modyfikować pewne wektory związane z macierzą widoku właśnie, dlatego tez nie możemy sobie zmienić macierzy poprzednio zachowanej, ponieważ tamta będziemy musieli przywrócić na samym końcu naszej tymczasowej funkcji aby po powrocie do funkcji renderującej wszystko wróciło do normy. A w tym akurat miejscu pobieramy jeszcze raz macierz widoku i zerujemy jej ostatni wiersz - ten, odpowiedzialny za przesuniecie kamery w świecie - po co? Już za moment wyjaśnimy. pD3DDevice->GetRenderTarget( &pBackBuffer ); 7 DirectX �% Env. Cube pD3DDevice->GetDepthStencilSurface( &pZBuffer ); Jeszcze zanim przystąpi do kolejnych działań pobieramy sobie wskazniki do powierzchni, które stanowią bufory docelowe naszych operacji rysunkowych. Metoda GetRenderTarget() naszego urządzenia powoduje pobranie wskaznika do powierzchni, na której tak naprawdę rysowane są nasze prymitywy przed przerzuceniem ich na powierzchnie przednia (ekran) podczas renderingu. To jest właśnie ten nasz tylny bufor (tak naprawdę po prostu zwykła powierzchnia), który w D3D jest nazywany bardzo ładnie "celem renderingu". Drugie wywołanie może się nie skojarzyć na pierwszy rzut oka ze swoim przeznaczeniem. Metoda urządzenia GetDepthStencilSurface() powoduje pobranie adresu bufora z naszego urządzenia, dzięki któremu mamy bardzo ułatwione zadanie, jeśli chodzi o usuwanie niewidocznych obiektów z naszych scen. Jak działa bufor opisywaliśmy już kilkakrotnie, więc jeśli ktoś nie pamięta to niech wróci do początków i do lekcji bodajże o bryłach. Te dwie zmienne spowodują to, że będziemy mieć w ręce dostęp do dwóch bardzo ważnych buforów, bez których nie zdołamy stworzyć żadnej mapy środowiska. for( int i = 0; i < 6; i++ ) { D3DXMATRIX matView; matView = GetCubeMapViewMatrix( (D3DCUBEMAP_FACES) i ); D3DXMatrixMultiply( &matView, &matViewDir, &matView ); pD3DDevice->SetTransform( D3DTS_VIEW, &matView ); pCubeMap->GetCubeMapSurface( (D3DCUBEMAP_FACES) i, 0, &pCubeSurface ); pD3DDevice->SetRenderTarget( pCubeSurface, pZBufferSurface ); DXRELEASE( pCubeSurface ); // Render the scene (except for the teapot) pD3DDevice->BeginScene(); RenderScene( FALSE ); pD3DDevice->EndScene(); } No i można być powiedzieć, że mamy w tym momencie najważniejszą pętlę w naszym programie (jeśli nie liczyć pętli komunikatów aplikacji ;). Tutaj odbywa się cała rzecz, czyli tworzenie mapy środowiska. D3DXMATRIX matView; matView = GetCubeMapViewMatrix( (D3DCUBEMAP_FACES) i ); D3DXMatrixMultiply( &matView, &matViewDir, &matView ); pD3DDevice->SetTransform( D3DTS_VIEW, &matView ); Ktoś pewnie znowu przeklnie szpetnie na widok zmiennej reprezentującej macierz i mającej w nazwie wyraz "View", ale spokojnie, tym razem nie będziemy już pobierać kolejnej kopii chyba już znienawidzonej w tym przykładzie, ale bardzo ważnej macierzy. Tym razem zmienna ta będzie stanowiła cel, w którym my taka macierz widoku (nowa!) sobie umieścimy. Zaraz potem mamy dosyć tajemnicza linie: matView = GetCubeMapViewMatrix( (D3DCUBEMAP_FACES) i ); GetCubeMapViewMatrix() to kolejna funkcja stworzona a właściwie skopiowana z D3D SDK na potrzeby naszego przykładu. W tym miejscu wypadałoby znowu wskoczyć do nie, no ale my może pokażmy sobie tylko jej kawałek, żeby już całkiem kodu sobie nie zaciemnić: D3DXMATRIX GetCubeMapViewMatrix( DWORD dwFace ) { D3DXVECTOR3 vEyePt = D3DXVECTOR3( 0.0f, 0.0f, 0.0f ); D3DXVECTOR3 vLookDir; D3DXVECTOR3 vUpDir; D3DXMATRIX matView; switch( dwFace ) { case D3DCUBEMAP_FACE_POSITIVE_X: vLookDir = D3DXVECTOR3( 1.0f, 0.0f, 0.0f ); vUpDir = D3DXVECTOR3( 0.0f, 1.0f, 0.0f ); break; case D3DCUBEMAP_FACE_NEGATIVE_X: vLookDir = D3DXVECTOR3(-1.0f, 0.0f, 0.0f ); vUpDir = D3DXVECTOR3( 0.0f, 1.0f, 0.0f ); 8 DirectX �% Env. Cube break; ... } D3DXMatrixLookAtLH( &matView, &vEyePt, &vLookDir, &vUpDir ); return matView; } I cóż tutaj takiego strasznego widać. Jak popatrzeć dokładnie w jej kod, to wszystko się wydaje dosyć banalne i takie tez jest w istocie. W zależności od parametru, jaki funkcja otrzymuje wykonuje ona identyczny zestaw kroków, dla każdego przypadku ino z każdym przypadkiem dobiera inne wartości dla pewnych elementów. Głównym zadaniem tej funkcji jest stworzyć macierz widoku. Macierz taka ma za zadanie być najprostsza z możliwych - po prostu ma ustawić kamerę w sześć rożnych stron, taka aby objąć cały nasz świat widokiem i żeby po ich ustawieniu można było wygenerować mapę środowiska. Aby stworzyć macierz widoku jak wiemy potrzebujemy trzech wektorów - położenia oka, celu oraz wektora pokazującego gore świata. W naszym przypadku upraszczamy sprawę maksymalnie jak tylko się da. Ponieważ renderujemy z położenia obiektu, więc nasze oko zawsze będzie w punkcie (0, 0, 0) - bo patrzmy z punktu widzenia obiektu. Cel dla każdej z sześciu płaszczyzn otaczających będzie inny - ale z faktu, że kolejne ściany są do siebie prostopadle wynika, że wektorami celu będą kolejne wersory (wektory jednostkowe) w lokalnym układzie współrzędnych obiektu. Ponieważ kamera dla poszczególnych przypadków będzie tez inaczej zorientowana trzeba tez dobrać odpowiednio zwrot wektora oznaczającego gore świata dla kamery - na podstawie położenia oka i celu możemy tego łatwo dokonać. I jak widać po funkcji to właśnie jest robione dla poszczególnych przypadków parametru dwFace, który pomimo tego, że przy wywołaniu GetCubeMapViewMatrix() jest rzutowany na jakiś tajemniczy typ i potem w kodzie jest również wykorzystywany nie jako liczba, tak naprawdę jest po prostu numer oznaczający kolejny element mapy środowiska. Po ustawieniu wszystkich potrzebnych wektorów następuje wywołanie niezastąpionej jak dotychczas funkcji z biblioteki D3DX - D3DXMatrixLookAtLH(), czyli utworzenie macierzy widoku na podstawie określonych wcześniej wektorów. I widać w tym miejscu doskonale po co nam to wszystko - za pomocą tej funkcji po prostu zmieniamy co chwila kierunek patrzenia kamery (sześć razy) no i zwracamy sobie tą macierz. No a skoro taka macierz mamy, to możemy ja teraz wykorzystać. Wróćmy więc do kawałku kodu, który omawialiśmy w przypadku funkcji RenderToCube(). D3DXMATRIX matView; matView = GetCubeMapViewMatrix( (D3DCUBEMAP_FACES) i ); D3DXMatrixMultiply( &matView, &matViewDir, &matView ); pD3DDevice->SetTransform( D3DTS_VIEW, &matView ); Po utworzeniu sobie naszej macierzy widoku dla konkretnego kierunku (określonego typem D3DCUBEMAP_FACES), na który jest rzutowany parametr funkcji do tworzenia macierzy widoku musimy zrobić jeszcze jedna rzecz. Ponieważ w naszym programie możemy patrzeć na obiekt z dowolnego w zasadzie punktu przestrzeni, więc nie możemy sobie tak po prostu zrenderować teraz naszej mapy środowiska. Gdybyśmy stali w którejś z płaszczyzn tworzących globalny układ świata to tak, ale jeśli uniesiemy się i będziemy krążyć nad obiektem to nasza mapa renderowana na podstawie macierzy obliczonej przed momentem przestanie przedstawiać dokładnie to, co nas otacza, tylko będzie się wydawać jakaś przekręcona. Aby tego uniknąć mnożymy nasza świeżą macierz przez macierz widoku, która zachowaliśmy poprzednio. Pamiętajmy, że tamtej macierzy wyzerowaliśmy wektor odpowiedzialny za przesuniecie w świecie (nie możemy się tutaj przesunąć, bo znowu w mapie wyjdą cuda). Po przemnożeniu takich macierzy będziemy mieć taka macierz widoku, która uwzględni nam przy renderingu mapy świata miejsce naszego pobytu - odpowiednio więc obróci kamerę, żebyśmy widzieli w miarę realistycznie otoczenie. No i na koniec, mając już odpowiednią macierz widoku ładujemy ja naszemu urządzeniu. Gdybyśmy teraz sobie wyrenderowali obraz na ekran to zobaczylibyśmy co? No właśnie... nasz świat, na który patrzylibyśmy z punktu widzenia naszego obiektu, który będzie odbijał wszystko! No ale my tego nie zrobimy, bo jest nam to do czego innego potrzebne: pCubeMap->GetCubeMapSurface( (D3DCUBEMAP_FACES) i, 0, &pCubeSurface ); pD3DDevice->SetRenderTarget( pCubeSurface, pZBufferSurface ); DXRELEASE( pCubeSurface ); Tutaj po raz pierwszy odwołamy się do naszego nowego obiektu, jakim jest obiekt reprezentujący mapę sześcienną. Jest to specjalny obiekt w D3D, który przechowuje dane dla takiej mapy, zarządza nimi i zawiera metody, które pozwalają manipulować taka mapa. My użyjemy w naszym programie tylko jednej jego metody a mianowicie GetCubeMapSurface(). Zanim jednak przejdziemy do konkretów małe słowo wyjaśnienia jak wygląda obiekt reprezentujący teksturę sześcienną. Jak wiemy, musimy mieć sześć tekstur reprezentujących wszystkie ściany sześcianu otaczającego nasz obiekt odbijający (ale masło maślane). Tekstury w D3D są przechowywane jak każdy inny obraz na jakiejś powierzchni. Obiekt tekstury sześciennej zawiera po prostu sześć powierzchni, na których będzie przechowywał każdą z map reprezentujących środowisko. Wracając do naszego kodu i metod obiektu tekstury sześciennej. GetCubeMapSurface() służy nam do tego, aby moc pobrać 9 DirectX �% Env. Cube od naszego obiektu adres powierzchni, która przechowuje obraz. Jako pierwszy parametr dostaje ona identyfikator powierzchni, której adres ma zostać zwrócony - ten identyfikator oznacza, przypominam jakiej to ściany sześcianu ta tekstura dotyczy. Jako drugi parametr przyjmuje poziom mipmapy, dla jakiej chcemy uzyskać powierzchnie. Ponieważ my o mipmapach nie wiemy jeszcze za wiele i ich nie używamy, więc ustawmy sobie tutaj na zero, bo używamy tylko jednego poziomu szczegółowości tekstur. Ostatni parametr dostanie od tej metody adres powierzchni dla konkretnej mapy. I właśnie to tutaj zrobimy - pobierzemy sobie adres kolejnej mapy w obiekcie tekstury i... pD3DDevice->SetRenderTarget( pCubeSurface, pZBufferSurface ); I co? Ano patrzmy co się dzieje. SetRenderTarget() to metoda naszego urządzenia. Co ona robi? Ano dostaje ona dwa wskazniki, obydwa bardzo, bardzo ważne! Pierwszy z nich jak mówi dokumentacja stanowi wskaznik do bufora koloru - po naszemu mówiąc wskaznik do tylnego bufora, który my potem metoda Present() przerzucimy na ekran. Tylko, że w tym momencie my mówimy urządzeniu co stanowi dla niego nowy bufor koloru - a co nim jest? No właśnie! Nowym tylnym buforem dla urządzenia jest jedna z map istniejących w obiekcie tekstury sześciennej. Więc wszystko, co namalujemy teraz za pomocą urządzenia pójdzie... do mapy środowiska! Drugim argumentem jest adres nowego bufora z (jego powierzchni), który będzie wykorzystany przy renderingu sceny. Dzięki niemu uzyskamy poprawna kolejność i widoczność obiektów na scenie. DXRELEASE( pCubeSurface ); Ponieważ takie wywołanie powoduje inkrementacje licznika odwołań do obiektu powierzchni reprezentującej mapę środowiska, więc od razu, żeby nie zapomnieć, zwolnijmy ja tutaj - robi to oczywiście doskonale znana nam metoda z obiektów COM - Release(). RenderScene( FALSE ); No i tutaj już powinniśmy mieć zupełnie dosyć. Brnąc przez całą funkcję renderującą dotarliśmy do funkcji tworzącej środowisko aby tutaj wrócić z powrotem do niej samej? Ano tak i trzeba przyznać uczciwie, że pachnie tutaj małą rekurencja - i tak jest w istocie. Tyko tym razem spójrzmy na parametr naszej funkcji renderującej. Jest nim wartość FALSE a co to oznacza? Wróćmy na chwile do odpowiedniego kawałka naszego pseudo-kodu: if( bRender ) // if not need don't render torus { RenderToCube(); // render teapot } Jeśli parametr bRender miał wartość TRUE to funkcja renderująca przystępowała do tworzenia mapy środowiska. W tym przypadku tak nie jest więc nie wykona nam się ten fragment kodu. Funkcja to ominie i zakończy swoje działanie. W ten właśnie sposób zostanie stworzony jeden obraz reprezentujący kawałek naszego świata, ale widziany już z zupełnie innego punktu. Zauważmy, że taka na pierwszy rzut oka skomplikowana procedura odbywa się sześć razy - dla każdego przebiegu pętli jest zmieniana macierz widoku za pomocą funkcji GetCubeMapViewMatrix() a następnie bazując na tej macierzy jest wywoływana funkcja renderująca, która rysuje odpowiedni kawałek świata, ale nie na ekranie tylko na kolejnych powierzchniach zawartych w obiekcie tekstury sześciennej - prawda jakie to wszystko proste i łatwe? ;). Oczywiście w takim przypadku funkcja renderująca nie rysuje obiektu, dla którego tworzymy mapę - dba min. o to właśnie jej parametr a także zapobiega jej rekurencyjnemu wywoływaniu w nieskończoność, co szybko doprowadziłoby nas do rozstroju nerwowego a naszego kompa do... nie wiem czego - z D3D można się spodziewać wszystkiego ;-). pD3DDevice->SetRenderTarget( pBackBuffer, pZBuffer ); DXRELEASE( pBackBuffer ); DXRELEASE( pZBuffer ); pD3DDevice->SetTransform( D3DTS_VIEW, &matViewSave ); pD3DDevice->SetTransform( D3DTS_PROJECTION, &matProjSave ); Po sześciu przebiegach pętli w funkcji generującej mapę sześcienną mamy wszystkie sześć powierzchni z tej mapy zapełnione obrazami świata, na który patrzyliśmy w rożnych kierunkach z położenia naszego obiektu. Teraz możemy już dać sobie spokój z tym szaleństwem i powrócić do normalności. Każemy więc urządzeniu aby z powrotem ustawiło sobie właściwe bufory (tylny i z-buffor) oraz żeby ustawiło sobie macierze, które pozwolą nam popatrzeć na świat z jakiegoś normalnego punktu widzenia. Jeszcze zwalniamy przy okazji wszystkie bufory, które wykorzystaliśmy podczas naszych szaleństw i możemy już wracać do naszej właściwej funkcji renderującej - przypomnijmy tylko, że w międzyczasie wywołaliśmy ja sześć razy! A przy wywołaniu funkcji tworzącej mapę sześcienną byliśmy akurat tutaj: if( bRender ) // if not need don't render torus { RenderToCube(); 10 DirectX �% Env. Cube // render teapot } ... pD3DDevice->EndScene(); Ponieważ dla przypadku kiedy parametr bRender był TRUE generowaliśmy mapę środowiska tak tez i w tym samym momencie możemy narysować nasz obiekt, który tę mapę zawiera - oczywiście po jej stworzeniu! Ustawiamy więc naszemu obiektowi tę mapę i rysujemy go na ekranie. Ponieważ macierze mamy przywrócone z przed wywołania funkcji RenderToCube(), więc z czystym sumieniem możemy sobie teraz zakończyć rysowanie (albo i nie) no i zaprezentować nam oszałamiające efekty na ekranie ;). Ufff! Jazda była chyba niezła, nie sadzicie? Ja sam w pewnym momencie myślałem, że się w tym wszystkim pogubię, ale chyba się udało oddać przynajmniej istotę działania, które służy do renderowania obiektów z odbiciami rzeczywistymi. Na koniec musze dodać jeszcze kilka uwag, które mogą wam pomoc. " Po pierwsze - jak widać w przykładzie nasze urządzenie jest strasznie obciążone rysowaniem - cały świat nieomal renderuje siedem razy w jednej klatce, więc pamiętajcie - należy ograniczać ilość rysowanej geometrii w odbiciach (mapa nie musi być super dokładna, bo na krzywiznach i tak się wszystko psuje). " Po drugie - pamiętajcie, że jeśli obiekt odbijający, dla którego liczymy mapę nie stoi w środku świata tylko gdzie indziej trzeba to uwzględnić przy tworzeniu macierzy widoku. " Po trzecie - możecie sterować wielkością map środowiska przechowywanych w obiekcie tekstury sześciennej - tym tez można trochę nadrobić na wydajności. " Po czwarte - mam nadzieje, że zrozumieliście wszystko i stworzycie nowe, fascynujące efekty, jakich świat do tej pory nie widział. " Po piąte - mam nadzieje, że się nimi nie omieszkacie pochwalić na stronie! No i to byłoby wszystko w tej lekcji - na pewno była męcząca i skomplikowana, ale efekt naprawdę jest niezły. Pamiętajcie o jednym - nie każde, zwłaszcza starsze urządzenia posiadają wspomaganie sprzętowe dla tekstur sześciennych - na takich nie uda się odpalić przykładu. Sama technikę znacie i wiecie wszystko, więc możecie się pokusić o programowe stworzenie takiego efektu - jeśli komuś się chce i uda, niech się pochwali