V minulé lekci jsme probrali úplné základy komponenty Direct3D. Dnes si vytvoříme první ukázkovou aplikace na bázi Direct3D. Jistě tušíte, že to nebude hra roku 2003, ale někde začít musíme.
Tak jako každá komponenta DirectX, i Direct3D má svůj objekt Direct3D8. Jak už to u DirectX bývá, pracujeme pouze s rozhraním objektů, takže budeme hledat rozhraní IDirect3D8 (pracujeme s SDK verze 8.0 nebo 8.1).
Abychom mohli použít všechny funkce a struktury Direct3D, musíme vložit do projektu několik hlavičkových souborů a přilinkovat pár knihoven. Později si vytvoříme samostatnou knihovnu, kde budeme používat Direct3D a v hlavním hlavičkovém souboru musíme mít tyto řádky:
//
// Define version of Direct3D
#define
DIRECT3D_VERSION 0x0800
//
// Include Direct3D headers
#include <d3d8.h>
#include
<D3dx8core.h>
První definicí upozorníme překladač jakou verzi Direct3D chceme použít. Dále použijeme dva hlavičkové soubory pro jádro D3D a pro rozšířenou knihovnu D3DX. Abychom toto mohli udělat, je důležité mít správně nainstalované DX SDK 8.0 a správně uvedené cesty v nastavení vývojového prostředí.
Dále je nutné přilinkovat dvě knihovny: d3d8.lib a D3dx8.lib. To provedeme v dialogu Settings... konkrétního projektu na kartě Linker (Input). Tento postup jsme již několikrát prováděli, takže není nutné vše podrobně rozepisovat. Opět je nutné mít správně nastavené cesty, tentokrát pro knihovny.
Vytvořte si tedy prázdnou Win32 aplikaci (projekt nazvěte D3DExample1). Projekt má obsahovat jen funkci WinMain(). V dnešní lekci nebudeme vytvářet složitější strukturu projektu a budeme psát Direct3D kód přímo do projektu D3DExample1. Později tento kód oddělíme do samostatné knihovny podobně jako tomu bylo v případě DirectDraw a vlastně všech do teď probraných komponent DirectX.
Na začátek souboru D3DExample1.cpp vložte výše uvedený kód, abychom mohli použít funkce Direct3D. Nezapomeňte samozřejmě vložit také knihovnu přes dialog Settings projektu. Nyní už máte projekt připravený pro Direct3D a můžete volat libovolné metody. Píšeme ovšem grafickou aplikaci, která se neobejde bez okna, takže ze všeho nejdřív je nutné zaregistrovat třídu okna a poté okno vytvořit. I tento postup jsme již několikrát probírali, ale pro úplnost kód ještě jednou zopakuji.
Vytvoříme proto nové funkce (nezapomeňte vložit prototypy) WinInit() a MainWndProc(). WinInit() zaregistruje třídu okna a okno podle této třídy vytvoří. MainWndProc() je obslužná funkce okna, zde tedy budeme obsluhovat zprávy přicházející našemu oknu:
HRESULT WinInit(HINSTANCE
hInstance)
{
DWORD dwRet;
WNDCLASSEX wc;
//
// Register the Window Class
wc.cbSize =
sizeof(WNDCLASSEX);
wc.lpszClassName = "D3DExample1Class";
wc.lpfnWndProc = MainWndProc;
wc.style = CS_VREDRAW|CS_HREDRAW;
wc.hInstance = hInstance;
wc.hIcon = NULL;
wc.hCursor = NULL;
wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.hIconSm = NULL;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
//
// Register window class
if(RegisterClassEx(&wc)
== 0 ) {
dwRet = GetLastError();
return dwRet;
}
//
// Create new main window
g_hWnd = CreateWindow("D3DExample1Class",
"D3DExample1Window",
0, 0, 0, 1024, 768, NULL, NULL, hInstance, NULL);
if(!g_hWnd) {
dwRet = GetLastError();
return dwRet;
}
ShowWindow(g_hWnd, SW_SHOW);
UpdateWindow(g_hWnd);
dwRet = ERROR_SUCCESS;
return dwRet;
}
LRESULT CALLBACK MainWndProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam
)
{
switch(msg) {
case WM_SETCURSOR:
SetCursor(NULL);
break;
case WM_DESTROY:
// Cleanup and close the app
PostQuitMessage( 0 );
return 0L;
case WM_SYSCOMMAND:
// Prevent moving/sizing and power loss in
fullscreen mode
switch( wParam )
{
case SC_MOVE:
case SC_SIZE:
case SC_MAXIMIZE:
case SC_KEYMENU:
case SC_MONITORPOWER:
return 1;
}
break;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
Funkce nebudu blíže rozebírat, protože se jedná o klasické Windows API. Dále ještě trochu upravíme funkci WinMain() a aplikaci zkusíme zkompilovat:
int APIENTRY WinMain(HINSTANCE
hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// Window
initialization
WinInit(hInstance);
MSG msg;
// Run message
loop
while( TRUE )
{
// Look for messages, if none are found then
// update the state and display it
if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
{
if( 0 ==
GetMessage(&msg, NULL, 0, 0 ) )
{
// WM_QUIT was posted, so exit
return (int)msg.wParam;
}
// Translate and dispatch the message
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
{
UpdateFrame();
}
}
return 0;
}
Zde je nutné zavolat funkci, která vytvoří okno a spustit smyčku zpráv.
Nyní začneme používat Direct3D a začneme s objektem Direct3D. Tento objekt vytvoří funkce Direct3DCreate8() a vrátí ukazatel na rozhraní IDirect3D8. Pomocí tohoto rozhraní vytvoříme tzv. D3D zařízení (device).
Udělejme tedy dva globální objekty:
IDirect3D8
*g_lpD3DObject;
IDirect3DDevice8 *g_lpD3DDevice;
Poté ve funkci WinMain() zinicializujeme objekt Direct3D a uložíme rozhraní do proměnné g_lpD3DObject:
// D3D inicialization
g_lpD3DObject = Direct3DCreate8(D3D_SDK_VERSION);
if(!g_lpD3DObject) {
MessageBox(NULL, "Please, install DirectX 8.1.", "Error", MB_ICONERROR);
Clean();
return 1;
}
Tímto vytvoříme objekt Direct3D a v případě neúspěchu zobrazíme hlášku. Konstanta D3D_SDK_VERSION zajistí, že naše aplikace bude zkompilovaná se správnou verzi DirectX.
Nyní nadefinujeme pár konstant, kterými budeme ovládat chování aplikace:
#define WINDOWED
TRUE
#define RES_X 800
#define RES_Y 600
#define DEPTH 16
Jedná se rozlišení a barevnou hloubku (s tou je to trochu složitější, ale v našem jednoduchém případě nám to postačí) a o to, zda-li aplikace běží v okně či v režimu fullscreen. Dále musíme naplnit strukturu D3DPRESENT_PARAMETERS a vytvořit device. Tato struktura má mnoho atributů, ale v prvním příkladě nás toho moc zajímat nebude:
D3DPRESENT_PARAMETERS d3dDeviceParam;
//
// Set properties of device
ZeroMemory(&d3dDeviceParam, sizeof(d3dDeviceParam));
d3dDeviceParam.Windowed = WINDOWED;
d3dDeviceParam.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dDeviceParam.BackBufferWidth = RES_X;
d3dDeviceParam.BackBufferHeight = RES_Y;
d3dDeviceParam.BackBufferFormat = (DEPTH == 16) ? D3DFMT_R5G6B5 :
D3DFMT_X8R8G8B8;
d3dDeviceParam.BackBufferCount = 1;
d3dDeviceParam.hDeviceWindow = g_hWnd;
d3dDeviceParam.MultiSampleType = D3DMULTISAMPLE_NONE;
if(WINDOWED) {
D3DDISPLAYMODE displaymode;
RECT rcWindowClient;
//
// Get window position
GetClientRect(g_hWnd,
&rcWindowClient);
// Get current display mode
g_lpD3DObject->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &displaymode);
//
// Set size of back buffer
d3dDeviceParam.BackBufferWidth
= rcWindowClient.right - rcWindowClient.left;
d3dDeviceParam.BackBufferHeight = rcWindowClient.bottom -
rcWindowClient.top;
// Set display
mode to current
d3dDeviceParam.BackBufferFormat
= displaymode.Format;
}
Nejprve celou strukturu vynulujeme a poté zinicializujeme některé atributy. Nastavujeme, zda-li aplikace běží v okně či nikoliv, typ prohazování předního a zadního bufferu, velikost a formát zadního bufferu, počet zadních bufferů, handle na okno aplikace, typ antialiasingu. Formát zde nenastavujeme úplně korektně, ale správná inicializace je na delší článek a probereme si ji v některých dalších článcích. V druhé části nastavíme některé atributy zvlášť pro případ, když aplikace běží v okně. Zde je potřeba nastavit velikost zadního bufferu stejnou jako je velikost okna a formát bufferu musí být stejný jako aktuální formát nastavený ve Windows. Tato struktura je velmi důležitá a tak si ji podrobně probereme v příštích lekcích.
V dalším kroku již můžeme vytvořit device pomocí metody CreateDevice():
//
// Create Device
dwRet = g_lpD3DObject->CreateDevice(0,
D3DDEVTYPE_HAL , g_hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dDeviceParam, &g_lpD3DDevice);
if(dwRet != D3D_OK) {
MessageBox(NULL, "Cannot create device.", "Error", MB_ICONERROR);
Clean();
return 2;
}
První parametr označuje pořadové číslo adaptéru ve vašem systému. Opět v tomto jednoduchém příkladě nepředpokládáme více než jeden grafický adaptér. Dále vyžadujeme zařízení typu HAL (tj. s hardwarovou akcelerací). Čtvrtý důležitý parametr označuje typ zpracování vertexů, chceme softwarové zpracování. Předposlední parametr je ukazatel na strukturu D3DPRESENT_PARAMETERS, kterou jsme vytvořili výše. Výsledek celé operace se uloží do posledního parametru v podobě ukazatele na rozhraní IDirect3DDevice8.
V další části příkladu vytvoříme v okně trojúhelník tvořený třemi transformovanými vertexy. Struktura vertexu musí obsahovat polohu v 3D prostoru a protože je to transformovaný vertex, musí také obsahovat atribut rhw. Dále jsem přidal do struktury barvu vertexu:
struct TLVERTEX {
float x, y, z, rhw;
DWORD dwColor;
};
Systém Direct3D ovšem potřebuje vědět, co náš vrchol obsahuje za atributy. Struktura vertexu se dá popsat několika konstantami:
#define TLVERTEXFORMAT (D3DFVF_XYZRHW|D3DFVF_DIFFUSE)
Takto definujeme formát vertexu, který má pozici+rhw a difúzní barvu. Komponenta RHW v praxi představuje převrácenou hodnotu vzdálenosti vertexu od pozorovatele podél osy z. Takže například pokud jeden z vertexů bude mít rhw=10.0 (ostatní 1.0), převrácená hodnota je 0.1 a tudíž je tento vertex nejblíže k pozorovatelovi (zkuste si to v praxi a uvidíte, že tento vertex má pak největší váhu).
Nadefinujeme globální pole tří vertexů takto:
TLVERTEX arVertices[] = {
{200.0f, 200.0f, 0.5f, 1.0f,
0xFFFF0000},
{600.0f, 200.0f, 0.5f, 1.0f,
0xFF0000FF},
{200.0f, 600.0f, 0.5f, 1.0f,
0xFFFFFF00}};
Poslední číslo je barva vrcholu: červená, modrá a žlutá. Můžete také požít makro D3DCOLOR_XRGB(r, g, b) (a=FF) nebo D3DCOLOR_ARGB(a, r, g, b). Z těchto vrcholů vytvoříme náš první trojúhelník. Je nutné si uvědomit, že souřadnice jednotlivých vertexů jsou v měřítku okna (2D souřadnice na monitoru).
Direct3D vykresluje vrcholy z tzv. Vertex bufferu. Vrcholy jdou vykreslovat i přímo z našeho pole, ale správně se to dělá přes vertexové buffery. Objekt vertex bufferu vytvoříme pomocí metody objektu zařízení CreateVertexBuffer() a uložíme si ukazatel na rozhraní IDirect3DVertexBuffer8.
//
// Create vertex buffer for our triangle
g_lpD3DDevice->CreateVertexBuffer(sizeof(TLVERTEX)
* 3, D3DUSAGE_WRITEONLY, TLVERTEXFORMAT, D3DPOOL_DEFAULT, &g_lpD3DVB);
Jak vidíte metoda má množství parametrů, které si vzápětí vysvětlíme. První
parametr je velikost bufferu v bytech tj. my potřebujeme místo pro 3 vrcholy
takže 3x vynásobíme velikost struktury TLVERTEX.
Druhý parametr nastavuje vlastnosti bufferu, můžete zadat tyto hodnoty:
- D3DUSAGE_WRITEONLY
- buffer určen jen pro zápis tzn., že vy z bufferu nemůžete číst data.
Direct3D vybere nejvhodnější místo v paměti pro takovýto buffer, aby přístup k
němu byl co nejrychlejší.
- D3DUSAGE_SOFTWAREPROCESSING - tento buffer bude
používán výhradně se softwarovým zpracováním.
- D3DUSAGE_POINTS - buffer je určen pro point
sprites. To jsou objekty složené z bodů nebo částic.
- D3DUSAGE_DYNAMIC - buffer je dynamický tj. během
života se mění jeho data. Ovladač tento buffer umísti do AGP paměti místo video
pamětí (opět kvůli optimalizaci).
- a další, které zatím nebudeme využívat.
Další parametr je formát vkládaných vertexů. Nyní využijeme konstantu, kterou je
popsaný náš vrchol. Dále tu máme typ paměti:
- D3DPOOL_DEFAULT - buffer je umístěn do takové
paměti, která je nejlepší pro daný účel vertex bufferu. Nejčastěji je to video
nebo AGP paměť. Pokud se device "ztratí" je nutné tyto buffery zcela
zrušit a znovu vytvořit, případně naplnit. O ztrátách zařízení si povíme v
dalších lekcích.
- D3DPOOL_MANAGED - buffer je vytvořen v systémové
paměti, ale pokud je potřeba, je zkopírován do paměti vhodné pro device.
Díky tomu, že data jsou v RAM, není potřeba tyto buffery rušit při ztrátě
device.
Do posledního parametru je uložena adresa rozhraní
IDirect3DVertexBuffer8.
Dále je nutné vytvořený buffer naplnit daty:
TLVERTEX *pVertices;
// Fill vertex buffer
if(SUCCEEDED(g_lpD3DVB->Lock(0, 0,
(BYTE**) &pVertices, 0))) {
pVertices[0] = arVertices[0];
pVertices[1] = arVertices[1];
pVertices[2] = arVertices[2];
g_lpD3DVB->Unlock();
}
Metodou Lock() uzamkneme buffer
a můžeme tak zapisovat přímo do bufferu. Metoda Lock()
má 4 parametry:
1. Offset v bytech odkud se mají data zamknout. Pokud zadáte 0, je buffer
uzamčen od začátku.
2. Tento parametr určuje, jak velký kus vertex bufferu bude uzamčen. Pokud
zadáte 0, je uzamčen (zpřístupněn) celý buffer.
Pomocí 3. parametru budeme přistupovat k obsahu vertex bufferu.
4. Vlastnosti zamykání, které si probereme v některé další lekci.
V dalším kroku překopírujeme obsah pole s vertexy do vertex bufferu. Máme jen tři vrcholy, takže není nutné psát cyklus. Nakonec buffer opět odemkneme. Stejný buffer můžete uzamknout i několikrát, ale pro každé volání metody Lock() je nutné volat i metodu Unlock().
Nakonec vytvoříme funkci UpdateFrame(), ve které náš trojúhelník vykreslíme:
void UpdateFrame()
{
if(g_lpD3DDevice) {
//
// Clear the back buffer to a blue
color
g_lpD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(100, 100, 100), 0,
0);
//
// Begin rendering
if(SUCCEEDED(g_lpD3DDevice->BeginScene())) {
//
// Set
current vertex buffer
g_lpD3DDevice->SetStreamSource(0, g_lpD3DVB, sizeof(TLVERTEX));
// and type of vertex data
g_lpD3DDevice->SetVertexShader(TLVERTEXFORMAT);
//
// Draw
triangle
g_lpD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
g_lpD3DDevice->EndScene();
}
// Flip front/back buffers
g_lpD3DDevice->Present(NULL, NULL, NULL, NULL);
}
}
Zkontrolujeme platnost ukazatele na zařízení a vyčistíme
zadní buffer (a nastavíme mu takovou podivnou šedivou barvu). Metodou
Clear() můžeme vyčistit i Z-buffer, ale ten zatím
nepoužíváme. Dále je nutné volat metody BeginScene(),
abychom mohli začít vykreslovat vrcholy. Metodou
SetStreamSource() nastavíme vstupní vertex buffer, ze kterého se budou
číst vertexová data (pozor, tato metoda je časově velmi náročná a proto je dobré
ji volat co nejméně). Dalším příkazem nastavíme vertex shader, v našem
případě ne programovatelný. Toto je potřeba udělat vždy (i když nepoužíváte
programovatelný vertex shader), aby se vědělo, co se s vrcholy bude dělat (zda
jsou transformované, osvětlené atd.) No a nakonec zavoláme metodu
DrawPrimitive(), která vykreslí požadovaný
trojúhelník. První parametr udává typ vykreslovaných dat:
- D3DPT_POINTLIST - ve vertex bufferu je uloženo
pole bodů a jako polygon se bere bod.
- D3DPT_LINELIST - ve vertex bufferu je
uloženo pole čar a jako polygon se bere spojnice dvou bodů. Spojují se dva
sousední vertexy (1. vrchol s 2., 3. se 4. atd.).
- D3DPT_LINESTRIP - vrcholy se spojují
všechny popořadě tj. 1. vrchol s 2., 2. vrchol s 3., 3. vrchol se 4. atd.
- D3DPT_TRIANGLELIST - z vertex bufferu se vezmou 3
vrcholy a vyrenderuje se trojúhelník, poté se vezmou další tři vrcholy a
renderuje se další trojúhelník nezávisle na tom prvním.
- D3DPT_TRIANGLESTRIP - nejprve se vyrenderuje
první trojúhelník, poté se vezme 4. vrchol a vyrenderuje se další trojúhelník,
který je tvořen 2.,3. a 4. vrcholem. Vznikne pás trojúhelníků.
- D3DPT_TRIANGLEFAN - je to podobné jako u pasů,
akorát všechny trojúhelníky sdílejí jeden vrchol a vznikne takový vějíř.
Podrobněji si tyto typy renderovaných polygonů probereme v příští lekci.
Druhý parametr je pořadí vertexu, který se bude vykreslovat jako první. A nakonec třetí udává počet polygonů, které se mají vykreslit. Zde je třeba dát pozor na to, jaký typ polygonu vykreslujeme. Máme-li ve vertexu bufferu 3 vrcholy, tak mužeme vytvořit 1 trojúhelník pomocí D3DPT_TRIANGLELIST nebo 3 body pomocí D3DPT_POINTLIST! V prvém případě bude počet polygonů rovno jedné a ve druhé třem!!! Nezapomeňte ukončit scénu metodou EndScene().
Na úplný závěr je třeba prohodit zadní a přední buffer metodou Present().
Pro úplnost jsem ještě přidal metodu Clean(), která uvolní všechny objekty Direct3D při ukončení aplikace či v případě nějaké chyby:
void Clean() {
if(g_lpD3DDevice) {
g_lpD3DDevice->Release();
}
if(g_lpD3DObject) {
g_lpD3DObject->Release();
}
if(g_lpD3DVB) {
g_lpD3DVB->Release();
}
}
Příklad, který jsme probrali v dnešní lekci, si můžete stáhnout v sekci Downloads.
V dnešní lekci jsme vytvořili jednoduchý příklad založený na Direct3D, i když to pravé 3D samozřejmě nebylo, protože jsme vykreslovali pouze transformované vrcholy. Také jsme se naučili používat vertex buffery, ve kterých jsou uloženy vrcholy objektů.
V příští lekci si povíme něco tzv. index bufferech. V těchto bufferech je uloženo pořadí vrcholů, které tvoří jednotlivé polygony. Například si představte, že chcete nakreslit čtverec, která má 4 vrcholy. Kdybychom ovšem měli k dispozici jen vertex buffer, museli bychom použít vrcholů 6, protože čtverec má dva trojúhelníky. Díky index bufferům ovšem stačí skutečně 4 vrcholy a některé se využijí vícekrát.
Těším se příště nashledanou.