2010. augusztus 19., csütörtök

SlimDX9 02 - Irány a 3D!

Az első SlimDX-es programunkban egy kétdimenziós háromszöget jelenítettünk meg. Megismerkedtünk a DX alapjaival, ideje hát tovább lépni a harmadik dimenzióba. Ebben a példában egy gúlát fogunk megforgatni a térben.
Fontos megjegyezni, 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é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.

A virtuális világ "lefényképezése"

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 ellipszisnek 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). Így a ebben a leckében főleg ezekkel a bealításokkal fogunk foglalkozni.
DirectX-ben egy pontot három koordinátájával adjuk meg, tehát ismernünk kell a Direct3D-ben használt koordinátarendszert. Mi most a balkezes (Left-handed) koordinátarendszert fogjuk használni, ami azt jelenti, hogy az X-tengely jobbra mutat, az Y-tengely felfele, a Z-tengely pedig a képernyőbe befelé:

Bal- és jobbsodrású koordináta rendszerek

Természetesen az OpenGL jobbkezes (Right-handed) rendszert alkalmaz, ezt érdemes megjegyezni. Ekkor már csak egy kérdés maradt: hogyan tudjuk ezt a három dimenziót leképezni két dimenzióra?

Projekció-, nézet- és világtranszformáció

Elméletben már ismerjük a képszintézis főbb elemeit, most a helyi, kamera, világ nézet és a vágás végrehajtásához szükséges gyakorlati alapismereteket tekintjük át. A képszintézis szakaszaihoz tartozó transzformációkat a Direct3D mátrixokkal valósítja meg. A mátrixműveletek lehetővé teszik a transzformációk összekapcsolást, így egy mátrixban több transzformáció is megadható. A megfelelő transzformációkat a device SetTransform függvény meghívással valósíthatjuk meg. A State paraméterben megadhatjuk a transzformációt (a képszintézis lépést), a második paraméter pedig maga a transzformációs mátrix:
  • Világ: TransformState.World
  • Kamera: TransformState.View
  • Projekció: TransformState.Projection
Először is nézzük meg mit is jelentenek az említett transzformációk. Világ nézeti transzformáció (World transform), ezzel a transzformációval helyezzük el objektumainkat a jelenetünk szintetikus világában. Nézettranszformáció (View transform), ezzel megadjuk a kamera helyzetét, vagyis azt a pontot ahonnan a jelenetet szemléljük. Projekciós transzformáció (Projection transform), ezzel határozzuk meg a vetítés paramétereit, amelynek eredményeként a képernyőnk kétdimenziós felületén megjelenő objektumok háromdimenziósnak tűnnek.
A fenti transzformációk beállítására a DirectX hasznos függvényeket biztosít. Kezdjük talán a leglátványosabbal, a projekciós transzformációval. Hozzunk létre egy mátrixot, aminek az elemeit a PerspectiveFovLH() függvény segítségével kényelmesen fel tudjuk tölteni.

A nézeti gúla és paramétereinek értelmezése

A paramétereknél megadjuk a látótér szögét (Field of View), méretarányát (Aspect Ratio) valamint az elülső és a hátsó vágósíkok z-koordinátáját (z-front és z-back):

Matrix projection = Matrix.PerspectiveFovLH((float)Math.PI/4,
(float)ClientSize.Width / (float)ClientSize.Height, 1.0f, 100.0f);

Lényeges tudnunk, hogy a látószöget nem fokban, hanem radiánban kell megadni. Használjuk a Math.PI előre definiált értékét, egy elfogadható FoV (Field of View) a Math.PI / 4. A projekciós transzformációt a device.SetTransform() hívással léptethetjük életbe, amelynek első paramétere a TransformState.Projection érték, a második pedig a projekciós transzformációs mátrix:

device.SetTransform(TransformState.Projection, projection);

Az elülső vágósík koordinátája 1.0f – tehát az ennél közelebb helyezkedő objektumok nem láthatóak. Természetesen mindig a kamera pozícióhoz képest kell értelmezni a közeli és a távoli vágósík értékeit.
A háromdimenziós tér legnagyobb előnye, hogy objektumaink mozoghatnak benne, tehát be kell állítani a világ transzformációkat. Példánkban adott tengely körüli elforgatást valósítunk meg, amit szerencsére ugyancsak függvényekkel támogat a DX:

device.SetTransform(TransformState.World, Matrix.RotationAxis(
new Vector3
(
angle / ((float)Math.PI * 2.0f),
angle / ((float)Math.PI * 4.0f),
angle / ((float)Math.PI * 6.0f)
),
angle / (float)Math.PI));

A TransformState.World paraméter megadásával jelezzük, hogy a világtranszformációt akarunk beállítani. A Matrix.RotationAxis() függvény segítségével megkaphatjuk azt a mátrixot aminek a segítésével a tetszőleges tengely körüli elforgatást valósíthatjuk meg. A tengely egy Vector3 típus definiálja, az elforgatás szögét pedig az angle változó tárolja.
Hátra van még a kamera pozíciójának beállítása, amin keresztül a jelentet szemléljük. Ez a kamera a térben bárhol lehet, és bármerre nézhet. Tehát ebből a kamerából látható kétdimenziós képre vagyunk kíváncsiak, azaz a csúcspontban lévő megfigyelő milyen képet lát a kamera síkján. A DirectX a kamerát három jellemzővel adja meg. Ez a három jellemző egy-egy vektor a modelltérben: a kamera helyzete (Eye), az a pont, amire a kamera néz (Target) és a felfele irány (Up). Ebből természetesen ki tudjuk számolni a kamera nézési irányát: Direction=Target-Eye, ami egy egyszerű kivonás vektorok közt. Ezek ismeretében már tudunk egy koordináta-rendszert illeszteni a kamerára is. A DX Matrix.LookAtLH függvény egy bal sodrású (Left Handed, LH) nézeti mátrixot tölt fel a megadott vektorok alapján:

Matrix view = Matrix.LookAtLH(
new Vector3(0, 0, zoom),
new Vector3(),
new Vector3(0, 1, 0));

device.SetTransform(TransformState.View, view);

Számunkara a kamera helyzetét meghatározó mátrix különösen fontos mivel a kamerapocizó módosításával egy fajta zoom hatást valósítunk meg. A zoom értékét a fel és a le billentyűkkel növeljük illetve, csökkentjük. A transzformációkat összefogva a void SetUpCamera(float zoom) metódus valósítja meg amit a Render() metódusban hívunk meg. Az előbbi transzformációkat együttesen a következő ábra mutatja be szemléletesen:

A DX rögzített grafikus csővezetéke

Látható, hogy az egyik oldalon belépnek a vertexek, végigfolynak a transzformációs műveleteken, majd végül a DX gépezet kiköpi a másik oldalon a rasztert, vagyis a képernyőre kerülő képet. Ezt folyamatot szépen grafikus csővezetéknek nevezik aminek csak egy eleme transzformációs modul. A hagyományos (Fixed Function Pipeline) főbb elemei a következők:
  • Transzformáló modul: A modul négy módosítható állapotregisztert tartalmaz: nézeti transzformáció mátrixa, modellezési transzformáció mátrixa, projektív transzformáció mátrixa valamint a képernyő koordináta-rendszerébe vivő transzformáció leírása (viewport). Tetszőleges vetítést (párhuzamos, perspektív) megvalósító mátrixot is támogat. Ahogyan a neve is mutatja, a geometria transzformációival foglalkozik, emiatt geometriai modulnak is hívják.
  • Árnyaló modul: Ez a modul a megvilágítással (árnyalással) és szín információkkal kapcsolatos számításokat végez. Egy veremszerű struktúrát használ fel, az aktuális fényforrások rekordjainak kezeléséhez. Ambiens, irány, pontszerű és szpot fényforrások megvalósítását támogatja.
  • Raszterizáló modul. Ez a modul az előbb említett két modul kimenetét használja fel a virtuális világ megjelenítéséhez. A raszterizáló modul végzi a 3D képszintézist (a színbufferbe rajzolást) a Direct3D-ben. Raszter műveletek, mint huzalváz, tömör kitöltési módok és textúra térképek is ebben a modulban vannak definiálva.
A hagyományos, huzalozott "transformation and lighting" (T&L) motort, rögzített műveleti sorrendű csővezetéknek nevezik, mivel a fejlesztő nem avatkozhat bele, nem módosíthatja a csővezeték működését, csupán vezérelheti. Innen ered a "fixed function pipeline” elnevezés. Felmerülhet a kérdés, hogy mi van akkor, ha a programozó nem elégedett a csővezeték működésével? A válasz egyszerű, semmi, ez van, ezt kell szeretni. Persze ez nem elfogadható válasz egy programozó számára így elég hamar (ez viszonylagos) megjelent a programozható csővezeték. És itt lépnek a képbe a shaderek, de nem menjünk ennyire elébe a dolgoknak. Ma már a merev csővezeték elavultnak számít, de ne higgye azt senki, hogy ettől el kell azt felejteni! Sőt! Igencsak hasznos, főleg kezdők számára.
Most, hogy megismerkedtünk a grafikus csővezeték fogalmával térjünk vissza pár gondolat erejéig az első példaprogramunkhoz. Első művünk egy háromszög volt képernyő koordinátarendszerben (2D) megadva. Itt elegendő lett volna egy vertexet két koordinátával megadni (x,y), de nem azt tettük, hanem négy koordinátát azaz Vector4 struktúrát használtunk. A z koordináta egyértelműen 0, míg a negyedik homogén tag (w) mindenhol 1 volt. Miért bonyolítottuk így meg a dolgokat?
Először is a DirectX alapvetően a 3D-re van felkészítve, gondoljunk csak a csővezetékre. Ezért, ha azt akarjuk, hogy vertex-ünk ne menjen keresztül a transzformációs folyamatokon, akkor azt közölni kell a DX-el. Ezért definiáltuk, úgy a vertex formátumát, hogy: VertexFormat.PositionRhw. A PositionRhw jelentése egyszerűen az, hogy ezek a vertexek már fel vannak dolgozva, azaz transzformáltak, koordinátáik pixelben értendőek. A transzformált vertexek pedig mindig homogén vágási térben helyezkednek el! Ezért szükséges a Vector4 struktúra. A struktúra x és y tagja maga a képernyő koordináta, a z koordináta természetesen teljesen jelentéktelen, csupán egy esetleges z-buffer esetén meghatározza a takarást, végül következik az rhw tag (reciprocal of homogenous w) aminek az értéke 1 szokott lenni. Ezek után jöjjön az új vertex formátumunk amivel már 3D-s vertexeket adhatunk meg:

[StructLayout(LayoutKind.Sequential)]
public struct Vertex
{
public Vector3 Position;
public int Color;
public static readonly VertexFormat Format =
VertexFormat.Position | VertexFormat.Diffuse;
}

Láthatjuk, hogy most már Vector3 struktúrát használunk és a vertex formátum simán Position vagyis térbeli és nem transzformált vertexről van szó! Ebből fogja tudni, a DX hogy milyen műveleteken kell átesnie ezeknek a vertexeknek, hogy a képernyő megjelenjenek. A pozíció mellet a vertex színét is eltároljuk. A VertexFormat.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. A VertexFormat formátumot célszerű magában a Vertex struktúrában megadni Format néven, így nem kell fejben tartanunk annak megadását a renderelés során. A struktúra elején lévő attribútum [StructLayout(LayoutKind.Sequential)] azt mondja meg, hogy az adattagok folytonosan jönnek egymás után.

A jelenet geometriája

A jelenet három dimenziós testekből álló tér. Alapvetően minden testet a pontjaival (vertex) adunk meg. Hogy a megadás módja egyértelmű legyen, ezért egy nagyon egyszerű módszert használunk: minden testet háromszögekre bontunk, ez lesz egy primitív, hiszen tényleg a legegyszerűbb síkidom, aminek területe van. Ezeket a háromszögeket a pontjaival adunk meg, ami egyértelmű, hiszen a három pont akármelyik sorrendben megadva egyértelműen jellemzi a háromszöget. A háromszög pontjainak van egy jellegzetes sorrendje: ha ránézünk a háromszögre, és úgy a pontjait olyan sorrendben adtuk meg, hogy jobb irányban haladnak, akkor pozitívan irányított (DirectX-ben). Azaz pl.:

Irányított vertex megadás

Itt például 0,1,2 pozitívan irányított, viszont 0,2,1 nem. Tehát sikerült megadni egy háromszöget, megismerve az irányítását is. Ha minden háromszöget ennek megfelelően adunk meg akkor bekapcsolhatjuk a hátsó lapok eldobását, hiszen a pozitív z koordinátájú háromszög normálisok (normálvektor, ami ugye merőleges a háromszög síkjára) azt jelentik, hogy az a háromszög a test túloldalán van, azaz nem látszik. Ezért is beszéltünk a pozitív körüljárásról, hiszen csak azok a háromszögek fognak látszani, melyek az aktuális nézőpontból pozitív körüljárásúak. Tehát ez a takarási munka egyszerű:


A hátsó lapok eldobását a következő sorral kapcsolhatjuk be:

device.SetRenderState(RenderState.CullMode, Cull.Counterclockwise);

Ekkor a rendszer minden poligonhoz normálvektort számol és ennek és a nézőpontnak (nézési irány alapján) veszi a közbe zárt szögét és ez alapján dönti el, hogy hátsó lap-e. Nyilván, ha a normálvektor felénk mutat, akkor hegyes szög lesz, ha meg ellenkező irányba, akkor tompaszög. Mivel nagyjából minden második poligon hátsó lap így akár megduplázhatjuk a program sebességét.
Ezek után már csak fel kell tölteni a vertices Vertex tömböt a szükséges adatokkal (gúla csúcspontjai), amit az OnResourceLoad() metódus meg is tesz.
Mielőtt még elkezdenénk a renderelést ne felejtsük el kikapcsolni a fényeket. Ha ezt nem tennénk meg, akkor a gúlából semmi sem látszódna a fekete háttér előtt. A fények kikapcsolását a következő sor engedélyezi:

device.SetRenderState(RenderState.Lighting, false);

Ezek után már nincs más hátra, mint gyönyörködni az első valódi 3D-s DX-es programunkba, aminek az eredménye itt látható:

Hello Pyramid!

Természetesen ebben a leckében sem tértem ki mindenre mivel a forrás tartalmaz megjegyzéseket így a hiányzó részek megértése házi feladat. A példa teljes forrása itt tölthető le:


Most is meg kell jegyeznem, hogy Nyisztor Károly könyvei is sokat segítettek a bejegyzés írása közben. Valamint felhasználtam Crusader DX leckéit is.

0 megjegyzés :

Megjegyzés küldése