játékfejlesztés.hu
FórumGarázsprojectekCikkekSegédletekJf.hu versenyekKapcsolatokEgyebek
Legaktívabb fórumozók:
Asylum:    5455
FZoli:    4892
Kuz:    4455
gaborlabor:    4449
kicsy:    4304
TPG:    3402
monostoria:    3284
DMG:    3172
HomeGnome:    2919
Matzi:    2521

Pretender:    2498
szeki:    2440
Seeting:    2306
Geri:    2189
Orphy:    1893
Joga:    1791
Bacce:    1783
MaNiAc:    1735
ddbwo:    1625
syam:    1491
(C++) DirectX programozás 13. - Direct3D 2. (Inicializálás, programozástech. alapok) 2006.05.25 12:18


Direct3D inicializálása

Akkor most nekiállunk igazából programozni, megismerjük a Direct3D-t programozói oldalról is. Kezdjünk is bele!

Programozási szempontból nagyon fontos, hogy ez a cikk DirectX 9-re épít, ugyanis az a legújabb, és egy picit különbözik is a 8-as verziótól. A másik nagyon fontos dolog, hogy mindenkinek ajánlom letölteni a DirectX 9 SDK-t, ez megtalálható a Mirosoft weblapján, pár száz megabájt, gondolom mindenki be tudja szerezni. Ez ugyanis például Visual C-hez elengedhetetlen, a másik pedig, hogy tartalmazza a DirectX dokumentációt, ez pedig elengedhetetlenül fontos, hiszen tartalmazza minden funkciónak a részletes magyarázatát, bár sajnos angolul. Enélkül a dokumentáció nélkül nem is érdemes elkezdeni programozni, hiszen itt nem fogunk mindent részletesen átvenni, hiszen arra sem hely, sem idő nincsen. Egyébként ez a dokumentáció megtalálható az MSDN-en is.

Az általam használt fordítóprogramról annyit, hogy Microsoft Visual Studio .NET 2003-ról van szó, és bár a programokat ebben írom, azok működnek más verziójú Visual Studio-ban is, csupán kell egy projektet csinálnunk, majd a .cpp fájlt bemásolni.

Tehát térjünk rá a tárgyra, méghozzá a Direct3D programozás megalapozására. Hogy átfogóan lássuk a működés alapjait, ezért az elejére nem három dimenziós példát ismertetek, hanem egy sima két dimenziós példát, méghozzá megjelenítünk egy fehér háromszöget kék alapon. Ez azért lesz tanulságos, mert nem keveredünk még bele a három dimenzióba, de megismerjük a Direct3D programozási módszerét.

Első és legfontosabb dolog a header fájl és a két statikus library fájl. A .lib fájlok neve: d3d9.lib és d3dx9.lib. Ezeket csupán hozzá kell adni ugye a linkernél. A header fájlt az #include direktívával hozzáadjuk:

#include <d3dx9.h>

illetve természetesen a windows.h-t is:

#include <windows.h>

Az első feladat nyílván egy ablak létrehozása, erről már írtam, szóval ezt át is ugranám. Egy fontos dologról még írnék azért, ugyanis nagyon fontos lesz később. Elsősorban ablakban fogunk dolgozni, de persze megismerjük a teljesképernyős módot is. Egyébként Direct3D-ben ezek közt kicsi különbség van csak. Tehát a kezdetek kezdetén megmondjuk, mekkora rajzterületet szeretnénk(ez nyilván teljes képernyős módban csak valamelyik ’híres’ felbontás lehet):

const                   width=800;
const                   height=600;

Na itt jön a probléma, ha ablakban akarunk dolgozni: mekkora ablakot csináljunk, hogy a rajzterülete 800x600-as legyen? Ezt már megbeszéltük a DirectDraw-nál, így egyszerű a dolgunk, csupán feltöltünk egy RECT típust egy ilyen méretű téglalappal:

RECT                    r={0,0,width,height};

Ez pedig átadjuk a híres

AdjustWindowRectEx(&r,WS_OVERLAPPEDWINDOW,0,0);

függvénynek, ami kiegészíti egy akkora téglalapra, amekkora nekünk kell, azaz megadjuk a szükséges stílust, majd annak a stílusnak a fejlécét, stb. hozzászámítja. Így az ablaklétrehozásnál nyílván r.right-r.left és r.bottom-r.top méreteket kell megadnunk. Ennyi volt az, amit az ablaklétrehozáshoz szerettem volna hozzáfűzni.

A program már megismert részéhez (Windows alapok, ablaklétrehozás) csupán három egyszerű függvényt fogunk hozzáfűzni. Ez a három függvény végzi a
  1. Direct3D inicializálását
  2. a képkockát folyamatos frissítését, amit renderelésnek is hívunk
  3. és a takarítást
A függvények deklarációi ebben a sorrendben:

bool                    initd3d(HWND);
void                    render();
void                    deleted3d();

Mint látjuk, az inicializáló függvénynek egy paramétere van, ez pedig az ablakunk handle-je (vagy ’nyele’), ez majd az inicializáláshoz kell. A visszatérési értéke true, ha minden sikerült, és false, ha hiba van. Természetesen az inicializáló függvényt az ablak létrehozása után kell meghívni, a takarító függvényt a kilépés előtt és a renderelő függvényt pedig az üzenethurokban. Ez a forráskódból egyértelmű.

Kezdjü ténylegesen az inicializálással! Észre fogjuk venni az analógiát a DirectDraw-val, mert itt is objektumorientáltság uralkodik. Tehát szükségünk lesz egy Direct3D objektumra, ez pedig az alábbi lesz:

LPDIRECT3D9                        lpD3D=NULL;

Amit egyből ki is nullázunk. Ez a legfontosabb objektumunk, erre épül minden. Létrehozása végtelenül egyszerű:

lpD3D=Direct3DCreate9(D3D_SDK_VERSION);

Ha az lpD3D esetleg NULL értéket kap ezután, akkor ugyebár hiba történt. A hibakezeléssel nem foglalkoznék a továbbiakban sem, hiszen a forráskódban benne van. Na, megvan az objektumunk, ezzel akkor már sokmindent tudunk kezdeni. Például a későbbiekben fontos szerepet fog játszani a színformátum. Nem kell lényegében semmi különöset tudnunk róla, mindenesetre az aktuális formátumot egy tagfüggvénnyel tudjuk lekérdezni. Ehhez szükséges egy formátumleíró, aminek a típusa:

D3DDISPLAYMODE      d3ddm;

Ehhez társul a függvény, amivel feltöltjük az aktuális értékekkel:

ZeroMemory(&d3ddm,sizeof(d3ddm));
lpD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT,&d3ddm);

Itt a D3DADAPTER_DEFAULT jelenti az elsődleges megjelenítőt (egy videokártya esetén nem lehet gond, ha azon nincs pl. tévékimenet, bár ott is a monitor az elsődleges általában). Ezután a d3ddm.Format-ban lesz az aktuális formátum. Ezt csak a példa kedvéért említettem, az SDK dokumentációban benne van az összes többi tagfüggvény.

Ezek után kell létrehozni a legfontosabb dolgot, ami a megjelenítéshez szükséges, ez pedig a Direct3D eszköz(device). Nyilván ennek is kell egy deklaráció, és ez szintén egy osztály:

LPDIRECT3DDEVICE9            lpD3D_Device=NULL;

Ezzel fogunk igazából renderelni, de erről majd később, hiszen először létre kell hozni. Mivel ennek is túl sok paramétere lenne, ezért ehhez is először ki kell töltenünk egy leírófájlt. Nézzük:

D3DPRESENT_PARAMETERS        d3dpp;

Ezt természetesen először ki kell nullázni:

ZeroMemory(&d3dpp,sizeof(d3dpp));

Majd beállítani, hogy a DirectX válassza ki a legjobb megjelenítési technikát(ugyanis itt is van backsurface és primarysurface, ezek közt váltogatni kell, akár több közt is):

d3dpp.SwapEffect        = D3DSWAPEFFECT_DISCARD;

Természetesen megadhatjuk, hogy másolja a backsurfacet(D3DSWAPEFFECT_COPY), vagy pedig flipping-eljen(D3DSWAPEFFECT_FLIP, lásd DirectDraw). Aztán elintézzük, hogy egy darab back surface legyen, illetve megadjuk, hogy melyik ablakba rendereljünk:

d3dpp.BackBufferCount   = 1;
d3dpp.hDeviceWindow     = hwnd;

Majd eldöntjük, hogy ablakban szeretnénk-e futni vagy nem. Nézzük először az ablakban való megjelenítést!

d3dpp.Windowed               = TRUE;
d3dpp.BackBufferFormat       = D3DFMT_UNKNOWN;

Az első nyilvánvaló, a második pedig mint mondtam, beállítja a színformátumot, azaz D3DFMT_UNKNOWN-ra, ez pedig azt jelenti, hogy ismeretlen, így az aktuális formátumot fogja használni. Teljes képernyős módban csak annyi a különbség, hogy meg kell adni a backsurface méreteit, illetve a színformátum nem lehet ismeretlen:

d3dpp.BackBufferFormat  = D3DFMT_R5G6B5;
d3dpp.Windowed          = FALSE;
d3dpp.BackBufferHeight  = height;
d3dpp.BackBufferWidth   = width;

Itt csupán az elsőről kell beszélnünk, azaz a színformátumról. Ez most D3DFMT_R5G6B5, azaz 16 bites, 5 bitnyi vörös, 6 bitnyi zöld és 5 bitnyi kék. Ez általában működik, ha nem, akkor a D3DFMT_X1R5G5B5 is megpróbálható. Itt 1 bit kárba megy. Egyébként az, hogy használható-e egy színformátum, az ellenőrizhető a CheckDeviceType tagfüggvénnyel, erről az SDK ír. Továbbá egyes videókártyák a 24 bites színformátumot is támogatják (D3DFMT_R8G8B8), mások pedig csak az alfa csatornás 32 bites módot(D3DFMT_A8R8G8B8).

Így ha most létrehozzuk az eszközt, megtörténik az esetleges teljes képernyőre váltás és egyéb dolgok, illetve készen áll a Direct3D a renderelésre.

hRet=lpD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd,
                                  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                  &d3dpp, &lpD3D_Device ) ;

Mint látjuk, van visszatérési érték, teljes mértékben analóg a DirectDraw-val, típusa HRESULT, és a fennálló hibákról tájékoztat. Ha D3D_OK a visszatérési érték, akkor minden rendben, egyébként nem. A függvények visszatérési értékéről az SDK részletesen szól. Az első paraméterről beszéltünk már, ez az elsődleges megjelenítőt jelenti, a második pedig az eszköz típusát adja meg, ez most hardveres raszterizációt jelent. A harmadik az ablakunk nyele, a negyedik szoftveres vertexszámítást jelent, tehát a geometriát a processzor számítja, azért kell, mert nem biztos, hogy van FPU. Az ötödik az előbbi leíró, az utolsó pedig magának az eszköznek a mutatója. A lehetséges paramétereket ugyancsak az SDK tárgyalja, nekünk ezek most tökéletesen megfelelnek.

Ezzel inicializáltuk a Direct3D-t, most már csak az érdemi munka szükséges. De előtte a Direct3D felszabadításáról beszélünk. Ez mindig a létrehozással ellentétes irányban halad, azaz először az eszközt(illetve mást, ami esetleg létezik), aztán a D3D objektumot. Minden osztály a Release() tagfüggvénnyel szüntethető meg. Tehát pl.:

if(lpD3D_Device!=NULL) lpD3D_Device->Release();

Ezzel meg is lennénk. Akkor most jelenítsünk meg egy háromszöget! Sajnos ez nem ilyen egyszerű… De nem is nehéz. Most szükség lesz az előző cikkben említettekre. Mert ugye egy háromszöget három pont meghatároz, azaz meg kell adnunk három pontot. Első kérdés, hogy egy pontot, vertexet mivel jellemzünk? Ez MI adjuk meg, méghozzá egy sor különböző jellemző vagy(|) kapcsolatával. Mi most csak egy jellemzőt fogunk használni:

#define CUSTOM_VERTEX (D3DFVF_XYZRHW)

Tehát definiáltuk egy vertexet. A D3DFVF_XYZRHW jelentése egyszerűen az, hogy ezek a vertexek már fel vannak dolgozva, azaz transzformáltak, koordinátáik pixelben értendőek. A z koordináta természetesen teljesen jelentéktelen, csupán egy esetleges z-buffer esetén meghatározza a takarást. Az rhw koordináta sajnos nem tudom mi, de az is jelentéktelen, simán 1-re állítjuk. Egyébként az FVF Flexible Vertex Format-ot jelent, hangsúlyozva, hogy a felhasználó definiálhatja a vertexeket, azok nem kötöttek. Egyébként a vertex téma nagyon bonyolúlt, lehet őket programozni, szép effekteket elérni, de ezt most nem tárgyaljuk.

Minden ilyen vertexformátumhoz tartozik egy struktúra, ezt az SDK megadja. Az általunk definiált vertexhez egy ugyancsak általunk létrehozandó struktúra tartozik:

struct CUSTOMVERTEX {
      FLOAT x,y,z,rhw;
};

Azaz minden koordináta egy egyszeres pontosságú valós szám. Ezzel akkor fel is építhetünk egy háromszöget! Az origó az ablakunk bal felső sarka, x tengely jobbra mutat, az y lefele. Az értékek pixelben értendőek:

CUSTOMVERTEX vertices[]=
{
      { 400.0f, 50.0f, 0.5f, 1.0f},
      { 700.0f, 500.0f, 0.5f,1.0f },
      { 100.0f, 500.0f, 0.5f,1.0f}
};

Ez ugyebár egy 3 elemű tömb, minden eleme egy CUSTOMVERTEX struktúra. Megfigyelhetjük, hogy a képernyő felöl nézve ezek a pontok pozitív bejárásuak, tehát a háromszög látható lesz. Most ezt a háromszöget kellene megjelenítenünk. Ekkor lép közbe egy nagyon fontos gyakorlati fogalom a Direct3D-ben, ez pedig a Vertex Buffer.

A vertex buffer egy olyan tároló, persze a memóriában, mely a vertex adatokat tárolja. A Direct3D ezt használja adatfolyamként, azaz ebből veszi ki a vertexeket sorrendben. Ezért fontos ismernünk a vertex buffer létrehozási módját, illetve feltöltését. Tehát nézzük hogy kell létrehozni egy vertex buffert. Nyilván kell egy buffer:

LPDIRECT3DVERTEXBUFFER9      lpD3D_Buffer=NULL;

Ezt kell most létrehoznunk:

hRet=lpD3D_Device->CreateVertexBuffer( 3*sizeof(CUSTOMVERTEX),
         0 , CUSTOM_VERTEX, D3DPOOL_DEFAULT, &lpD3D_Buffer, NULL );

Itt csupán a paramétereket kell megbeszélni. Az első természetesen a vertexbuffer mérete. A második általánossan a használat körülményeiről szól. Ilyen lehet például a D3DUSAGE_WRITEONLY flag. Ez azt jelenti, hogy csak írható. Több ilyen van, az SDK-ban megtalálhatóak. A következő paraméter a tárolt vertexek típusát adja meg, ezt tudnia kell a tárolás módja miatt. A D3DPOOL_DEFAULT a memóriakezeléssel kapcsolatos(hova rakja a buffert), ezt általánosra állítjuk(ez a második paramétertől függő helyre rakja), de lehet például D3DPOOL_MANAGED, ahol a DirectX automatikusan abba a memóriába menti a vertexeket, amihez hozzáfér(általában a rendszermemória). Később ez utóbbit használjuk, csupán lusta voltam átjavítani:). Az utolsó előtti a buffer mutatója, az utolsó pedig szükségszerűen NULL, ugyanis a fejlesztők későbbi célokra fenntartották.

Most már csak fel kellene tölteni a buffert. Ezért először a buffer címe kell, amit a DirectDraw surface-hoz hasonló módon, egy Lock paranccsal tudjuk lekérni(illetve ez engedélyezi az írást illetve olvasást, mivel a Lock és az Unlock között lezárjuk a memóriát, kívülről nem elérhető). Tehát akkor kell egy változó a buffer címének, ez típus nélküli mutató:

VOID*                              pVertices;

Ekkor zároljuk a buffert:

lpD3D_Buffer->Lock( 0,0, &pVertices, 0 );

Ez is természetesen HRESULT értékkel tér vissza. Az első paraméter azt adja meg, hogy hányadik bájttól kezdve akarjuk zárolni, a második pedig azt, hogy onnantól kezdve mekkora területet(bájtokban). Ez mindkettő 0, hiszen ekkor az egész buffert zárolja. A harmadik paramétert cím szerint adjuk át, így megkapjuk a buffer memóriaterület címét, innentől lehet bele írni. Az utolsó paraméter a zárolás módját jellemzik, flagek, nekünk most nem kell. Ekkor a vertices tömbben tárolt vertexeket át kellene ide másolni, azaz a pVertices helyre a vertices helytől, méghozzá a vertices tömb méretű területet. Ez sima memcpy művelet:

memcpy( pVertices, vertices, sizeof(vertices) );

Ebben semmi különös nincs, mindenki ismeri, sima C függvény. Ezután lezárjuk a buffert:

lpD3D_Buffer->Unlock();


Kész is lennénk, már csak meg kell jeleníteni. Ez a vertex bufferes módszer azért jó, mert sokkal gyorsabb, mint sima tömbből töltögetni be a vertexek adatait, illetve az adatfolyamban a típus igazítás is gyorsít.

Akkor nézzük a megjelenítést(egy képkockáét)! Először természetesen képkockánként törölni kell a képernyőt:

lpD3D_Device->Clear(0,NULL,D3DCLEAR_TARGET,0x000000ff,1.0f,0);

Az első két paraméter azért felelős, hogy ha téglalapokat akarunk törölni a képernyőn(azaz csak egyes részeket), ekkor az első paraméter ezen téglalapok számát adja meg, a második pedig ezen téglalapok RECT elemű tömbjére mutat. A harmadik paraméter azt mondja meg, mit akarunk törölni, itt most a képernyőt töröljük, de törölhetnénk pélául a z-buffert is a D3DCLEAR_ZBUFFER flaggel(ha lenne…). A következő, hogy milyen színnel töltsük fel a célablakot, ez most az RGB rendszer miatt csupa kék szín. Az utolsó kettő pedig azt jelenti, hogy ha a z-buffer, vagy a stencil-buffert töröljük, akkor milyen értékre állítsuk(első a z-bufferé, második a stencil-bufferé). A stencil bufferrel maszkolni tudjuk az egyes pixeleket.

Miután töröltük, közölni kell, hogy elkezdjük a jelenetet kirajzolni. Ezt a

lpD3D_Device->BeginScene();

függvénnyel tehetjük meg. Elsőként beállítjuk az aktuális vertexformátumot:

lpD3D_Device->SetFVF(CUSTOM_VERTEX);

Ez fix vertexformátumoknál használatos, programozottaknál a SetVertexShader(…) használatos, igaz, DirectX 8-ban még ezzel kellett a fixeket is beállítani. A következő beállítandó az aktuális adatfolyam, amelyből a vertexeket kivesszük:

lpD3D_Device->SetStreamSource(0,lpD3D_Buffer,0,sizeof(CUSTOMVERTEX));

Az első paraméter az adatfolyam száma, azaz ezt állítjuk be. Alapértelmezésben a 0 aktív. Ez azért jó, mert több adatfolyamot egyszerre beállíthatunk, majd azt megjelenítésnél gyorsan váltogathatjuk a SetStreamSourceFreq(…) függvénnyel, de ez most nem fontos. A következő annak a vertexbuffernek a mutatója, amelyikhez akarjuk kötni az adatfolyamot, ez nyilván az előbb létrehozott bufferünk, benne a háromszög pontjaival. A következő paraméter egy eltolás paraméter, azaz honnan kezdődnek az adatfolyamban a vertexek adatai(bájtokban). Ezt nem is biztos, hogy támogatja az eszköz. Az utolsó paraméter FVF vertexeknél egy vertex mérete, ugyanis ekkora ’adagokban’ kapjuk a vertexeket.

Most pedig jön az érdemi rajzolás, méghozzá a

lpD3D_Device->DrawPrimitive(D3DPT_TRIANGLELIST,0,1);

függvénnyel. Ez most különálló háromszögeket rajzol(D3DPT_TRIANGLELIST), de ez lehetne D3DPT_TRIANGLEFAN, vagy D3DPT_TRIANGLESTRIP is(lásd előző cikk). Ez a függvény az aktuális adatfolyamban található, második paraméterben megadott sorszámú vertextől kezdődően rajzol annyi primitívet, ahányat az utolsó paraméterben megadtunk.

Ezután közöljük, hogy vége a jelenetnek:

lpD3D_Device->EndScene();

Ezután már csak a képkocka megjelenítése van hátra, azaz a backsurface megjelenítése:

lpD3D_Device->Present(NULL,NULL,NULL,NULL);

Láthatjuk, csupa NULL paraméter, az elsőnek ennek is kell lenni, ugyanis a D3DSWAPEFFECT_DISCARD-dal hoztuk létre az eszközt, ez pedig nem kompatibilis az első paraméterrel. Az első paraméter egyébként azt jelenti, hogy mekkora téglalapot akarunk a forrás surface-ből megjeleníteni. A második paraméter viszont használatos, azt jelenti, hogy a képernyő melyik téglalapjára akarunk renderelni(RECT* típusú). A harmadik paraméter azt adja meg, hogy melyik ablakba rendereljünk. Ez ha NULL, akkor az eszköz létrehozásnál megadott ablakba renderel. Az utolsó az első paraméterbeli okok miatt ugyancsak NULL.

Ezzel meg is jelenítettük a háromszöget, kész is vagyunk. Következő részben már tényleg három dimenzióban programozunk, de addig ajánlom az SDK dokumentációjának átböngészését, hiszen a függvények pontos prototípusai ott találhatók meg, anélkül pedig a pontos paraméterezés nehéz. Ez a cikk pedig lényegében a példa forráskód bő kommentezése.

A forráskód: d3d1.exe


Crusader

2006. február 7.


Kapcsolódó linkek:

A cikksorozat további részei:

Értékelés: 0

Új hozzászólás
Lazarus          2006.05.26 14:43
Bár a C++ról letettem már pár hónapja, de nagyon tetszenek a cikkjeid, annó a DDrawal sokat szórakoztam