background image

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.

 

 

background image

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: 

background image

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 

background image

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

background image

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

background image

6 

DirectX ▪ Env. Cube 

  DXRELEASE( pZBuffer ); 
 
  pD3DDevice->SetTransform( D3DTS_VIEW, &matViewSave ); 
  pD3DDevice->SetTransform( D3DTS_PROJECTION, &matProjSave ); 

 

Na pierwszy rzut oka wygląda to dosyć groźnie, no ale nie z takimi już żeśmy sobie radzili, dlatego bądźmy 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 ); 

background image

7 

DirectX ▪ Env. Cube 

pD3DDevice->GetDepthStencilSurface( &pZBuffer ); 
 

Jeszcze zanim przystąpi do kolejnych działań pobieramy sobie wskaźniki do powierzchni, które stanowią bufory docelowe 
naszych operacji rysunkowych. Metoda 

GetRenderTarget()

 naszego urządzenia powoduje pobranie wskaźnika 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 ); 

background image

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ć 

background image

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 

wskaźniki, obydwa bardzo, bardzo ważne! Pierwszy z nich jak mówi dokumentacja stanowi wskaźnik do bufora koloru - po 
naszemu mówiąc wskaźnik 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(); 

background image

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