OpenGL.Hu 02 – A GLUT library

Sok helyen látni a Neten tutorialokat, melyek azzal indulnak, hogy hosszadalmas kódokkal a Win32 API-t használva hoznak létre OpenGL ablakokat, s ezzel nem egy esetben el is bátortalanítják a kezdőket. A vicces az egészben, hogy azok, akik ezeket a példaprogramokat és tutorialokat írják, sosem foglalkoznak a dolognak ezzel a részével, hanem helyette egyszer megírnak egy keretrendszert, ami ezt elrejti előlük, vagy éppen egy már létezőt használnak. Ebben a cikkben az egyik legrégebbi, legismertebb és legegyszerűbb ilyen keretrendszerről, a GLUT-ról lesz szó.

A GLUT nem új dolog, majdnem olyan régi, mint maga az OpenGL. A fejlesztésekor egy cél lebegett a kitalálói szeme elott: egy egyszerű, platformfüggetlen OpenGL keretrendszer (vagy idegen szóval: framework) kifejlesztése. A GLUT megfelel ezeknek az elvárásoknak: jónéhány fordítót és operációs rendszert támogat és a használata meglepően egyszerű. Azok kívül, hogy keretrendszerként funkcionál, számos segédfüggvényt tartalmaz, így ezek közül is fogunk csemegézni.

A GLUT, mint keretrendszer

A GLUT egyik fő feladata az Input (billentyű, egér) és az Output (ablak / grafikus képernyő) egyszerű kezelése. Ehhez a GLUT úgynevezett callback-függvényeket használ. Ezek egyszerű függvények, melyeknek adott paraméterezése van és mi hozzuk létre oket a GLUT számára. Ezek a függvények aztán bizonyos feltételek mellett, akár periodikusan is meghívásra kerülnek a GLUT által.

Mint említettem, a callback-eket mi hozzuk létre, majd pedig a GLUT használja őket. Ehhez természetesen valahogy a tudomására kell hoznunk, hogy milyen néven hoztuk őket létre. Ez az alábbihoz hasonló, elsőre talán furcsának tűnő paraméterezésű függvényekkel történik:

void glutDisplayFunc ( void (*func)(void));

Ez a paraméterezés azt jelenti, hogy egy olyan függvényt vár paraméterként a glutDisplayFunc, amelynek a visszatérési értéke void és nincs paramétere (tehát void a paraméterlistája). Az alábbiakban nézzünk meg néhány ilyen callback-et!

A legfontosabb callback-ek

Természetesen mivel kezdhetném ezt a listát, mint a renderelő callback-el, amely periodikusan hívódik meg, amikor az OpenGL az aktuális frame (vagyis képkocka) tartalmát rendereli. A renderelő callback-nek az alábbi paraméterezést kell követnie:

void MyRender(void) {
...
}

A GLUT számára az alábbi hívással állítjuk be a render callback-et:

glutDisplayFunc(MyRender);

Egy másik, periódikusan meghívódó callback, az idle (vagyis üresjárati) callback függvény, amelynek a haszna pedig annyi, hogy meghívódik minden olyan alkalommal, amikor a grafikus vezérlő dolgozik – pl. éppen renderel – viszont a processzor “ráér”. Itt elvégezhetők olyan számítások például, amelyek az animációkhoz kellenek, stb.

Megjegyezném, hogy az M.J. Kilgard (Silicon Graphics Inc.) által összeállított GLUT API v3 referencia szerint nem ajánlott dolog az idle callback függvényben túl sok számítást végezni, mert előfordulhat, hogy a ló túloldalára esünk át azáltal, hogy már nem a processzor fog szabad lenni, hanem más alkalmazások futtatása akad meg, miközben a videovezérlő már készen áll az új renderfolyamat indításához. (Személyes tapasztalat: nekem ezt még – ésszerű kereteken belül – szándékosan sem sikerült így elérnem, tehát nem igazán tudom elmondani, mennyi az a “nem túl sok” számítás.)

Az üresjárati callback formátuma:

void MyIdle(void) {
...
}

és beállítása:

glutIdleFunc(MyIdle);

Még egy dolog, amit az idle callback-el kapcsolatban el kell mondani: ha létrehoztuk ilyen callback-et, akkor a futása végén eldönthetjük, hogy akarjuk-e, hogy a GLUT frissítse a képernyőt, vagy ne. Ha szükséges (mert pl. változott egy objektum helye, etc), akkor ezt a GLUT-nak a

glutPostRedisplay();

hívással kell jeleznünk.

Szintén hasznos lehet még a timer callback függvény, amellyel beállíthatunk egy szabályos időközönként meghívott függvényt. Ez főleg akkor lesz majd hasznos, mikor adott sebességű animációkat akarunk előállítani. Ugyanis az idle callback-es megoldás nagyban függ a processzor órajelétol és gyorsabb számítógépen így az animáció (játék) is gyorsabbá válik, ami nem biztos, hogy jó. A timer függvény az előzőekkel ellentétben már rendelkezik paraméterrel is, s a következőképpen néz ki:

void MyTimer(int value) {
...
}

Beállítani az alábbi hívással lehet:

glutTimerFunc(interval, MyTimer, value);

Észrevehetjük, hogy mind a két függvénynek van value paramétere. A callback, mikor a GLUT meghívja, azt kapja meg a value paraméterben, amit a glutTimerFunc() value-jának adtunk annak meghívásakor. Ennek azért van haszna, mert ilyen időzített hívást nem lehet visszavonni, hanem helyette – érdekes megoldás! – a value paraméter alapján figyelmen kívül lehet hagyni szükség esetén a végrahajtását.

A glutTimerFunc interval nevű paramétere az időintervallum, milliszekundumban megadva, amely időközönként a MyTimer függvény meg fog hívódni.

Egy másik callback, amit sokat fogunk használni, a reshape (átméretezési) függvény, mely akkor hívódik meg, amikor az aktuális ablak mérete megváltozik. A reshape callback paraméterezése:

void MyReshape(int w, int h) {
...
}

A (w,h) páros értelemszerűen az új méretet hordozza, amelyet az ablak felvett. A beállításért felelős GLUT függvényhívás ebben az esetben csak a callback függvény nevét várja el, mint paramétert:

glutReshapeFunc(MyReshape);

Input-kezelés a GLUT világában

Eddig még nem volt szó arról, hogy hogyan történik az egér és a billentyű kezelése, pedig jó néhány callback foglalkozik ezekkel. Mivel a GLUT callback-ekről bőséges anyag található a már fent említett Kilgard-féle referencia-könyvben (angol nyelvű!), amely .PDF változatban jó néhány OpenGL oldalról elérhető, csupán 1-2 fontosabb input-kezelő callback-et említenék itt meg.

Első körben kezdjük a billentyűkkel! A GLUT kétféle callback definiálására ad lehetőséget: az egyik akkor hívódik meg, ha lenyomunk egy billentyűt, a másik meg ? meglepő módon ? akkor, mikor felengedjük azt. A billentyűlenyomáshoz a callback a következő formátumú kell, hogy legyen:

void MyKeyCallback(unsigned char key, int x, int y) {
...
}

Mindjárt adódik is a kérdés: mi a bánat az az x ill. y? A válasz röviden: egérkurzor-koordináták, amelyek akkor érvényesek, mikor a billentyűt lenyomtuk. Hogy ez miért van így? Nos, miután a GLUT rendszerfüggetlen dolognak készült, így ? és ez személyes vélemény, lehet, hogy más okai vannak ? a Unix rendszerek X nevű ablakozófelületével való kompatibilitást szolgálja. Ott ugyanis, egy ablakon történő fókuszáláshoz bevett dolog, hogy nem kell annak a területére kattintani, elég egy másik ablak vagy kontroll fölé vinni az egeret ? ellenben a Windows esetében megszokott módszerrel. Mostanság ez a két paraméter szerintem a ?még jó lehet valamire? kategóriába tartozik. Most pedig lássuk, hogyan állítjuk be ezt a callback-et:

void glutKeyboardFunc  (MyKeyCallback);

Mint említettem, a GLUT a billentyű felengedéséhez is ad segítséget, mégpedig egy, a fentihez teljesen hasonló callback megadásának lehetőségével. Ezt most nem részletezném a hasonlóság miatt, így csak a beállítására térek ki. Íme:

void gluKeyboardUpFunc (MyKeyCallback);

Ezzel kb. a keyboard-témát ki is merítettük. Hogy mi maradt? Természetesen az egér! (Igazából a GLUT támogat még pl. TrackBall-t is, de szóljon, akinek van otthon és nem kompatibilis Windows alatt egy sima, mezei egérrel!) Kezdjük rögtön a kattintások figyelésére alkalmas callback-el. Ennek valami ilyesminek kell lennie:

void MyMouseClick (int button, int state, int  x, int y) {
...
}

Na, itt azért van 1-2 paraméter! Tehát, szépen sorban: a button mutatja meg, melyik gombbal kattintottunk. Itt kapásból elszomorítom a népet: a GLUT nem mai dolog, így támogat kb. három gombot és ennyi. Se görgő, se 7 gombos gamer egér… Azért egy átlagos játékhoz persze ez is elég. A button paraméter értékei a következők lehetnek:

  • GLUT_LEFT_BUTTON – bal egérgomb
  • GLUT_MIDDLE_BUTTON – középso egérgomb
  • GLUT_RIGHT_BUTTON – jobb egérgomb

A következő a state nevu paraméter. Ez mutatja, hogy egérgomb-lenyomás vagy felengedés történt. Figyelem! Abból kifolyólag, hogy a GLUT ugyanazt a callback-et hívja mindkét esetben, ez a függvény egy sima kattintás esetén mindig 2x hívódik meg! A state paraméter lehetséges értékei:

  • GLUT_UP – felengedés történt
  • GLUT_DOWN – lenyomás történt

Végül – meglepő módon – a callback beállítása:

void glutMouseFunc  (MyMouseClick);

A maradék két callback-et egy kalap alá venném, tekintve a hasonlóságukat: mindkettő az egér elmozdulása esetén hívódik meg. Az egyik akkor, ha az egérgomb le van nyomva (tehát dragging esetén), a másik pedig akkor, ha fel van engedve (a GLUT doksi ezt passive motion vagyis passzív elmozdulás néven emlegeti). A két callback paraméterezése teljesen egyforma, de mivel egyetlen paraméter sem jelzi, milyen állapotban vannak az egérgombok, mégsem lehet egy callback-et használni a kettő helyett. Íme egy lehetséges definíció:

void MyMotionFunc(int x, int y) {
...
}

illetve a passzív változat:

void MyPassiveMotionFunc(int x, int y) {
...
}

és természetesen a beállításuk:

void glutMotionFunc (MyMotionFunc);
void glutPassiveMotionFunc  (MyPassiveMotionFunc);

A GLUT, mint megjelenítő felület

Habár a GLUT elég egyszerű működésű, pár dolgot nem árt az inicializálásánál megadni. Az egyik ezek közül, hogy milyen tulajdonságokkal fog rendelkezni a renderelő felület, amit használni fogunk. Habár kezdőknek ezek a paraméterek még nem mondanak sokat, később hasznos lehet esetleg majd ide visszaugrani. Tehát, az elso és legfontosabb inicializációs függvény a

void glutInitDisplayMode  ( unsigned int mode );

Egyetlen paramétere a mode, amely bitmaszkként funkcionál, tehát több értéket is beállíthatunk neki, logikai VAGY (c++ban: |) művelettel összekapcsolva. Pl:

glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_STENCIL);

Ez a mód pl. egy stencil (nem témája a cikknek) és depth bufferrel (hasonlóan nem témája a cikknek) ellátott ablakot hoz létre. A GLUT_DOUBLE érték azt jelenti, hogy double buffered ablakunk lesz, de ebbe most nem másznék nagyon bele. A lényege röviden: két surface van, az egyik látszik, a másikra rajzolunk, majd ha a rajzolás kész, az OpenGL kicseréli oket. Ez elég alap technológia, jó tudni, hogy van, de mostanság ez már nem nagyon kavar bele egy átlagos 3D programozó munkájába.

A lehetséges mode értékeket nem sorolom fel, mert egyrészt jó sok van, másrészt meg csak pár van, amiket az esetek 90%-ában használunk, azokat meg fent leírtam. Ha valakit nagyon érdekel a dolog, a webes keresőbe beírt “glut reference” dolog elég gyorsan szolgáltathat plusz infót. Emellett még megjegyezném, hogy a stencil buffer egyszerűbb progiknál elég opcionális, s ha későbbiekben használatra kerül, úgyis részletezni fogom.

A másik inicializációs függvény, amely csak ablakos alkalmazások esetén kell, a

int glutCreateWindow  ( char *name );

melynek egyetlen paramétere az ablak cimkéje. Erre nem is szándékozom több szót pazarolni, használata úgyis majd minden példaprogramban alapdolog lesz.

Teljes képernyőn…

Ennek a résznek a lényege röviden: hogyan váltsunk teljes képernyős módra a GLUT-tal. Lássunk is egyből egy kis kódot! A GLUT-alapú programunk inicializációs része ablakos mód esetén valahogy így néz ki:

glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutCreateWindow("OpenGL Ablak");
glutDisplayFunc(RenderScene);
glutMainLoop();

Igazából elég egyszerű kód, semmi extra: GLUT inicializálás, OpenGL-felület beállítása, ablak-létrehozás, render (és ha kell más egyéb) callback beállítása és aztán indul is a móka a glutMainLoop() függvénnyel – ez indítja a GLUT eseményfigyelő/renderelő ciklusát. Nos, ez a pár sor az, amelyik teljes képernyős alkalmazásoknál egy kicsit másképp alakul?

A teljes képernyős módot a GLUT-ban igen találóan Game Mode-nak nevezték el, utalva arra, hogy főleg mire fogják a fejlesztők használni. A Game Mode inicializálása némileg eltér az ablakos módétól, hiszen nem kell például címsor, viszont szükség van a felbontás, színmélység direkt beállítására. Ez utóbbira eddig azért nem volt szükség mert grafikus felületű operációs rendszerek (továbbiakban: OS) esetén minden ablak ugyanazokat beállításokat létrejöttekor automatikusan lekérdezi le az OS-tol. Az új inicializáló rész az alábbihoz hasonló módon kell, hogy kinézzen:

glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH);
glutGameModeString("1024x768:32");
glutEnterGameMode();
glutDisplayFunc(RenderScene);
glutReshapeFunc(SetupScreen);
glutIdleFunc(SceneIdle);
glutMainLoop();
return 0;

A glutGameModeString() és a glutEnterGameMode() függvények meghívásával történik a fenti beállítás. Az előbbi segítségével átadjuk a GLUT-nak azt, hogy milyen paraméterekkel szeretnénk a Game Mode-ot inicializálni. Az utóbbi függvény teszi meg ténylegesen a Game Mode bekapcsolását. Megjegyzés szintjén hozzátenném, hogy a már korábban is említett M. J. Kilgard által írt referencia a glutFullScreen() használatát ajánlja erre a célra, azonban az a függvény nem teszi lehetővé a felbontás/színmélység beállítását, hanem az OS GUI-tól (az operációs rendszer grafikus felülete) lekért adatokat használja.

Egyéb hasznos GLUT függvények

void glutSolidCube  ( GLdouble size );

Egy tömör kockát jelenít meg, melynek size egység méretű élei vannak.

void glutWireCube  ( GLdouble size );

Ugyanaz mint a fenti, csak “drótháló” lesz belőle, nem pedig tömör objektum.

void glutSolidTeapot  ( GLdouble size );

Ez a klasszikus teáscsésze, amivel általában mindent demonstrálni szoktak más site-okon, könyvekben, etc.

void glutWireTeapot  ( GLdouble size );

Ez pedig ugyanaz, csak szintén drótvázzal.

És ami kimaradt…

A GLUT az eddig említett funkciókon kívül rendelkezik még számos segédfüggvénnyel, melyekre nem tértem ki. Ilyenek például kezdetleges GUI elemek megjelenítése, etc. Ezekrol a többször is említett Kilgard-féle referenciában minden szükséges információ megtalálható.

Pár szó a példaprogramról

A példaprogramban próbáltam a GLUT használatával megmutatni mindent, amit a cikkben leírtam. Most pedig, vágjunk is bele a dolgokba! A program teljes képernyőn (game mode) egy fehér négyzetet jelenít meg, amit az A, S, D, F gombokkal, illetve az egérrel lehet mozgatni. Igyekeztem minél több callback használatát demonstrálni, s úgy tűnik, ez többé-kevésbé sikerült is.

A program futtatásához szükséges a glut32.dll, amely megtalálható a glut több file-jával együtt a Letöltések részben. Amennyiben lenne a kódban olyan rész, amelyik nem elég egyértelmű, szivesen válaszolok bármilyen kérdésre emailben.

OpenGL.Hu 02 (8.6 KiB)

Címke: ,
Kategória: OpenGL.Hu

What the …

Egy megszállott kóder semi-oldschool blogja, mindenről, ami programozás, 3D, meg ilyesmi. Tapasztalatok, vélemények, megmondás és hasonlók, legtöbbször koffeinmámoros állapotban. Na meg cikkek...

Látogatók

Ezt az oldalt
322013
alkalommal látogatták meg.


(Beleértve a botokat is. Nesze neked, WWW! ;) )