A háromdimenziós megjelenítés
Ez a cikk már tényleg a háromdimenziós megjelenítésről fog szólni, ugyanis megjelenítünk egy y tengely körül forgó négyzetet. Ez a kis feladat is sok tanulsággal fog szolgálni, úgyhogy lássuk is!
A forráskódból egyértelműen ki fog derülni, hogy mi az ami maradt a régi, és mit írunk át, de azért próbálok utalni ezen dolgokra. Kezdjük tehát az elején. Mivel mondtam, hogy egy négyzet kell, ezért meg kellene adnunk a térben egy négyzetet. Emlékezzünk vissza, hogy már említettem a használt koordinátarendszert. Tehát így az x tengely jobbra mutat, az y tengely felfele, a z pedig befele a képernyőbe. A négyzetünk így most legyen az xy síkban, és legyen a négyzet közepe az origó. Ekkor ugye minden vertex z koordinátája 0 lesz, az x és y koordinátákat pedig ki kell találnunk. Legyen a négyzet oldala két egységnyi, ekkor a négyzet koordinátái egységben:
Ekkor természetesen könnyű lesz forgatni, mert az y tengely lesz a négyzet forgástengelye, és mi pont a négyzet forgástengelye körül szeretnénk forgatni. Látni fogjuk, hogy a transzformációk komoly gondot fognak még okozni, de erről majd később.
Először inkább lássuk, hogy milyen vertex formátummal fogunk dolgozni:
#define CUSTOM_VERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE) |
Ez azt jelenti, hogy x,y,z modelltérbeli koordinátákat adunk meg, illetve a
D3DFVF_DIFFUSE azt jelenti, hogy minden vertex kap egy színt is. Ez azt fogja jelenteni, hogy vertex színe a megadott lesz, a primitíven belül pedig a vertexek között folyamatos színátmenet lesz. Látható, hogy több tulajdonságot ’vagy’ kapcsolattal adhatunk meg.
Ekkor meg is adhatjuk a vertexünk adatainak struktúráját:
struct CUSTOMVERTEX {
FLOAT x,y,z;
DWORD color;
}; |
Az SDK dokumentációja egyértelműen megadja, hogy a szín egy
DWORD típus. Ez 32 bites számot jelent, azaz a színkomponensek illetve egy alfa csatorna(ezt most nem használjuk). Ezzel meg is lennénk, adjuk meg a négyzet koordinátáit:
CUSTOMVERTEX vertices[]=
{
{ -1.0f, -1.0f, 0.0f, 0x0000ff00 },
{ -1.0f, 1.0f, 0.0f, 0x00ff00ff },
{ 1.0f, -1.0f, 0.0f, 0x00ffff00 },
{ 1.0f, 1.0f, 0.0f, 0x00ffaaff },
}; |
A szín természetesen ARGB formátumú, tehát például az első vertex tisztán zöld. Vegyük észre azt is, hogy
TRIANGLESTRIP tipusú primitívet fogunk használni, tehát a negyedik vertex a másodikkal és a harmadikkal alkot egy háromszöget. Ez a négyzet nyílván el fog tűnni, ha megfordítjuk, mert akkor már nem pozitív bejárású lesz arról az oldalról. Erre a legjobb megoldás, ha megadunk még négy vertexet, csupán azokat ellenkező bejárással, hogy azok a túloldalról legyenek pozitív bejárásuak. Mi most egy sokkal rosszabb, ám egyszerűbb megoldást választunk: kikapcsoljuk azt a funkciót, hogy a nem pozitív bejárású primitíveket eldobja a Direct3D. De erről majd később, viszont emlékezzünk az itt elmondottakra!
Ezután teljesen azonos módon inicializáljuk a D3D-t, majd az eszközt is létrehozzuk és létrehozunk egy vertex buffert(most már négy vertexnek). Viszont most jönnek a különbségek: beszéltünk róla, hogy 3D-ben nem lehet csak úgy vaktában renderelni, hiszen meg kell mondanunk, hogy milyen nézőpontból, látószöggel, stb. szeretnénk lefékényképezni a modellteret. Hiszen ebben a modelltérben van egy négyzet, mint mondjuk az asztalunkon a valóságban, ezt pedig egy fényképezőgéppel(ezt most mi kamerának fogjuk hívni) akárhonnan lefényképezhetjük. De annak a fényképezőgépnek a lencséje állítható, így lehet, hogy a beállításai miatt például egy kört ellpiszisnek fényképez, stb. Ezért kell nekünk is megadnunk egy kamerát a modelltérben, az elhelyezkedését, valamint a beállításait. Ezen a kamerán át látható képet fogjuk mi a monitorunkon át látni.
A Direct3D ezt úgy oldja meg, hogy tárol három mátrixot: a modelltérbeli transzformáció mátrixot, a nézeti mátrixot illetve a projekciós, azaz vetítési mátrixot. Ezeken a mátrixokon a modelltérbeli pontjaink „végigfolynak”, így kerülnek a képernyőre(persze egyéb effektusok után). Az elsőről később fogunk beszélni, most egyenlőre az utolsó kettő a lényeg.
Nézzük először a nézeti mátrixot! Ezt a mátrixot már láttuk a bevezetőben hogyan kell létrehozni: a modelltérbeli pontokat kell megadni a kamera koordinátarendszerében. Direct3D-ben jelentős előny, hogy ezt a mátrixot nem nekünk kell létrehozni, ugyanis van egy D3DX-nek nevezett segédlet(függvénycsomag), amiben többek közt megtalálhatók a matematikai függvények is, mint például mátrixszorzás, létrehozás, stb. Ennek segítségével fogjuk létrehozni a nézeti mátrixot.
Emlékezzünk vissza, mivel is jellemezzük egy kamera térbeli elhelyezkedését? A poziciójával(eye), azzal a ponttal, ahova néz(lookat), illetve a felfele iránnyal(up). Ezért is hozunk először létre három vektort, melyek ezeket az adatokat tartalmazzák. A D3DX-ben egy 3D-s vektor típusa:
D3DXVECTOR3. Ez egy osztály, így van olyan konstruktora, amellyel létrehozáskor feltölthető, nyilván sorban az x,y,z koordinátájával:
D3DXVECTOR3 vEyePt ( 0.0f, 0.0f, -5.0f );
D3DXVECTOR3 vLookatPt( 0.0f, 0.0f, 0.0f );
D3DXVECTOR3 vUpVec ( 0.0f, 1.0f, 0.0f ); |
Az első vektor a kamera poziciója, ez most a z tengelyen van a -5 pontban. A második vektor szerint a kamera az origót nézi, és a harmadik vektor szerint a kamera függőlegesen van, mert a felfele irány az y tengely. Ennyire egyszerű. Ezekkel a vektorokkal, és egy D3DX függvénnyel megalkothatjuk a nézeti mátrixot:
D3DXMATRIX matView;
D3DXMatrixLookAtLH( &matView, &vEyePt, &vLookatPt, &vUpVec ); |
Itt deklaráltunk egy mátrixot, majd feltöltjük a megfelelő értékekkel. Gondolom a függvény paraméterezése egyértelmű, csupán a nevében rejlő
LH-ról említeném meg, hogy az Left Hand-et jelent, és a balkezes koordinátarendszerre utal, amelyikről már beszéltünk, hiszen azt használjuk. Lehetőség van arra, hogy jobbkezest használjunk, akkor a z tengely kifele mutat a képernyőből.
Beszéljünk most a vetítési mátrixról! A vetítés mint mondtam, jellemezhető a látószöggel(ez most 45 fokos lesz), az aspektus aránnyal és az elülső és hátulsó vágósíkokkal. Ez utóbbi kettő 1.0-tól 100.0-ig lesz, az aspekus arány pedig nyílván az ablakunk oldalainak az aránya, azaz 800/600. A vetítési mátrixot ugyancsak létrehozhatjuk egy D3DX függvénnyel:
D3DXMATRIX matProj;
D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4, 800.0f/600.0f, 1.0f, 100.0f ); |
Itt a második paraméter a látószög(pi/4=45 fok, ha nem lenne világos), a harmadik a aspektus arány, a negyedik és ötödik a vágósíkok(nyílván csak ezek közt renderelünk).
Most hasznosítani kellene ezeket a mátrixokat, ezért megismerünk egy fontos függvényt. Ezt is ügyesen oldották meg, hiszen a világ, nézeti és vetítési mátrixok beállítása nem három függvény, hanem egy, aminek a paraméterlistájában meg kell adni, hogy éppen melyiket szeretnénk beállítani:
lpD3D_Device->SetTransform( D3DTS_VIEW, &matView );
lpD3D_Device->SetTransform( D3DTS_PROJECTION, &matProj ); |
A konstansok szerintem egyértelműek(van még egy, ez pedig a modelltérbeli transzformáció,
D3DTS_WORLD), a második paraméter pedig természetesen a mátrixok. Ezzel megadtuk azt, hogy például amikor a Direct3D nézeti transzformációt hajt végre, akkor a megadott mátrixot használja. Ez pedig a továbbiakban nagyon hasznos lesz, meglátjuk. Hangsúlyozom, hogy ezen transzformációk egyike sem módosítja a vertex bufferben lévő vertexadatokat, hiszen akkor az első képkocka után elvesznének! A Direct3D csupán lemásolja a vertexeket, majd a másolatokat transzformálja.
A következő nagyon fontos függvényünk a renderelési opciók beállítását végző függvény. Ez is egy függvény, csupán meg kell adni melyik opciót szeretnénk állítani, majd értéket adunk neki. Akkor nézzük ezzel a függvénnyel például a világítás kikapcsolását:
lpD3D_Device->SetRenderState(D3DRS_LIGHTING, false); |
Az opciókat leíró konstansokat, és azok lehetséges értékeit az SDK dokumentációja tartalmazza(a beállítható opciók
D3DRS_-el kezdődnek, utalva a RenderState-re). Itt most a világítást azért kapcsoltuk ki, mert akkor (fény híján) minden teljesen fekete lenne a hátteret kivéve. A másik beállítandó opcióról már beszéltünk, azaz nem szeretnénk, hogy a nézőpontból nem pozitív bejárású primitíveket eldobjuk. Cullmode-nak hívjuk, ha ezeket a primitíveket megtartjuk. Ezt általában kapcsoljuk ki(alapállapotban a D3D eldobja ezeket a primitíveket), ugyanis lassít(pl. egy kocka belsejét minek lerenderelni). Ennek a beállítása:
lpD3D_Device->SetRenderState(D3DRS_CULLMODE, true); |
Ezzel látható lesz a primitívek „hátoldala” is.
Ezek után már meg is jeleníthető a négyzet térben, ez semmiben sem különbözik a 2D-s háromszög megjelenítésétől, csupán 2 primitívet jelenítünk meg, és a primitív típusa
D3DPT_TRIANGLESTRIP. Viszont említettem, hogy forgatni szeretnénk. Forgatni a koordinátatengelyek körül tudunk, így figyelni kell a négyzetünk poziciójára. De már észrevettük, hogy pont jó helyen van, hogy az y tengely körüli 360 fokos forgatással önmagába menjen át. Ezért nézzünk egy mátrixot, ami az y tengely körül forgat:
D3DXMATRIX matWorld;
D3DXMatrixRotationY( &matWorld, 0.01f ); |
Ez is egy D3DX függvény, és most a
matWorld mátrixot feltöljük a 0.01 radiánnal való forgatáshoz szükséges értékkel. A z tengely körüli forgatáshoz nyilván a
D3DXMatrixRotationZ(…) függvény kell. Most jön az érdekesség. Mint már ezt is mondtam, be kell állítani, hogy a modelltér összes vertexére alkalmazza ezt a transzformációt. Ezt ugyebár a
SetTransform (…) függvénnyel tudjuk beállítani, méghozzá megmondva, hogy a modeltérre alkalmazza:
lpD3D_Device->SetTransform( D3DTS_WORLD, &matWorld ); |
De ha ezután lefuttatnánk a programot, a négyzet nem forogna, hanem csak egyhelyben állna, igaz 0.01 radiánnal elforgatva. Ez azért van, mert a Direct3D bölcsen NEM nyúl hozzá a vertex bufferbeli vertexeinkhez, hanem minden egyes periódusban(képkockarenderelénél) kiveszi onnan a vertexeinket, majd azokat transzformálja. Tehát minden periódusban az eredeti modellteret transzformálja, nem a transzformáltat. Így a forgatáshoz minden periódusban egyre nagyobbat kell forgatni:
D3DXMatrixRotationY( &matWorld, i+=0.01f ); |
ahol
i egy statikus változó, azaz kívülről nem látható, de megőrzi értékét a
render() függvényen belül. Ekkor már tényleg forgatni fogunk. Aki nagyon sokáig szeretne forgatni, figyeljen a túlcsordulásra, hiszen
i-t vég nélkül növeljük, mert kihasználjuk a forgatás periódicítását.
Észrevehetjük, hogyha a négyzetnek az y tengely nem forgástengelye, akkor a négyzet nem a tengelye körül fog forogni, hanem akkor is az y tengely körül(próbáljuk ki!). Ezen tudunk segíteni, méghozzá a már megbeszélt módon, méghozzá transzformációk sorozatával: a négyzetet eltoljuk az origóba(ahová a feladatban is raktuk), ott forgatjuk, majd visszatoljuk az eredeti helyére. Az a kérdés, ezt hogy tudjuk megtenni? De ismereteink birtokában tudnunk kell a választ:
egy transzformációsorozat az egyes transzformációk mátrixainak a sorrendbeli szorzata. A mátrixszorzásra pedig van egy
D3DXMatrixMultiply(…) függvény. Az eltolásra pedig ott van a
D3DXMatrixTranslation(…). Ekkor a fenti műveletet így tudjuk elvégezni:
D3DXMatrixTranslation(&mat,-x,-y,-z);
D3DXMatrixRotationY( &matWorld, angle );
D3DXMatrixMultiply(&matWorld,&mat,&matWorld);
D3DXMatrixTranslation(&mat,x,y,z);
D3DXMatrixMultiply(&matWorld,&matWorld,&mat); |
Itt
mat és
matWorld mátrixok. Tehát ha a test középpontja(ami körül forgatni akarjuk) az (x,y,z) pontban van, akkor először eltoljuk (-x,-y,-z)-vel, ebből kapunk egy mátrixot(
mat). Csinálunk egy
angle szöggel való forgatás mátrixot(
matWorld). Ezeket összeszorozzuk sorrendben(
D3DXMatrixMultiply(…) első paramétere az eredmény, a másik kettő sorrendben a szorzatban érvényes sorrend:), azaz először az eltolás(
mat), aztán a forgatás(
matWorld), majd az eredmény a
matWorld-be kerül, ami azért jó, mert így nem kell egy csomó mátrixot deklarálni. Szóval ezek után csinálunk egy (x,y,z)-vel való eltolás mátrixot
mat-ba, ugyanis annak tartalma már nem kell, és azt összeszorozzuk
matWorld-el, persze
matWorld*mat sorrendben. Az eredmény
matWorld-be kerül, láthatjuk, hogy
mat csak ideiglenesen kellett… Végül
matWorld-ben lesz a transzformáció, ami nekünk kell, ezt beállíthatjuk modelltér transzformációnak. Kész is vagyunk.
forráskód:
d3d2.exe
2006. február 8.
Crusader
Kapcsolódó linkek:
A cikksorozat további részei: