„Számítógépes grafika házi feladat tutorial” változatai közötti eltérés
309. sor: | 309. sor: | ||
* A projekciós mátrixot állító függvényekkel ellentétben, ezeket akkor is szoktuk használni, ha a modellezési mátrix nem egységmátrix. | * A projekciós mátrixot állító függvényekkel ellentétben, ezeket akkor is szoktuk használni, ha a modellezési mátrix nem egységmátrix. | ||
* Ezen függvényeknek tetszőleges kombinációját lehet használni, de a sorrend nem mindegy. | * Ezen függvényeknek tetszőleges kombinációját lehet használni, de a sorrend nem mindegy. | ||
* Egy transzformáció meghívásakor a | * Egy transzformáció meghívásakor annak a mátrix hozzászorzódik a GL_MODELVIEW mátrixhoz (jobbról). Emlékeztető: a mátrix szorzás, és a mátrix-vektor szorzás asszociatív. Ez azt jelenti, hogy két transzformációs mátrix összeszorzása ugyan úgy transzformál egy tetszőleges vektort, mint ha a két mátrixal külön szoroztuk volna be. | ||
* A transzformációk fordított sorrendben fejtik ki hatásukat, mint ahogy meghívjuk őket, de ez így intuitív, így haladhatunk a hierarhiában föntről lefele, ha nem így lenne, akkor pl. egy autó kirajzolásánál, azzal kéne kezdenünk, hogy megmondjuk, hogy a dísztárcsa a kerékhez képest hogy helyezkedik el, és csak a legvégén mondhatnánk meg, hogy az autó egyáltalán hol van. | * A transzformációk fordított sorrendben fejtik ki hatásukat, mint ahogy meghívjuk őket, de ez így intuitív, így haladhatunk a hierarhiában föntről lefele, ha nem így lenne, akkor pl. egy autó kirajzolásánál, azzal kéne kezdenünk, hogy megmondjuk, hogy a dísztárcsa a kerékhez képest hogy helyezkedik el, és csak a legvégén mondhatnánk meg, hogy az autó egyáltalán hol van. | ||
* [http://pastebin.com/0UiK3fVa Példa a transzformációk sorrendjére:] | * [http://pastebin.com/0UiK3fVa Példa a transzformációk sorrendjére:] |
A lap 2014. január 23., 20:51-kori változata
Ez az oldal a korábbi SCH wikiről lett áthozva.
Ha úgy érzed, hogy bármilyen formázási vagy tartalmi probléma van vele, akkor, kérlek, javíts rajta egy rövid szerkesztéssel!
Ha nem tudod, hogyan indulj el, olvasd el a migrálási útmutatót.
Ez a tutorial alapvetően azt hivatott elmagyarázni, hogy hogyan kell az OpenGL-t használni. Ebből adódóan még minimális kitérőt se tesz a házik mögött álló elméleti háttér kifejesére, csak abba ad segítséget, hogy hogyan oldd meg a házidat. A háziknak pont az a célja, hogy az órán elsajátított elméletet gyakorolni tudd, így ha az elémlet ismerete nélkül vágsz neki a házinak, az lehet, hogy sikerülni fog, de a tárgy szempontjából semmi értelme nem lesz... Ez a tutorial nem helyettesíti az órán való jelenlétet.
Az oldalról kódot a saját házidba átemelni TILOS! Még ha pár sornyi kódról is van szó, gépeld be szépen magadtól, addig is gyakorlod a dolgot. Meg persze nem is érdemes másolgatni, mert csak borzolod vele a plágiumkereső idegeit, és sokban ronthat az esélyeiden az aláírás megszerzésére
Ez a lap még erősen fejlesztés alatt áll, ha van ötleted akkor írd bele nyugodtan, vagy egészítsd ki az egyik hiányzó TODO részt
Az OpenGL és GLUT alapjai
Az OpenGL
- Az OpenGL egyszerű térbeli alakzatok (primtívek), pl.: pontok, vonalak, háromszögek rajzolására specializálódott.
- Ezekből az építőkockákból csodákat lehet művelni, a mai számítógépes játékok nagyrészt hárszomszögek rajzolásából építkeznek.
- A primitíveket nagyon sok féle képpen és nagyon hatékonyan lehet az OpenGL-lel kirajzolni, de ezen kívül semmi mást nem tud, nem tud képet betölteni, árnyékokat számolni, de még egy kocka kirajzolásához is "küzdeni" kell.
- A hatékonyság növelése érdekében az OpenGL a videókártyát is használja a rajzoláshoz.
- Egy rajzolófüggvény viselkedése több száz paramétertől függ, persze nem kell az összeset függvény argumentumként átadni, ehelyett az OpenGL egy állapotgép-en alapszik.
- Ha valamit átállítasz (pl. a rajzolószínt), akkor az onnantól kezdve minden rajzolás-hívást befolyásol.
- A legtöbb OpenGL függvényt több különböző típusú paraméterrel is meg lehet hívni, viszont az OpenGL egy C könyvtár, amiben nincs függvény overload. Ennek kiküszöbélésére a függvények neve Hungarian Notation szerűen a név végén tartalmaz pár karaktert, ami a paraméterekre utal, pl.: 3f - 3 db float, 2iv - 2 elemű int vector (azaz pointer egy tetszőleges memóriaterületre, ahol 2 db int van egymás után)
- pl.: glVertex3f()
- Az OpenGL elnevezési konvenciója:
- A függvények neve mindig kisbetűvel kezdődik, a gl, glu vagy glut szavakkal, attól függően hogy melyik függvénykönyvtárban található a függvény
- Ha több szóból áll, azokat egybeírjuk, és a szavakat nagy betűvel kezdjük (camelCase).
- pl.: glDrawElementsInstancedBaseVertexBaseInstance() - amúgy ez a leghosszabb nevű OpenGL függvény, de a tárgyból nincs rá szükség.
- A grafikában a lebegőpontos számokkal float alakban szeretünk dolgozni. A double-el nem az a gond, hogy kétszer annyi helyet foglal, hanem hogy a double pontosságú műveletvégzés sokkal lassabb, mint ha megelégednénk a float által elvárt pontossággal. Ez az oka annak, hogy a videókártyák, kb 2011-ig csak floatokkal tudtak számolni (double-el csak szoftveresen emulálva, nagyjából 25-ször olyan lassan). Az OpenGL-nek az a verziója, amit a tárgyban kell használni (1.1), csak floatokkal tud dolgozni a videókártyán, ami azt jelenti, hogy ha double-t adsz neki, attól nem csak, hogy nem lesz pontosabb, de még plusz munkát is adsz neki ezzel, mert neki vissza kell kasztolni a számot floattá, és ez a kasztolás nagyon nincs ingyen. A floatok használatának egy további előnye, hogy az x86 processzorok 4 db float műveletet tudnak egyszerre elvégezni SSE-vel. Ez az oka annak, hogy a tárgyból a legtöbb függvénynek csak a floatot váró alakját lehet használni, például a glVertex3f-et lehet, de a glVertex3d-t nem.
- Az OpenGL RGB színskálán állítja elő a képet, és neki is RGB érétéket kell adni, ha egy színt akarunk leírni. A grafikában általában nem a megszokott komponensenként egy byte-on (0, 255) specifikáljuk a színeket. Ezzel alapvetően az baj, hogy a színhez tartozó fényerősség csak nagyon kis tartományon változhat, ahhoz képest amekkora különbségek a valóságban előfordulnak, pl. a Nap színe, vagy éjszaka egy sötét szobának a színei között több mint 10000-szeres fényerősségbeli különbség van. A másik gond, hogy a byte színekkel nehéz műveletet végezni, pl. a valóságban két fehér fény összege egy még fényesebb fehér, míg az egy bájton leírt színeknél a fehér már önmagából a lehető legvilágosabb szín, amit meg tudunk jeleníteni. Ennek az orvoslására a színeket komponensenként floatokkal írjuk le. Nem véletlen egybeesés, hogy a megvilágítást a videókártya számolja az OpenGL-ben, ami mint tudjuk, floatokkal szeret dolgozni. Egy fényt két dolog is jellemez, az egyik a színe (hullámhosza), ami jelen esetben az RGB komponensek aránya. Ha csak ezt akarjuk megadni, akkor a komponenseket a (0, 1) tartományon írjuk le. De a fényt jellemzi még az erőssége (luminanciája) is, ami független magától a színtől. A fényerősség tetszőlegesen nagy, vagy kicsi, de akár még negatív is lehet. Ha egy fényforrást akarunk leírni, akkor a szín és a fényerősség szorzatára vagyunk kíváncsiak, a (-végtelen, végtelen) tartományon komponensenként (erre a sugárkövetésnél lesz szükség), de ha az OpenGL-nek akarunk megadni egy színt, akkor azt a (0, 1), esetleg a (-1, 1) tartományon tegyük. Technikailag byte-ot is lehet adni az OpenGL-nek, de pedagógia okokból a házikban kötelező float színeket használni.
- Az OpenGL csak a rajzolással foglalkozik, az, hogy hogyan jön létre az a valami (célszerűen egy ablak), amire ő tud rajzolni, az viszont már nem az ő dolga. Itt jön a képbe a GLUT.
A GLUT
- A GLUT egy platformfüggetlen ablak- és eseménykezelő, lényegében egy híd az oprendszer és az OpenGL context között. A GLUT beállításának nagyrésze a keretben előre meg van írva, csak az eseménykezelő függvényekről kell gondoskodnunk, amiket majd a GLUT meghív (ezek a függvények határozzák meg, hogy mit csinál a programunk).
- GLUT eseménykezelő függvények:
- onDisplay() - a legfontosabb függvény, ide írjuk a képernyő törlését, majd a szükséges rajzoló részeket. Ha valami változás hatására frissíteni szeretnénk a képernyőt, azaz szeretnénk az onDisplay()-t lefuttatni, hívjuk meg a glutPostRedisplay() függvényt (ne közvetlenül az onDisplay-t!). Fontos hogy az onDisplay()-en belül tilos meghívni a glutPostRedisplay()-t, az így megírt program elvi hibás (a képernyő mindig érvénytelen marad), ez a beadón nem fog működni.
- onInitialization() - inicializáló rész, pl. globális változók inicializálására. Tipikus hiba, hogy a globális változóknak egy gl/glu/glut függvény visszatérési értéket adjuk, vagy a változó konsktrukorában meghívunk ilyen függvényt. Így ugyanis még a main() kezdete előtt futattánk le egy ilyen típusú függvényt, amikor még ezek a könyvtárak nincsenek inicilaiálva. Ennek az elkerülésére van ott az onInitialization - ebben már nyugodtan használhatunk bármilyen függvényt az inicializációhoz.
- onKeyboard() - Itt tudjuk kezelne egy billentyű lenyomását. Erre a házikban általában csak minimális szükség van.
- onMouse() - Itt kapunk értesíést arról, ha valamelyik egérgomb állapota megváltozott, és azt is megtudjuk, hogy az ekkor az egér az ablak koordináltái szerint (figyelem itt bal felül van az origó!) hol volt.
- onMouseMotion() - Itt tudjuk meg, ha a felhasználó lenyomott egér gomb mellett mozgatta az egeret. A koordinálta értékére ugyan az vonatkozik, mint az onMouse esetén.
- onIdle() - Ez a függvény az idő múlását hivatott jelezni, így itt kell kezelni mindent ami az időtől függ (animáció).
Az első házihoz szükséges elmélet
Rajzolás az OpenGL segítségével
- Az OpenGL néhány csak néhány típusú primitívet tud rajzolni, ezekből kell építkeznünk.
- A típusok:
- Pontok: GL_POINTS
- Vonalak: GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP
- Háromszögek: GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_LOOP
- Háromszögekből összetett alakzatok:
- Négyszögek (igazándiból 2 háromszög): GL_QUADS, GL_QUAD_STRIP
- Sokszög (ez is háromszögenként rajzolódik ki): GL_POLYGON
- A rajzolás az alábbi módon történik:
glBegin("Primitív típus"); "Pontok felsorolása" glEnd();
- A pontok megadásához glVertex{2,3}f függvények valamelyikét kell használnod, az alapján, hogy hány dimenzóban dolgozol.
- Tehát 2 dimenzióban a glVertex2f-et, 3 dimenzióban a glVertex3f-et kell használnod.
- A pontok sorrendje nagyon fontos, már egy quad esetében sem lehet "csak úgy" felsorolni a négy pontot, ha rossz sorrendben adjuk meg őket, akkor két egymásba csúszott háromszöget fogunk látni.
2D rajzolás
- A koordináták amiket átadsz azok a normalizált eszköz koordinátarendszerben vannak értelmezve, ahol a (0,0) a képernyő közepe, a (-1, -1) pedig a bal alsó sarok.
- Példaprogram: Háromszögek
glBegin(GL_TRIANGLES);
glVertex2f(0.0f, 0.0f);
glVertex2f(0.2f, 0.2f);
glVertex2f(0.2f, 0.0f);
glVertex2f(0.6f, 0.8f); // Egy pass-on belül több háromszög csúcspontjait is fel lehet sorolni.
glVertex2f(0.9f, 0.3f);
glVertex2f(0.5f, 0.9f);
glEnd();
Az eredménye:
- Minden egyes ponthoz külön színt is tudunk megadni. A glColor3f()-el lehet állítani a rajzolószínt, ami utána az összes glVertex hívásra érvényes lesz. Az összetettebb alakzatoknál az egyes pontok színei interpolálódnak, és szép színátmenetet kapunk.
- Példaprogram: Smiley
#define CIRCLE_RESOLUTION 32
// Piros kor, a kepernyo bal oldalan
glBegin(GL_TRIANGLE_FAN); {
float radius = 0.25f, center_x = -0.5f, center_y = 0.4f;
glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(center_x, center_y);
for(int i = 0; i <= CIRCLE_RESOLUTION; i++) {
float angle = float(i) / CIRCLE_RESOLUTION * 2.0f * M_PI;
// Itt a kor paramtetrikus alakjat hasznaljuk: x = x0 + r*cos(t), y = y0 + r * sin(t)
glVertex2f(center_x + radius*cos(angle), center_y + radius*sin(angle));
}
} glEnd();
// Szinatmenetes kor, a kepernyo jobb oldalan
glBegin(GL_TRIANGLE_FAN); {
float radius = 0.25f, center_x = 0.5f, center_y = 0.4f;
glColor3f(0.0f, 1.0f, 1.0f);
glVertex2f(center_x, center_y);
for(int i = 0; i <= CIRCLE_RESOLUTION; i++) {
float angle = float(i) / CIRCLE_RESOLUTION * 2.0f * M_PI;
glColor3f(0.0f, 0.5f + 0.5f*cos(angle), 0.5f + 0.5f*sin(angle));
glVertex2f(center_x + radius*cos(angle), center_y + radius*sin(angle));
}
} glEnd();
// Felkoriv
glBegin(GL_LINE_STRIP); {
float radius = 0.75f, center_x = 0.0f, center_y = 0.0f;
glColor3f(1.0f, 1.0f, 1.0f);
for(int i = CIRCLE_RESOLUTION/2; i <= CIRCLE_RESOLUTION; i++) {
float angle = float(i) / CIRCLE_RESOLUTION * 2.0f * M_PI;
glVertex2f(center_x + radius*cos(angle), center_y + radius*sin(angle));
}
} glEnd();
Az eredménye:
Eseménykezelés
- A grafikus programok általában eseményvezéreltek, azaz a felhasználó által generált események (pl. egérkattintás) vezérlik a programot. A GLUT ehhez nagyon sok segítséget ad, nekünk csak be kell regisztrálnunk egy-egy függvényt, hogy melyik esemény hatására mi történjen, pl az egérkattintásokat az onMouse() függvényben kezeljük.
- A három legfontosabb eseménytípus:
- Billentyűzet esemény:
- Billentyű lenyomás - onKeyboard
- Billentyű felengedés - onKeyboardUp
- Idő múlása (lényegében ez is egy esemény) - onIdle
- Egér esemény:
- Egér mozgatás - onMouseMotion
- Egér gombbal kattintás - onMouse
- Az egér eseményekkel kapcsolatban egy apró kellemetlenség, hogy a GLUT a kattintások helyét az oprendszer koordináta rendszerében adja át nekünk (ablak bal fölső sarka az origó, x jobbra, y lefelő nő, az egység pedig a pixel), míg mi normalizált eszköszkoordinátákkal dolgozunk (az albak közepe az origó, a x jobbra, az y felfele nő, és mindkét dimenzióban az ablak méretének a fele az egység). Ezért kénytelenek vagyunk átszámolni azokat az értékeket, amiket a GLUT ad nekünk. Erre egy lehetséges megodás:
- Billentyűzet esemény:
struct Vector {
float x, y;
}
const int kScreenWidth = 600, kScreenHeight = 600;
Vector convertToNdc(float x, float y) {
Vector ret;
ret.x = (x - kScreenWidth/2) / (kScreenWidth/2);
ret.y = (kScreenHeight/2 - y) / (kScreenHeight/2);
return ret;
}
- Példa: Egyszerű rajzolóprogram
void onMouse(int button, int state, int x, int y) {
if(button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN) {
glClear(GL_COLOR_BUFFER_BIT); // Jobb klikkre toroljuk a kepernyot.
glutPostRedisplay(); // Szolunk, hogy az ablak tartalma megvaltozott, kerjuk a GLUT-ot, hogy hívja meg az onDisplay-t.
} else if(button == GLUT_LEFT_BUTTON) { // Ha a bal gomb allapota megvaltozott.
if(state == GLUT_DOWN) {
drawing = true; // Ha lenyomtuk akkor rajzolni akarunk.
Vector pos = convertToNdc(x, y); // Atvaltjuk a pontot.
glBegin(GL_POINTS); { // Kirajzoljuk.
glVertex2f(pos.x, pos.y);
} glEnd();
last_mouse_pos = pos; // Elmentjuk, hogy az elso szakasz, majd ebbol a pontbol indul.
glutPostRedisplay(); // Szolunk, hogy az ablak megvaltozott, kerjuk az ujrarajzolasat.
} else if(state == GLUT_UP) {
drawing = false; // Ha most engedtuk fel, akkor mar nem akarunk rajzolni.
}
}
}
void onMouseMotion(int x, int y) {
if(drawing) {
Vector pos = convertToNdc(x, y); // Kiszamoljuk az eger jelenlegi helyzetet NDC-ben.
glBegin(GL_LINES); { // Kirajzolunk egy vonalat az elozo es a mostani helyzete koze.
glVertex2f(last_mouse_pos.x, last_mouse_pos.y);
glVertex2f(pos.x, pos.y);
} glEnd();
glutPostRedisplay(); // Szolunk, hogy az ablak megvaltozott, kerjuk az ujrarajzolasat.
last_mouse_pos = pos; // Frissitjuk a elozo helyzetet.
}
}
Az eredménye:
Animáció
- Az animáció annyit jelent, hogy az egyes objektumok állapota az időnek is a függvénye. A pillatanyi időt a
glutGet(GLUT_ELAPSED_TIME);
függvényhívással tudjuk lekérdezni, célszerűen az onIdle függvényben. - Egy mozgó testet legjobban a fizika tövényeivel tudunk leírni, egy egyenes vonalú egyenletes mozgás leírásához mindössze a
v = s / t
képletre van szükségünk. - Az animáció onnantól kezd bonyolultá válni, hogy ha több mozgó test állapota egymástól függ (pl: mikor ütköznek). Ilyenkor ugyanis a korrekt szimuláció egy differenciálegyenlet megoldását jelentené. Ennek egy egyszerű közelítése a diszkrét idő-szimuláció, ahol az ötlet az, hogy válasszunk egy időegységet, amennyi idő alatt a testek állapota csak minimálisan változik meg, ez tipikusan pár milliszekundum, és legfeljebb ilyen időközönként kiválasztott statikus pillanatokban vizsgáljuk csak az egymásrahatásokat. Manapság a számítógépes játékok nagyrésze is ezt a módszert használja.
- Egyszerű példaprogram: Pattogó labda
const float ball_radius = 0.1f;
Vector ball_pos, ball_speed(-0.46f, 1.13f);
void onIdle() {
static int last_time = glutGet(GLUT_ELAPSED_TIME); // Visszaadja a jelenlegi időt miliszekundumban
int curr_time = glutGet(GLUT_ELAPSED_TIME);
int diff = curr_time - last_time; // Az előző onIdle óta eltelt idő
last_time = curr_time; // A következő meghíváskor az előző idő majd a mostani idő lesz.
// Két onIdle között eltelt idő nagyon változó tud lenni, és akár elég nagy is lehet
// ahhoz, hogy a labda látványosan bele menjen a falba, mielőtt visszapattan. Ezért
// osszuk fel az elelt időt kisebb részekre, pl. max 5 miliszekundumos egységekre,
// és ilyen időközönként nézzük meg, hogy a labda ütközött-e a fallal.
const int time_step = 5;
for(int i = 0; i < diff; i += time_step) {
// Az időosztás végén egy kisebb egység marad, mint az idő egység. Pl. ha a diff 11,
// akkor azt akarjuk, hogy 5, 5, 1 egységekre bontsuk azt, ne 5, 5, 5-re.
// Meg is kell számolnunk másodperce, azaz osztanunk kell 1000-el.
float dt = min(diff-i, time_step) / 1000.0f;
// Módosítsuk a sebességet ha ütközött a fallal, teljesen rugalmas ütközést feltételezve.
// Ilyenkor a labda a fal irányára merőlegesen pontosan ellentétes irányba halad tovább.
if(ball_pos.x + ball_radius > 1) {
ball_speed.x = -fabs(ball_speed.x);
} else if(ball_pos.x - ball_radius < -1) {
ball_speed.x = fabs(ball_speed.x);
}
if(ball_pos.y + ball_radius > 1) {
ball_speed.y = -fabs(ball_speed.y);
} else if(ball_pos.y - ball_radius < -1) {
ball_speed.y = fabs(ball_speed.y);
}
// Mozgassuk a labdát a ds = v * dt képlet alapján.
ball_pos += ball_speed * dt;
}
glutPostRedisplay(); // Megváltozott a jelenet, újra kell rajzolni
}
Az eredménye:
A második házihoz szükséges elmélet
Koordináta rendszerek
- Az első háziba valószínűleg feltűnt, hogy a pontok NDC (normalizált eszköz koordináta) megadása nem túl kényelmes, még akkor se ha, a világnak mindig ugyan azt a részét nézzük. De mit tegyük akkor, ha a képzeletbeli kamera amivel "lefényképezzük" a jelenetet mozoghat, sőt akár még forghat is.
- Az OpenGL kitalálónak az ötlete az volt erre, hogy a kamera mindig maradjon egyhelyben, de ha pl. balra akarnánk forgatni, akkor helyette inkább a világ forogjon jobbra, a kamera maradj ugyanott, és ezzel ugyan azt a hatást érjük el. Persze nem kell minden egyes pontot nekünk elforgatni, ezt rábízhatjuk az OpenGL-re is, hogy mindig mielőtt rajzolna, azelőtt végezzen el valamilyen transzformációt a pontokat amiket kapott.
- A lineáris (és affin) transzformációkat a legkényelmesebb a mátrixuk segítségével tudjuk megadni, több egymás utáni transzformáció pedig egyszerűen a mátrixok szorzatát jelenti. Viszont fontos megjegyezni, hogy 3D-ben az eltolás nem lineáris transzformáció, és nem is lehet mátrixal felírni. Ennek kiküszöbölésére használhatunk 4D-be 'w' = 1 koordinátával beágyazott 3D-s koordinátarendszert, ahol az eltolás is egy lineáris trafó.
- Az OpenGL két mátrixot ad nekünk, amiknek módosíthatunk, és amivel az összes kirajzolandó pontot mindig beszorozza helyettünk. Az egyik a GL_MODELVIEW, a másik a GL_PROJECTION. A GL_MODELVIEW-val mozgathatunk objektumokat egymáshoz képest, és itt tudjuk megadni, hogy hogyan kell transzformálni a világot, hogy az origóban lévő, -z irányba néző képzeletbeli azt lássa, amit meg akarunk jeleníteni. A GL_PROJECTION pedig azt adja meg, hogy a kamerával hogyan kell fényképzni.
- Két dimezióban a két mátrix különválasztása gyakorlatilag fölösleges, ennek csak 3D-be lesz szerepe, majd a megvilágításkor. 2D-ben a kényelmesebb a GL_PROJECTION-ra bízni a kamera mozgatást is, és a GL_MODELVIEW-t pedig meghagyni csak az objektumok közötti transzformációk leírására.
- Projekció 2D-ben: a fényképezés módja nagyon egyszerű, egyszerűen eldobjuk a 'z' koordinátát, és az x-y pozició alapján rajzolunk. Vagy legalábbis ezt csináltuk eddig, de az NDC nem volt kényelmes. Itt viszont lehetőséget kapunk saját koordinátarendszer megválasztására, ahol az egység lehet pl. 1 méter, és a kamera pedig mondjuk követhet egy karaktert egy játékban.
- Példaprogram: Sidescroller
// Megmondjuk a OpenGL-nek, hogy ezután a projekciós mátrixot
// akarjuk módosítani a transzformációs mátrix műveletekkel.
glMatrixMode(GL_PROJECTION);
// Egység mátrixot töltük be a jelenleg módosítható
// mátrix helyére (ez a projekciós mátrix).
glLoadIdentity();
// Ez egy olyan merőleges (Orthogonális) vetítés mátrixát "írja be" a GL_PROJECTION-be,
// aminek eredménye képpen a karakter az x tengely mentén középen lesz, és 10 egység
// (méter) széles részt látunk a világból míg az y tengely mentén a képernyő alsó egy
// ötödében lesz, és itt is 10 egység magas részt látunk. Fontos megjegyezni, hogy ez
// csak akkor működik, ha a GL_PROJECTION előtte egység mátrix volt!
gluOrtho2D(
stickman.pos.x - 5, stickman.pos.x + 5,
stickman.pos.y - 2, stickman.pos.y + 8
);
Az eredménye:
Transzformációk
- GL_MODELVIEW egyik legfontosabb használata, hogy segítségével könyebben tudjuk elhelyezni az objektumokat a világban. Például ha van egy összetett, mondjuk 10 000 háromszögből álló alakzatunk, akkor annak elforgatását manuálisan úgy tudnánk megoldani, hogy az alakzat összes pontján elvégzünk valami undorító trigunometrikus képletet. Egy másik lehetőség, hogy a GL_MODELVIEW segítségével az egész világot elforgatjuk az ellenkező irányba, kirajzoljuk az alakzatot normál állapotában, majd visszaforgatjuk a világot. Ez első ránézésre bonyolultabbnak tűnik, de mindössze 2 sor kód.
- A világ transzformálásához használható függvények:
glTranslatef(GLfloat x, GLfloat y, GLfloat z);
glRotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z);
glScalef(GLfloat x, GLfloat y, GLfloat z);
- A projekciós mátrixot állító függvényekkel ellentétben, ezeket akkor is szoktuk használni, ha a modellezési mátrix nem egységmátrix.
- Ezen függvényeknek tetszőleges kombinációját lehet használni, de a sorrend nem mindegy.
- Egy transzformáció meghívásakor annak a mátrix hozzászorzódik a GL_MODELVIEW mátrixhoz (jobbról). Emlékeztető: a mátrix szorzás, és a mátrix-vektor szorzás asszociatív. Ez azt jelenti, hogy két transzformációs mátrix összeszorzása ugyan úgy transzformál egy tetszőleges vektort, mint ha a két mátrixal külön szoroztuk volna be.
- A transzformációk fordított sorrendben fejtik ki hatásukat, mint ahogy meghívjuk őket, de ez így intuitív, így haladhatunk a hierarhiában föntről lefele, ha nem így lenne, akkor pl. egy autó kirajzolásánál, azzal kéne kezdenünk, hogy megmondjuk, hogy a dísztárcsa a kerékhez képest hogy helyezkedik el, és csak a legvégén mondhatnánk meg, hogy az autó egyáltalán hol van.
- Példa a transzformációk sorrendjére:
glTranslatef(2.7f, -3.1f, 0.0f);
glRotatef(67, 0, 0, 1);
glScalef(2, 2.5f, 1);
Ami a koordinátarendszerrel történik:
- Intuitív, pontosan az történik, mint amit a kódról első ránézésre hinnénk, hogy csinál, az origó a transzformáció után a (2.7, -3.1) pontba kerül, a nagyítás az x tengely mentén 2, az y tengely mentén 2.5.
glScalef(2, 2.5f, 1);
glRotatef(67, 0, 0, 1);
glTranslatef(2.7f, -3.1f, 0.0f);
Ami a koordinátarendszerrel történik:
- Egyáltalán nem intuitív, az origó a (7.817, 3.185) pontba kerül, és a két tengely nagyítása 2.084 és 2.43. Ezeknek az értékeknek semmi köze a kódban szereplő konstansokhoz!!
- Tanulság: általában az eltolás - forgatás - nagyítás sorrendet szeretjük. Ez nem jelenti azt, hogy más sorrendnek ne lenne értelme, vagy egy konkrét problémának ne lehetne egyszerűbb megoldása másmilyen sorrendet használva.
- A probléma ami felmerül, hogy a transzformációk hatása permanens, azaz, ha egyszer elforgattad a világot, akkor az úgy marad, amíg vissza nem forgatod. Tehát ha egy objektum kirajzolása miatt akarsz hasznáni egy transzformációt akkor a rajzolás után azt mindenképpen, mindig vissza is kell csinálnod. De mi van ha egy összetett objektum kirajzolásához akár több száz transzformáció is kellet? Akkor a végén az összeset egybe vissza kell csinálni? Nincs erre jobb megoldás? A válasz természetesen az, hogy van, ez a megoldás a matrix stack.
Matrix stack
- Az OpenGL két függvényt ad amivel a matrix stack-et használhatjuk:
glPushMatrix();
- A jelenleg aktív mátrixot (
GL_PROJECTION
vagyGL_MODELVIEW
) elmenti annak a stackjébe.
- A jelenleg aktív mátrixot (
glPopMatrix();
- A jelenleg aktív mátrix stackjéből a legutóbb elmentett mátrixot visszaáálítja. A következő glPopMatrix() mátrix az az előtt elentett mátrixot állítja vissza.
- Megjegyzések:
- A
GL_MODELVIEW
stack mélysége legalább 32, a GL_PROJECTION mátrixé pedig legalább 2.- Ez azt jelenti, hogy lehet, hogy nálad mindkét érték tízszer ekkora, de ha hordozható kódot akarsz írni, ekkor ennél nagyobb számokat nem feltételezhetsz.
- Overflow / Underflow esetén a mátrix értéke meghatározatlan lesz.
- A
- Pl:
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glPushMatrix(); {
glRotatef(90, 0, 0, 1);
rajzolas(); // A rajzolaskor a világ el van forgatva
} glPopMatrix();
rajzolas2(); // It a világ már nincs elforgatva.
- A matrix stack hierarchikusan felépülő testek rajzolását nagy mértékben megkönnyíti.
- Hierarhikus test pl: az emberi kéz.
- Ha a felkarod elforgatod a vállad körül, akkor azzal az alkarod a csuklód és az ujjaid elmozdulnak. Minden izület transzformációja befolyásolja az összes csontot, ami belőle nő ki, és az összes csontot, ami a belőle kinövő csontokból nő ki, rekurzívan. Ha ezt manuálisan akarnánk leprogramozni, egy olyan fát kéne felépítenünk az izületekből, ahol a gyerekek száma változó. Ez nem lehetelen, de is kifejezetten izgalmas, viszont a matrix stack segítségével ez nagyon egyszerűen megoldhaó.
- Pszeudó kóddal, feltételezve, hogy origó középpontú, egység hosszú, x-el párhuzamos főtengelyű hengert tudunk rajzolni a "henger kirajzolása" utasítással:
- Hierarhikus test pl: az emberi kéz.
glPushMatrix(); { váll körüli forgatás felkar hosszának a felével eltolás az x tengely mentén glPushMatrix(); { nagyítás a felkar méreteivel henger kirajzolása } glPopMatrix(); felkar hosszának a felével eltolás az x tengely mentén könyök körüli forgatás alkar hosszának a felével eltolás az x tengely mentén glPushMatrix(); { nagyítás a alkar méreteivel henger kirajzolása } glPopMatrix(); alkar hosszának a felével eltolás az x tengely mentén kéz kirajzolása } glPopMatrix();
- Sajnos ezt 2D-be nem lehet jól megmutatni, ezért kivételesen az ehhez kapcsolódó példaprogram 3D-s lesz.
- Technikailag a 3D rajzolást rábízzuk a glut-ra.
- Kizárólag a
glutSolidCube()
függvényt fogjuk használni. - Ez a függvény - nem meglepő módón - egy kockát rajzol ki:
- Kizárólag a
- Technikailag a 3D rajzolást rábízzuk a glut-ra.
- Ezt felhasználva a példaprogram: Robot kar
- A program irányítása:
- 'q' - Ujjak szétnyitása, 'a' - Ujjak összezárása
- 'w' - Alkar felemelése, 's' - Alkar lehajtása
- 'e' - Felkar felemelése, 'd' - Felkar lehajtása
- 'r' - Az alap forgatása jobbra, 'f' - Az alap forgatása balra
- A program irányítása:
struct Vector {
float x, y, z;
Vector(float x, float y, float z) : x(x), y(y), z(z) { }
void translate() { glTranslatef(x, y, z); }
void rotate(float angle) { glRotatef(angle, x, y, z); }
void scale() { glScalef(x, y, z); }
};
Vector x_axis(1, 0, 0), y_axis(0, 1, 0), z_axis(0, 0, 1);
Vector pos_crate(0, 0, -5), pos_left_base(1, 0, 0), pos_right_base(-1, 0, 0), scale_base(1, 1, 3),
pos_main_arm(0, 0, -2), scale_main_arm(1, 1, 4), pos_lower_arm(0, 0, -1.5f),
scale_lower_arm(0.7f, 0.7f, 3.0f), scale_wrist(1, 1, 1), pos_left_finger(0.5f, -1.0f, 0.0f),
pos_right_finger(-0.5f, -1.0f, 0.0f), scale_finger(0.2f, 1.0f, 0.2f);
float rot_base = 0, rot_main_arm = 70, rot_lower_arm = -60,
rot_finger = 20, rot_finger_relative = 20;
void onDisplay() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix(); {
y_axis.rotate(rot_base);
// Jobb oldali alap
glPushMatrix(); {
pos_right_base.translate();
scale_base.scale();
glutSolidCube(1.0f);
} glPopMatrix();
// Bal oldali alap
glPushMatrix(); {
pos_left_base.translate();
scale_base.scale();
glutSolidCube(1.0f);
} glPopMatrix();
x_axis.rotate(rot_main_arm);
pos_main_arm.translate();
// Felkar
glPushMatrix(); {
scale_main_arm.scale();
glutSolidCube(1.0f);
} glPopMatrix();
pos_main_arm.translate();
x_axis.rotate(rot_lower_arm);
pos_lower_arm.translate();
// Alkar
glPushMatrix(); {
scale_lower_arm.scale();
glutSolidCube(1.0f);
} glPopMatrix();
pos_lower_arm.translate();
// Csukló
glPushMatrix(); {
scale_wrist.scale();
glutSolidCube(1.0f);
} glPopMatrix();
// Jobb 'ujj'
glPushMatrix(); {
z_axis.rotate(-rot_finger);
glTranslatef(0, pos_right_finger.y, 0);
glPushMatrix(); {
glTranslatef(pos_right_finger.x, 0, 0);
z_axis.rotate(-rot_finger_relative);
scale_finger.scale();
glutSolidCube(1.0f);
} glPopMatrix();
pos_right_finger.translate();
z_axis.rotate(rot_finger_relative);
scale_finger.scale();
glutSolidCube(1.0f);
} glPopMatrix();
// Bal 'ujj'
glPushMatrix(); {
z_axis.rotate(rot_finger);
glTranslatef(0, pos_left_finger.y, 0);
glPushMatrix(); {
glTranslatef(pos_left_finger.x, 0, 0);
z_axis.rotate(rot_finger_relative);
scale_finger.scale();
glutSolidCube(1.0f);
} glPopMatrix();
pos_left_finger.translate();
z_axis.rotate(-rot_finger_relative);
scale_finger.scale();
glutSolidCube(1.0f);
} glPopMatrix();
} glPopMatrix();
// Láda
glPushMatrix(); {
pos_crate.translate();
glutSolidCube(1.0f);
} glPopMatrix();
glutSwapBuffers();
}
Az eredménye:
Görbék
- Görbék alatt grafikában olyan függvényeket értünk, amik diszkrét ponthalmazból folytonos ponthalmazt állítanak elő.
- Példa: A Nyugatitól el akarunk jutni az egyetemig, de úgy, hogy közbe megadott sorrendben érinteni akarjuk a három kedvenc kocsmánkat. Az útvonal, amin ezt megtudjuk tenni, az egy görbe. Az öt helyet, amit értinti akarunk, kontrollpontnak nevezzük. Nem csak egy ilyen útvonal létezik, mint ahogy a különböző típusú görbéknek is lehet más a kimenete, ugyan az a bemenet mellett.
- A görgbéket szinte mindig valamilyen mozgás leírára használjuk. A görbe kimenete egy hely-idő pontpárokból álló halmaz (vagy ezzel ekvivalens egy folytonos függvény aminek az idő a paramtére, és a hely a visszatérési értéke), ami azt jelenti, hogy a görbéből még a sebesség és a gyorsulás is kiolvasható.
- A görbék egyik legfontosabb tulajdonsága a folytonosság mértéke, vagyis, hogy mekkora az a legnagyobb szám (n), amennyiedik deriváltja még folytonos. Jelölése: c(n).
- c0 folytonos görbe az, aminek szakadása van. Egy valós útvonal esetében ez azt jelenti, hogy valahol teleportálnuk is kell. Tekintve, hogy 3 kocsmát is érinteni akarunk, ez nem tűnik lehetetlennek :)
- A valós tárgyak mozgása legalább c2 folytonos (azaz a gyorsulás folytonosan változik). Ezt a agyunk megszokta, és a nem c2 folytonos mozgás nem tűnik valóságosnak. Ebből fakadóan az olyan egyszerűbb függvények, mint hogy a pontokat egyenes vonalakkal összekötjük, nem eredményeznek hihető mozgást.
- A tipikus görbék amiket a grafikában használni szoktunk:
- Bézier-görbe.
- Egyszerű implementálni, de a megfelelő kontrollpontok megadása nehézkes.
- B-Spline görbe család (több görbéből összetett görbék).
- Lokálisan vezérelhetőek, nagyon kényelmes velük dolgozni, viszont az implementálásuk általában bonyolult.
- Pl: Kochanek–Bartels görbe
- Gyakori implentációja a közelítőleg c2 folytonos Catmull-Rom görbe (Ezt akkor kapjuk, ha a Kochanek–Bartels összes paraméterét nullának választjuk).
- Bézier-görbe.
- Példaprogram: Tenziós Catmull-Rom görbe
Régi wikiről áthozott rész, frissitésre szorul
Projekciós mátrixok, transzformációk
- Az OpenGL a megjelenítő csővezetékben két transzformációs mátrixot használ, ezek a ModelView és a Projection mátrixok. A keretrendszer nem módosítható részében mindkét mátrixba az egység mátrixot töltik a glLoadIdentity() függvény segítségével. Ez alkotja a már említett 2D orthogonális vetítést (-1,+1) méretű koordináta-rendszerben.
- A két mátrix között csak elvi különbség van. A keretrendszerben is használt glMatrixMode(GL_MODELVIEW) ill. glMatrixMode(GL_PROJECTION) paranccsal állíthatjuk be, hogy melyiket szeretnénk módosítani. Mivel a keretben utoljára a GL_PROJECTION kerül beállításra, ezért innentől minden függvényhívás azt módosítja. Ezt hagyhatjuk így, a feladat egy mátrixal is tökéletesen megoldható.
- Ezek a mátrixok három módon módosíthatók:
- Projekció módosító függvénnyek: glOrtho2D, gluPerspective... - lásd a Koordináta-rendszerek részben
- Transzformációs függvénnyek: glRotatef, glTranslatef és glScalef
- a függvények végén szereplő f a float paramétereket jelenti
- *glRotatef(angle,x,y,z)*: forgatás angle fokkal az X, Y, Z irányú tengely körül. Ha pl. a Z tengely mentén szeretnénk forgatni 90 fokkal, akkor a következő paramétereket adjuk meg: (90,0,0,1)
- *glTranslatef(x,y,z)*: eltolás a 3 tengely mentén megadott mértékkel
- *glScalef(x,y,z)*: nagyítás a 3 tengely mentén megadott szorzóval. Egynél kisebb értékkel nyilván kicsinyítünk
- a függvények magát a koordináta rendszert változtatják, így ha valamit két egységgel el szeretnénk tolni az X tengelyen, használjuk a glTranslatef(2,0,0) függvényt, majd rajzoljunk az origóba, ami ugye most már két egységgel arrébb került
- a Z tengelyre vonatkozó paramétert is mindig meg kell adni, bár 2D rajzolás esetén nincs hatása
- Az első házikhoz fölösleges ilyen transzformációs függvényeket használni, egyszerű 2D elforgatás kiválóan megoldható sin és cos függvények használatával. Arra azonban figyeljünk, hogy ezek a függvények radiánban várják a szög értékét!
- Közvetlen mátrix-beállító függvények: glLoadMatrix, glMultMatrix - ezekre jó eséllyel nem lesz szükség
- Az OpenGL állapotgép jellege miatt a mátrixok módosítása maradandó, így meg kell oldanunk a mátrixok visszaállítását:
- A mátrixokba az alapértelmezett egységmátrix tölthető a glLoadIdentity paranccsal. Ha ezt a módszert használjuk, az onDisplay elején mindig be kell töltenünk az egységmátrixot, majd az esetleges projekciót. Nem szép megoldás.
- glPushMatrix, glPopMatrix - Az OpenGL beépített mátrix-vermét hívjuk segítségül. A glPushMatrix() függvénnyel eltároljuk az aktuális mátrixot, míg a glPopMatrix() meghívásával visszatöltjük. Ha például szeretnénk egy rajzolást eltolni, de utána nem szeretnénk a módosult koordináta-rendszerrel bajlódni, tegyük a következőt:
glPushMatrix(); glTranslatef(0.5,0,0); //rajzolás az eltolt helyen glPopMatrix(); //rajzolás az eredeti helyen
- A Push és Pop nem véletlenül egy vermet használ a mátrixok tárolására. Egymásba ágyazott Push-Pop párok segítségével hierarchikus objektumok kirajzolását könnyedén megoldhatjuk:
Vegyünk példának egy pálcikaember kirajzolását. Az origó legyen a pálcikaember testének közepe. Rajzoljunk egy vonalat (0,-0.5) és (0,+0.5) pontok között. Tároljuk el az aktuális koordináta-rendszert a glPushMatrix meghívásával. A lábszár kirajzolásához először toljuk el az origót a glTranslatef(0,-0.5,0) paranccsal a test aljához, hívjunk újabb Push-t, majd forgassuk el a koordináta-rendszert 30 fokkal a Z tengely mentén: glRotatef(30,0,0,1). Ekkor a 0.8 hosszú lábat egyszerűen megrajzolhatjuk a (0,0) és a (0,-0.8) közötti vonallal, mely már elforgatva és megfelelően eltolva jelenik meg. Ha vissza szeretnénk térni az elforgatás nélküli koordináta-rendszerhez, csak hívjuk meg a glPopMatrix függvényt. Ez után az ellenkező irányba forgatva (-30 fok) megrajzolhatjuk a másik lábat. Két glPopMatrix hívás után visszatértünk az eredeti origóba a test közepéhez, és folytathatjuk a test többi részének kirajzolását.
Ugyanez kódban:
glBegin(GL_LINES); glVertex2f(0,-0.5); glVertex2f(0,+0.5); glEnd(); //test kirajzolása glPushMatrix(); glTranslatef(0,-0.5,0); glPushMatrix(); glRotatef(30,0,0,1); glBegin(GL_LINES); glVertex2f(0,0); glVertex2f(0,-0.8); glEnd(); //egyik lába glPopMatrix(); glPushMatrix(); glRotatef(-30,0,0,1); glBegin(GL_LINES); glVertex2f(0,0); glVertex2f(0,-0.8); glEnd(); //másik lába glPopMatrix(); glPopMatrix(); //további testrészek kirajzolása
Görbék
- A görbékről igen sok anyag található a könyvben, diákon, még itt a wiki-n is, így csak néhány hasznos tippet és programozás-technikai dolgot írok itt le
- A görbe egy elvont matematikai fogalom, a kirajzoláskor nem használjuk! A görbéket vektorizáljuk (vonalakra bontjuk), és ezeket a törött-vonalakat rajzoltatjuk ki az openGL segítségével.
- A program elején hozzuk létre a görbéket, vektorizáljuk őket, és a vonalak kezdő és végpontjait tároljuk el. Semmiképpen se az onDisplay-ben számoljuk ki minden egyes kirajzoláskor a görbe pontjait (ha csak nem változó alakú görbéről van szó)! A görbére a vektorizálás után már nincs is szükségünk, csak a vonalakat rajzoljuk ki.
- A görbék egyik közös vonása, hogy az egész görbét (vagy annak egy szegmensét) egy folytonos t változóval járhatjuk végig. Vektorizáláskor ezt a t változót léptetjük végig 0 és 1 között adott lépésközzel, így megkapjuk a görbe egyes pontjait, amit utána egyenessel összekötve közelítünk. A t végigjárásának lépésköze lesz a törött-vonal felbontása.
- A görbék kontrollpontjainak megadása nem egyszerű, a rajzolás és a tesztelés egyszerűbbé tehető, ha a görbénket egy másik program segítségével "megtervezzük". Ilyen Java-applet-eket könnyen találhatunk az Interneten, elég rákeresni a "<görbenév> applet" kifejezésre, és máris grafikus felületen kattingathatjuk össze a görbénket.
Időzítés, animáció
- Az animáció kezelésére és az újrarajzolás elhelyezésére (a Gordiuson történő hibás működések miatt) sokféle megoldás született. Én a szerintem leglogikusabb elrendezést írom itt le, amely garantáltan működik otthon és a Gordiuson is. Az animáció elve a következő:
- Létezik a világ modellje, gyakorlatilag a globális változóink. Azért globálisak, hogy elérhetőek legyenek a különböző GLUT függvényekből. A modellt csak az események, azaz az onKeyboard, onMouse és onIdle függvények módosíthatják.
- Van egy jól megírt onDisplay részünk, ami a modell alapján kirajzolja a világ aktuális állapotát. Az onDisplay nem módosítja a modellt. Az onDisplay-ben ne hívjuk meg saját magát, illetve ne hívjunk glutPostRedisplay-t!
- A keretrendszer időnként úgy érzi, hogy ráérne újrarajzolni a képet, ilyenkor meghívja az onIdle függvényt. Ez a függvény nem rendszeres időközönként hívódik meg, meghívását tekintsük véletlenszerűnek!
- Az onIdle függvényben kiszámítjuk az eltelt időt, ez alapján módosítjuk a modellt, majd meghívjuk a glutPostRedisplay-t a világ újrarajzoltatásához.
- Nem gond ha minden onIdle hívásra meghívjuk a glutPostRedisplay-t, bár kicsit pazaroljuk az erőforrásokat, de legalább garantáltan minden módosítás érvényesül a képen is.
- Az eltelt idő számításához kérjük le a program indítása óta eltelt időt. Ezt a _glutGet(GLUT_ELAPSED_TIME)_ függvény adja vissza egy long típusú változóban. Ha az onIdle végén egy globális változóba eltároljuk az így kapott értéket, akkor az onIdle következő hívásakor már kiszámíthatjuk a két hívás között eltelt időt. Ezt a globális változót az onInitialization-ben állítsuk 0-ba, nehogy az esetleg benne lévő memóriaszemét az első onIdle híváskor indokolatlanul elmozdítsa a modellünket.
- Ezek alapján az onIdle felépítése:
//globális változók long previousTime; void onIdle( ) { long time = glutGet(GLUT_ELAPSED_TIME); long deltaTime = previousTime - Time; //... - mozgatás deltaTime alapján previousTime = time; //akt. idő elmentése glutPostRedisplay();
- Saját gép VS. Gordius
- A saját gépünkön teszteléskor az onIdle igen gyakran meghívódik. A program szabad idejében folyamatosan hívogatja, és minden onIdle végén újrarajzolja a képet. Ennek eredménye a folyamatos CPU használat és több száz FPS-en futó program. (A játékprogramokba azért tesznek Limit Framerate kapcsolót, hogy elkerüljék hogy az indokolatlanul magas FPS generálás teljesen elfogyassza a CPU-t)
- Az ilyen sűrű hívás rendkívül rövid eltelt időket eredményez, ezért készüljünk fel, hogy két hívás között alig mozdul az animációnk. Ha az elmozdulást tároljuk, mindenképpen float vagy double változóban tegyük, hiszen könnyen lehet hogy az elmozdulás nem éri el az egy egész egységet.
- A Gordius ezzel szemben összesen 3-4-szer hívja meg az onIdle-t, pontosan annyiszor, ahány képet generál. Ezen meghívások között viszont több másodperc is eltelhet, ezért készítsük fel a programot az igen nagy eltelt idők lekezelésére is!
- A ritka onIdle hívások tesztelésére jó módszer, ha az ablakunkat fejlécénél fogva megfogjuk, majd csak pár másodperc utána engedjük el. Ekkor ugyanis az ablak mozgatása közben nem rajzolódik újra a kép, az onIdle csak az elengedés után fog legközelebb meghívódni.
- Animáció:
- Az animálás alapja a két onIdle hívás között eltelt idő, amit ezredmásodpercben kapunk meg. Ha elmozdulást, forgást vagy hasonló mozgást animálunk, használjuk a jól ismert u = v*t képletet
- Ha ciklikusan ismétlődő cselekvést akarunk animálni, figyeljünk rá hogy az eltelt idő lehet nagyobb mint a ciklusidő. Ha pl. 2 másodpercenként újrainduló animációnk van, ki kell tudnunk számolni, hogy 6.8 másodperc múlva hogy áll a modellünk. Ennek számítása a következő módon történhet:
- Használhatunk pl. modulo osztást valós számokkal. (Vigyázat, a % operátor csak egészekkel működik, így itt nem használható!) A példánál maradva: hogy megtudjuk, hogy az eltelt 6.8 mp után az aktuális cikluson belül hol tartunk, számoljunk a következő módon: 6.8 - ( floor(6.8/2.0) * 2.0 ) = 0.8
- Pontosan ezt csinálja az fmod függvény is: lebegőpontos osztás maradékát adja vissza. Így fmod(6.8, 2.0) = 0.8.
Tesszelláció
- A tesszelláció a világban található objektumok felbontása háromszögekre. Hogy miért van erre szükség? Mert az openGL csak vonalakat és háromszögeket képes kirajzolni, minden test ezekből épül fel.
- 2D házik
- Már itt is megjelenhet tesszelláció. Ha egy görbe vonallal körülvett kitöltött síkidomot várnak tőlünk, azt nyilván háromszögekre bontva tudjuk kirajzolni.
- Ha a síkidomunk konvex, vagy "majdnem konvex" akkor szerencsénk van. Vegyünk egy olyan pontot, mellyel a görbe összes másik pontja összeköthető anélkül hogy metszenénk az éleket. Konvex síkidom esetén ez nyilván bármelyik pont lehet. Ha a választott ponttól kezdve körüljárjuk a síkidomot, és a kapott pontokat egy TRIANGLE_FAN pontjainak adjuk meg, akkor már kész is a kirajzolás.
- Ha a síkidom konkáv, akkor sajnos egy bonyolultabb megoldásra lesz szükségünk. Ez a fülvágó algoritmus. Ennek megvalósításáról olvashatunk a diákon vagy akár internetes tutorialokban is, erre most nem térnék ki.
- Sugárkövetéses házik - Ha a házi háromszöghálót vagy tesszellált testet említ, akkor bizony nekünk kell leprogramozni a bonyolult test háromszögekre bontását. Ez a 3D-s házikhoz hasonlóan megoldható, kicsit lejjebb olvashatsz róla. A kész háromszögeket tároljuk el, hiszen ezekre egyesével meg kell majd hívnunk a sugár ütköztetést.
- *3D házik
- Bár a glu és glut könyvtárak sokféle könnyen használható objektumot tartalmaznak, ezek a házikban *nem használhatóak*!
- TODO: árnyaló normálok
- TODO: tipikus testek
Sugárkövetés
- A sugárkövetés házinak nem sok köze van az openGL-hez, inkább sok 3D-s koordináta-geometria és némi fizika szükséges hozzá.
- A sugárkövetés nagy hátránya, hogy mivel mi írjuk a "renderelő motort", így igen nehéz a tesztelés és debuggolás, hiszen egy apró hiba miatt is előfordulhat hogy a képen semmit sem látunk. Könnyítsük meg a saját dolgunkat azzal, hogy egy papírra lerajzoljuk a testek elhelyezkedését, többször is ellenőrizzük a tengelyek irányát. Ha sehogy nem azt látjuk amit szeretnénk, írjuk ki a változóink értékét a konzolra, és úgymond szövegesen teszteljük a programot.
OpenGL rész
- Sugárkövetésnél OpenGL függvényt csak a kész kép kirajzolásához használunk, a következő módon:
glRasterPos2d(-1,-1); glDrawPixels(600,600,GL_RGB,GL_FLOAT,picture);
- A RasterPos segítségével a pixelenkénti rajzolás kezdő helyét adjuk meg, (-1,-1) a bal alsó sarok.
- A DrawPixels hívásal egy 600*600-as képet rajzolunk pixelenként a rasztertárba. A képet a picture tömb tárolja, melynek minden eleme RGB adatokat tárol float típussal. Az egész sugárkövetés célja, hogy ezt a szín-táblázatot feltöltsük az oda érkező fény színével és intenzitásával.
- Egy ilyen tömböt a következő módon definiálhatunk: Color picture[600][600]; ahol Color egy 3 float értéket tartalmazó osztály. A színkomponensek természetesen itt is 0-1 skálán értendők.
- A 600*600 pixel nem véletlen, a keretben ez az ablak fixen megadott mérete.
Osztályok
- Megkönnyítjük a saját dolgunkat, ha az alábbi osztályokat létrehozzuk, és azokhoz megfelelő operátorokat készítünk:
- Vector/Coordinate/Point
- Elnevezés ahogy tetszik, a lényeg hogy egy 3D-s vektor X,Y,Z koordinátáit tárolja double vagy float típusú változókban
- Ugye tudjuk hogy a vektor lehet helyvektor és irányvektor is, erre nem kell két külön osztály
- Hasznos operátorok: +=, *=, +, *, skaláris szorzat, vektoriális szorzat
- Hossz lekérdezés (egyszerű Pitagorasz tétel 3D-ben), illetve normalizálás (a koordinátákat külön-külön leosztjuk a hosszal, így a vektor hossza 1 lesz)
- Az irányvektorokat mindig normalizáljuk, ezzel megkönnyítjük a későbbi számításokat
- Color - szigorúan float típusú adattagok a DrawPixels-el való kompatibilitás végett. A színkomponensek 0 és 1 között mozognak, ám a sugárkövetéskor sokszor a színen valójában intenzitást értünk, így 1-nél nagyobb értékeket is felvehet (pl.: a sugarak színe "összeadódik") Figyeljünk rá hogy ekkor a végső kép pixeleit 0 és 1 közé hozzuk -> Tone Mapping
- Ray - sugár, melynek van kiinduló pontja és iránya (normalizálva)
- Triangle
- A tesszellált testek alapvető építőeleme, három pontja és egy normál-vektora van, mely a síkjára merőleges két irány egyikébe mutat. Ez határozza meg, hogy melyik a háromszög "előlapja" és melyik a "hátlapja".
- Létfontosságú függvénye a metszéspont keresés, amely egy sugárra visszaadja, hogy az metszi-e, és ha igen, hol metszi a háromszöget. Lényeges lesz majd a sugár kiindulópontjának valamint a metszéspontnak a távolsága is. Ha ez az érték negatív, a háromszög a sugár "mögött" van, így nem találja el.
- Mesh - háromszögekből álló test, mely egy közös metszéspont kereső függvényt definiál, az összes háromszög metszéspontjai közül azt adja vissza, amelyiknek legkisebb a távolsága (de még pozitív), tehát amit legelőször talál el a sugár.
- Vector/Coordinate/Point
Metszéspontok, önárnyékolás és az EPSILON
- TODO
Sugár indítások, egy és kétirányú sugárkövetés
- TODO: egy/kétirányú: ...
- Az indítandó sugarak létrehozásához nekünk kell definiálnunk a perspektivikus kamerát, hasonlóan ahhoz, ahogy az openGL 3D perspektivikus leképzését definiáljuk: TODO: ...
Színek, fények, optikai modellek
- Tone Map
- A megkapott nagy intenzitáskülönbségeket tartalmazó képünket kirajzolás előtt a 0-1 tartományba hozzuk
- Legegyszerűbb módja az egyszerű osztás: megkeressük a legnagyobb intenzitású képpontot, és annak intenzitásával leosztunk mindent. Hátránya hogy a kép nagyon sötét lehet.
- Egy fokkal jobb megoldás, ha legnagyobb intenzitású képpont intenzitásának kb. 80%-val osztunk le, és az így kapott 1-nél nagyobb színértékeket 1-el helyettesítjük (kvázi levágjuk a tartomány tetejét) Kis kísérletezéssel egész jó dinamikatartományú képet kaphatunk, kevés beégett fehér résszel.
- Az igazi professzionális Tone Map ennél bonyolultabb, részletesen olvashatunk róla a diákon vagy az interneten.
- Egyik fontos eleme, hogy figyelembe veszi a három színkomponens fényességének eltérését, és a következő kísérleti alapon megállapított intenzitásképletet használja: Y = 0.2126 R + 0.7152 G + 0.0722 B
3D alapok
- TODO: kamera, Z-buffer VS. clipping pane, árnyalás VS. normálvektorok, glut tesszellátorok
3D extrák
- TODO: fények, csillanás, textúrázás
Hibakezelés
-- BlackGhost - 2011.04.05.