Direct3D Env Cube


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


Wyszukiwarka