DirectX (19.)

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.

19.1. Objekt Direct3D

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.

19.2. Projekt D3DExample1

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.

19.3. První trojúhelník

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.

19.4. Závěr

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.

Jiří Formánek