Háromdimenziós játékfejlesztés Opengl-ben Tartalomjegyzék 1. Előszó 2. OpenGl bemutatása 2.1. Keletkezése 2.2. Képességei 2.3. Telepítése C++ környezetbe 3. Ablakkezelés alapjai 3.1. Grafikus szoftverek felépítése 3.2. Windowsos ablakkezelés 4. OpenGL főbb parancsai 4.1. OpenGL terminológiája 4.2. Híváslista 4.3. Mátrixok 4.4. Rajzolási parancsok 5. Modellezés 5.1 Koordinátarendszerek 5.2 Transzformációk 6. Kamerakezelés 6.1. Nézeti transzformáció 6.2. Perspektív transzformáció 6.3. Camera osztály 7. Textúra 7.1.Textúra definiálása 7.2. Textúrázás használata 7.3. Textúrázási módszerek 8. Animáció 8.1. Fájl kezelés alapjai 8.2. MY1 formátum leírása
1
8.3. Karakterek mozgatása 8.4. Egyszerű ütközés tesztelés
2
1. Előszó Mi lehetne jobb egy jó elkészített játéknál, csak is az amikor mi hozzuk létre a saját játékunkat. Amikor a semmiből teremtünk egy új világot, ahol mi állíthatjuk be a játékszabályokat. Mi mondhatjuk meg mikor keljen fel a hold vagy süssön a nap, mikor essen az eső. Hol tehetjük eszt meg a legkönnyebben, természetesen a számítógépen ahol mostanra már rengeteg igen jó eszköz született azon bátorlelkü programozóknak akik a játékfejlesztésre vetemednének. Ezen eszközök egyike az OpenGL is melyre bátran mondhatjuk hogy kiállta az idő próbáját. 2. Opengl bemutatása 2.1 Keletkezése Fejlesztését 1992-ben fejezték be a Silicon Graphicsnál. Kezdetben a neve csak GL volt az OpenGl nevet csak később vett a fel ahol az Open a platformfüggetlenségre a GL pedig grafikus könyvtár rövidítése (Graphics Libary).A platformfüggetlenség jegyében mára már a programcsomag elérhető Unix, Linux, MacOs és Windows felhasználók számára, és az OpenGL-es alkalmazásfejlesztést megkönnyítve ma már nem csak C és C++-ban hanem számos más fejlesztőkörnyezetben is elérhető mint például Visual Basicben, Delphiben vagy Pythonban. 2.2. Képességei Maga az OpenGL nem rendelkezik ablakkezelő tulajdonságokkal erről a programozónak kell gondoskodni a megfelelő szoftvere és hardverre környezetben. Nem tartalmaz magmaszintű utasításokat a háromdimenziós modellek leírásához, ezeket pontokból poligonokból kell felépíteni. Képes a drótvázas megjelenítésre, élsimításra, árnyékok, anyagok, megjelenítésére vagy a kép elmosására. Ezeken kívül léteznek hozzá különböző könyvtárak melyekkel meg lehet könnyíteni a fejlesztést ilyen például a GLUT, mely tartalmaz egy platform független ablakozó rendszert vagy a GLU melyben megtalálhatók benne még magas szintű objektumok gömb, teáskanna, NURBS görbék és felületek . 3
2.3. Telepítése C++ környezetbe Miután az előző részben szót ejtettünk a programozási könyvtárakról nézzük meg hogyan is lehet őket beilleszteni a fordítóba. Az OpenGL könyvtár (Windows esetében) önmagában tartalmaz egy gl.h, opengl32.a opengl32.dll. A gl.h a fordítónk include/Gl könyvtárába érdemes behelyezni míg a opengl32.dll a Windows/System32 könyvtárba kell helyezni az opengl32.a pedig a fordító lib könyvtárába helyezzük. A forráskód elején az #include
-val közöljük a linkernek, hogy szúrja be a gl.h található kódrészt illessze a sajátunk elé, erre azért van szükség mert a függvényeket a függvényhívás helye előtt definiálni kell C++ -ban. Miután elkészültünk a programunkkal és le szeretnénk fordítani most még a fordító valószínűleg hibaüzenettel térne vissza. De Miért? - jó a kérdés. Most még a fordítónk nem találja meg a különböző függvényekhez tartozó programrészt, ezt kiküszöbölendő a linkernek meg kell adni, hogy mely bináris fájlokban található az adott kód,itt lehetnek bizonyos eltérések Dev-C++ a l*.lib bináris fájlokat ok elérési útját kell megadni. Ez a terminológia természetesen alkalmazható a többi könyvtárra is GLU vagy GLUT. 3. Ablakkezelés 3.1. Grafikus szoftverek felépítése Ha játékot szeretnénk készíteni és túl szeretnénk lépni a karakteres felület adta lehetőségeken, akkor találkozni fogunk az ablakkezelés problémájával. Ablakok készítésének a módja platformról platformra változik. Bár már léteznek platformfüggetlen ablakozó könyvtárak is, ezek általában leegyszerűsítik az ablakok kezelését, és bár ezzel felgyorsítják a fejlesztési időt, de lassíthatják a futást. OpenGL-nek létezik egy GLUT-nak nevezett csomagja amely képes a platformfüggetlen ablak és eseménykezelésre. Programvezérelt interakció
4
1. ábra Ilyenkor általában a felhasználó vár a gép által feltett kérdésekre Amint a programnak új bemeneti adatra van szüksége a program futása során üzenetet küld a felhasználónak aki addig vár amíg a program a végére nem ér. Egyszerre csak egy beviteli eszköz kezelésére képes Nincsenek felhasználói felületet kezelő rutinok, nehéz a felhasználói felület készítés. Nehézkes kommunikáció a gép és számítógép között. Esemény vezérelt interakció: Itt a különböző események váltják ki a változásokat a program futásában például az egér gomb lenyomása egy gomb felet, szabja meg a program futásának a menetét. Mivel itt a gép vár a felhasználóra szükséges egy üzenetkezelő hurok létrehozása ahol a felhasználó által küldött üzenetek tárolódnak, feldolgozás céljából. A felhasználó minden pillanatban szabadon választhat Nehezebb programírás Könnyebb felhasználói felület készítés 3.2. Windows ablakkezelés: 1. Windowsos alkalmazások készítéséhez el kell készítenünk a WinMain nevű függvényt itt kap helyet a program üzenetkezelő hurka, amely elkapja a különböző input eszközöböl érkező üzeneteket (egér, billentyűzet) 2. Regisztráljuk az ablakosztályt beállítva az ablakot jellemző 5
tulajdonságokat méret, pozíció, név 3. Elkészítjük az ablakosztály egy példányát 4. Megírjuk a WndProc függvényt melyben kezeljük a programhoz érkező üzeneteket Példa a fentiekre: int WINAPI WinMain( HINSTANCE hThisInstance, // az aktuális példány azonosítója HINSTANCE hThisPrevInstance, // az előző példány azonosítója 32 és 64 bites operációs rendszer esetén mindig NULL LPSTR lpCmdLine, // mutató a parancssorra int nCmdShow // megjelenítési állapot
) { HWND hwnd; //a példány azonosítója MSG messages; // üzenetek tárolásához WNDCLASSEX wincl; wincl.hInstance = hThisInstance; wincl.lpszClassName = szClassName; wincl.lpfnWndProc = WindowProcedure; wincl.style = CS_DBLCLKS; wincl.cbSize = sizeof (WNDCLASSEX); wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION); wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION); wincl.hCursor = LoadCursor (NULL, IDC_ARROW); wincl.lpszMenuName = NULL; wincl.cbClsExtra = 0; wincl.cbWndExtra = 0; wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND; //a fentiekben történt az ablak tulajdonságainak beállítása pl. kurzor típusa, fejléc beállítások if (!RegisterClassEx (&wincl))// itt regisztráljuk a példányt return 0; hwnd = CreateWindowEx (0,szClassName,"Windows App", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,CW_USEDEFAULT, 544,375,HWND_DESKTOP,NULL,hThisInstance,NULL); 6
//regisztráció után létrehozzuk és a következő sorban gondoskodunk a megjelenítésről ShowWindow (hwnd, nFunsterStil); while (GetMessage (&messages, NULL, 0, 0)) { TranslateMessage(&messages); DispatchMessage(&messages); } //az abalak üzenetkezelő hurka, a TranslateMessage() átalakítja a Dispatch message szétosztja az üzeneteket játékoknál GetMeassge()et érdemes PeekMessage()-re cserélni ugyanis az 0-val tér vissza ha nincs üzenet return messages.wParam; } LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage (0); break; default: return DefWindowProc (hwnd, message, wParam, lParam); } return 0; } //A WindowProcedure az ablak eseményere válaszol, itt most csak a kilépést //kezeltük le. Modellezés Képszintézis során a virtuális világot úgymond lefényképezzük. OpenGlben a világot különböző geometriai alakzatok szimulálják. Példáúl a Naprendszer szimulálásánál a különböző bolygók szerepét gömbökkel a legcélszerűbb betőlteni. A gömbök helyzetét a térben és az egymáshoz viszonyított távolságukat legcélszerűbb vektorokkal kifejezni. 7
4 OpenGL főbb parancsai 4.1 OpenGl terminológia Az OpenGl a kép előállítása során különböző lépések sorozatain megy át a következő ábra mutatja ezeket a lépéseket:
2. ábra Display list (megjelenítési lista):Minden ami geometriát vagy pixeleket tartalmaz lementhető a megjelenítési listába azonnali vagy későbbi használatra. Amikor a megjelenítési lista végrejtódott a megmaradt adatokat tovább küldi mintha maga az alkalmazás küldte volna közvetlenül. Miért is jó ez? Tegyük fel, hogy van egy programunk, egy alkalmazásunk ami minden egyes alkalommal kirajzol egy háromszöget a képernyőre, a megjelenítési lista alkalmazásával az OpenGl azt kiszámolja majd lementi a memóriába, innentől kezdve ha hivatkozunk a megjelenítési lista egy objektumára azt a memóriából azonnal elérhetjük. Ez főleg nagy részletességű modellek esetén jelen komoly növekedést a teljesítményben. Evaluators: -minden geometriai primitívet pontok(vertexek) alkotnak. Parametrikus görbéket és felületeket kontrollpontok és polinomiális függvények írnak le. A kiértékelő eljárást ad arra hogy egy pont a felülethez tartozik és hogy a felületet milyen kontroll pontok írnak le. Ezt az eljárást polinomiális térképezésnek hívnak, ami előállíthat felületi normálist, textúra koordinátákat, színeket, térbeli koordinátákat a kontrollpontok értékeiből. Pre-vertex műveletek: Itt lesznek a pontokból geometriai alakzatok. A térbeli
8
kordinátákat a képernyőre vetíti. Ha be vannak kapcsolva bizonyos megvilágítási és textúrázási műveletek szintén itt mennek végbe. Primitive assambly: Itt történik azoknak a pontoknak az eldobása amik nem láthatók a képernyőn. Pixel műveletek: Amíg a geometria adatok egy utat járnak be addig a pixel adatok egy másik utat járnak be az OpenGl képszintézis vezetékében. A rendszermemóriából kiveszi és átalakítja a megfelelő formátumra, egy pixel térképet készít belőle a végén elküldi a textúra memóriának, ez a videokártya saját memóriája, és még tovább küldi a raszterizációs lépésbe is. Texture Assembly: Azért hogy az objektumok realisztikusabbak legyenek textúrákat használunk esetenként egy objektumra akár többet is, azért hogy könnyebben kezelhetőek legyenek itt objektumokat készít belőlük az OpenGl. Rasterization: Itt a pixelekből és geometriai adatokból darabokat képezünk. Minden egyes töredék egy pixelnek felel meg a framebuffer-ben, a vonal vastagsága a pont nagysága vagy épen az elsimítás is itt kerül kiszámításra. Itt kapcsolja össze a pontokat vonalakká, itt állnak össze a pixelek poligonokká. Minden egyes töredék szín és mélység értékeket is kap. Fragment operations: 1. A textúra egységek(texelek) kerülnek előállításra majd minden egyes texelt hozzárendel egy töredékhez. 2.
Alpha test, stencil test, depht-buffer műveletek kerülnek végrehajtásra, ha
3.
be vannak kapcsolva.
Logikai műveletek végrehajtása
Ezek után a töredékeket az OpenGl beírja végső helyére a framebuffer-be. 4.2 Híváslista 9
Ahhoz hogy használatba vehessük először egy azonosítót kell kérnünk, ezt a glGenlist() függvénnyel tehetjük meg. Minek bemenő paramétere a listák maximális száma.. A függvény visszatérési értéke pedig az első lista azonosítója. A lista feltöltését a glNewList paranccsal kezdhetjük meg , aminek első paramétere az előbb generált azonosító a második paramétere GL_COMPILE vagy a GL_COMPILE_AND_EXECUTE egyike. Az első csak megjegyzi, míg a második fel is rajzolja a képernyőre. Fontos hogy a rajzolási műveleteket a glEndList() paranccsal zárjuk le. A már elmentet listáinkat a glCallList függvénnyel hívhatjuk meg. Melynek első paramétere a megjeleníteni kívánt objektum azonosítója. Az esetlegesen feleslegessé vált listáinkat a glDeleteList()el törölhetjük. Itt az első paraméter a törölni kívánt elem azonosítója, míg a második paraméterrel a törölni kívánt elemek darabszámát adhatjuk meg. void init_draw() { id = glGenList(1); glNewList(id, GL_COMPILE); glBegin(GL_TRIANGLES); glVertex2i(1, 1); glVertex2i(1, -1); glVertex2i(-1, -1); glEnd(); glEndList(); } void draw() { //itt lehetnek egyéb rajzolási kódrészek glCallList(id); } 10
4.3 Mátrixok OpenGl minden egyes nézeti vagy modellezési transzformáció egy új 4*4es mátrixot hoz létre. Hasznosságuk egyáltalán nem lebecsülendő és bizonyos problémák megoldhatatlanok lennének nélkülük. Képzeljük el, hogy létre szeretnénk hozni egy pálcika emberkét a térben és ennek külön-külön szeretnénk mozgatni az ízületeit. Az egyszerűség kedvéért emberünk álljon csak egy törzsből két karból és két lábból. Erről egy nagyon jó példát találhatunk az OpenGl Red Book című dokumentációjában 3-4 példaprogramban. A másik alkalmazási mód amikor szeretnénk egyszerre használni egy 2D vetítést a szövegek kiíratásához és 3D vetítést. void draw() { glLoadIdentity(); glMatrixMode(GL_PROJECTION); glPushMatrix(); glOrtho2D(0.0, 1.0, 0.0, 1.0); drawStrings(); glPopMatrix(); glMatrixMode(GL_MODELVIEW); } glLoadIdentity parancstól a legelső mátrix a veremben(az aktuális mátrix) egy egységmátrix lesz. glMatrixMode() a mátrix típusát adhatjuk meg, ami lehet GL_MODELVIEW, GL_PROJECTION, GL_TEXTURE glMultMatrix() az aktuális mátrixot szorozza meg a paraméterként átadott mátrix-al. glPushMatrix() és glPopMatrix() az aktuális mátrixot eggyel lejeb helyezi a veremben, eldobja az aktuális mátrixot és az alatta lévő lesz az aktuális.
11
4.4 Rajzolási parancsok Először nézzük meg a parancsok szerkezetét, az első az előtag gl, glu stb attól függően, hogy a parancs milyen modulból való, ezután jön egy olvasmányosabb név. Bizonyos parancsoknál még lehetséges egy két betűs végződés 2i,2f,3d, ezek a paraméterek számára és azok típusára tesznek utalást. Példa erre a térbe pontot elhelyező parancs glVertex3f() aminek három float típusú paramétert kell átadni egy pont megjelenítéséhez a képernyőn. Mivel az OpenGl nem rendelkezik összetettebb modellekkel, ezért azokat nekünk kell megterveznünk őket geometriai primitívekből (háromszögek, négyszögek). A rajzolás első lépésében eldöntjük, hogy milyen típusú primitívet akarunk használni. Ebben ad segítséget a glBegin() aminek paraméterként átadjuk a típust leíró konstansot, a rajzolási folyamat végét a glEnd() paranccsal jelezzük.
12
3.ábra glBegin() paraméteri void Draw() { glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0, 1.0, 0.0); glBegin(GL_QUADS); glVertex2i(1, 1); glVertex2i(1, -1); glVertex2i(-1, -1); glVertex2i(-1, 1); glEnd(); glFlush(); } A glClear() paranccsal törölhetjük a képernyőt, míg a glColor3f() színt adhatunk a megrajzolni kívánt modellnek az egészet pedig lezárjuk a glFlush()-el aminek hatására biztosan lefut az összes parancsunk a Draw() függvény elhagyása előtt. 5. Modellezés 5.1. Koordinátarendszerek
13
Descartes koordinátarendszer Ebben a rendszerben 3 egymást egy pontban az origóban metsző és egymásra merőleges tengelyből áll. A térben (x, y, z) számhármas míg síkban (x, y) számpár. A következő program rész a Descartes koordinátarendszert valósítja meg: typedef struct tVector3 { tVector3() {} tVector3 (float new_x, float new_y, float new_z) {x = new_x; y = new_y; z = new_z;} tVector3 operator+(tVector3 vVector) {return tVector3(vVector.x+x, vVector.y+y, vVector.z+z);} tVector3 operator-(tVector3 vVector) {return tVector3(x-vVector.x, yvVector.y, z-vVector.z);} tVector3 operator*(float number) y*number, z*number);}
{return tVector3(x*number,
float x, y, z; }tVector3;//Vektorok összeadását kivonását és skaláris szorzását végző struktúra Itt most létrehoztunk tVector3 típust ami rendelkezik x,y,z változókkal ezek a megfelelő egy vektor koordinátáinak mivel két vektor összegén és különbségén a megfelelő tagok összegét és különbségét értjük, ahhoz hogy ezt megértessük a számítógéppel operátor túlterhelést hajtunk végre, vagyis megmondjuk hogy milyen műveleti jelhez pontosan mint kell csinálni. Vegyük például két tVector3 típus változó összeadását megadjuk a + operator majd összeadjuk a megfelelő koordinátákat és visszatérünk egy tVector3 típusú változóval. Ugyanez történik kivonásnál is. Skaláris szorzásnál pedig az egyes tagokat összeszorozzuk a számmal. Gömbi és polár koordinátarendszer Polár koordinátarendszerben egy pontot a középponttól mért távolságával
14
és a pont és a távolság által bezárt szöggel jellemezhetünk. Gömbi koordinátarendszerről akkor beszélhetünk, ha középpontból két félegyenest indítunk a pontot pedig a félegyenesek és a pont által bezárt szögekkel és a pont és az origó közötti távolsággal jellemezhetjük. A térben ábrázolásra egyaránt alkalmas a gömbi vagy a Descartes -féle koordinátarendszer. A gömbi koordinátáinkat könnyen átszámíthatjuk Descartes koordinátákká: x=r*cosΦ∗cosσ, y=r*sinΦ∗sinφ, z=r*cosφ 5.2. Τranszformációk A transzformációknak három csoportját különböztetjük meg. Az első a lineáris transzformáció itt sokféle geometriai művelet végezhető el forgatás, nagyítás, tükrözés, nagyítás, merőleges vetítés. Ezeket mind felírhatjuk egy 3x3 mátrixszorzással. A második egy bővebb család ezt affin transzformációknak nevezzük és a lineáris transzformációkon túl itt már elvégezhető az eltolás is. Jellemzőjük, hogy párhuzamos egyeneseket párhuzamos egyenesekbe viszik át. A harmadik családot projektív transzformációnak nevezzük ez már tartalmazza a középpontos vetítést is. Mielőtt ismertetném az előző műveleteket térjünk ki a mátrix műveletekre , mivel ezeket a műveleteket legjobban mátrixos alakban végezhetjük el. Az összeadás és a kivonás igen egyszerű, ez nem más mint a megfelelő tagok összege és különbsége, csak egyenlő oszlop és sorszámú mátrixoknál tudjuk ezt megcsinálni.
[ ][ ][
a 11 ... a 1m b11 ... b1m a 11b 11 ... a1m b1m a 21 ... a 2m a21 ... b2m a 21 ... a2m b2m . . . . . . = . . . . . . . . . . . . a n1 ... a nm bn1 ... b nm a n1b n1 ... a nmbnm
]
Skalárral történő szorzásnál sem túl bonyolult a helyzet a mátrix elemit egyenként megszorozzuk a számmal.
15
[ ][
a 11 ... a1m ∗a 11 ... ∗a 1m a 21 ... a 2m ∗a 21 ... ∗a 2m ∗ . = . . . . . . . . . . . a n1 ... a nm ∗a n1 ... ∗a nm
]
Két mátrix összeszorzása pedig csak akkor végezhető el ha az első mátrix oszlopainak száma megegyezik a második mátrix sorainak számával, ha az első mátrix m*A a második A*n elemű akkor a kapott mátrix m*n elemű lesz. Maga a művelet pedig úgy néz ki, hogy vesszük az A mátrix első sorát és annak minden elemét külön-külön megszorozzuk a B mátrix első oszlopában lévő elemekkel, majd a szorzatokat összegezzük és így kapjuk meg a C mátrix egy elemét.
[ ][ ][ a 11 ... a 1A b11 ... b 1m a 21 ... a 2A b21 ... b 2m . . . ∗ . . . = . . . . . . a n1 ... a nA b A1 ... b Am
∑ a 1a∗b a1 ∑ a 2k∗bk1 . .
∑ a nk∗bk1
... ... . . ...
∑ a1k∗b km ∑ a 2k∗b km . .
∑ a nk∗b km
]
double *MatrixMul (void *mx, void *mx2, int m, int p, int n) { int k = 0; double *mxv; mxv = (double *) calloc(m*n, sizeof(double)); if(!mxv) exit(1); for (int i=0; i
16
*((double *)mxv+k*n+i) += *((double *)mx2+i*n+j) * *((double *)mx+k*p+j); } if ((i==(p-1)) && (k<(m-1))) { i=-1; k++; } } return mxv; } void main() { . double *mxv; mxv = (double *) calloc(4*3, sizeof(double)); mxv=MatrixMul(A, B,4, 3, 3); . } A MatrixMul függvény C++-ban a mátrix szorzást valósítja meg ahol A egy 4x3 B pedig egy 3x3 mátrix. Ezt az utolsó három paraméter adja meg. Most pedig nézzünk egy pár példát a különböző transzformációkra Az első legyen az origó középpontú tetszőleges szöggel történő forgatás. Itt a z tengely körül elforgatott pont koordinátai a következők: x ' =x∗cos − y∗sin , y ' = x∗sin y∗cos ezt mátrixos alakba átírva ahol az r oszlop vektor az elforgatandó pont koordinátája
[
]
[
cos sin 0 1 0 0 r ' z , =r∗ −sin cos 0 r ' x , =r∗ 0 cos sin 0 0 1 0 −sin cos
] 17
[
cos 0 −sin r ' y , =r∗ 0 1 0 sin 0 cos
]
Három egymás utáni forgatással bármely orientáció előállítható. Tetszőleges objektumot elforgathatunk a térben a mátrix szorzó függvény segítségével ha első paraméternek megadjuk az objektumot leíró pontok mátrixát, második paraméternek pedig a három forgatási mátrix valamelyikét. Másodjára jöjjön az eltolás, ilyenkor a pont koordinátáihoz hozzáadjuk v vektor koordinátáit. x ' =xv x , y '= yv y , z '=z v z x '=S x∗x , y ' =S y∗ y , z ' =S z∗z Utoljára pedig a skálázás: 6. Kamerakezelés 6.1. Nézeti transzformáció Ahhoz hogy a játékos szabadon mozoghasson szükségünk van egy eszközre ami lefényképezi nekünk a virtuális világot (alapesetben) a felhasználó szemszögéből. Ezt az eljárást nézeti transzformációnak nevezzük és ezt legegyszerűbben a gluLookAt() fügvénnyel tehetjük meg. A nézeti transzformáció a világ koordináta rendszeréből a képernyő koordináta rendszerére vált. Ezt úgy éri el, hogy a kamerához három egymásra merőleges vektort rendel ezeket a vektorokat a következő módon határozza meg: c=
kpont−npont f ×c , h= , t =c×h [ kpont−npont ] [ f ×c ]
ahol c a nézeti irányba mutató vektor h a vízszintes t pedig a függőleges, kpont a kamera pozíciója npont pedig ahová néz. A tényleges átváltást pedig a következő képlet adja, M n a nézeti transzformáció: [ x ' , y ' , z ' ,1]=[x , y , z ,1]∗M n=[ x , y , z ,1]∗M m∗M f M
f
a világot úgy forgatja, hogy a kamera és a világ bázisvektorai egybe 18
essenek,
M m pedig a világot tolja el, addig amíg a kamera az origóba nem
kerül.
[
hx t x cx h t cy M f= y y hz t z c z 0 0 0
] [
0 1 0 0 0 0 1 0 , M m= 0 0 1 0 −npont x −npont y −npont z 1
0 0 0 1
]
6.2. Perspektív transzformáció Célja a modellezési és nézeti transzformáció képernyőre történő vetítése. Ezt a gluPerstective() paranccsal tehetjük meg. Az első paramétere a függőleges látószöget adja meg, míg a másodikkal az ablak szélességének és magasságának az arányát adhatjuk meg, az utolsó két paraméterrel az első és hátsó vágósíkok távolságát adhatjuk meg (4.ábra).A képernyőn csak azok a dolgok lesznek láthatóak amik benne vannak az első és hátsó vágósík közötti tartományban.
4. ábra Ezután normalizáljuk a csonka gúlát úgy hogy a bezárt szög 90° legyen. A következő képletben k a kép aránya. 19
[ ] 1 tg∗ ∗k 2
M norm =
0
0 0
1
0
tg∗
0 0
k 2
0 0
0 0 1 0 0 1
Ahhoz hogy a térbeli mozgásra válaszolni tudjunk célszerű elkészíteni egy Camera osztályt amely tartalmazza annak tulajdonságait pl.: helyzetét, irányát, a kamera irányításához szükséges függvényeket. Azt hogy a kameránkat mozgatni tudjuk a térben legegyszerűbben vektorok segítségével oldhatjuk meg. Mivel alapesetben a C++ nem képes vektor típusú változok kezelésére ezért definiálnunk kell egyet (5.1 fejezetben megtettük). class CCamera { public: tVector3 mPos; tVector3 mView; tVector3 mUp; tVector3 mSide; double sk; double fk; void Mouse_Move(int wndWidth, int wndHeight); void Move_Straight(float speed); void Move_Side(bool key, float speed); void Rotate_View(float speed); void Position_Camera(float pos_x, float pos_y, float pos_z, 20
float view_x, float view_y, float view_z, float up_x, float up_y, float up_z); }; 7. Textúrák 7.1 Textúrák definiálása A világban a legtöbb dolog igen komplex anyagi jellemzőkkel rendelkezik ezek mindegyikét lemodellezni szinte lehetetlen vállalkozás „még” , ezért a grafikában a szín megadásár, hogy ne keljen a az objektum adataiból kiszámolni azt kifejlesztették a a bittérképes textúrát. Paraméterezése során pedig az u , v ∈[0,1]2 leképzést adjuk meg ahol az egységnégyzet pontjait hozzárendeljük egy objektum felületi pontjaihoz.
21
5. ábra Nézzünk egy pár példát egyszerűbb felületek paraméterezésére: Háromszögek paraméterezésénél a textúratérben adott 2D háromszöghöz egy a térben adott háromszöget rendelünk. x= Ax ∗uB x∗vC x , y=A y∗u B y∗vC y , z =A z∗uB z∗vC z Ha a fenti képletbe behelyettesítjük a térbeli háromszög pontjait V 1= x 1, y 1, z 1 ,V 2= x 2, y 2, z 2 ,V 3= x 3, y3, z 3 és a textúra háromszög pontjait p 1=u1, v 1 , p 2=u 2, v 2 , p3=u 3, v 3 akkor egy kilenc lineáris egyenletet kapunk ezt megoldva a térbeli háromszög pontjaira kapjuk a megoldást a leképzésünkre.
6. ábra 22
Gombfelületek paraméterezésekor egy r sugarú origó középpontú felület a gömbi koordináta rendszerben: x , =r∗sin ∗cos , y , =r∗sin ∗cos , z ,=r∗cos ahol a a [0,] a pedig [0,2] tartományban van értelmezve. De a test paraméterezését az egységintervallumba eső u és v koordinátákkal kell elvégezni ezért a textúra koordinátákat kifejezzük gömbi koordinátákkal: u=
,v= 2
A gömbfelület paraméterezése ezek után: x u , v=r∗sinv ∗cosu2 , y u , v =r∗sinv ∗sinn2 , z u , v =r∗cosv Általában egy játékban sokkal bonyolultabb testek szerepelnek, ezért közvetítő felületeteket használunk a paraméterezésnél: 1. A testhez hozzárendelünk egy egyszerűbb geometriai alakzatot 2. a közvetítő felület (x',y',z') pontjait a textúratér (u, v) koordinátáival paraméterezzük, ezt S-leképzésnek nevezik 3. az (x',y',z')-hoz hozzárendeljük a textúrázni kívánt test (x,y,z) pontjait, ezt nevezzük O-leképezésnek. O-leképzésnél a vetítősugarak mindig merőlegesek a közvetítő felületre. Az (x',y',z') vetületet az (x,y,z) átmenő vetítősugár és a közvetítő felület metszéspontjaként határozhatjuk meg. 7.2 Textúrázás használata: Az OpenGl 1.1-es kiadása már kezelte a textúrákat. Itt a textúra leképezése két feladatra bomlik a textúra definiálására és a textúra leképezésére. Ahhoz hogy ezt használni tudjuk OpenGl-ben egy nevet kell kérnünk, ezt a glGenTextures(unsigned int x) függvénnyel tehetjük meg, ahhoz x előjel nélküli egész szám. Majd a glBindTexture(unsigned int k, unsigned int x)-val létrehozunk egy x azonosítójú textúra objektumot. A legvégén a glTexImage() függvénnyel pedig az objektumhoz hozzárendeljük a textúrát. A glTexImage() 23
függvény első paraméter GL_TEXTURE_2D bittérképes textúrák esetén a második paraméter nyugodtan maradhat nulla abban az esetben ha csak egyféle felbontással használjuk a textúrát a következő paraméter megadhatjuk, hogy az RGBA színkomponensek közül melyiket szeretnénk használni a negyedik és az ötödik paraméterben a szélességet és a hosszúságot határozhatjuk meg a következő paraméter lehet nulla, ezzel kikapcsoljuk a határsáv használatát, a következő két paraméterben a tárolás módját adjuk meg az utolsó paraméterben pedig átadjuk a képet leíró tömböt .A tovább használni nem kívánt textúra objektumokat a glDeleteTextures() függvénnyel tehetjük meg melynek első paramétere a törölni kívánt textúrák darabszámát míg a másodikba a textúra tömböt adjuk át void Draw() { unsigned int id=1; unsigned char *texture; //Az RGB komponensek egy tömbje texture = loadBMP(p_filename); // A loadBMP egy megadott BMP file-t //tölt a memóriába glGenTextures(1, 1); glBindTexture(GL_TEXTURE_2D, 1); glTexImage2D(GL_TEXTURE_2D, 0, 3, 512, 512, 0, GL_RGB, GL_USIGNED_BYTE, texture); } 7.3 Textúrázási módszerek Az OpenGl képességei nem merülnek ki azzal, hogy különböző objektumokhoz textúrákat rendelünk. A különböző textúrákkal műveleteket hajtunk végre, például nagyíthatjuk őket, vagy két különböző textúrát összemoshatunk, vagy elhagyhatunk bizonyos színkomponenseket egy képből. Ezzek az eljárásokkal nagyon könnyen szimulálhatunk egy pár a valóságban jól 24
ismert jelenséget, ablakokon átszűrődő fényt, vagy egy falat amit ép egy zseblámpa fénye világit meg. Multitextúrázás Abban az esetben ha egy modellen, „gondoljunk itt most egy ember alakra azt szeretnénk ha a program külön kezelné a ruháit tehet külön képfájlban legyenek az egyes összetevői (póló,cipő,nadrág) ilyen és ehhez hasonló esetekben jön jól a multitextúrázás amely használatával egy objektumhoz akár több textúrát is rendelhetünk. Ráadásul sokkal gyorsabban valósítja ezt meg mintha több textúrát mosnák össze a textúra térben, ugyanis a multitextúrázást az OpenGl egy megjelenítési meneteben tudja elvégezni ellentétben a GL_BLEND-el. void DrawB() { glBindTexture(GL_TEXTURE_2D, 1); glBegin(GL_TRIANGLES); glTextCoord2f(0.0, 0.0);glVertex3f(0.0, 0.0, 0.0); glTextCoord2f(1.0, 0.0);glVertex3f(1.0, 0.0, 0.0); glTextCoord2f(1.0, 1.0);glVertex3f(1.0, 1.0, 0.0); glEnd(); glEnable(GL_BLEND); glBelendFunc(GL_ZERO, GL_SRC_COLOR); glBindTexture(GL_TEXTURE_2D, 2); glBegin(GL_TRIANGLES); glTextCoord2f(0.0, 0.0);glVertex3f(0.0, 0.0, 0.0); glTextCoord2f(1.0, 0.0);glVertex3f(1.0, 0.0, 0.0); glTextCoord2f(1.0, 1.0);glVertex3f(1.0, 1.0, 0.0); glEnd(); glDisable(GL_BLEND); } 25
A fenti kódrészben bizony jól észrevehető a GL_BLEND hátránya, ahhoz hogy több textúrát rajzoljunk egy felületre többször is kell azt kirajzoltatni. void DrawM() { glActiveTextureARB(GL_TEXTURE0_ARB); glBindTexture(GL_TEXTURE_2D, 1); glActiveTextureARB(GL_TEXTURE0_ARB); glBindTexture(GL_TEXTURE_2D, 2); glBegin(GL_QUADS); glMultiTexCoord2fARB(GL_TEXTURE0_ARB, 0.0, 0.0); glMultiTexCoord2fARB(GL_TEXTURE1_ARB, 0.0, 0.0); glVertex3f(0.0, 0.0, 0.0); glMultiTexCoord2fARB(GL_TEXTURE0_ARB, 1.0, 0.0); glMultiTexCoord2fARB(GL_TEXTURE1_ARB, 1.0, 0.0); glVertex3f(1.0, 0.0, 0.0); glMultiTexCoord2fARB(GL_TEXTURE0_ARB, 1.0, 1.0); glMultiTexCoord2fARB(GL_TEXTURE1_ARB, 1.0, 1.0); glVertex3f(1.0, 1.0, 0.0); glEnd(); } Plakát és fénytérkép A plakátozás egyik leghasznosabb felhasználási módja amikor különböző virágokat, fákat, szeretnénk elhelyezni. Az OpenGl képes a textúrát RGBA szín négyessel megadni ahol A az átlátszóságot jelenti, nulla és egy között értékek megadásával szabályozhatjuk a anyag áteresztő képességét, ahol nulla a teljesen átlátszó anyagot jelenti. Ezt a módszert a GL_BLEND-el tehetjük meg, ez a textúratérben a már meglévő és az új kép pixeljeinek súlyozott átlagát képzi le. A súlyozó tényezőt a glBelendFunc(forrás, cél) függvénnyel állítható be. 26
Az
R f ,G f , B f , A f a forráskép pixeljeit leíró színnégyes,
Rc , G c , B c , Ac a
célkép pixeljeit, ebben az esetben az összemosó művelet: R=r f ∗R f r c∗R c ,G=g f ∗G f g c∗G c , B=b f ∗B f b c∗Bc , A=a f ∗A f a c∗Ac ahol r f , b f , g f , a f és az r c , bc , g c , ac a glBelendFunc első és második paraméterével állíthatok be. Ilyen paraméterek például a GL_ZERO, GL_DST_COLOR?GL_SRC_ALPHA,GL_DST_ALPHA, amelyekkel a forrás súlyozási értékeit állíthatjuk.. void Draw() { glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 1); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 0); //Itt következik a modell rajzolását végző programrész.. glDisable(GL_BLEND); glDisable(GL_ALPHA_TEST); glDisable(GL_TEXTURE_2D); } Fénytérképek Fentebb említett lámpa és fal esete itt kétféle megoldás is létezik az egyik, hogy kiszámoljuk a világítás helyét, ez egy elég költséges művelet. A második lehetőség a multitextúrázás amely során két textúrából készítünk egyet. Itt az első textúra legyen a fal, a második a fénytérkép textúrája. Ennek az eljárásnak a hibája, hogy csak két dimenzióban képzi a 27
megvilágítást, ezért ha más-más irányból nézünk rá mindig ugyanazt a megvilágítást látjuk.
7. ábra 8. Animált karakterek 8.1. Fájl kezelés alapjai Fájlok alatt a számítógép háttértárain névvel ellátót adatokat értjük. Csoportosításuk a bennük tárolt adatok elérése alapján történik. Az első csoportba tartoznak azok amelyek adatait soronként vagy másképpen szekvenciálisan érhetjük el. A második csoportba tartoznak a közvetlen elérésű fájlok, ezekben a pozicionálás műveletét használva mondjuk meg melyik részt szeretnénk olvasni a fájlból. A továbbiakban a bináris állománykezeléssel foglalkozunk. Első lépesben készítünk egy mutatót ami a FILE-struktúrára mutat, második lépésben megnyitjuk a fájlt még ugyanitt megadjuk a fájl elérésének típusát(szöveges,bináris) és annak módját(olvasás,írás,bővítés), majd következik az állomány tartalmának a feldolgozása. Parancsok: FILE *fopen(const char *fájlnév, const char *mode); memória terület írása fwrite(const void *ptr, sizeof(típus), n, FILE *stream); memória terület olvasása size_t fread(const void *ptr, sizeof(típus), n FILE *stream); pozicionálás fseek(FILE *stream, long offset, int type); a fájl lezárása fclose(FILE *stream);
28
A const void *ptr a memória terület címét jelenti amelyből olvasunk vagy írunk a type a következő lehet SEEK_CUR-a fájl mutató aktuális pozíciója, SEEK_END a fájl vége, SEEK_SET-a fájl eleje. 8.2. MY1 formátum A legtöbb játékban találkozhatunk különféle karakterekkel amelyek sétálnak, futnak, felugranak, lőnek stb. . Az ilyen belső mozgással rendelkező figurák leírásához az összes mozgásfázist ismerni kell. Bizonyos fájlformátumok tárolják ezeket, és még más az objektumra jellemző adatokat textúra azonosítójukat, az animáció kezdet és végét a maximális vertex és poligon számot ilyenek 3DS, MAX, MD2, MD3, VRML. Ilyen fájlokat például 3D Studio Max, Maya, Milkshape, Blender hozzhatunk létre. Most nézzünk meg egy egyszerűbb csak a karakter geometriájának és mozgásfázisai tárolására alkalmas programot. Ezek a programok általában fejrésszel kezdődnek, itt kerülnek tárolásra fájl belső szerkezetét jellemző adatok.
29
8. ábra struct sHeader { int magic; int numVertex; int numFace; int numFrame; int numUV; int offsetFrame; int offsetFace; int offsetTFace; int offsetUV; int offsetEnd; }; class MY1_Object { public: sVertex *pVertex; sFace *pFace; sFace *pTFace; sFrame *pFrame; sUV *pUV; sHeader Header; int c_Frame; unsigned int id; GLuint theId[255][255]; 30
MY1_Object(char *file_name, unsigned int id); ~MY1_Object(); void Time(int n_Frame); int HitTesting(tVector3 p, tVector3 d, int j); void Draw(); }; A fenti adatszerkezettel lehet leírni egy MY1 objektumot, ahhoz hogy létrehozzunk egyet elég meghívnunk a konstruktorát MY1_Object(char *file_name, unsigned int id) a megfelelő paraméterekkel és máris betölti a memóriába a modelt. MY1_Object ptrDragon(models/Dragon.MY1, 1); //statikus memóriafoglalás MY1_Object *ptrDragon; ptrDragon = new MY1_Object(models/Dragon.MY1, 1);//dinamikus helyfoglalás Az első esetben a fordító kezeli az objektumot, míg a dinamikus helyfoglalásnál nekünk kel gondoskodni a példány törléséről, akkor van szükségünk erre ha az objektum még a program futása közben megszűnik létezni. Az osztálytagokhoz történő elérés dinamikus esetben -> operátoral történik míg statikus esetben a . operátorral hivatkozunk rá. //MY1_Object konstruktorát megvalósító programrész MY1_Object::MY1_Object(char *filename, unsigned int id) { int i,j = 0; FILE *fp = fopen(filename, "rb");//file megnyitása this->id = id; le
fread(&Header, sizeof(sHeader), 1, fp);//beolvassuk a header részt ez írja a fájl szerkezetét pFrame = new sFrame [Header.numFrame];//itt foglaljuk le a memória területet pVertex = new sVertex[Header.numVertex]; pFace = new sFace [Header.numFace]; pTFace = new sFace [Header.numFace]; 31
nFace = new sVertex[Header.numFace]; pUV = new sUV [Header.numUV]; fseek(fp, Header.offsetFace, SEEK_SET);//betöltjük a memóriába a különböző blokkokat fread(pFace, sizeof(sFace), Header.numFace, fp); fseek(fp, Header.offsetTFace, SEEK_SET); fread(pTFace, sizeof(sFace), Header.numFace, fp); fseek(fp, Header.offsetUV, SEEK_SET); fread(pUV, sizeof(sUV), Header.numUV, fp); fseek(fp, Header.offsetFrame, SEEK_SET);//egyenként beolvassuk az összes Frame tartalmát for(i=0; i
finish = clock(); } while((finish - start)/CLOCKS_PER_SEC < speed);//itt addig várunk amíg elnem érjük az általunk definiált sebességet this->c_Frame = n_Frame;//itt pedig beállítjuk aktuális Frame-nek a következőt } A fentebbi eljárást természetesen a program fő eseménykezelő hurkában célszerű meghívni a különböző mozgó karakterekre. A képernyőre történő vetítést pedig az OpenGl megjelenítési implementációján belül a Draw() metódus valósítja meg, melyet az inicializációs részben hívunk meg, majd a glCallList() használatával férünk hozzá a különböző Framek-hez. 8.1. Ütközés tesztelés Ez egy eléggé nagy számítást igénylő rész, ezért egy jobb programban kétféle ütközéstesztelést kezelnek. Az első amikor az objektumok az őket körülvevő tájjal ütköznek, a második amikor az objektumok egymással vagy kisebb dolgokkal tűzgolyó, lézer stb. ütköznek. Ennek azért van értelme mert második esetben nem ütköztetik az objektumok minden pontját, hanem ezeket egy alacsonyabb részletességű modellbe rendszerint gömbbe vagy kockába helyezik el és ezekkel a pontokkal végzik el az ütközés tesztelést, így spórolnak a számítási költségen. Itt mi most csak az első esettel fogunk foglalkozni. Ezen típusú ütközés kezelésére elég sok megoldás született már, pont-féltér ütközés vagy pont-poliéder,poliéder-poliéder. A programban a sugár-háromszög ütközéskezelés került megvalósításra.
33
9.ábra Befoglaló testek
34
10. ábra Az első lépésben meghatározzuk, hogy az R(sugár) metszi-e -t ,ha nem akkor nem metszi a T háromszöget sem, ebben az esetben természetesen nincs ütközés. Abban az esetben ha R metsz a síkot akkor meg kell határozni, hogy P1 pont rajta van e a T háromszögön V 0, V 1, V 3 a háromszög pontjai, n pedig a háromszögre merőleges normálvektor V 1−V 0×V 2−V 0 .Ahhoz hogy meghatározzuk egy pont benne van-e egy háromszögben a következő összefüggést használjuk V s , t =V 0s V 1−V 0tV 2−V 0=V 0sutv ahol s ,t ∈ R és u=V 1−V 0 , v =V 2−V 0 a T háromszög élei. A P=V(s,t) pont rajta van a háromszögön, ha s>=0, t>=0 és s+t<=1.Ezért az adott P1 ponthoz elég csak meghatározni (s1 és t1) és megvizsgálni teljesülnek-e rájuk a fentebbi egyenlőtlenségek, ha igen akkor rajta vannak a T háromszögön. Továbbá a P=V(s,t) pont a T éléi egyikén van ha az alábbi feltételek közül egy igaz s=0,t=0, s+t=1. V0(0,0) V1(1,0) V2(0,1). Vegyük w=(P1-P0) ami egy újabb vektor a síkon akkor a következő egyenlethez jutunk w=sutv mindkét oldalra kereszt szozást végzünk v^-vel és u^-val ahol u^ és v^ egységvektorok w*v^=su*v^ + tv*v^=su*v^ megoldjuk s1 w*u^=su*u^+tv*u^ = tv*u^ ezt t1-re
35
oldjuk meg. Az ütközés tesztelést a HitTesting(tVector3 p, tVector3 d, int j) metódus kezeli az első paraméterben átadjuk az objektum helyét a másodikban a nézőpontot, a harmadik pedig a tesztelni kívánt háromszög indexére. int MY1_Object::HitTesting(tVector3 p0, tVector3 p1, int j) { tVector3 v0(this->pFrame[c_Frame].pVertex[pFace[j].a].x, this->pFrame[c_Frame].pVertex[pFace[j].a].y, this->pFrame[c_Frame].pVertex[pFace[j].a].z); tVector3 v1(this->pFrame[c_Frame].pVertex[pFace[j].b].x, this->pFrame[c_Frame].pVertex[pFace[j].b].y, this->pFrame[c_Frame].pVertex[pFace[j].b].z); tVector3 v2(this->pFrame[c_Frame].pVertex[pFace[j].c].x, this->pFrame[c_Frame].pVertex[pFace[j].c].y, this->pFrame[c_Frame].pVertex[pFace[j].c].z); //a a háromszög pontjaiból vektorokat készítünk tVector3 u, v, n, I; tVector3 dir, w0, w; float r, a, b; u=v1-v0; v=v2-v0; n=u%v; if(n.x==0 && n.y==0 && n.z==0)//degenerált-e ha igen kilépünk return 1; dir=p1-p0; w0=p0-v0; a=-(n*w0); b=n*dir; if(fabs(a)<0.0000001) return 1; r=a/b; if(r<0.0) return 1;
36
I=p0+(dir*r); float uu,uv,vv,wu,wv,D; uu=u*u; uv=u*v; vv=v*v; w=I-v0; wu=w*u; wv=w*v; D=uv*uv-uu*vv; //itt meg keressük az s és t koordinátákat float s,t; s=(uv*wv-vv*wu)/D; if(s<0.0||s>1.0) return 1; t=(uv*wu-uu*wv)/D; if(t<0.0||(s+t)>1.0)//ha kívül esik a háromszögön kilépünk return 1; tVector3 l = I-p1; float d = l.Length(); if( (d<=800.0) && (d>=0.0) )//ha egy meghatározott távolságon belül volt ütközés azt jelezzük return 0; return 1; }
37
Irodalomjegyzék Szirmay-Kalos László, Antal György, Csonka Ferenc,2004 Háromdimenziós grafika, animáció és játékfejlesztés The Official Guide to Learning OpenGL, Version 1.1 1997 www.opengl.org/developers/documentation
38