DirectX (27.)

Dnes budeme upravovat dále náš engine, přidáme například třídu kamery. Ukážeme si, co se vlastně na kameře dá programovat. Kameru pak budeme potřebovat v příštích lekcích, kdy se budeme zabývat optimalizačními metodami nad terénem.

27.1. Kamera a třída XCamera

Do dnešní lekce jsme měli pouze statickou kameru tj. dívali jsme se stále na stejné místo v prostoru, jen jsme pohybovali objekty. Dnes přidáme do projektu Display novou třídu, která se bude starat o kameru scény (zatím budeme podporovat pouze jednu kameru). S touto kamerou půjdou provádět všechny možné kousky: pohyb (translace) do všech směrů, rotace nebo třeba naklápění. To nám bude prozatím stačit. V našich příkladech jsme zatím pohybovat kamerou nepotřebovali, tak proč novou třídu? Cíl příštích lekcí bude vysvětlit princip optimalizace metodou quadtree, kterou si ukážeme na terénu. Tento terén bude podobný známému terénu například ze hry Transport Tycoon (i když tam je ve skutečnosti 2D:). O tomto terénu si povíme více v další lekci. Nyní zpět ke kameře.

Již víme, že kamera je v Direct3D určena několika vektory. Za prvé je třeba určit jaká osa bude v 3D světě vertikální. Často se volí osa Y, ale já raději volím osu Z (přijde mi to logičtější). Dále se definují dva body, pomocí nichž je určen směrový vektor. Jeho směrem "pozorujeme" scénu. Tohle jsme už jsme použili v našem příkladu, ale nastavovali jsme vše "natvrdo" a toto nastavení nešlo změnit za chodu aplikace. Jakým způsobem zinicializujeme a nastavíme tyto parametry si ukážeme za chvilku.

Perspektiva

Dále je třeba pečlivě nastavit perspektivu. I zde máme několik parametrů, navíc perspektivu můžeme nastavit několika způsoby, z nichž každý použije jiné hodnoty. Definujeme tyto hodnoty:

Těmito parametry vlastně přesně definujeme jakýsi čtyřstěn. Nejlépe je vše vidět na následujícím obrázku:

Vypadá to docela složitě, ale je to velice jednoduché. Vše co je za fialovým obdélníkem se už nevykreslí, takže například když chceme vykreslit rozlehlou scénu, omezíme takto "nekonečně" velké obzory a zvýšíme FPS. Stejně tak se nic nevykreslí před modrým obdélníkem. Nyní budeme chtít rotovat kolem bodu, na který se díváme. Budeme pohybovat pouze s tímto bodem a poté dopočítáme EyePoint. Zde budeme potřebovat další obrázek, kde je vidět výpočet souřadnic obou bodů:

Opět obrázky vypadají poněkud složitě, tentokrát řešení nemusí být ihned vidět. Vždy nejdříve umístíme LookAtPoint a poté z něho dopočítáme EyePoint (to platí pro všechny souřadnice). Vidíte, že jsme použili tři nové parametry scény: úhel Z, což je vlastně úhel sklopení kamery, dále výška kamery nad terénem a úhel pohledu, což je úhel určující směr pohledu. Pomocí těchto parametrů můžeme určit polohu obou bodů.

Metoda billboardů

Užitečným atributem kamery by mohla být tzv. Billboard matice. Jistě jste si v některé hře všimli, že některé "3D" objekty vlastně nejsou 3D, ale jsou to pouze 2D placky, na kterých je nanesená textura. Těchto jednoduchých objektů může být ve scéně řádově mnohem víc, než kdyby to byly skutečně 3D objekty (meshe). Typickým příkladem jsou stromy, když jsou udělány šikovně, laik ani nepozná, že je to billboard. Aby byl efekt co nejlepší, je třeba tyto placky natáčet kolmo vůči kameře při každém pohybu kamery. A právě billboard matice je speciální transformační matice, kterou transformujeme všechny tyto objekty, aby se otočily kolmo ke kameře. Tuto matici není velký problém vytvořit, neboť se jedná pouze o rotační matici tak ve směru osy Z, aby se těleso ocitlo kolmo ke kameře a my známe směrový vektor kamery, takže snadno spočítáme úhel, který naše placka svírá právě s tímto vektorem a připočteme (případně odečteme) 90 stupňů, čímž těleso natočíme proti kameře. Podrobně si postup ukážeme v příkladu za chvilku.

V DirectX SDK je příklad, který demonstruje billboarding - jmenuje se Billboard.

Třída XCamera

Pojďme se nyní věnovat třídě XCamera. Tento objekt bude interní v knihovně Display a bude jedinečný (povolíme tedy pouze jednu kameru v systému). Ovládat kameru bude samozřejmě možno i zvenku (jinak by to celé nemělo smysl). Vždy když budeme chtít pohnout s kamerou, nastavíme směr a rychlost pohybu. Třída je relativně jednoduchá. Budeme mít metodu pro inicializaci, další metodu, která spočítá aktuální pozice všech bodů a nastaví matici pohledu a projekční matici. Tuto metodu budeme volat každý cyklus aplikace.

Ještě jedna poznámka, tato kamera je určena pro pohyb nad komplexním terénem tj. kamera se může pohybovat ve směru os X a Y. Umožňuje přiblížení (Zoom) (tj. zkrácení směrového vektoru), pohyb kamery nahoru a dolů, naklápění tj. změna úhlu Z a samozřejmě rotace tj. změna úhlu pohledu. Dále bychom mohli přidat funkci volného letu (ovládanou například pomocí myšky).

Nejdříve nadefinujeme pár konstant, určující parametry kamery (tyto parametry můžete podle potřeby udělat jako proměnné).

#define _C_MAX_ROTATION_SPEED 5.0f
#define _C_MAX_ZOOMING_SPEED 5.0f
#define _C_MAX_SCROLLING_SPEED 20.0f
#define _C_MAX_ZANGLE_SPEED 2.0f

#define MAX_ZOOM 40
#define MIN_ZOOM 5

#define Z_ANGLE D3DX_PI / 2

#define MAX_Z_ANGLE D3DX_PI / 2
#define MIN_Z_ANGLE D3DX_PI / 7

const float NEAR_CLIP = 0.1f;
const float FAR_CLIP = 200.0f;
const float FOV = D3DX_PI / 4.0f;
const float ASPECT = 4.0f / 3.0f

Dále uveďme deklaraci třídy XCamera:

class DISPLAY_API XCamera
{

// D3D device object...
LPDIRECT3DDEVICE8 m_lpDevice;
//
// Camera bounds

int m_iMinPos;
int m_iMaxPos;
//
// Velocities in all directions

float m_fScrlVelX;
float m_fScrlVelY;
float m_fRotVel;
float m_fZoomVel;
float m_fZAngleVel;
//
// Camera position

D3DXVECTOR3 m_vLookAtPoint;
D3DXVECTOR3 m_vEyePt;
float m_fZoomDistance;
float m_fZAngle;
float m_fViewAngle;
float m_fCameraHeight;
//
// Camera matrices

D3DXMATRIX m_matBillboard;
D3DXMATRIX m_matProjection;
D3DXMATRIX m_matView;

public:

//
// Inicialization

HRESULT InitCamera(IDirect3DDevice8* lpDevice, int iMinBound, int iMaxBound);
// Update
HRESULT ProcessCamera(float fElapsedTime);
// Camera motion
void ZoomIn() {m_fZoomVel = -(_C_MAX_ZOOMING_SPEED);}
void ZoomOut() {m_fZoomVel = _C_MAX_ZOOMING_SPEED;}
void RotateLeft() {m_fRotVel = _C_MAX_ROTATION_SPEED;}
void RotateRight() {m_fRotVel = -(_C_MAX_ROTATION_SPEED);}
void MoveLeft() {m_fScrlVelX = _C_MAX_SCROLLING_SPEED;}
void MoveRight() {m_fScrlVelX = -(_C_MAX_SCROLLING_SPEED);}
void MoveUp() {m_fScrlVelY = -(_C_MAX_SCROLLING_SPEED);}
void MoveDown() {m_fScrlVelY = _C_MAX_SCROLLING_SPEED;}
void IncreaseZAngle() {m_fZAngleVel = _C_MAX_ZANGLE_SPEED;}
void DecreaseZAngle() {m_fZAngleVel = -(_C_MAX_ZANGLE_SPEED);}
void SetCameraHeight(float fHeight) {m_fCameraHeight = fHeight;}
//
// Get methods

float GetViewAngle() {return m_fViewAngle;}
float GetZoom() { return m_fZoomDistance;}
float GetZAngle() {return m_fZAngle;}
D3DXVECTOR3* GetCameraPos() {return &m_vLookAtPoint;}
D3DXVECTOR3* GetEyePoint() {return &m_vEyePt;}
D3DXMATRIX* GetBillboardMatrix() {return &m_matBillboard;}
void GetWorldViewProjMatrix(D3DXMATRIX * matWVP);

XCamera();
~XCamera();

}

Zde najdeme všechny výše vysvětlené atributy. Nejprve je zde odkaz na objekt zařízení Direct3D, dále omezení kamery v prostoru, rychlosti pohybu ve všech možných směrech, pozice kamery, směr natočení atd. a nakonec matice projekce, pohledu a billboard matice. Ještě se zmíním o inline funkcích pohybu. Tyto funkce pouze nastavují rychlost v požadovaném směru pohybu. Tato rychlost je zachycena v metodě ProcessCamera(), kde je zároveň vynulována, aby se anuloval efekt v dalším cyklu.

Při inicializaci objektu XCamera je nejprve nutné volat metodu InitCamera(), kde nastavíme zařízení a omezení kamery. Ve funkci UpdateFrame() je třeba volat metodu ProcessCamera(), pomocí níž aplikujeme pohyb kamery, spočítáme novou matici pohledu a nastavíme ji. Na závěr také spočítáme billboard matici. Nyní si podrobně rozebereme všechny metody třídy XCamera.

Začneme konstruktorem a destruktorem:

XCamera::XCamera()
{

m_lpDevice = NULL;
m_fZoomDistance = 10.0f;
m_vLookAtPoint = D3DXVECTOR3(10, 10, 0);
// origin

m_fViewAngle = D3DX_PI / 4; // 45 degree

m_fScrlVelX = 0.0f;
m_fScrlVelY = 0.0f;
m_fZAngleVel = 0.0f;

m_iMinPos = 0;
m_iMaxPos = 10;
m_fZAngle = Z_ANGLE;

m_fCameraHeight = 10.0f;

}

XCamera::~XCamera()
{

m_lpDevice = NULL;

}

Ukazatel na objekt zařízení je třeba vynulovat, abychom poznali, že je objekt ziniciliazován či nikoliv. Dále nastavíme implicitní hodnoty všech proměnných na nějaké rozumné hodnoty. Kamera se bude "dívat" z počátku systému kamsi do prostoru pod úhlem 45 stupňů.

Dále se podívejme na zajímavější metodu InitCamera():

HRESULT XCamera::InitCamera(IDirect3DDevice8* lpDevice, int iMinBound, int MaxBound)
{

HRESULT dwRet = S_FALSE;
if(lpDevice)
{

m_lpDevice = lpDevice;
m_iMinPos = iMinBound;
m_iMaxPos = iMaxBound;

//
// Set transformations

D3DXMATRIX matWorld;
//
// Set world matrix

D3DXMatrixIdentity(&matWorld);
m_lpDevice->SetTransform(D3DTS_WORLD, &matWorld);
//
// Create view matrix

D3DXVECTOR3 vUpDir;
m_vEyePt.x = m_fZoomDistance * cosf(m_fViewAngle) + m_vLookAtPoint.x;
m_vEyePt.y = m_fZoomDistance * sinf(m_fViewAngle) + m_vLookAtPoint.y;
m_vEyePt.z = m_fZoomDistance * sinf(Z_ANGLE);
//
vUpDir = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
//
D3DXMatrixLookAtLH(&m_matView, &m_vEyePt, &m_vLookAtPoint, &vUpDir);
D3DXMatrixPerspectiveFovLH(&m_matProjection, FOV, ASPECT, NEAR_CLIP, FAR_CLIP);

m_lpDevice->SetTransform(D3DTS_VIEW, &m_matView);
m_lpDevice->SetTransform(D3DTS_PROJECTION, &m_matProjection);
dwRet = S_OK;

}
return dwRet;

}

Zde si zapamatujeme vstupní parametry. Druhým úkolem je spočítat pozici kamery, když známe bod, na který se kamera "dívá". Tento bod jsme nastavili v konstruktoru třídy. Zde použijeme trochu chytré matematiky a obrázků z úvodu lekce. Nastavení světové matice je zde jen symbolické, neboť se nastavuje pro každý objekt jiná matice. Ovšem další řádky nemusí být zcela jasné. Vycházíme z bodu, na který se díváme. Zaměříme se zatím na X a Y souřadnice. Důležité hodnoty jsou přímá vzdálenost od LookAtPointu, což je hodnota m_fZoomDistance. Dále potřebujeme úhel, pod kterým pozorujeme scénu, to je m_fViewAngle. Pomocí goniometrických funkcí spočítáme protilehlou resp. přilehlou stranu trojúhelníka z obrázku pro získání X resp. Y souřadnice kamery. Nakonec bod posuneme na správné místo v prostoru. Nyní se soustřeďme na Z-tovou souřadnici. Zde je situace o trochu jednodušší, jen využijeme úhel Z definovaný jako konstanta (mimochodem, tato konstanta se nastaví jen při inicializaci tj. sklon můžeme také nastavovat).

Nakonec tu máme metodu, která spočítá všechny parametry kamery v průběhu programu:

HRESULT XCamera::ProcessCamera(float fElapsedTime)
{

DWORD dwRet = S_FALSE; // CAMERA NOT INITIALIZED
if(m_lpDevice) {


D3DXVECTOR3 vUpDir;
vUpDir = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
//
// Apply rotation

m_fViewAngle += m_fRotVel * fElapsedTime;
//
// Apply zooming

m_fZoomDistance += m_fZoomVel * fElapsedTime;
//
// Check zoom distance

if(m_fZoomDistance > MAX_ZOOM)
m_fZoomDistance = MAX_ZOOM;
if(m_fZoomDistance < MIN_ZOOM)
m_fZoomDistance = MIN_ZOOM;
//
// Apply Z angle

m_fZAngle += m_fZAngleVel * fElapsedTime;
if(m_fZAngle > MAX_Z_ANGLE)
m_fZAngle = MAX_Z_ANGLE;
if(m_fZAngle < MIN_Z_ANGLE)
m_fZAngle = MIN_Z_ANGLE;
//
// Apply position

m_vLookAtPoint.x += (m_fScrlVelX * cosf(m_fViewAngle + D3DX_PI / 2)) * fElapsedTime;
m_vLookAtPoint.y += (m_fScrlVelX * sinf(m_fViewAngle + D3DX_PI / 2)) * fElapsedTime;
m_vLookAtPoint.x += (m_fScrlVelY * cosf(m_fViewAngle)) * fElapsedTime;
m_vLookAtPoint.y += (m_fScrlVelY * sinf(m_fViewAngle)) * fElapsedTime;
m_vLookAtPoint.z = m_fCameraHeight;
//
// Bound camera position

if(m_vLookAtPoint.x < m_iMinPos)
m_vLookAtPoint.x = (float)m_iMinPos;
if(m_vLookAtPoint.y < m_iMinPos)
m_vLookAtPoint.y = (float)m_iMinPos;
if(m_vLookAtPoint.x > m_iMaxPos)
m_vLookAtPoint.x = (float)m_iMaxPos;
if(m_vLookAtPoint.y > m_iMaxPos)
m_vLookAtPoint.y = (float)m_iMaxPos;
//
// Computes eye point position

m_vEyePt.x = m_fZoomDistance * cosf(m_fViewAngle) + m_vLookAtPoint.x;
m_vEyePt.y = m_fZoomDistance * sinf(m_fViewAngle) + m_vLookAtPoint.y;
m_vEyePt.z = m_fZoomDistance * sinf(m_fZAngle) + m_fCameraHeight;
//
// Create new view matrix

D3DXMatrixLookAtLH(&m_matView, &m_vEyePt, &m_vLookAtPoint, &vUpDir);
// Apply transfromation
m_lpDevice->SetTransform(D3DTS_VIEW, &m_matView);
//
// Reset velocities

m_fScrlVelX = 0;
m_fScrlVelY = 0;
m_fRotVel = 0;
m_fZoomVel = 0;
m_fZAngleVel = 0;

dwRet = ERROR_SUCCESS;
//
// Computes billboard matrix

D3DXVECTOR3 vDir = m_vLookAtPoint - m_vEyePt;
if( vDir.y > 0.0f ) {

D3DXMatrixRotationZ(&m_matBillboard, -atanf(vDir.x/vDir.y)+D3DX_PI/2 );

}
else {

D3DXMatrixRotationZ(&m_matBillboard, -atanf(vDir.x/vDir.y)-D3DX_PI/2 );

}

}
return dwRet;

}

Zde je situace hodně podobná. Zde ovšem navíc musíme přepočítat i LookAtPoint. Zde tedy modifikujeme všechny parametry v závislosti na rychlosti v daném směru.Dále jsou tu omezení pohybu (například sklápění kamery je možné jen v určitém intervalu atd.). Zde použijeme podobný princip jako v předchozí metodě. Ke každé složce vektoru LookAtPoint připočteme přírůstky dané rychlostmi ve směru X a Y. V dalším kroku omezíme kameru na jistém prostoru. Dále spočítáme EyePoint úplně stejně jako v metodě InitCamera(). Vytvoříme a nastavíme novou matici pohledu, vynulujeme rychlosti ve všech směrech a nakonec spočítáme billboard matici. U této činnosti se na chvilku zastavíme. Proměnná vDir je vektor směřující ve směru pohledu kamery. Z funkce atan() (arc tg) vypadne úhel, o který je třeba pootočit případný billboard tak, aby byl ve směru vektoru vDir. My ale chceme, aby objekt byl kolmo na kameru, tudíž je třeba ho otočit o dalších 90 stupňů. Určitě jste si všimli, že při změně pozice kamery násobíme vše ještě jakousi konstantou (i o tom jistě byla řeč). Není to samozřejmě nic jiného než uplynulý čas od předchozího snímku a děláme to z toho důvodu, aby kamera byla na všech systémech stejně rychlá-pomalá tj. aby pohyb-rychlost kamery nebyl závislý na FPS.

27.2. Příklad

Na závěr lekce trochu upravíme náš příklad (projekt Tester), abychom vyzkoušeli novinky v našem enginu.

Za prvé je třeba přidat objekt kamery do třídy XDisplay, dále přidáme inline metodu GetCamera(), která nám bude vracet ukazatel na uvedený objekt. Od teď můžeme s kamerou pracovat.

Nejprve tedy zavoláme metodu InitCamera() a předáme nějaké rozumné parametry:

// inicializace Direct3D
g_theDisplay.Init(g_hWnd);
g_theInput.Init(hInstance, g_hWnd, g_theDisplay.GetResolution(), g_theDisplay.GetDevice());

g_theDisplay.GetCamera()->InitCamera(g_theDisplay.GetDevice(), -20, 20);

Náš vesmír je malý, jen 40x40 (čeho? třeba metrů:) Dále upravíme funkci UpdateFrame():

DWORD dwOldAdrMode;
float fFactor = cmnGetTime(TIMER_GETELAPSEDTIME1);

g_theInput.ProcessInput();
g_theDisplay.GetCamera()->ProcessCamera(fFactor);

Výpočet faktoru (času) přesuneme na začátek funkce, abychom ho mohli využít v metodě ProcessCamera().

Nakonec přidáme ovládaní kamery:

if(g_theInput.IsKeyDown(DIK_ADD , FALSE)) {

g_theDisplay.GetCamera()->ZoomIn();

}
if(g_theInput.IsKeyDown(DIK_SUBTRACT , FALSE)) {

g_theDisplay.GetCamera()->ZoomOut();

}
if(g_theInput.IsKeyDown(DIK_NUMPAD1 , FALSE)) {

g_theDisplay.GetCamera()->RotateLeft();

}
if(g_theInput.IsKeyDown(DIK_NUMPAD3 , FALSE)) {

g_theDisplay.GetCamera()->RotateRight();

}
if(g_theInput.IsKeyDown(DIK_NUMPAD8 , FALSE)) {

g_theDisplay.GetCamera()->IncreaseZAngle();

}
if(g_theInput.IsKeyDown(DIK_NUMPAD2 , FALSE)) {

g_theDisplay.GetCamera()->DecreaseZAngle();

}

int iMouseZone = int(g_theDisplay.GetResolution()->x * 5 / 1024);
if(g_theInput.IsKeyDown(DIK_UP, FALSE)) {

g_theDisplay.GetCamera()->MoveUp();

} else {

if(g_theInput.GetCursor()->y < iMouseZone) {

g_theDisplay.GetCamera()->MoveUp();

}

}
if(g_theInput.IsKeyDown(DIK_LEFT , FALSE)) {

g_theDisplay.GetCamera()->MoveLeft();

} else {

if(g_theInput.GetCursor()->x < iMouseZone) {

g_theDisplay.GetCamera()->MoveLeft();

}

}
iMouseZone += int( g_theDisplay.GetResolution()->x * 32 / 1024);
if(g_theInput.IsKeyDown(DIK_DOWN , FALSE)) {

g_theDisplay.GetCamera()->MoveDown();

} else {

if(g_theInput.GetCursor()->y >= g_theDisplay.GetResolution()->y-iMouseZone) {

g_theDisplay.GetCamera()->MoveDown();

}

}
if(g_theInput.IsKeyDown(DIK_RIGHT , FALSE)) {

g_theDisplay.GetCamera()->MoveRight();

} else {

if(g_theInput.GetCursor()->x >= g_theDisplay.GetResolution()->x-iMouseZone) {

g_theDisplay.GetCamera()->MoveRight();

}

}

Zde využijeme metody z minulé lekce, kde jsme vkládali projekt Input. Vždy otestujeme požadovanou klávesu (bez obsluhy tzn. projeví se reakce na vícenásobné držení klávesy). Poté voláme příslušnou metodu, která pohne s kamerou. U pohybu ve směru X a Y je zapojena i myška. Sledujeme, když je myš na okraji obrazovky a poté spustíme pohyb v požadovaném směru.

Po kompilaci je možno ovládat kameru šipkami ve směrech osy X a Y. Zoom (přiblížení, oddálení) provedeme klávesou + a -. Sklopení kamery je na klávesách Num 8 a Num 2. Rotaci provedeme pomocí Num 1 doleva a Num 3 doprava.

27.3. Závěr

Jak bylo zmíněno v úvodu, v příští lekci se budeme věnovat optimalizacím při vykreslování terénu, který se bude skládat ze sítě polygonů, ale my chceme vykreslit pouze ty polygony, které jsou viditelné. Na to je několik metod, my začneme s metodou quadtree.

Jiří Formánek