„Számítógépes grafika házi feladat tutorial” változatai közötti eltérés

A VIK Wikiből
 
(333 közbenső módosítás, amit 5 másik szerkesztő végzett, nincs mutatva)
1. sor: 1. sor:
{{GlobalTemplate|Infoalap|SzgGrafHaziTutorial}}
{{RightTOC}}
{{Vissza|Grafika}}


{{Vissza|Számítógépes_grafika_és_képfeldolgozás}}
== Előszó ==


Ez a tutorial nem helyettesíti az órán leadott tananyagot, de sokat segíthet a házi megírásában, ha esetleg nem volt időd órára járni, vagy valamelyik előadás nem volt teljesen világos. A leírás célja, hogy segítsen az alapvető buktatók elkerülésében, valamint választ adjon az évről-évre a listán újra és újra elhangzó kérdésekre.
Ez a segédlet a házikhoz kellő ötletek és OpenGL függvények működését hivatott elmagyarázni. Az elméletbe csak olyan mélységig megy bele, hogy a házidat meg tudd írni, de nem olyan mélységig, hogy a védésen, vagy vizsgán feltett kérdések mindegyikére válaszolni tudj. Arra ott vannak a hivatalos források: az előadásdiák, a sünis könyv, és - sokak számára meglepő - de maga az előadás is. Amiben ez a összefoglaló más az előbb említett forrásoktól, az az, hogy ez sokkal tömörebb és lényegre törőbb,  mindenhol megpróbálja felhívni a figyelmed a tipikus hibákra, illetve tartalmaz több ezer sornyi kipróbálható példaprogramot. Ezek a programok azért érhetőek el forráskóddal együtt, hogy könnyen ki tudd próbálni őket, részekre tudd bontani, apró módosításokat is ki tudj rajta próbálni, egyszóval azért vannak itt, hogy a megértést segítsék. Nem azért, hogy ezekből oldd meg a feladatodat. Én megbízok bennetek, de ha ezzel visszaéltek, le fogom szedni a példaprogramokat.  


'''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'''
Az oldalról '''kódot a saját házidba átemelni TILOS!''' Más házijába lehet, oda csak nem ajánlott. Még ha pár sornyi kódról is van szó, akkor is inkább gépeld be magadnak, addig is gyakorolsz... (nem csak gépelni). Meg persze nem is érdemes másolni, mert csak borzolod vele a plágiumkereső idegeit.


'''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 ==
 
__TOC__
 
==Az OpenGL és GLUT alapjai==
=== Az OpenGL ===
=== Az OpenGL ===
* Az OpenGL egyszerű térbeli alakzatok (primtívek), pl.: pontok, vonalak, háromszögek rajzolására specializálódott.
* Az OpenGL (Open Graphics Library) egyszerű térbeli alakzatok (primití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árszömszögek rajzolásából építkeznek. A primitíveket nagyon sokféleképpen és nagyon hatékonyan lehet az OpenGL-lel kirajzolni, de ezen kívül mást nem tud, pl. képek betöltéséhez külső könyvtárat kell használnod, nem tud árnyékokat számolni, de még egy kocka kirajzolásához is "küzdeni" kell.
** 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 hatékonyság növelése érdekében az OpenGL a videókártyát is használja a rajzoláshoz.  
** 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.
* 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épen 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 hatékonyság növelése érdekében az OpenGL a videókártyát is használja a rajzoláshoz.  
* 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.: glVertex3f() - 3f = 3 db float, glTexCoord2iv() - 2iv = 2 elemű int vector (azaz pointer egy tetszőleges memóriaterületre, ahol 2 db int van egymás után).  
** 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:
* 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
** 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).
** 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.
*** pl.: glDrawElementsInstancedBaseVertexBaseInstance() - amúgy ez a leghosszabb nevű OpenGL függvény, de a tárgyból nincs rá szükség.
** A függvényeknek lehetnek változatai különböző tulajdonságok alapján, ilyenkor ez a tulajdonság megjelenik a névben is.
* 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.
* A grafikában a lebegőpontos számokkal float alakban szeretünk dolgozni. Az esetek többségében a float és double használatával előállított kép között nem lehet szabad szemmel észrevenni a különbséget, viszont a teljesítményben elég jelentős eltérések adódhatnak. 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 double-t váró alakot, 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 RGB színskálán állítja elő a képet, és neki is RGB érté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 byte-on 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ámhossza), 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 (a fotonok száma) is, ami független magától a színtől. A fényerősség tetszőlegesen nagy, vagy kicsi, sőt, grafikában akár még negatív is lehet, speciális hatások eléréséhez. 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.
* C++-ban a beépített típusok mérete (az egy bytehoz képest) nem fix. Ezt sajnos az OpenGL nem nagyon veszi figyelembe, ő pl. int alatt mindig 4 byteos intet ért. Viszont definiál nekünk jó pár makrót, pl. a GLint-et, ami minden architektúrán garantáltan olyan méretű lesz, mint ami neki kell. Ezeknek a típusoknak az elnevezési konvenciója egyszerű: GL{u}típusnév. Az 'u' betű az unsignedra utal, például a GLushort az olyan gépeken, ahol a bájt 8 bit, megegyezik a uint16_t-vel, és általában az unsigned short-tal is. Egy kivétel van az elnevezési konvencióba: nincs GLchar, helyett GLbyte meg GLubyte van. Ezeket a típusokat nagyon ajánlott használni, főleg ha az adatunkat cím szerint adjuk át, például tömbök esetén.  
* 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.
* 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 ===
* 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).
* A GLUT (OpenGL Utility Toolkit) 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 nagy ré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:
* 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.  
** <code>onDisplay()</code> - 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 <code>onDisplay()</code>-t lefuttatni, hívjuk meg a <code>glutPostRedisplay()</code> függvényt (ne közvetlenül az <code>onDisplay()</code>-t!). Fontos hogy az '''<code>onDisplay()</code>-en belül tilos meghívni a <code>glutPostRedisplay()</code>-t,''' az így megírt program elvi hibás (a képernyő mindig érvénytelen marad), és annak ellenére, hogy ez nálad valószínűleg működni fog, a beadón sajnos nem.  
** '''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.
** <code>onInitialization()</code> - inicializálós 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ó konstruktorában meghívunk ilyen függvényt. Így ugyanis még a main() kezdete előtt futattnánk le egy ilyen típusú függvényt, amikor még ezek a könyvtárak nincsenek inicializálva. Ennek az elkerülésére van ott az <code>onInitialization()</code> - ebben már nyugodtan használhatunk bármilyen függvényt.
** '''onKeyboard()''' - Itt tudjuk kezelne egy billentyű lenyomását. Erre a házikban általában csak minimális szükség van.  
** <code>onKeyboard()</code> - Itt tudjuk kezelni egy billentyű lenyomását. Ez az esemény nem csak akkor generálódik, amikor a billentyűt éppen lenyomtuk, hanem akkor is, ha egy lenyomott betű hatására egy újabb karaktert gépelnénk be. 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.
** <code>onKeyboardUp()</code> - Itt tudjuk kezelni egy billentyű felengedést. Valós játékokban ez nagyon hasznos tud lenni, de házikhoz ezt szinte soha nem kell használni.
** '''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.
** <code>onMouse()</code> - Itt kapunk értesítést arról, ha valamelyik egérgomb állapota megváltozott, és azt is megtudjuk, hogy az ekkor az egér az ablak koordinátái szerint hol volt.
** '''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ó).
** <code>onMouseMotion()</code> - Itt tudjuk meg, ha a felhasználó lenyomott egér gomb mellett mozgatta az egeret. A koordinálta értékére ugyanaz vonatkozik, mint az onMouse esetén.
** <code>onIdle()</code> - 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ó).
 
== 2D OpenGL ==


== Rajzolás az OpenGL segítségével ==
=== 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.
Az OpenGL csak néhány típusú primitívet tud rajzolni, ezekből kell építkeznünk. A típusok:
* A típusok:
* Pontok: <code>GL_POINTS</code>
** Pontok: GL_POINTS
* Vonalak: <code>GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP</code>
** Vonalak: GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP
* Háromszögek: <code>GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_LOOP</code>
** Háromszögek: GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_LOOP
* Háromszögekből összetett alakzatok:
** Háromszögekből összetett alakzatok:
** Négyszögek (igazándiból 2 háromszög): <code>GL_QUADS, GL_QUAD_STRIP</code>
*** 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): <code>GL_POLYGON</code>
*** Sokszög (ez is háromszögenként rajzolódik ki): GL_POLYGON


<div style="text-align:center;margin:0px auto;">
<div style="text-align:center;margin:0px auto;">
54. sor: 50. sor:
</div>
</div>


* A rajzolás az alábbi módon történik:
A rajzolás az alábbi módon történik:
<pre>
<pre>
glBegin("Primitív típus");
glBegin("Primitív típus");
62. sor: 58. sor:
glEnd();
glEnd();
</pre>
</pre>
* 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.
A pontok megadásához glVertex{2,3}f függvények valamelyikét kell használnod, az alapján, hogy hány dimenzióban dolgozol. Tehát 2 dimenzióban a glVertex2f-et, 3 dimenzióban a glVertex3f-et kell használnod.
** 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.


== Az első házihoz szükséges elmélet ==
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 ===
=== 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.
A koordináták amiket átadsz azok a normalizált eszköz koordináta-rendszerben vannak értelmezve, ahol a (0,0) a képernyő közepe, a (-1, -1) pedig a bal alsó sarok.
** Példaprogram: [http://pastebin.com/zAMBmSz5 Háromszögek]
 
Példaprogram: [[Média:Grafpp_haromszogek.cpp|Háromszögek]]


<br/> <syntaxhighlight lang="c">
<br/> <syntaxhighlight lang="c">
88. sor: 83. sor:


Az eredménye:
Az eredménye:
<div style="text-align:left;margin:0px auto;">
 
http://i.imgur.com/bOSuMin.png
http://i.imgur.com/bOSuMin.png
</div>
 
<br/>
Minden egyes ponthoz külön színt is meg 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.
* 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: [http://pastebin.com/GpEqzmB1 Smiley]
Példaprogram: [[Média:Grafpp_smiley.cpp|Smiley]]


<br/> <syntaxhighlight lang="c">
<br/> <syntaxhighlight lang="c">
#define CIRCLE_RESOLUTION 32
#define CIRCLE_RESOLUTION 32


// Piros kor, a kepernyo bal oldalan
// Piros kor, a képernyő bal oldalán
glBegin(GL_TRIANGLE_FAN); {
glBegin(GL_TRIANGLE_FAN); {
   float radius = 0.25f, center_x = -0.5f, center_y = 0.4f;
   float radius = 0.25f, center_x = -0.5f, center_y = 0.4f;
105. sor: 100. sor:
   glVertex2f(center_x, center_y);
   glVertex2f(center_x, center_y);


   for(int i = 0; i <= CIRCLE_RESOLUTION; i++) {
   for(int i = 0; i < CIRCLE_RESOLUTION; i++) {
     float angle = float(i) / CIRCLE_RESOLUTION * 2.0f * M_PI;
     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)
     // Itt a kor paramtetrikus alakjat hasznaljuk: x = x0 + r*cos(t), y = y0 + r * sin(t)
112. sor: 107. sor:
} glEnd();
} glEnd();


// Szinatmenetes kor, a kepernyo jobb oldalan
// Színátmenetes kör, a képernyő jobb oldalán
glBegin(GL_TRIANGLE_FAN); {
glBegin(GL_TRIANGLE_FAN); {
   float radius = 0.25f, center_x = 0.5f, center_y = 0.4f;
   float radius = 0.25f, center_x = 0.5f, center_y = 0.4f;
119. sor: 114. sor:
   glVertex2f(center_x, center_y);
   glVertex2f(center_x, center_y);
    
    
   for(int i = 0; i <= CIRCLE_RESOLUTION; i++) {
   for(int i = 0; i < CIRCLE_RESOLUTION; i++) {
     float angle = float(i) / CIRCLE_RESOLUTION * 2.0f * M_PI;
     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));
     glColor3f(0.0f, 0.5f + 0.5f*cos(angle), 0.5f + 0.5f*sin(angle));
126. sor: 121. sor:
} glEnd();
} glEnd();


// Felkoriv
// Félkörív
glBegin(GL_LINE_STRIP); {
glBegin(GL_LINE_STRIP); {
   float radius = 0.75f, center_x = 0.0f, center_y = 0.0f;
   float radius = 0.75f, center_x = 0.0f, center_y = 0.0f;
140. sor: 135. sor:


Az eredménye:
Az eredménye:
<div style="text-align:left;margin:0px auto;">
 
http://i.imgur.com/6yfh7q2.png
http://i.imgur.com/6yfh7q2.png
</div>


=== Eseménykezelés ===
=== 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 grafikus programok általában eseményvezéreltek, azaz a felhasználó által generált események (pl. egérkattintás) irányítják 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:  
A három legfontosabb eseménytípus:
*** Billentyű lenyomás - onKeyboard
* Billentyűzet esemény:  
*** Billentyű felengedés - onKeyboardUp
** Billentyű lenyomás - onKeyboard
** Idő múlása (lényegében ez is egy esemény) - onIdle
** Billentyű felengedés - onKeyboardUp
** Egér esemény:
* Idő múlása (lényegében ez is egy esemény) - onIdle
*** Egér mozgatás - onMouseMotion
* Egér esemény:
*** Egér gombbal kattintás - onMouse
** Egér mozgatás - onMouseMotion
*** 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:
** 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özkoordinátákkal dolgozunk (az ablak 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 megoldás:


<br/> <syntaxhighlight lang="c">
<br/> <syntaxhighlight lang="c">
172. sor: 167. sor:
</syntaxhighlight> <br/>
</syntaxhighlight> <br/>


* Példa: [http://pastebin.com/Ns0fiFL2 Egyszerű rajzolóprogram]
Példa: [[Média:Grafpp_rajzoloprogram.cpp‎|Egyszerű rajzolóprogram]]


<br/> <syntaxhighlight lang="c">
<br/> <syntaxhighlight lang="c">
void onMouse(int button, int state, int x, int y) {
void onMouse(int button, int state, int x, int y) {
   if(button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN) {
   if(button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN) {
     glClear(GL_COLOR_BUFFER_BIT); // Jobb klikkre toroljuk a kepernyot.
     glClear(GL_COLOR_BUFFER_BIT); // Jobb klikkre töröljuk a képernyőt.
     glutPostRedisplay(); // Szolunk, hogy az ablak tartalma megvaltozott, kerjuk a GLUT-ot, hogy hívja meg az onDisplay-t.
     // Szólunk, hogy az ablak tartalma megváltozott,  
   } else if(button == GLUT_LEFT_BUTTON) { // Ha a bal gomb allapota megvaltozott.
    // kérjuk a GLUT-ot, hogy hívja meg az onDisplay-t.
    glutPostRedisplay();
   } else if(button == GLUT_LEFT_BUTTON) { // Ha a bal gomb állapota megváltozott.
     if(state == GLUT_DOWN) {
     if(state == GLUT_DOWN) {
       drawing = true; // Ha lenyomtuk akkor rajzolni akarunk.
       drawing = true; // Ha lenyomtuk akkor rajzolni akarunk.
       Vector pos = convertToNdc(x, y); // Atvaltjuk a pontot.
       Vector pos = convertToNdc(x, y); // Átváltjuk a pontot.
       glBegin(GL_POINTS); { // Kirajzoljuk.
       glBegin(GL_POINTS); { // Kirajzoljuk.
         glVertex2f(pos.x, pos.y);
         glVertex2f(pos.x, pos.y);
       } glEnd();
       } glEnd();
       last_mouse_pos = pos; // Elmentjuk, hogy az elso szakasz, majd ebbol a pontbol indul.
       last_mouse_pos = pos; // Elmentjük, hogy az első szakasz, majd ebből a pontból indul.
       glutPostRedisplay(); // Szolunk, hogy az ablak megvaltozott, kerjuk az ujrarajzolasat.
       glutPostRedisplay(); // Szolunk, hogy az ablak megváltozott, kérjük az újrarajzolását.
     } else if(state == GLUT_UP) {
     } else if(state == GLUT_UP) {
       drawing = false; // Ha most engedtuk fel, akkor mar nem akarunk rajzolni.
       drawing = false; // Ha most engedtük fel, akkor mar nem akarunk rajzolni.
     }
     }
   }
   }
196. sor: 193. sor:
void onMouseMotion(int x, int y) {
void onMouseMotion(int x, int y) {
   if(drawing) {
   if(drawing) {
     Vector pos = convertToNdc(x, y); // Kiszamoljuk az eger jelenlegi helyzetet NDC-ben.
     Vector pos = convertToNdc(x, y); // Kiszámoljuk az egér jelenlegi helyzetet NDC-ben.
     glBegin(GL_LINES); { // Kirajzolunk egy vonalat az elozo es a mostani helyzete koze.
     glBegin(GL_LINES); { // Kirajzolunk egy vonalat az előző és a mostani helyzete közé.
       glVertex2f(last_mouse_pos.x, last_mouse_pos.y);
       glVertex2f(last_mouse_pos.x, last_mouse_pos.y);
       glVertex2f(pos.x, pos.y);
       glVertex2f(pos.x, pos.y);
     } glEnd();
     } glEnd();
     glutPostRedisplay(); // Szolunk, hogy az ablak megvaltozott, kerjuk az ujrarajzolasat.
     glutPostRedisplay(); // Szólunk, hogy az ablak megváltozott, kérjük az újrarajzolását.
     last_mouse_pos = pos; // Frissitjuk a elozo helyzetet.
     last_mouse_pos = pos; // Frissítjük a előző helyzetet.
   }
   }
}
}
208. sor: 205. sor:


Az eredménye:
Az eredménye:
<div style="text-align:left;margin:0px auto;">
 
http://i.imgur.com/4WDimmL.png
http://i.imgur.com/4WDimmL.png
</div>


=== Animáció ===
=== 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ó annyit jelent, hogy az egyes objektumok állapota az időnek is a függvénye. A pillanatnyi időt a <code> glutGet(GLUT_ELAPSED_TIME); </code> függvényhívással tudjuk lekérdezni, célszerűen az onIdle függvényben. Az animáció alatt leggyakrabban testek mozgását, néha testek deformálódását értjük. Egy mozgó testet legjobban a fizika törvényeivel tudunk leírni, egy egyenes vonalú egyenletes mozgás leírásához mindössze a <code> v = s / t </code> képletre van szükségünk. Az animáció onnantól kezd bonyolulttá válni, amikor több mozgó test állapota egymástól függ (például ütköznek egymással). Ilyenkor ugyanis a korrekt szimuláció egy differenciálegyenlet megoldását jelentené. Numerikus módszerekkel viszont nem szeretünk diffegyenletet megoldani. Ennek a problémának 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ásra hatásokat. Így a képletek nagyon leegyszerűsödnek, és semmi szükség nem lesz differenciálegyenletekre. Manapság a számítógépes játékok nagy része is ezt a módszert használja.
* 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: [[Média:Grafpp_pattogo_labda.cpp|Pattogó labda]]
* Egyszerű példaprogram: [http://pastebin.com/7FHzfjA7 Pattogó labda]
 
<div style="text-align:left;margin:0px auto;">
<br/> <syntaxhighlight lang="c">
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 milliszekundumban
  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 eltelt időt kisebb részekre, pl. max 5 milliszekundumos 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
}
</syntaxhighlight> <br/>
 
Az eredménye:
 
http://i.imgur.com/ezFQ4l4.png
http://i.imgur.com/ezFQ4l4.png
</div>


==Projekciós mátrixok, transzformációk==
=== Koordináta rendszerek ===
 
Az korábbi példákban 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 ugyanazt 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 foroghat is. Az OpenGL kitalálóinak az ötlete az volt erre, hogy a kamera mindig maradjon egy helyben, de ha pl. balra akarnánk forgatni, akkor helyette inkább a világ forogjon jobbra, a kamera viszont maradjon ugyanott, ezzel is ugyanazt a hatást érjük el. Első ránézésre nem látszik, hogy ez miért jó, de ez valójában egy nagyon jó ötlet. Szerencsére ha a világot el kell forgatnunk, akkor nem kell minden egyes pontot nekünk külön-külön elforgatni egy ronda trigonometrikus képlet alapján, 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 grafikában általában affin (egyenestartó) transzformációkat szoktunk használni, a vetítéseket leszámítva. Ezeket a 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átrixszal felírni. Pedig erre a műveletre biztos, hogy szükségünk lesz. Ennek kiküszöbölésére használhatunk 4D-be 'w' = 1 koordinátával beágyazott 3D-s koordináta-rendszert, ahol az eltolás is egy lineáris trafó.
 
Az OpenGL két mátrixot ad nekünk, amiket 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 kamera azt lássa, amit meg akarunk jeleníteni. A GL_PROJECTION pedig azt adja meg, hogy a kamerával hogyan kell fényképezni.
 
Két dimenzió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. Két dimenzióban 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 pozíció alapján rajzolunk. Vagy legalábbis ezt csináltuk eddig, amikor a GL_PROJECTION egységmátrix volt, de az NDC koordináta-rendszerben nem volt kényelmes dolgozni. Itt viszont lehetőséget kapunk saját koordináta-rendszer 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: [[Média:Grafpp_sidescroller.cpp|Sidescroller]]
 
<br/> <syntaxhighlight lang="c">
// 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ünk be a jelenleg módosítható
// mátrix helyére (ez a projekciós mátrix).
glLoadIdentity();
// Ez egy olyan merőleges (ortogonális) vetítés mátrixát "írja be" a GL_PROJECTION-be,
// aminek eredményeké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
);
</syntaxhighlight> <br/>
 
Az eredménye:
 
http://i.imgur.com/S3m5Lmv.gif
 
 
=== Transzformációk ===
 
A GL_MODELVIEW egyik legfontosabb használata, hogy segítségével könnyebben 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ó trigonometrikus 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. Az utóbbi megoldás 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:
* <code> glTranslatef(GLfloat x, GLfloat y, GLfloat z); </code>
* <code> glRotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z); </code>
* <code> glScalef(GLfloat x, GLfloat y, GLfloat z); </code>
 
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átrixa hozzászorzódik a GL_MODELVIEW mátrixhoz (balról). Emlékeztető: a mátrix szorzás szorzás asszociatív. Ez azt jelenti, hogy két transzformációs mátrix összeszorzása után az eredmény ugyanúgy transzformál egy tetszőleges vektort, mint ha a két mátrixszal 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 hierarchiá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 egyáltalán hol van az az autó, aminek a részeiről eddig beszéltünk.
 
[[Média:Grafpp_transzf_sorrend.cpp‎|Példa a transzformációk sorrendjére]]:
 
<br/> <syntaxhighlight lang="c">
glTranslatef(2.7f, -3.1f, 0.0f);
glRotatef(67, 0, 0, 1);
glScalef(2, 2.5f, 1);
</syntaxhighlight> <br/>
 
Ami a koordináta-rendszerrel történik:
 
http://i.imgur.com/gUqk4pi.gif
 
- 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. <br/> <br/>
 
[[Média:Grafpp_transzf_sorrend2.cpp‎|Egy másik lehetséges sorrend]]:
<br/> <syntaxhighlight lang="c">
glScalef(2, 2.5f, 1);
glRotatef(67, 0, 0, 1);
glTranslatef(2.7f, -3.1f, 0.0f);
</syntaxhighlight> <br/>
 
Ami a koordinátarendszerrel történik:
 
http://i.imgur.com/XQcsrHs.gif
 
- 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.43 és 2.084. Ezeknek az értékeknek semmi köze a kódban szereplő konstansokhoz!! <br/> <br/>
 
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.
 
De egy probléma még felmerül, 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álni 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 egyenként vissza kell csinálni? Nincs erre jobb megoldás? A válasz természetesen az, hogy van, ez a megoldás a mátrix stack.
 
=== Matrix stack ===
 
Az OpenGL két függvényt ad amivel a matrix stack-et használhatjuk:
* <code>glPushMatrix()</code>. Ez a jelenleg aktív mátrixot (<code>GL_PROJECTION</code> vagy <code>GL_MODELVIEW</code>) elmenti annak a stackjébe.
* <code> glPopMatrix()</code>. Ez a jelenleg aktív mátrix stackjéből a legutóbb elmentett mátrixot visszaállítja. A következő <code>glPopMatrix()</code> mátrix az az előtt elmentett mátrixot állítja vissza.
 
Megjegyzések:
* A <code>GL_MODELVIEW</code> stack mélysége legalább 32, a <code>GL_PROJECTION</code> 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.
 
Például:
<br/> <syntaxhighlight lang="c">
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.
</syntaxhighlight> <br/>
 
A mátrix stack hierarchikusan felépülő testek rajzolását nagy mértékben megkönnyíti. Hierarchikus 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 is elmozdulnak. Minden ízü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. Ez természetesen egy fa struktúrát jelent, minden ízületnek van egy saját transzformációja, és változó számú gyereke. Ez leprogramozni nem lehetetlen, sőt nem is nehéz, de nem is kifejezetten izgalmas, viszont a mátrix stack segítségével ez nagyon egyszerűen megoldható.


* 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.
Egy emberi kéz kirajzolása pszeudokóddal, hengerekből, 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:
* 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:
<pre>
<pre>
glPushMatrix(); {
  váll körüli forgatás
  felkar hosszának a felével eltolás az x tengely mentén


glPushMatrix();
  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


glTranslatef(0.5,0,0);
  glPushMatrix(); {
//rajzolás az eltolt helyen
    nagyítás a alkar méreteivel
    henger kirajzolása
  } glPopMatrix();


glPopMatrix();
  alkar hosszának a felével eltolás az x tengely mentén


//rajzolás az eredeti helyen
  kéz kirajzolása


} glPopMatrix();
</pre>
</pre>


** 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:
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 <code> glutSolidCube(GLdouble size); </code> függvényt fogjuk használni a rajzoláshoz. Ez a függvény - nem meglepő módon - egy 'size' élhosszúságú kockát rajzol ki.


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.
Próbáld ki nyugodtan: [[Média:Grafpp_glutSolidCube.cpp‎|glutSolidCube]]


Ugyanez kódban:
http://i.imgur.com/PA2A3eQ.png
<pre>
 
glBegin(GL_LINES); glVertex2f(0,-0.5); glVertex2f(0,+0.5); glEnd();  //test kirajzolása
<br/>
glPushMatrix();
 
glTranslatef(0,-0.5,0);
A 3 dimenzió ennek a kódnak a felhasználásával csak annyiban fog különbözni a 2 dimenziótól, hogy van egy harmadik koordináta is. Az OpenGL kamera koordináta-rendszerében (és az általam definiált világ koordináta-rendszerben is) az X tengely továbbra is jobbra, az Y tengely pedig továbbra is felfele mutat, mint ahogy 2D-ben is, csak bejött egy új tengely, a Z ami felénk (a monitorból kifele) mutat.
 
Természetesen ez eltér attól a koordináta-rendszertől, amit matematikában szoktunk használni (X felénk, Y jobbra, Z felfele), ami valószínűleg sokakat zavarni fog.
 
Nekik annyi vigaszt tudok mondani, hogy a GL_MODELVIEW mátrix segítésével tetszőleges koordináta-rendszert definiálhattok magatoknak, ami akár megegyezhet azzal, amit matekórán használni szoktunk.
 
Én ennek ellenére a példaprogramokban nem fogok így tenni, aminek az egyik oka, hogy a példakódokban nem a konkrét vektorok értéken van a lényeg, hanem az ötleteken, és így legalább kisebb késztetést fogsz érezni, hogy rólam másolj. Másrészt meg az OpenGL-ben ezt a koordináta-rendszert "szokás" használni, ahol az Y mutat felfele. Ez főleg az árnyékoló programok világában fontos, ahol sokkal több koordináta-rendszerrel kell dolgozni, mint itt, és ott rengeteg hibalehetőséggel jár, ha mindegyik koordináta-rendszerben mást jelentenek a tengelyek.
 
Ezt felhasználva a példaprogram: [[Média:Grafpp_robot_kar.cpp|Robot kar]]
 
http://i.imgur.com/tpAuxBa.gif
 
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 releváns kód, ebből a programból:
 
<br/> <syntaxhighlight lang="c">
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();
}
</syntaxhighlight> <br/>
 
=== 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éldául 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 érinteni akarunk, kontrollpontnak nevezzük a görbék esetében. 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, ugyanaz a bemenet mellett.
 
A görbéket szinte mindig valamilyen mozgás leírásá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 paramétere, é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). Például c0 folytonos görbe az, aminek nincs szakadása. Ha egy valós útvonal nem c0 folytonos, akkor azt jelenti, hogy valahol teleportálnunk 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 kontrollpontokat 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. Ezt 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). Ezek lokálisan vezérelhetőek, nagyon kényelmes velük dolgozni, viszont az implementálásuk általában bonyolult. Ebből a görbecsaládból például a Kochanek–Bartels görbe nagyon elterjedt. Viszont sok problémához túl bonyolult implementálni, ezért létezik néhány leegyszerűsített változata is, például 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).
 
A görbéknek a képletét nem szokás fejből tudni, bár általában könnyen levezethetőek. Az a tapasztalat a korábbi házikból, hogy azoknak a görbéknek a képlete benne szokott lenni az előadásdiákba, amiket a házihoz használni kell. Sőt, amikor több görbét kell használni, akkor általában legalább az egyiknek pszeudokóddal meg is szokták adni az implementációját is. 
     
Példaprogram: ''<Törölve, túl sokan másolták>''
 
http://i.imgur.com/C1iKaHx.gif
 
== Sugárkövetés ==
 
A sugárkövetős házinál a programod teljes mértékben a CPU-n fog futni, ami a videókártya segítsége nélkül nagyon meg fog izzadni, hogy a képet előállítsa neked. Az előző feladatokkal ellentétben itt az optimalizálás nagyon sokat segít, egy release build (-O3) és egy debug build (-O0) között akár több mint ötszörös sebességkülönbség is lehet. Ezért, ha éppen nem debuggolsz, mindenképpen release buildet fordíts.
 
=== A Sugárkövetés alapjai ===
 
A 2D OpenGL-es példaprogramok között volt egy 3D-s is. Most ezzel a témakörrel fogunk foglalkozni. Technikailag abban a példaprogramban a 3D rajzolást a GLUT csinálta helyettünk. De mielőtt belemennénk a részletekbe, hogy pontosan mit is csinált (ezt majd a 3D OpenGL résznél), vegyük észre, mi is ki tudunk rajzolni egy olyan kockát, mint amit a GLUT csinált, akár az OpenGL segítsége nélkül is.
 
Először gondoljuk át hogy a valóságban hogyan csinálnánk képet egy kockáról. Szükségünk van egy fényforrásra, enélkül garantáltan nem látnánk semmit, és szükségünk van egy ernyőre is (pl: retina), amin a képet felfoghatjuk. Továbbá nem árt, ha van egy kocka is, amit lefényképezhetünk.
 
Ha pontosan azt akarnánk lemodellezni, ahogy a valóságban a kép keletkezik, akkor gondba lennénk, mert a számítógép teljesítményéhez képest gyakorlatilag végtelen fotonnal kéne dolgoznunk. És ráadásul a fényforrásból kiinduló fotonok döntő többsége még csak nem is megy az ernyőnek a közelébe se. Ezt kiaknázandó, a sugárkövetés egyik alapötlete, hogy az ernyőből induljuk ki, ne a fényforrásból, és megfordított irányú fotonokat kövessünk, így csak a releváns fény részecskékkel fogunk foglalkozni.
 
A másik alapötlet, hogy a fotonok olyan sokan vannak, hogy a Nagy Számok Törvénye alapján gyakorlatilag teljesen pontos becsléseket kaphatunk a fotonok viselkedéséről, anélkül, hogy azokkal egyesével foglalkoznunk kellene. Ezt felhasználva nagy mennyiségű fotonból álló csomagok, úgynevezett sugarak útját követjük, és nem fotonokét. Ez talán megmagyarázza, hogy miért hívjuk a technikát sugárkövetésnek. A sugárkövetéshez szükségünk van egy képzeletbeli kamerára, és egy ernyőre (egy téglalapra). A téglalapot felosztjuk annyi egyenlő részre, ahány pixelből áll az ablakunk. Jelen esetben, 600x600-as ablak esetében ez azt jelenti, hogy a téglalap négyzet lesz(két egység élhosszúságú). Ezek után az ablak minden egyes pixelére azt a színt rajzoljuk ki, amit a képzeletbeli kamera látna az ernyőnek a pixelhez tartozó részén keresztül.
 
Az OpenGL használata nélkül ezt úgy kivitelezhetnénk, hogy képet mint egy színekből álló tömböt eltároljuk magunknak, abba renderelünk, majd valamilyen megfelelő kép formátumába kiírjuk ezt egy fájlba. Ezt a megoldást viszont nem lenne túl kényelmes használni. De az OpenGL-t is megkérhetjük arra, hogy jelenítse meg a képet, amit lerendereltünk a <code>glDrawPixels(GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *pixels)</code> függvény segítségével. A házikban tipikusan az utóbbi megoldást szoktuk használni. Például egy lehetséges megvalósítása:
 
<br/> <syntaxhighlight lang="c">
struct Screen {
  static const int width = 600;
  static const int height = 600;
  static Color image[width * height];
  static void Draw() {
    glDrawPixels(width, height, GL_RGB, GL_FLOAT, image);
  }
};
</syntaxhighlight> <br/>
 
A kamera megvalósítása már egy picit trükkösebb. Az implementálása az alábbi lépésekből áll:
* Meg kell adnunk a képzeletbeli kamera pozícióját. Kódban pl: <code>pos</code>.
* Meg kell adnunk, hogy a kamera, merrefelé néz. Kódban pl: <code>fwd</code> (egységvektor).
* Azt is tudnunk kell, hogy melyik iránynak felel meg a felfele ("What's up?"). Kódban pl nevezzük <code>up</code>-nak.
* Tegyük fel, hogy téglalap (vagy sík) egységnyi távolságra van a kamerától. Ekkora annak a középpontja: <code>pos + fwd</code>.
* Tudnunk kell még, hogy melyik irány van jobbra. Ezt az előre és a felfele pozícióból ki tudjuk számolni: <code>right = cross(fwd, up) // a cross-product a vektor szorzat angolul, a dot-product pedig a skalár szorzat. </code>
* A felfele vektor amit megadtunk nem biztos, hogy merőleges az előre vektorra, pedig nekünk olyanra van szükségünk. Pl: ha rézsútosan előre és lefele nézünk, de az 'up' vektor az ég fele mutat. Ez valójában nem baj, mert a jobbra és előre vektor ismeretében már ki tudjuk számolni a pontos felfele vektort: <code>up = cross(right, fwd)</code>.
* Ha ezek megvannak, akkor ki kell tudnunk számolni, hogy egy (x, y) koordinátájú pixelnek a téglalap (ami most egy 2 egység oldalhosszúságú négyzet) melyik része felel meg. Ezt így tehetjük meg:
 
<br/> <syntaxhighlight lang="c">
Vector pos_on_plane = Vector(
  (x - Screen::width/2) / (Screen::width/2),
  (y - Screen::height/2) / (Screen::height/2),
  0
);
</syntaxhighlight> <br/>
* Ezt az értéket pedig át kell számolnunk a világ koordináta rendszerébe:
<br/> <syntaxhighlight lang="c">
Vector plane_intersection = plane_pos + pos_on_plane.x * right + pos_on_plane.y * up;
</syntaxhighlight> <br/>
 
Az előadásdiának az ide vágó ábrája talán segíti a megértést (a lookat-nek a plane_pos felel meg nálam):
 
http://i.imgur.com/J6PfVhq.png
 
* És innen már tudunk mindent a sugárról, amit követnünk kell. Ezeket az adatok célszerű egy struktúrába zárni:
<br/> <syntaxhighlight lang="c">
struct Ray {
  Vector origin, direction;
};
Ray r = {pos, (plane_intersection - pos).normalize()};
</syntaxhighlight> <br/>
 
Megjegyzések az algoritmussal kapcsolatban:
* Azzal, hogy kijelentettük, hogy téglalap egy 2 egység élhosszúságú négyzet, és egységnyi távolságra van a kamerától, implicit kimondtuk, hogy a kamera látószöge 2 * arctg(1/1) = 90 fok. Egy nem túl arányos rajz arról, hogy ez hogy jött ki:
 
http://i.imgur.com/gC7f6l1.png
 
De nem biztos, hogy ennyit szeretnénk, úgyhogy a látószög (Field of View - Fov) is legyen inkább paraméter. Én az ernyő méretét fogom a változtatni, és az ernyő-kamera távolságot meghagyom egységnyinek. Az arány amit akarunk az a tan(fov/2).
* Ha teljesen korrektek akarnánk lenni, akkor fél pixellel el kéne tolni az ernyőt metsző pontokat, hogy azok ne a pixelek bal alsó sarkán keresztül haladjanak át, hanem a közepükön. Bár én szabad szemmel nem látok különbséget ez a változtatás után.
 
Ezeket a változtatásokat is felhasználva egy lehetséges megvalósítás:
 
<br/> <syntaxhighlight lang="c">
struct Camera {
  Vector pos, plane_pos, right, up;
 
  Camera(float fov, const Vector& eye, const Vector& target, const Vector& plane_up)
      : pos(eye), plane_pos(eye + (target-eye).normalize())
  {
      Vector fwd = (plane_pos - pos).normalize();
      float plane_half_size = tan((fov*M_PI/180)/2);
     
      // A jobbra és felfele vektorokban is lehet tárolni a sík méretét, bár nem szép...
      right = plane_half_size * cross(fwd, plane_up).normalize();
      up = plane_half_size * cross(right, fwd).normalize();
  }
 
  void takePicture() {
    for(int x = 0; x < Screen::height; ++x)
      for(int y = 0; y < Screen::width; ++y)
        capturePixel(x, y);
  }
 
  void capturePixel(float x, float y) {
    Vector pos_on_plane = Vector(
      (x + 0.5f - Screen::width/2) / (Screen::width/2),
      (y + 0.5f - Screen::height/2) / (Screen::height/2),
      0
    );
 
    Vector plane_intersection = plane_pos + pos_on_plane.x * right + pos_on_plane.y * up;
 
    Ray r = {pos, (plane_intersection - pos).normalize()};
    Screen::Pixel(x, y) = scene.shootRay(r);
  }
}
</syntaxhighlight> <br/>
 
Megjegyzés: én általában nem ilyen kamerát szoktam használni, de ez ugyanazt az eredményt adja, mint amit a 3D OpenGL résznél fogunk kapni, a gluLookAt() függvény segítségével.
 
Most már mindent tudunk a sugárkövetésről, azt leszámítva, hogy hogyan kell egy sugarat követni.
 
=== Hogyan kövessük a sugarakat? ===
 
Az ötlet az, hogy keressük meg a kamerához legközelebbi objektumot, aminek van metszéspontja a sugárral. Ha találtunk egy metszéspontot, akkor minket a metszéspontja helye és a felületi normál is érdekel (az a vektor, ami merőleges a felületre abban a pontban). Továbbá valahogy azt is jeleznünk kell, ha nem találtunk metszéspontot. Ezeknek az információknak a tárolására egy lehetséges struktúra:
 
<br/> <syntaxhighlight lang="c">
struct Intersection {
  Vector pos, normal;
  bool is_valid;
};
</syntaxhighlight> <br/>
 
Ahhoz, hogy eldöntsük, hogy egy objektumnak van-e metszéspontja a sugárral, fel kell írnunk annak az alakzatnak az egyetlenét, és meg kell oldaniuk egy 't' ismeretlenre azt az egyenletet, hogy ha a sugár kiindulási pontjából 't' egységet megyünk előre a sugár irányába, akkor ki fogjuk elégíteni az alakzat egyenletét.
* Az ilyen egyenletek megoldásához hihetetlen sokat tud segíteni, ha egyszerű ábrákat rajzolsz hozzá.
* Nagyon sok esetben az okoskodás, pl transzformációk használata ''nagyon'' le tud egyszerűsíteni egy ilyen problémát.
* A síkbeli (elsőrendű) alakzatok követésekor első fokú egyenleteket fogunk kapni, míg a "görbülő" (másodrendű) alakzatok, pl kör, ellipszis, henger palást, kúp palást, hiperboloid stb... másodfokú egyenletekre vezetnek.
* Általában véges objektumokat szoktunk rajzolni (pl. négyzet), így ha a hozzá tartozó alakzat (pl. sík) nem véges, akkor meg kell néznünk, hogy a sugár mely pontokban metszi az alakzatot, és ezekről a pontokról eldönteni, hogy azok a véges részbe is benne vannak-e. Ez utóbbi művelet lehet bonyolultabb mint az előző. Pl. egy háromszög követése lényegesen több ötletet igényel mint egy gömbbé.
 
Egy háromszög követésére egy lehetséges algoritmus:
(Ne feledd, amit innen másolsz, az nem számít bele a saját kontribúcióba.)
 
<br/> <syntaxhighlight lang="c">
struct Triangle {
  Vector a, b, c, normal;
 
  // Az óra járásával ellentétes (CCW) körüljárási irányt feltételez ez a kód a pontok megadásakor.
  // A köröljárási irányból döntjük el a normálvektor "előjelét".
  Triangle(const Vector& a, const Vector& b, const Vector& c)
    : a(a), b(b), c(c) {
      Vector ab = b - a;
      Vector ac = c - a;
      normal = cross(ab.normalize(), ac.normalize()).normalize();
  }
 
  // Ennek a függvénynek a megértéséhez rajzolj magadnak egyszerű ábrákat!
  Intersection intersectRay(Ray r) {
    // Először számoljuk ki, hogy melyen mekkora távot
    // tesz meg a sugár, míg eléri a háromszög síkját
    // A számoláshoz tudnunk kell hogy ha egy 'v' vektort
    // skalárisan szorzunk egy egységvektorral, akkor
    // az eredmény a 'v'-nek az egységvektorra vetített
    // hossza lesz. Ezt felhasználva, ha a sugár kiindulási
    // pontjából a sík egy pontjába mutató vektort levetítjük
    // a sík normál vektorára, akkor megkapjuk, hogy milyen
    // távol van a sugár kiindulási pontja a síktól. Továbbá,
    // ha az a sugár irányát vetítjük a normálvektorra, akkor meg
    // megtudjuk, hogy az milyen gyorsan halad a sík fele.
    // Innen a már csak a t = s / v képletet kell csak használnunk.
    float ray_travel_dist = dot(a - r.origin, normal) / dot(r.direction, normal);
 
    // Ha a háromszög az ellenkező irányba van, mint
    // amerre a sugár megy, vagy ha az előző műveletben
    // nullával osztottunk, akkor nincs metszéspont.
    if(ray_travel_dist < 0 || isnan(ray_travel_dist))
      return Intersection();
 
    // Számoljuk ki, hogy a sugár hol metszi a sugár síkját.
    Vector plane_intersection = r.origin + ray_travel_dist * r.direction;
 
    /* Most már csak el kell döntenünk, hogy ez a pont a háromszög
      belsejében van-e. Erre két lehetőség van:
   
      - A háromszög összes élére megnézzük, hogy a pontot a háromszög
      egy megfelelő pontjával összekötve a kapott szakasz, és a háromszög
      élének a vektoriális szorzata a normál irányába mutat-e.
      Pl:
   
                a
              / |
              /  |
            /  |
            /  x |  y
          /    |
          b------c
 
      Nézzük meg az x és y pontra ezt az algoritmust.
      A cross(ab, ax), a cross(bc, bx), és a cross(ca, cx) és kifele mutat a
      képernyőből, ugyanabba az irányba mint a normál vektor. Ezt amúgy a
      dot(cross(ab, ax), normal) >= 0 összefüggéssel egyszerű ellenőrizni.
      Az algoritmus alapján az x a háromszög belsejében van.
 
      Míg az y esetében a cross(ca, cy) befele mutat, a normállal ellenkező irányba,
      tehát a dot(cross(ca, cy), normal) < 0 ami az algoritmus szerint azt jelenti,
      hogy az y pont a háromszögön kívül van.
     
      - A másik lehetőség a bary-centrikus koordinátáknak azt a tulajdonságát használja
      ki, hogy azok a háromszög belsejében lévő pontokra kivétel nélkül nem negatívak,
      míg a háromszögön kívül lévő pontokra legalább egy koordináta negatív.
      Ennek a megoldásnak a használatához ki kell jelölnünk két tetszőleges, de egymásra
      merőleges vektort a síkon, ezekre le kell vetítenünk a háromszög pontjait, és
      kérdéses pontot, és az így kapott koordinátákra alkalmaznunk kell egy a Wikipédiáról
      egyszerűen kimásolható képletet:
      http://en.wikipedia.org/wiki/Barycentric_coordinate_system#Converting_to_barycentric_coordinates
     
      Én az első lehetőséget implementálom. */
 
    const Vector& x = plane_intersection;
 
    Vector ab = b - a;
    Vector ax = x - a;
 
    Vector bc = c - b;
    Vector bx = x - b;
 
    Vector ca = a - c;
    Vector cx = x - c;
 
    if(dot(cross(ab, ax), normal) >= 0)
      if(dot(cross(bc, bx), normal) >= 0)
        if(dot(cross(ca, cx), normal) >= 0)
          return Intersection(x, normal, true);
 
    return Intersection();
  }
};
</syntaxhighlight> <br/>
 
A legközelebbi metszéspont kiszámolásához a legegyszerűbb (de leglassabb) megoldás, ha végigmegyünk az összes objektumon, és amikkel találtunk metszéspontot, azokra a metszéspontokra kiszámoljuk a kamerától vett távolságot, és ezeknek az értékeknek nézzük a minimumát. Ehhez persze el kell tárolni az összes objektumot egy helyre, hogy végig tudjuk iterálni rajtuk. De az objektumok különböző típusúak is lehetnek, itt sokat segít a heterogén kollekció használata. Az objektumok az én implementációmba azt is eltárolják, hogy milyen anyagból vannak.
 
<br/> <syntaxhighlight lang="c">
struct Object {
  Material *mat;
  Object(Material* m) : mat(m) { }
  virtual ~Object() { } // Ne feletkezzünk el a virtuális destruktorról.
  virtual Intersection intersectRay(Ray) = 0;
};
</syntaxhighlight> <br/>
 
És kell egy struktúra, ami tárolja ezeket, és végig tud menni rajtuk. Az én megoldásom erre:
 
<br/> <syntaxhighlight lang="c">
struct Scene {
  static const size_t max_obj_num = 100;
  size_t obj_num;
  Object* objs[max_obj_num];
 
  // Dinamikus foglalt objektumokat felételezek itt
  void AddObject(Object *o) {
    objs[obj_num++] = o;
  }
 
  Scene() : obj_num(0) { }
 
  ~Scene() {
    for(int i = 0; i != obj_num; ++i) {
      delete objs[i];
    }
  }
 
  Intersection getClosestIntersection(Ray r) const {
    Intersection closest_intersection;
    float closest_intersection_dist;
    int closest_index = -1;
 
    for(int i = 0; i < obj_num; ++i) {
      Intersection inter = objs[i]->intersectRay(r);
      if(!inter.is_valid)
        continue;
      float dist = (inter.pos - r.origin).length();
      if(closest_index == -1 || dist < closest_intersection_dist) {
        closest_intersection = inter;
        closest_intersection_dist = dist;
        closest_index = i;
      }
    }
    return closest_intersection;
  }
}
</syntaxhighlight> <br/>
 
Ha ennél gyorsabb algoritmusra van szükséged, akkor ajánlom egy BSP-fa implementálását, mármint konkrétan egy KD-fát csinálj, ne általános BSP-t. Ez nagyon gyors, és nem nehéz implementálni... csak meg kell írni...
 
Ami a kódból is látszódik, hogy még nem vagyunk készen, amikor meghatároztuk a legközelebbi metszéspontot, ugyanis nekünk egy színre van szükségünk, amit megjeleníthetünk, nem egy helyvektorra.
 
=== Megvilágítás ===
 
A hihető, valóságosnak tűnő képek hatásának kb. 90%-át a megvilágítás adja. De ahhoz, hogy ilyeneket tudjuk renderelni előbb bele kell hatolnunk a fényforrások lelki világába, és egy kis fizikára is szükségünk lesz.
 
==== Az ambiens fényforrás ====
 
A legegyszerűbb fényforrás, amit bevezethetünk, az a környezeti világítás. Ez a valóságban nem létezik, csak egy modell, ami azt hivatott utánozni, hogy nappal a tárgyaknak az a része sem teljesen fekete, amit közvetlenül nem világít meg egy fényforrás se. Ugyanis a tárgyakról a környezetében minden irányba verődik vissza fény, nem csak a szemünk irányába. Ez például egy szobában egy nagyjából konstans, iránytól független háttérvilágítást hoz létre. Ez a modell nagyon sok környezetben nem állja meg a helyét, például nagy nyílt terepen, bár vannak technikák a hibáinak kiküszöbölésére, vagy helyettesítésére (SSAO, Hemisphere lighting, Light probes stb...). Ez kódban csak annyit fog jelenteni a környezeti (ambiens) fényerőt változtatás nélkül hozzáadjuk az objektum színéhez.
 
==== Az irány fényforrás ====
 
Egy másik fontos fényforrás az irányfényforrás. Ilyen például a Nap. A Nap olyan távol van tőlünk, hogy a Föld felszínén egy adott területen nagyjából teljesen mindegy, hogy hol helyezkedik el egy objektum, a nap mindig ugyanolyan irányból és intenzitással világítja meg. Az irányfényforrás irányának természetesen fontos szerepe van, itt a számolás lényegesen bonyolultabb, mint az ambiens fényforrás esetén. Például egy felülről megvilágított szobában az asztal teteje sokkal világosabb, mint az asztal alja. Ahhoz, hogy ezt a hatást el tudjuk érni, kizárólag egyszerű fizikára van szükségünk. Tegyük fel, hogy egy anyagra két azonos erősségű fénysugár esik, az egyik merőlegesen, a másik theta szögben.
* Ha a merőlegesen eső sugár átmérője egységnyi, akkor a theta szögben eső sugár esetében az a felület amin ugyanannyi energia eloszlik sokkal nagyobb. Könnyen levezethető, hogy az egységnyi felületre eső energia (azaz a megvilágítás ereje) cos(theta)-val arányos.
* A beesési szög kiszámításához szükségünk van a felületi normálra. Még jó, hogy korábban gondoltunk erre. A cos(theta) kiszámításának egy egyszerű módja a skaláris szorzat használata. Ugyanis definíció szerint u * v = |u| * |v| * cos(theta). De ha u-t és v-t úgy választjuk meg, hogy egységnyi hosszúak legyenek, akkor a skaláris szorzat a cos(theta)-t adja. Ha a cos(theta) negatív, akkor a test takarásban van, és az irányfény semmit nem befolyásol a színén.
 
Ezzel a modellel csak olyan anyagok jeleníthetőek meg jól, amikre egy felületi pont, konstans megvilágítás esetén mindig ugyanúgy néz ki, akárhonnan is nézzük. Ilyen anyag például a legtöbb műanyag, vagy mondjuk egy szivacs. Ezek diffúz anyagok, a fényt minden irányba ugyanolyan intenzitással szórják. Ez a modell kiegészítés nélkül nem működik az olyan anyagokra, amiken a fény megtud csillanni, vagy amikben látjuk a tükörképünket, és azokra se amiken átlátunk.
Ennek a modellnek egy lehetséges implementációja:
 
<br/> <syntaxhighlight lang="c">
struct Light {
  enum LightType {Ambient, Directional} type;
  Vector pos, dir;
  Color color;
};
 
struct Material {
  virtual ~Material() { }
  virtual Color getColor(Intersection, const Light[], size_t) = 0;
};
 
struct DiffuseMaterial : public Material {
  Color own_color;
 
  DiffuseMaterial(const Color& color) : own_color(color) { }
 
  Color getColor(Intersection inter, const Light* lgts, size_t lgt_num) {
    Color accum_color;
 
    for(int i = 0; i < lgt_num; ++i) {
      const Light& light = lgts[i];
      switch(light.type) {
        case Light::Ambient: {
          accum_color += light.color * own_color;
        } break;
        case Light::Directional: {
          float intensity = max(dot(inter.normal, light.dir.normalize()), 0.0f);
          accum_color += intensity * light.color * own_color;
        } break;
      }
    }
 
    // Negatív vagy egynél nagyobb fényerősségeket nem szabad odaadni az OpenGLnek
    // rajzolásra. Egyelőre legyen az a megoldás, hogy az invalid részt levágjuk (szaturáljuk).
    return accum_color.saturate();
  }
};
</syntaxhighlight> <br/>
 
Az eddigi elmélet összerakva egy programmá: ''<Törölve, túl sokan másolták>''
<br/>
Az sugárkövetés eredménye(baloldalt), összehasonlítva azzal, amit az OpenGL tud (glutSolidCube, jobboldalt), hasonló beállítások mellett:
<br/>
<br/>
 
{|
|-
| http://i.imgur.com/ZC86LKY.jpg || http://i.imgur.com/PA2A3eQ.png
|}
 
==== A pontfényforrás ====
 
A pontfényforrás a grafikában egy nagyon gyakran használt modell, de a valóságban nem létezik. A valós fényforrásoknak egyáltalán nem elhanyagolható a kiterjedése, nem pontszerűek, viszont a kisebbeket  például egy izzót közelíthetünk így, és ezzel sokkal könnyebb lesz vele számolni. A pontfényforrás az irányfényforrástól annyiban különbözik, hogy az intenzitása és az iránya se állandó.
 
Az intenzitásról annyit tudunk mondani, hogy az bármely, a fényforrást körülvevő zárt felületen állandó (hiszen a fotonok nem vesznek el a semmibe, és nem is születnek a semmiből). Ebből következik, hogy a fényforrás középpontú gömbök felületén is állandó az energia. Viszont erről a felületről tudjuk, hogy a távolság négyzetével arányos (A = 4*r^2*Pi), így az egy pontra jutó energia a távolság négyzetének reciprokával lesz arányos. Az arányossági tényező persze jelenettől függ, általában ez egy szabad paraméter. A fény iránya pedig egyszerűen a fényforrásból az megvilágítandó anyag felületi pontjába mutató egységvektor. A beesési szög figyelembevételéhez itt is használhatjuk  az irányfényforrásoknál megismert koszinuszos képlet.
 
A pontfényforrás esetében fontos, hogy ne csak a színét adjuk meg, hanem az energiáját is, pl. egy húsz wattos zölden világító izzót a Color(0.0f, 20.0f, 0.0f) vektorral jellemezhetünk. Természetesen itt egy egység nem fog pontosan megfelelni egy wattnak, ez függ attól is, hogy a távolságot hogyan választottuk meg.
 
Egy lehetséges implementáció:
 
<br/> <syntaxhighlight lang="c"> 
case Light::Point: {
  Vector pos_to_light = light.pos - inter.pos;
  float attenuation = pow(1/pos_to_light.length(), 2);
  float intensity = max(dot(inter.normal, pos_to_light.normalize()), 0.0f);
  accum_color += attenuation * intensity * light.color * own_color;
} break;
</syntaxhighlight> <br/>
 
A kamera fölül - fejlámpaként - világító pontfényforrás hatása:
 
http://i.imgur.com/k3hzUir.jpg <br/>
 
==== A spot lámpa ====
 
A spot lámpa a pontfényforrásnak egy változata. Majdnem mindenben ugyanúgy viselkedik, azt leszámítva, hogy csak egy bizonyos térszög alatt fejti ki hatását.
 
Az ötlet egyszerű, tároljuk a lámpa irányát, és a maximális még megvilágított szög koszinuszát. Azért nem magát a szöget, mert a koszinuszát a skaláris szorzatból nagyon egyszerűen ki tudjuk számolni, míg ahhoz képest az acos() függvény nagyon nagyon drága (és amúgy fölösleges). Tehát nézzük meg, hogy az adott pontot megvilágítja-e a spot lámpa, és ha igen, akkor kezeljük azt utána pont fényforrásként.
 
Ez egy fall-through switchel nagyon egyszerűen megírható:
 
<br/> <syntaxhighlight lang="c"> 
case Light::Spot: {
  Vector light_to_pos = inter.pos - light.pos;
  if(dot(light_to_pos.normalize(), light.dir) < light.spot_cutoff) {
    break; // Ha nincs megvilágítva, akkor ne csináljuk semmit.
  } // Különben számoljuk pont fényforrással.
} // NINCS break!
case Light::Point: {
...
} break;
</syntaxhighlight> <br/>
 
A spot lámpánál esetleg azzal is lehet játszani, hogy az adott térszögben a megvilágítás nem egyenletes eloszlású, hanem mondjuk kifele egyre halványodik. Ennek az implementálása plusz egy sor kódot jelentene.
 
A spot lámpa segítségével sokkal meggyőzőbb fejlámpát lehet csinálni. Az más kérdés, hogy nekem nem sikerült... :D
 
http://i.imgur.com/kW7kwHl.jpg <br/>
 
=== Árnyékok ===
 
Eddig úgy tűnik, hogy minden erőfeszítésünk ellenére az agyunk a képekről szinte gondolkodás nélkül el tudja dönteni, hogy azok nem valóságosak. Vagy az is lehet, hogy csak ritkán látunk semmi közepén lebegő kék színű diffúz kockákat, amin környezeti világítás is megjelenik, annak ellenére, hogy a kockának semmi sincs a környezetében.
 
Ezen változtassuk, rakjuk rá a kockát valami talajra, hogy ne lebegjen (így legalább csak a talaj lebeg, nem a kocka), és használjunk valami hihetőbb háttérszínt, mint a teljesen fekete, a megvilágítás szemléltetéséhez.
 
Például: ''<Törölve, túl sokan másolták>''
 
http://i.imgur.com/16ohvtS.jpg <br/>
 
Ez máris egy fokkal jobb, de sajnos a valósághűbb környezet választása nem oldotta meg minden problémánkat, hanem inkább újakat vetett fel, például, hogy a kocka nem vet árnyékot a síkra, mint ahogy azt a valóságban tenné.
 
A sík azon részeinek kéne árnyékban lenni, ahol valami útban van a fényforrásból az adott pontba menő sugárnak. Ezt úgy tudjuk felhasználni, hogy ha a fényforrásból egy sugarat lövünk az adott pont felé, és megnézzük, hogy  a legközelebbi tárgy, amit eltalált, az az-e, amit éppen rajzolni akarunk. Ennek egy praktikusabb alternatívája, hogy megnézzük, hogy a metszéspont fényforrástól vett távolsága megegyezik-e az adott pont távolságával a fényforrástól. Mert ha nem, akkor amit rajzolni akarunk az árnyékban van. Egy másik ezzel ekvivalens módszer, hogy a tárgyból lövünk egy sugarat a lámpa felé, és ha nem metsz semmit, vagy amit metsz, az távolabb van mint a fényforrás, csak akkor számolunk megvilágítást.
 
Technikailag mindkét algoritmust fogjuk használni. A második - bár bonyolultabbnak tűnhet - de irányfényforrásokra csak az módszer működik. Az irányfényforrások ugyanis végtelen távol vannak, ahonnan nem tudok elindulni, viszont a felületi pontból a fény irányába el tudunk. A pontfényforrások esetén viszont az első algoritmus egyszerűbb egy picivel.
 
Fontos megjegyezni, hogy például irányfényforrások esetén a sugarat nem indíthatjuk pontosan a felületi pontról, hiszen akkor az ahhoz legközelebbi metszéspont maga a felületi pont lenne amiből indítottuk. Ez persze nem mindig teljesülne, a számolási pontosság miatt, ezért néhol árnyékban lesz, néhol nem. Ezt a jelenséget "árnyék pattanás"-nak hívja a szakirodalom (shadow acne). Ez nagyon jellemző mintázatot okoz, amit általában könnyű detektálni. Az alábbi kép mutat egy példát erre:
 
http://i.imgur.com/TeDR81x.png
 
A megoldás erre, hogy az indításkor a kezdő pontot eltoljuk egy kis számmal a sugár haladási irányába. Pl. 0.0001f-el. A pontfényforrás esetén is szükség van erre, csak ott a távolság összehasonlításánál.
 
Egy lehetséges implementáció:
 
<br/> <syntaxhighlight lang="c">
case Light::Directional: {
  // Lőjjünk egy sugarat a fényforrás irányába
  // (Az irányfény iránya nálam a forrás felé mutat)
  Ray shadow_checker = {inter.pos + 1e-3*light.dir, light.dir};
  Intersection shadow_checker_int = scene.getClosestIntersection(shadow_checker);
  if(shadow_checker_int.is_valid) {
    break; // Ha bármivel is ütközik, akkor árnyékban vagyunk
  }
 
  /* Megvilágítás számolása */
}
case Light::Point: {
  // Lőjjünk egy sugarat a fényforrás felől a konkrét pont irányába
  Ray shadow_checker = {light.pos, (inter.pos - light.pos).normalize()};
  Intersection shadow_checker_int = scene.getClosestIntersection(shadow_checker);
  if(shadow_checker_int.is_valid &&
    (shadow_checker_int.pos-light.pos).length() + 1e-3
    < (inter.pos-light.pos).length()) {
      // Ha bármivel is ütközik, ami közelebb van a 
      // fényhez, mint mi, akkor árnyékban vagyunk.
      break;
  }
 
  /* Megvilágítás számolása */
}
</syntaxhighlight> <br/>
 
Példaprogram: ''<Törölve, túl sokan másolták>''
 
<br/>
 
Pont fényforrás esetén:
 
http://i.imgur.com/3PNCclh.jpg
 
Irány fényforrás esetén:
 
http://i.imgur.com/1o1ose4.jpg
 
Irány és pont fényforrás esetén egyszerre:
http://i.imgur.com/Kkc48I9.jpg <br/>
 
=== Tonemapping ===
 
Eddig a képalkotáskor a színeknek a (0, 1) tartományon kívül eső részét levágtuk. Ez nagyon sok esetben nem túl valósághű eredményhez vezet. Például vegyünk két 20 W-os izzót és világítsunk meg vele egy kockát.
 
Az eddigi módszereinkkel ez így néz ki:
 
http://i.imgur.com/ZRgJgHx.png <br/>
 
A kép baloldala jól néz ki, van két egészen hihető árnyékunk. Maga a kocka is egész jó. Viszont a képnek szinte a teljes jobb oldala kiégett, és minden ugyanolyan fehér, ordít róla, hogy mű. És ezek csak 2 db 20 W-os izzó...
 
Vegyünk két 50 W-os izzót:
 
http://i.imgur.com/z4fAqu0.png <br/>
 
A kocka még mindig jól néz ki. Ellenben itt már árnyékok is kiégtek, és az árnyékok nagy része ugyanolyan fényes, mint az, ami közvetlenül meg van világítva. És ezek még mindig teljesen hétköznapi értékek, két db 50 W-os izzó... Ha ez nem megy, akkor a Nappal mit kezdjünk?
 
Egy megoldás lehetne, hogy mindent sokkal sötétebbre veszünk, úgy, hogy a Nap fénye legyen a teljesen fehér. Annál világosabbal nem nagyon szoktunk dolgozni. Igen ám, de az emberi szem egy sötét szobában se teljesen vak, ha hihető képet akarunk, akkor egy olyan algoritmus kéne, ami ilyen körülmények között is élvezhető képet ad.
 
A megoldás az, hogy ne lineáris színskálát használjuk. Ez azt jelenti, hogy pl. kétszer akkora fényerősségű képponthoz ne kétszer olyan fényes pixelt rajzoljunk ki, hanem csak egy picivel világosabbat. Például használjuk logaritmikus skálát, ahol ha egy fényerősség értékét négyzetre emeljük, akkor kétszer olyan fényes pixelt rajzolunk hozzá. Ez persze csak 1-nél nagyobb fényerősségekre működik, és ez mindig véges tartományt eredményez. Olyan függvény kéne, ami a (0, végtelen) tartományt (ún. High Dynamic Range - HDR) a (0, 1) tartományra (Low Dynamic Range - LDR) képzli le, de úgy, hogy még a (0, 1) tartománybeli értékekből is élvezhető képet állítson elő. A HDR színből LDR szín előállítását tonemappingnek hívjuk.
 
==== Reinhard operátor ====
Egy lehetséges megoldás a Reinhard operátor. Előadáson ez szokott szerepelni.
 
Először is kell számolnunk az adott fénynek a luminanciáját (jele: Y).
* Ez azt adja meg, hogy az adott fény az emberi szem számára mennyire látszódik fényesnek.
* A látható spektrum szélén lévő színek (pl a kék és a piros) adott fényerősség mellet nem látszanak nagyon fényesnek. Viszont a spektrum közepén lévő színek, főleg a zöld, ugyan akkora fényerősség mellett, mint a többi szín, sokkal világosabbnak látszik.
 
A kísérletileg meghatározott képlet, ami az érzékelt fényerősséget meghatározza: <code>Y = 0.2126f*r + 0.7152f*g + 0.0722f*b</code>
 
Az új luminancia értéke: <code>Y' = Y / (Y + 1);</code>
 
Az új szín pedig: color' = <code>color * Y' / Y;</code>
 
Az új szín luminanciája garantáltan a (0, 1) tartományba esik, de szín komponenseire ez nem feltétlen igaz. Pl: Color(0, 0, 10) -> Color(0, 0, 4.1928). Ebből következik, hogy az eredményt továbbra is szaturálni kell. Ez igazándiból nem zavaró. Nagyon ritkán okoz különbséget, és akkor se feltűnő.
 
Az eredmény viszont lényegesen sötétebb lesz mint az eredeti kép.
 
Megjegyzés: a tonemap-et a háttérszínen én inkább nem végzem el. Az indok emögött az, hogy a háttérszín kiválasztásakor általában abban a színben gondolkodunk, amit a képernyőn látni akarunk, nem egy olyanban, amit valamilyen műveletvégzés után, módosítva akarunk látni. Vagy például rákeresünk arra, hogy sky blue, és az első google találat által mutatott RGB értéket átváltjuk floatokká, és azt használjuk. De ha erre tonemapet alkalmaznánk, akkor egy másik színt kapnánk. Háttérszínnek amúgy is garantáltan LDR színt választunk, amit nem kell tonemap-elni.
 
Például 2 db 50 W-os izzó esetén:
 
Bal oldalt tone map nélkül, Jobb oldalt Reinhard tonemappel.
 
{|
|-
| http://i.imgur.com/z4fAqu0.png || http://i.imgur.com/P4cANQa.png
|} <br/>
 
Nagyon fényes képek élvezhetőségén a Reinhard operator nagyon sokat javít.  Viszont az alapból sötét képeken csak tovább ront.
 
Például két db 5 W-os izzó esetén:
 
Bal oldalt tonemap nélkül, Jobb oldalt Reinhard tonemap-pel.
{|
|-
| http://i.imgur.com/2Fp5XFB.png || http://i.imgur.com/rSwo1ot.png
|} <br/>
 
A Reinhard operátor a semminél jobb, de nem az igazi.
 
==== Filmic tonemap operátor ====
 
Az egyik legszebb tonemap, az az a leképezés, amit Kodak filmek használnak. Ki gondolta volna, hogy Kodak ért ahhoz, hogy hogyan kell szép képet csinálni... Az algoritmus, ami ezt leutánozza a "filmic tonemap" nevet kapta, az eredetére utalva.
 
A pontosan algoritmus lassú, és bonyolult, egy logaritmus képzésen alapul, de 3 texture lookup is van benne.
De Jim Hejl (az EA games egyik fejlesztője) talált hozzá egy nagyon egyszerű, de meglepően pontos közelítést.
 
<br/> <syntaxhighlight lang="c">
  x = max(0, InputLuminance-0.004);
  OutputLuminance = (x*(6.2*x+0.5))/(x*(6.2*x+1.7)+0.06);
</syntaxhighlight> <br/>
 
Először is, WTF???? Nyugi, én se ismerek ismerek olyan embert, aki értené, hogy ez miért működik. Ennek a "random képletnek" egy apró szépséghibája, hogy sRGB színtérbe állítja elő a színt, viszont nekünk sima RBG színt kell adnunk az OpenGL-nek, ezért az eredményt komponensenként 2.2-edik hatványra kell emelnünk, hogy használni tudjuk. De legalább az eredményt szaturáni már nem kell.
 
<br/>
A tonemap nélküli kép bal oldalt, Reinhard tonemap középen, míg a Filmic tonemap jobb oldalt látható.
{|
|-
| http://i.imgur.com/pdC31jE.png || http://i.imgur.com/1dhh2lu.png || http://i.imgur.com/EX4hRKs.png<br/>
|-
| http://i.imgur.com/rEABMJg.png || http://i.imgur.com/o1dmQYz.png || http://i.imgur.com/QBUda13.png<br/>
|-
| http://i.imgur.com/PDOptWq.png || http://i.imgur.com/4I6eUH5.png || http://i.imgur.com/qdFGAU1.png<br/>
|-
| http://i.imgur.com/Zy5ESZh.png || http://i.imgur.com/vhRyuqI.png || http://i.imgur.com/GKOhIq8.png<br/>
|-
| http://i.imgur.com/rRGFCE9.png || http://i.imgur.com/fQeP0vk.png || http://i.imgur.com/mR3Vmn1.png<br/>
|} <br/>
 
Az előnyei:
* A filmic operátor a túl sötét képeket világosabbá teszi.
* A túl világos képeken továbbra is érződik, hogy ott világos van, nem úgy, mint a Reinhard operátor esetében.
* Szinte az összes képet élvezhetőbbé teszi.
* Csak egyszerű ALU műveleteket használ.
 
=== Spekulráis anyagok ===
 
Az anyagok amikkel eddig dolgoztunk teljesen diffúzak voltak. A teljesen diffúz anyag egy pontja, konstans megvilágítás mellett mindig ugyanúgy néz ki, akárhonnan, akármilyen szögből is nézzük. A valós anyagok viszont nem mind így viselkednek.
 
Most azzal fogunk foglalkozni, hogy bizonyos anyagokon egy lámpa fénye meg tud csillanni, ha megfelelő irányból nézzük. Ilyen pl. egy lakkozott fa felület. Fontos megjegyezni, hogy ez nem úgy viselkedik mint egy tükör, nem látjuk rajta a tükörképünket, csak egy megcsillanó foltot.
 
A viselkedésének leíráshoz meg kell értenünk, hogy ez a hatás hogy jön létre. A simának tűnő anyag felülete is valójában rücskös mikroszkóppal nézve. De a legtöbb anyagra igaz, hogy egy mérettartomány alatt már simának tekinthető mikro-lapokból (micro facet) épül fel. Ezek a kis lapok teljesen tükrözőnek tekinthetőek, és az anyagról a szemünkbe érkező fény, valójában a megfelelő irányba álló mikro-lapokról visszaverődő fényt jelenti.
 
http://i.imgur.com/knD4RV4.jpg
 
A mikro-lapok iránya normál-eloszlást követ. A várható értékük - nyilván - a felületi normál. A szórásuk viszont az anyag jellemzője, ez legyen egy paraméter. Ha a szórás nagyon nagy, akkor a lapok elhelyezkedése szinte teljesen véletlenszerű, és így minden irányba ugyanannyira tükrözőek, vagyis egy teljesen diffúz anyagot alkotnak. De ha a szórás kicsi... Akkor a lapok nagyobb része fog a normál irányába elhelyezkedni, így ha olyan szögből nézzük az objektumot, hogy ahhoz a visszaverődéshez tartozó normál, amikor a fény a szemünkbe jut, a felületi normál közelébe van, akkor abból az irányból nézve sokkal több fény a szemünkbe fog tükröződni, mint a többi irányból, ilyenkor érezzük azt, hogy az anyag megcsillan.
 
De mit jelent az, hogy a "közelébe"? Mennyire a közelébe? És a normál közelébe mennyivel gyengébb ez a hatás, mint pont a normálban. A lapok irányát egy harang görbe jellemzi. Hogy megtudjuk, hogy abba az irányba a lapok hányadrésze néz, ami a visszatükröződésnek kedvez, be kell helyettesítenünk a normál eloszlás sűrűségfüggvényébe. A képlet nem bonyolult, de van benne egy exponenciális függvény, aminek kiszámítása lassú. Viszont skaláris szorzattal egy koszinuszt ki tudunk számolni, ami egy picit hasonlít a haranggörbére. Ha a koszinusznak vesszük egy polinomját, pl az a*cos(x)^b, azzal nagyon jól közelíteni lehet a haranggörbét.
 
A leggyakoribb megoldás, a Blinn-Phong modell, a visszaverődési normál (a fényből a felületi pontba menő, és az abból a szemünk felé mutató egységvektorok átlaga) és a tényleges felületi normál által bezárt szög koszinuszát használja. A visszaverődési normál, vagy más nevén a félszög-vektor jele legyen 'H' (mint Half-angle), a normál pedig 'N'.
 
A koszinusz, amit használni akarunk a <code>dot(H, N)</code> képletből áll elő. A negatív érték nekünk nem jó, és ennek még kell vennünk egy polinómját, így végül a <code>specular_power = pow(max(dot(H, N), 0), shininess)</code> képletet kapjuk.
 
A spekuláris anyag egyben diffúz is. A spekuáris megcsillanás (specular_power * specular_color) hozzáadódik a diffúz megvilágításból származó színhez.
 
Például irányfényforrásokra én így implementáltam:
<br/> <syntaxhighlight lang="c">
Vector L = light.dir.normalize(), V = (camera.pos-inter.pos).normalize();
Vector H = (L + V).normalize(), N = inter.normal;
float specular_power = pow(max(dot(H, N), 0.0f), shininess);
accum_color += specular_power * light.color * specular_color;
</syntaxhighlight> <br/>
 
Itt se feledkezzünk el az árnyékokról. Szerencsére az árnyékszámítás itt is teljesen ugyanaz, ezért célszerű a láthatóságot egy külön függvényben eldönteni.
 
A 'shininess' meghatározása teljesen mértékben hasra-ütésre, próbálgatással szokott menni. Én személy szerint a kettőhatvány shinnines értékekkel szoktam először próbálkozni (8 - 16 - 32 - 64 a leggyakoribb nálam), és utána esetleg "finom hangolom" az értéket, az alapján, hogy melyik éréték néz ki jól.
 
A spekuláris megcsillanások nagyon sokat tudnak dobni egy kép hihetőségén.
 
Például: ''<Törölve, túl sokan másolták>''
 
http://i.imgur.com/uCHgf9g.jpg
 
=== A tökéletes tükör ===
 
A mikroszkopikus tükrök figyelembevételével a fényforrások fénye tükröződni tud. De mi van a valódi tükrökkel? Amikben más objektumok képeit is látjuk, nem csak a fényforrások hatását? Sugárkövetéssel ilyen tükröket renderelni meglepően egyszerű. Az egyetlen dolog amit a tükör tulajdonságú anyag csinál a modellünkbe az az, hogy a tükröződés irányába továbblövi a sugarat.
 
Először is ki kell tudnunk számolni, hogy 'N' normálvektorral rendelkező tükör merre ver vissza egy beérkező 'I' sugarat. Azt tudjuk, hogy a három vektor egy síkban van, és a beesési szög megegyezik a visszaverődési szöggel. De mégse így fogunk számolni, egyszerű vektor műveletekkel is ki lehet fejezni.
 
http://i.imgur.com/RsTWJVC.jpg
 
Próbáld a visszavert sugarat csak a négy alapművelet és a skaláris szorzás segítségével kifejezni.
 
A legegyszerűbb megoldás:
 
<br/> <syntaxhighlight lang="c">
inline Vector reflect(Vector I, Vector N) {
  return I - (2.0 * dot(N, I)) * N;
}
</syntaxhighlight> <br/>
 
Ezt felhasználva a tükröződést implementálni már egyszerű, pl.:
 
<br/> <syntaxhighlight lang="c">
Color getColor(Intersection inter, const Light* lgts, size_t lgt_num) {
  Ray reflected_ray;
  reflected_ray.direction = reflect(inter.ray.direction, inter.normal);
  reflected_ray.origin = inter.pos + 1e-3*reflected_ray.direction;
  return scene.shootRay(reflected_ray);
}
</syntaxhighlight> <br/>
 
Az egyetlen kényelmetlenséget az okozhatja, hogy ha az eddig használt adatstruktúránk nem tárolta, hogy milyen irányból érkezett a sugár, mert az nyilván kell ahhoz, hogy tudjuk, hogy melyik irányba verődik vissza.
 
Nagyon fontos, hogy innentől a spekuláris megcsillanás számolásakor a nézeti vektor nem a kamerából az adott pontba mutat, hanem a sugár haladási irányának a mínusz egyszerese. Ha azt elfelejted átírni a kocka visszatükröződései olyan helyeket fognak megcsillanni, ahol nem kellene.
 
Például ha a padló anyagát lecserélem egy tükörre, akkor az eredmény így néz ki:
 
http://i.imgur.com/GLBoT7h.jpg
 
Ezzel akad egy érdekes probléma, ami nekem egy héten át fel se tűnt. A költői kérdés a következő: Ha tökéletes a tükrünk, és pont ugyanazt látjuk benne, mint ami felette is van, akkor miért különöl el ilyen egyértelműen a háttérszíntől? A válasz persze az, hogy 5 példaprogrammal ezelőtt hoztam egy olyan döntést, hogy a tonemap-et a háttérszínre nem alkalmazom. Ez akkor még jó ötletnek tűnt, de a tükröző anyagok bevezetésével teljesen fals eredményt okoz. Ha a tonemapet a teljes képernyőre alkalmazom, akkor ezt az eredményt kapom:
 
http://i.imgur.com/M09ZnHA.jpg
 
Ez már valóban egy tükörnek néz ki, de két apró probléma még akad vele... Az egyik, hogy lámpa fénye nem csillan meg rajta. Erre korrekt megoldást majd csak később fogunk tudni adni. A másik probléma, az az, hogy mi történik ezzel a modellel, ha két tükröt rakunk egymással szembe? A sugár a végtelenségig fog pattogni a kettő között? Nem egészen. Ugyanis ez egy rekurzív algoritmus, ahol a függvényhívásoknak a stackbe is lesz nyoma, ahol viszont a hely előbb utóbb elfogy, és ilyenkor a programunk megáll.
 
A sugárkövető függvényünkbe követnünk kell, hogy ez hanyadik függvényhívás volt, és ha ez a szám, meghalad valamilyen értéket, pl. 8-at, akkor a sugarat már ne lőjük tovább.
 
<br/> <syntaxhighlight lang="c">
Color shootRay(Ray r, int recursion_level = 0) const {
  if(recursion_level >= 8) {
    return env_color;
  }
  //...
}
Color getColor(Intersection inter, const Light* lgts, size_t lgt_num, int recursion_level) {
  // ...
  return scene.shootRay(reflected_ray, recursion_level + 1);
}
</syntaxhighlight> <br/>
 
Példaprogram: ''<Törölve, túl sokan másolták>''
 
http://i.imgur.com/jwtHLsb.jpg
 
=== A valós tükröző anyagok ===
 
A valós tükröző anyagok, nem csak a tükrök, de pl. a fényesre csiszolt fémek is, nem viselkednek tökéletes tükörként. A különbség az, hogy ezek az anyagok nem a fény 100%-át verik vissza, hanem egy részét elnyelik (az nagyrészt hővé alakul). Az elnyelt fény mértéke a hullámhossztól is függhet, ezért pl. egy sima aranyfelület elszínezi a tükörképet. Egy fürdőszobai tükör persze minden hullámhosszon nagyjából ugyanannyi fényt nyel el.
 
A képlet amire szükségünk lenne, az egy adott hullámhosszon a törésmutató, és a kioltási tényező függvényében megmondaná, hogy a fény hanyadrésze verődik vissza.
 
Az, hogy bemenet és a kimenet is hullámhossz függő elég nagy problémákat okozhat. Egy lehetséges egyszerűsítés, hogy mi csak három kitüntetett színre (a pirosra a zöldre és a kékre) számoljuk ki a képlet eredményét, és ezt ezzel megszorozzuk az RGB színskálán leírt színünket.
 
A képlet, ami ezt a jelenséget leírja közvetlenül a Maxwell-egyenletekből levezethető, bár az eredmény, a [http://hu.wikipedia.org/wiki/Fresnel-egyenletek Fresnel-egyenletek] jóval bonyolultabb, mint amit mi használni szeretnénk. Én csak a képletnek egy közelítését írom itt le, ami eltekint a polarizációtól, mert a grafikában általában ezt szokták használni. (A [http://en.wikipedia.org/wiki/Schlick%27s_approximation Schlick's approximation] kiegészített alakját alakját fogom használni, lásd pl. [http://http.developer.nvidia.com/GPUGems3/gpugems3_ch17.html GPU Gems 3])
* n - törésmutató (RGB vektor)
* k - kioltási tényező (RGB vektor)
* F0 - egy az anyagra jellemző konstans.
** <code> F0 = ((n-1)*(n-1) + k*k) / ((n+1)*(n+1) + k*k); </code>
* theta - beesési szög
* F(theta) - a visszaverődő relatív intenzitást adja meg, komponensenként.
** F(theta) = F0 + (1-F0) * pow(1-cos(theta), 5)
 
Például:
 
<br/> <syntaxhighlight lang="c">
struct ReflectiveMaterial : public Material {
  const Color F0;
  ReflectiveMaterial(Color n, Color k)
    : F0(((n-1)*(n-1) + k*k) /
        ((n+1)*(n+1) + k*k))
  { }
  Color F(float cosTheta) {
    return F0 + (1-F0) * pow(1-cosTheta, 5);
  }
  Color getColor(Intersection inter, const Light* lgts, size_t lgt_num, int recursion_level) {
    Ray reflected_ray;
    reflected_ray.direction = reflect(inter.ray.direction, inter.normal);
    reflected_ray.origin = inter.pos + 1e-3*reflected_ray.direction;
    return F(dot(-inter.ray.direction, inter.normal))
          * scene.shootRay(reflected_ray, recursion_level+1);
  }
};
</syntaxhighlight> <br/>
 
Az 'n' és 'k' paraméterek az anyagra jellemzőek, általában a házi kiírásban meg vannak adva. Például ezüst esetén n = (0.14, 0.16, 0.13), k = (4.1, 2.3, 3.1)
 
Példaprogram: ''<Törölve, túl sokan másolták>''
 
http://i.imgur.com/vJKsR53.jpg
 
Ez egyelőre rosszabbnak tűnik mint volt, de ennek az az oka, hogy a jelenet nagyon üres, az ezüstön kívül egy darab kocka van benne összesen. Összetettebb jelenetekben a fémek általában nagyon jól néznek ki.
 
=== Valós spekuláris anyagok ===
 
A valós tükröző anyagok részben láttuk, hogy az anyagok a különböző beesési szöggel érkező sugaraknak különböző hányadát verik vissza. A Blinn-Phong spekuláris modellünk viszont az anyagoknak ezt a tulajdonságát teljesen figyelmen kívül hagyja.
 
Például vegyünk két esetet, az egyikben a beesési szög sokkal nagyobb, mint a másikban.
{|
|-
| http://i.imgur.com/SfioK3h.png || http://i.imgur.com/Hv6jrKf.png
|}
 
A Blinn-Phong szerint mindkét esetben a fény a maximális lehetséges mértékben verődik vissza. A Fresnel összefüggés szerint viszont minél kisebb a beesési szög (minél közelebb vagyunk a merőlegeshez), annál több fény nyelődik el.
 
Oké, van két ellentmondó modellünk, de mi alapján mondjuk azt a Fresnel összefüggésre, hogy az jobb? Egyáltalán melyik áll közelebb a valósághoz. Az eldöntéséhez nézzünk néhány hétköznapi anyagot. A képeket a [http://filmicgames.com/archives/557 Filmic Games] weboldaláról szedtem, van ott még jó pár érdekes cikk az érdeklődőknek. A baloldali képek természtes-, míg a jobboldaliak mesterséges megvilágítással készültek.
 
http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_03.jpg
 
http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_20.jpg
 
http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_01.jpg
 
http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_23.jpg
 
http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_08.jpg
 
http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_16.jpg
 
http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_10.jpg
 
http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_12.jpg
 
Mondanom se kell, hogy a valóság nagyon egyértelműen a Fresnel összefüggést igazolja. Szerencsére a spekulráis modellünkön csak minimális módosítást kell eszközölnünk, hogy ezt a hatást megkapjuk.
 
A Fresnelhez használandó beesési szög, az jelen esetben a félszög vektor és a nézeti vektor között értendő. Azért ez a két vektor kell nekünk, mert a visszaverődésben résztvevő mikro-tükröknek a félszög vektor a felületi normálja, és a nézeti irányba verődnek vissza. Tehát egyszerűen a spekuláris megcsillanás színét meg kell szoroznunk a <code> F(max(dot(V, H), 0.0f)) </code> vektorral.
 
Példaprogram: ''<Törölve, túl sokan másolták>''
 
Ha az F0-ra az üvegre jellemző 0.04f-re választjuk meg, az ilyen hatást eredményez:
 
(Megjegyzés: a kocka spekuláris színét a két esetben úgy választottam meg, hogy nagyjából azonos legyen a spekuláris megcsillanás fényessége az első képen, de nyilván a Fresnel tag miatt ugyanaz a spekuláris szín mellet az sokkal sötétebb volt)
 
Fresnel (bal oldalt) összehasonlítása a Blinn-Phong-gal (jobb oldalt).
 
{|
|-
| http://i.imgur.com/ps4hF5X.jpg || http://i.imgur.com/cii4bVs.jpg
|-
| http://i.imgur.com/detsaXr.jpg || http://i.imgur.com/wbthpam.jpg
|-
| http://i.imgur.com/NELw5EV.jpg || http://i.imgur.com/xd1Sr99.jpg
|-
| http://i.imgur.com/6bOHiXF.jpg || http://i.imgur.com/i4qxqJE.jpg
|}
 
=== A spekuláris tükör ===
 
A spekulráis anyagokról alkotott modellünkbe azt használtuk ki, hogy ezek az anyagok tükrökből állnak, és ezeknek a tükröknek egy részéből a fényforrások fénye a szemünkbe verődik vissza.
 
A spekulráis anyagokkal ellentétben a tükröző anyagokról alkotott modellünk teljesen figyelmen kívül hagyta az elsődleges fényforrásokat, színüket csak a környezetben lévő objektumokról visszaverődő fény befolyásolta. Pedig a valóságban nem az a tapasztalat, hogy ha egy tükrön keresztül nézünk a Napba, akkor nem látjuk azt.
 
Az elsődleges fényforrások visszaverődésével a legfőbb problémánk az, hogy például egy pontfényforrás esetében a fényforrás tükörképe továbbra is pontszerű, ami olyan kicsi, hogy azt nem látjuk. Az irányfényforrásokkal is ugyanez az eset, csak egy végtelenül kicsi térszög alól látszódnak.
 
Technikailag a modellnek két részlete okozza ezt az anomáliát: ideális tükröt és ideális fényforrásokat feltételezünk egyszerre. A kettő egyszerre nem az igazi, ezért az egyikről le kell mondanunk. Az ideális tükörről sokkal könnyebb lemondani, ezért én azt választom.
 
A nem ideális tükrök esetében a mikro-tükrök irányának szórása nem nulla... csak majdnem nulla. Tekintsük úgy, hogy a szórás elég kicsi, hogy a tükör diffúz színe elhanyagolható, de a spekuláris megcsillanás viszont legyen látható.
 
A spekuláris megcsillanás számolásánál a természetesen a Fresneles kiegészített alakra van szükségünk.  A shininess legyen egy nagyon nagy szám, legalább 1000.
 
Egy kis trükkre azonban szükségünk van. a pontfényforrások távolságfüggéséhez ugyanis követnünk kell, hogy a sugár összesen mennyi utat tett meg eddig.
 
Példaprogram: ''<Törölve, túl sokan másolták>''
 
A spekuláris tükör fizikailag nem túl korrekt, de jól néz ki. Például ilyen hatást lehet vele elérni:
 
http://i.imgur.com/6WR6EXf.jpg
 
=== A látható fényforrások ===
 
Az előző példában láttuk, hogy hihetőbb képet kapunk, ha egy tükrön keresztül látjuk a fényforrásokat is. De mi van azzal az esettel, ha közvetlenül nézzük azokat? Tükörből látszanak, de direktbe nem?
 
A spekuláris tükör analógiájára itt használhatunk egy olyan modellt, hogy minden fényforrás, ami nincsen takarásban a képernyő egy bizonyos térszögét világosabbá teszi. Kb 10 sor, és semmi új ötlet nem kell hozzá, de legalább nem azt látjuk, hogy a fény a semmiből jön.
 
http://i.imgur.com/rL6DMNy.jpg
 
A pontfényforrásokra persze ez nem néz ki olyan jól, főleg azért, mert a képen nem látunk mélységet.
 
De irányfényforrásokra sokkal hihetőbb hatást ér el az ötlet.
 
Például: ''<Törölve, túl sokan másolták>''
 
http://i.imgur.com/BxeEztT.jpg
 
Nem mondom, hogy a Nap a valóságban így néz ki, de ez nagyságrendekkel hihetőbb, mint egy láthatatlan Nap.
 
=== A fényt megtörő anyagok ===
 
A Fresnel egyenlet eddigi felhasználásakor azt feltételeztük, hogy a fénynek az a része, ami nem verődik vissza, az megpróbál továbbmenni, de pl. egy fém belsejében ezt nem tudja megtenni, ezért elnyelődik, energiává alakul. De nem minden anyag viselkedik így.
 
Például az üveg esetében a fény, ha nem verődik vissza, akkor továbbmegy az üvegben, de egy picit el is térül. Az irányának a megváltozását a Snelius-Descart törvény írja le <code> sin(Alpha1) / sin(Alpha2) = n1 / n2 </code>. Az irány kiszámoláshoz jobb lenne egy képlet, ami egyszerű vektorműveleteket használ. Továbbá a kiszámolásnál figyelnünk kell a teljes visszaverődés esetére is. Ilyenkor gyakorlatilag a fény 100%-ka visszaverődik, még az a rész is, ami a Fresnel egyenlet szerint továbbmenne. És persze a teljes visszaverődés esetén a spekuláris megcsillanás számolásakor se szabad megszorozni az eredményt a Fresneles taggal.
 
Én az irány kiszámolásához az alábbi képletet fogom használni. A képlet másolása helyett inkább próbáld meg levezetni magadnak.
 
<br/> <syntaxhighlight lang="c">
inline Vector refract(Vector I, Vector N, double n) {
  double k = 1.0 - n * n * (1.0 - dot(N, I) * dot(N, I));
  if (k < 0.0) {
    return Vector();
  } else {
    return n * I - (n * dot(N, I) + sqrt(k)) * N;
  }
}
</syntaxhighlight> <br/>
 
Fontos megjegyezni, hogy ebben a képletben az ''' 'n' a relatív törésmutató'''. Pl. ha a sugár levegőből üvegbe megy, akkor - mivel a levegő törésmutatója 1-nek tekinthető - így a relatív törésmutató az üveg törésmutatója: 1.5 / 1 = 1.5. Viszont amikor a sugár az üvegből távozik, akkor a relatív törésmutató 1 / 1.5 = 0.666. Gyakori hiba ennek a reciprok képzésnek a lehagyása.
 
Egy másik hibalehetőség ezzel a függvénnyel kapcsolatban, hogy ha normálvektor ellentettjét használjuk, akkor rossz eredményt ad. Erre fontos figyelni, pl. amikor a kocka belsejéből kifele jön a sugár, hiszen ilyenkor a befele mutató normállal kell számolni, nem a kifele mutatóval. Ugyanígy a Fresnel egyenlet is rossz eredményre vezet, ha a normál ellentettjével számolunk, ezért célszerű a számolások legelején megfordítani a normált, ha arra szükség van:
 
<br/> <syntaxhighlight lang="c">
if(dot(inter.ray.direction, inter.normal) > 0) {
  inter.normal = -inter.normal;
}
</syntaxhighlight> <br/>
 
Fontos még, hogy a Fresnel egyenletben kifele menet is ugyanazt a törésmutatót kell használni. Az F0 kiszámolásakor az egyesek a levegő törésmutatója helyén állnak, általános esetben a képlet a <code>((n1 - n2)^2 + k*k) / ((n1 + n2)^2  + k*k)</code> alakot veszi fel. Ez a képlet viszont n1 és n2 szempontjából szimmetrikus, így '''nem kell két F0-t számolni''', egyet a befele, egyet meg a kifele menő sugarakhoz.
 
Csak olyan törő anyagokkal foglalkozunk, amiknek a törésmutatója a hullámhossztól független. A nem így viselkedő anyagok, a prizmák nagyon látványos képeket tudnak eredményezni, de sokkal számításigényesebbek és bonyolultabbak, ezért most nem fogok velük foglalkozni.
 
Az én implementációm leegyszerűsítve:
 
<br/> <syntaxhighlight lang="c">
struct RefractiveMaterial : public ReflectiveMaterial {
  float n, n_rec;
  RefractiveMaterial(float n, Color k, Color specular_color, float shininess)
    : ReflectiveMaterial(n, k, specular_color, shininess), n(n), n_rec(1 / n)
  { }
  Color getColor(Intersection inter, const Light* lgts, size_t lgt_num, int recursion_level) {
    if(dot(inter.ray.direction, inter.normal) > 0) {
      inter.normal = -inter.normal;
    }
 
    Ray reflected;
    reflected.direction = reflect(inter.ray.direction, inter.normal);
    reflected.origin = inter.pos + 1e-3*reflected.direction;
    Color reflectedColor, refractedColor;
    Ray refracted;
    refracted.direction = refract(inter.ray.direction, inter.normal, inter.ray.in_air ? n : n_rec);
    if(!refracted.direction.isNull()) {
      refracted.origin = inter.pos + 1e-3 * refracted.direction;
      refracted.in_air = !inter.ray.in_air;
      Color F_vec =  F(dot(-inter.ray.direction, inter.normal));
      reflectedColor = F_vec * scene.shootRay(reflected, recursion_level+1)
                      + getSpecularHighlight(inter, lgts, lgt_num, reflected.travelled_dist, false);
      refractedColor = (1 - F_vec) * scene.shootRay(refracted, recursion_level+1);
    } else {
      reflectedColor = scene.shootRay(reflected, recursion_level+1)
                      + getSpecularHighlight(inter, lgts, lgt_num, reflected.travelled_dist, true);
    }
    return reflectedColor + refractedColor;
  }
};
</syntaxhighlight> <br/>
 
Példaprogram: ''<Törölve, túl sokan másolták>''
{|
|-
| http://i.imgur.com/ULyutJC.jpg || http://i.imgur.com/bO4qAbx.jpg
|}
 
A kocka egészen hihetően néz ki, a bal oldali képen, meredek szögből nézve, nagyrészt tükörként viselkedik, a jobb oldali képen pedig nagyrészt átlátszó. Viszont '''a talaj megvilágítása teljesen rossz'''. Az árnyékszámító algoritmus azt feltételezte, hogy fény a nem megy át a - jelenleg átlátszó - kockán. De ha az árnyékokat elhagynánk, akkor is teljesen rossz képet kapnánk. Az üveg kocka megtöri a fényt, de néhol tükröz is, esetleg sok fénysugarat ugyanabba a pontba fókuszál... ezeknek a jelenségeknek a hatását az eddigi megvilágítási modellünk egyáltalán nem vette figyelembe.
 
A klasszikus megvilágítási modell (ahol az anyagok színe az ambiens, diffúz és spekuláris tagok összege) azt feltételezte, hogy a fény, a fényforrásból a jelenet bármely pontjába csak egyenes úton juthat el. Ennek az a nagy előnye, hogy egy felületi pont színéhez nem kell tudnunk a többi pont színéről semmit. Az ezzel a tulajdonsággal rendelkező világításszámoló algoritmusokat lokális illuminációnak nevezzük. Ha a jelenetben van tükröző vagy törő anyag akkor ez értelemszerűen nem működik. Az ilyen jelenteknél másképp kell megvilágítást számolnunk. Ilyenkor globális illuminációra van szükségünk.
 
=== A globális illumináció ===
 
A lokális illuminációt azért szerettük, mert nagyon lecsökkenti a probléma számításigényét. A ténylegesen jelen lévő fotonok számától függetlenül, pixelenként egyetlen sugár követésével megtudtuk oldani a feladatot. Azonban a törő vagy tükröző anyagok ezt az algoritmust reménytelenül elbonyolítják. Ilyenkor a valóság utánzása nyers erővel a leginkább járható út.
 
Az ötlet, hogy a fényforrásból lőjünk nagyon sok fotont random irányba, és nézzük meg, hogy miután a törő / tükröző anyagok továbblőtték a fotonokat, hova csapódnak be.
 
A diffúz anyagokat osszuk fel részekre, és egy textúrába tároljuk el az egyes részeibe becsapódó fotonok színeinek összegét. A rendereléskor ezt a színt jelenítsük meg az adott részhez. Így nagyon szép eredményeket érhetünk el - de nagyságrendekkel lassabban...
 
Először szükségünk van egy függvényre, ami a diffúz felület térbeli pontjaihoz hozzárendel egy pontot a textúrán.
* A textúra koordináták x komponensét 'u'-nak az y komponenst pedig 'v'-nek szokás hívni.
* Célszerű a textúrán belüli koordinátát is valós értéknek tekinteni, nem egészeknek, és bilineáris elérést használni. Ez azt jelenti, hogy ha egy 'c' színű fotonnak 4.7 az 'u' koordinátája, akkor 0.3*c-t írjuk a 4-es 'u' koordinátára és 0.7*c-t az 5-ös koordinátára. Két dimenzióban analóg módon 4 helyre is írunk.
* A kiolvasáshoz is használjuk bilineáris elérést.
* Profiknak célszerű textúra helyett KD-fába tárolni a fotonok becsapódását. Sokkal szebb eredményt lehet így elérni, és ráadásul még a spekuláris megcsillanások is implementálhatóak, hiszen a fotonok iránya is könnyen eltárolható.
 
Pont fényforrás esetén nem kell külön számolni azzal, hogy a foton mekkora távolságot tett meg. A foton nem lesz gyengébb, attól, hogy x utat megtett, csak minél távolabb vagyunk a fényforrástól, egy adott pontra annál kevesebb foton fog esni. De ez nem a foton tulajdonságából adódik, ez a fotontérképből implicit ki fog jönni.
 
A fotontérkép méretének és a fotonok számának megválasztása teljesen ad-hoc. De nem érdemes túlzásba vinni, hiszen a globális illumináció nagyon számításigényes. A házihoz reális értékek pl: fotontérkép mérete: 2^5 * 2^5 - 2^7 * 2^7, fotonok száma: 10^4 - 10^6.
 
A "foton" igazándiból foton csomagot jelent, pl. a törő anyagok kétfelé választhatnak egy ilyen csomagot.
A foton tudja magáról, hogy milyen színű. A kölcsönhatásokkor a Fresnel egyenlet következtében a foton színe megváltozhat. Az egyes fotonok energiája legyen fordítottan arányos a fotonok számával. A fényforrás energiája az összes fotonra egyenletesen oszlik szét. Ha megkétszerezzük a fotonok számát, akkor is ugyanolyan fényes jelentet akarunk kapni.
 
A törő és tükröző anyagok pont ugyanúgy lépnek kölcsönhatásba a fotonokkal, mint ahogy a sugarakkal is. Ennek az implementáláshoz semmi új ötlet nem kell.
 
Például egy felülről megvilágított üvegkocka így szórja a fényt: ''<Törölve, túl sokan másolták>''
 
{|
|-
| http://i.imgur.com/1YvdpgA.jpg || http://i.imgur.com/4NYiFSE.jpg
|}
 
=== A kétirányú sugárkövetés ===
 
A globális illumináció implementálásakor sokak fájó szívvel válnak meg a kódban a lokális illumináció résztől, lévén, hogy hiába írták meg, ha nem jó semmire. De ez nem így van. Egyrészt a 3D OpenGL-es házikhoz nagyon nagy segítséget fog nyújtani, hogy érted, hogy hogyan működik a lokális illumináció, hiszen az OpenGL is ezt fogja használni. Másrészt még a sugárkövetés házi végleges formájába is hasznos lehet az a kód.
 
A kétirányú sugárkövetés ötlete, hogy használjuk a lokális és a globális illuminációt egyszerre. A diffúz anyagot világítsuk meg lokálisan, és csak azok a fotonok keltsenek rajta kausztikát, amik nem triviális úton (egyenes vonalon, végig a levegőben, kölcsönhatás nélkül) jutottak el a fényforrásból az anyagig. Tehát ha a foton ütközésekor a rekurziós szint 0, akkor az közvetlenül a fényforrásból jutott el hozzánk, azt ne mentsük el.
 
A kétirányú sugárkövetés előnyei:
* Rengeteg erőforrást meg tud spórolni. Az üvegkocka esetében sokkal kevesebb foton elég egy ugyanolyan minőségű kép előállításához.
* A spekuláris hatást is figyelembe tudjuk venni.
 
A kétirányú sugárkövetés hátrányai:
* Nem triviális a konstansokat úgy beállítani, hogy a lokális és a globális illumináció konzisztens legyen.
* Az árnyékok széle recésebb lesz. A bilineáris szűrés miatt a globális illumináció pontosabban határozza meg az árnyékok szélét, mint az a módszer, amit a lokális illuminációnál használtunk.
 
Példaprogram: ''<Törölve, túl sokan másolták>''
 
A korábbi jelenet kétirányú (bal oldalt) és csak globális (jobb oldalt) megvilágítással, mindkét esetben 500 000 fotonnal
{|
|-
| http://i.imgur.com/fU0yrlg.jpg || http://i.imgur.com/1YvdpgA.jpg
|}
 
<br/>
 
Ennél a példaprogramnál egy kicsivel összetettebb jelentnél már rengeteget tud dobni a kétirányú sugárkövetés. Viszont ezeknél meg sokkal jobban fáj a szép árnyék hiánya. Például:
 
{|
|-
| http://i.imgur.com/hPu5FGv.jpg || http://i.imgur.com/nNVSlsW.jpg
|}
 
<br/>
 
Két lehetséges megoldás a recés árnyékok problémára:
* A triviális utakon is vegyük figyelembe a globális illuminációt, csak ott sokkal kisebb súllyal, úgy, hogy ott továbbra is a lokális illumináció domináljon. Például a fotonokat szorozzuk be a <code>recursion_level == 0 ? 0.25f : 1.0f</code> számmal. Ez a megoldás ingyen van, és egy picivel jobb eredményhez vezet. Ennek a hatása:
 
http://i.imgur.com/OkgFCZh.jpg
 
* A másik megoldás a multi-sampling használata. Itt az az ötlet, hogy minden pixelhez több képet is fényképezzünk le, és ezeket mossuk össze. Ez az megoldás ''nagyon'' drága, a beadón ezt nem illik használni viszont rengeteget javít a képminőségen és a triviálisan lehet vele mélység-élességet is implementálni.
 
Mivel a tárgyból ezt úgy se nagyon kéne használnod, ezért csak érdekességképpen mutatom meg a legegyszerűbb multi-sampling algoritmust, ami random mintavételezést használ:
 
<br/> <syntaxhighlight lang="c">
void takePicture() {
  for(int x = 0; x < Screen::height; ++x) {
    for(int y = 0; y < Screen::width; ++y) {
      Color sum;
      const int sample_num = 16;
      for(int i = 0; i < sample_num; ++i) {
        float x_ = x + float(rand())/RAND_MAX - 0.5f;
        float y_ = y + float(rand())/RAND_MAX - 0.5f;
 
        sum += capturePixel(x_, y_);
      }
      Screen::Pixel(x, y) = sum / sample_num;
    }
  }
}
</syntaxhighlight> <br/>
 
http://i.imgur.com/hFG6KmA.jpg
 
Természetesen egy állókép messze nem olyan izgalmas, mint egy animáció. De sajnos a sugárkövetéssel real-time animációt nagyon nehéz elérni, az utolsó példaprogrammal (minimális módosítás után) 8 másodpercnyi animáció lerenderelése 3 perc volt, az eredménye a bal oldali képen látszódik.
 
Persze ez még messze nem olyan szép, mint amit te a házid eredményeként kaphatsz, aminek az egyik legfőbb oka, hogy a jelenet nagyon egyszerű. Ha ezt egy tükörszobába rakom be, akkor a jobb oldali kép lesz az eredmény. De ez még mindig csak egy darab kocka. Több másodrendű alakzat ennél sokkal izgalmasabb eredményhez vezet.
 
<br/>
 
{|
|-
| http://oi61.tinypic.com/14d22s4.jpg || http://oi57.tinypic.com/25rm1yf.jpg
|}
 
<br/>
 
== 3D OpenGL ==
 
=== Kedvcsináló ===
 
Sugárkövetéssel nagyon látványos képeket tudunk elérni, ha vesszük a fáradtságot, hogy érdekes objektumokat helyezzünk el a világban. Főleg a másodrendű felületek tudnak nagyon szép képeket adni. Az árnyékszámításhoz gyakorlatilag ölünkbe hullott egy algoritmus, amihez pont ugyanazt kellett csinálni, mint amit rajzoláskor is csináltunk. A globális illuminációval ráadásul olyan hatásokat is implementálni tudtunk, amikkel a mai játékokba szinte sehol nem lehet találkozni. Akkor mi a gond a sugárkövetéssel, miért nem ezt használjuk mindenhol, miért kell akkor egyáltalán OpenGL? A probléma az vele, hogy lassú. Az általam mutatott programok mindössze 14 darab háromszögből álltak, messze elmaradva a mai játékok komplexitásától, és így is, a globális illuminációval kb. 5 másodperc volt, mire a kép előállt. Ez nem tűnik soknak, de optimális esetben egy játékhoz másodpercenként legalább 60szor kéne képet alkotnunk, a sugárkövető ettől messze elmarad. Ha a kódot két héten át optimalizáltam volna, akkor akár 0.1 másodperc is lehetne a renderelési idő. Igen ám, de ez csak a képalkotásra szánt idő! A játék logika, főleg ütközés detektálások, vagy ruha-, víz szimuláció stb. egyáltalán nincsenek ingyen, és még azoknak is bele kéne férnie az időbe. És ez még mindig csak 14 darab háromszög.
 
A általunk használt sugárkövetéssel a legnagyobb gond az, hogy nem használja a videókártyát. Hiába van a gépünkbe egy szuperszámítógép teljesítményű hardware, ha nem használjuk semmire. A mai videókártyák nem csak előre meghatározott műveletek tudnak végrehajtani, hanem már programozhatóak is. Az ezt kihasználó grafikus programok hihetetlen hatékonyak. Például az én grafika nagyházim egy 72 millió háromszögből álló jelenetet tudott valós időben (kb. 20 FPS-el) kirajzolni (fizikával együtt).
 
 
<div style="text-align:center;margin:0px auto;">
http://i.imgur.com/zYUuZ0L.png
</div>
 
Csak emlékeztetőül, sugárkövetéssel 14 háromszög nem ment valós időben... A videókártya segítségével a 72 millió háromszögből álló jelenet real-time kirajzolása még messze nem a maximum amit el lehet érni.
 
A videókártyát használhatnánk arra, hogy gyors raytracert írjunk. Ehhez viszont meg kellene tanulni, hogy hogyan kell a videókártyát programozni... Ehelyett mi a videókártya kezelését az OpenGLre bízzuk, és az inkrementális elvet használva fogunk rajzolni (ugyanúgy, mint a 2D OpenGL-es résznél, csak 3D-ben).
 
=== A 3D-s kocka ===
 
A 2D OpenGL-es résznél mutattam egy glut függvényt, ami egy kockát rajzol ki. Ezt a házikhoz nem lehet használni, inkább írjunk meg magunknak.
 
<br/> <syntaxhighlight lang="c">
void glVertex3f(const Vector& v) {
  glVertex3f(v.x, v.y, v.z);
}
 
void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  glVertex3f(a); glVertex3f(b); glVertex3f(c); glVertex3f(d);
}
 
void drawCube(const Vector& size) {
  glBegin(GL_QUADS); {
    /*      (E)-----(A)
            /|      /|
            / |    / |
          (F)-----(B) |
          | (H)---|-(D)
          | /    | /
          |/      |/
          (G)-----(C)        */
 
    Vector s = size / 2;
 
    Vector A(+s.x, +s.y, -s.z), B(+s.x, +s.y, +s.z), C(+s.x, -s.y, +s.z), D(+s.x, -s.y, -s.z),
          E(-s.x, +s.y, -s.z), F(-s.x, +s.y, +s.z), G(-s.x, -s.y, +s.z), H(-s.x, -s.y, -s.z);
 
    glQuad(A, B, C, D); glQuad(E, H, G, F); glQuad(A, E, F, B);
    glQuad(D, C, G, H); glQuad(B, F, G, C); glQuad(A, D, H, E);
 
  } glEnd();
}
</syntaxhighlight> <br/>
 
Az eredménye: [[Média:Grapp4_negyzet.cpp‎|Egy négyzet]]
 
http://i.imgur.com/iWkEuQj.png
 
Na várjunk csak, mi egy kockát rajzoltattunk ki, akkor miért egy négyzetet látunk? Azért mert kameránál a "fényképezést", mint transzformációt leíró mátrix (a GL_PROJECTION) egy egységmátrix, így a fényképezés egyszerűen a z koordináta eldobására korlátozódik. A kockának mindössze két oldala van, aminek az XY síkra vett vetülete nem nulla területű, de mindkét vetület egy-egy négyzet. Mi a kettő közül a később kirajzolt oldalnak a vetületét látjuk.
 
A megoldás: állítsuk be a projekciós mátrixot. Első közelítésként próbálkozhatnánk a sugárkövetésnél használt fényképezés leírásával. Ez részben működne, az algoritmus bemenete a látószög (FoV), és a képernyő szélességének és magasságának az aránya, és ezekből meghatározza a képen az X és Y koordinátákat. Ez a transzformáció önmagában viszont nekünk nem elég, mert a láthatóság eldöntése miatt szükségünk van egy 'z' koordinátára is, amibe azt akarjuk eltárolni, hogy az adott objektum milyen távol van a kamerától (előjelesen, a kamera nézeti iránya a pozitív). De ez az érték a jelenetben a (-végtelen, végtelen) tartományon mozog, amit mi a (-1, 1) tartományra akarunk transzformálni.
 
Ilyen problémával már találkoztunk, a tonemappingnél, és két lehetséges megoldást is mutattam rá. De itt egyik se működik. Az ötlet ami itt szoktunk használni, az az, hogy egy bizonyos távolságnál (zNear) közelebb lévő és egy másik távolságnál (zFar) pedig a távolabb lévő objektumokat nem rajzoljuk ki. Ez gyakorlatban egy elég jól alátámasztható döntés, ha egy objektumokkal teli zsúfolt világban végtelen távolra ellátnánk, akkor végtelen erőforrásra is szükségünk lenne ahhoz, hogy arról képet alkossunk. A szemünkhöz nagyon közeli objektumok pedig olyan nagynak látszódnának, hogy nem látnánk tőlük semmi mást. A két távolság két vágósíkot határoz meg. Egy transzformáció kell nekünk, amire egy a közeli vágósíkon lévő objektum 'z' koordinátája 0 lesz, míg a távoli vágósík pontjai 1 mélység koordinátákkal rendelkeznek. Kb. 10 sor lenne levezetni az ezt megvalósító transzformáció mátrixát, de ettől megkíméllek titeket, mert a házihoz úgy sincs rá szükség, ugyanis egy glu függvény ezt megcsinálja helyettünk.
 
A függvény amit használhatunk az a <code> gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar)</code>. Megjegyzések a paramétérek megválasztásához:
* A fov-ot szögben, nem radiánba kell megadnunk. A reális értéke 40 - 150 fok között mozog.
* Az ascpect a képernyő szélessége / képernyő magassága. A házikba ennek az értéke egy.
* A zNear-nél sokan nagy késztetést éreznek, hogy egy nagyon kicsi értéket állítsanak be, hogy semmi se kerüljön az első vágósík elé. A gond viszont ezzel az, hogy az mélység számító algoritmus a természeténél fogva több különböző ponthoz is ugyanazt az értéket rendeli, hiszen csak egy véges tartományt használhat (általában egy 24 bites fixpontos számot). Amiért ez zavaró, az az, hogy minél nagyobb a zFar / zNear értéke, annál nagyobb tartományt kell ugyanarra a (0..1) intervallumra transzformálni. Ez pedig azzal jár hogy az egyre távolabb lévő pontokhoz is ugyanazt a mélységet fogja használni. Ilyenkor pedig nem tudjuk eldönteni, hogy mi látszódik, és mi nem, aminek szinte mindig nagyon ronda eredménye szokott lenni. Egy ökölszabály, hogy a zFar / zNear értéke ne legyen (sokkal) nagyobb 1000-nél. A házikhoz tipikusan nincs szükség 100-nál nagyobb zFar-ra, ilyenkor a zNear ne legyen 0.1-nél kisebb.
 
Például:
 
<br/> <syntaxhighlight lang="c">
void onInitialization() {
  glMatrixMode(GL_PROJECTION);
  gluPerspective(60, 1, 0.1, 10);
  glMatrixMode(GL_MODELVIEW);
}
</syntaxhighlight> <br/>
 
A kockát kirajzoló (kirajzolni próbáló) program eredménye ezzel a pár sorral kibővítve:
 
[[Média:Grafpp4_feher_kep.cpp|Egy teljesen fehér kép]]
 
http://i.imgur.com/2B4efRi.png
 
Igen, én képes voltam képes erről feltölteni egy screenshotot. Én mondtam, hogy sugárkövetéssel látványosabb eredményeket lehet elérni :D
 
Igazándiból annyira nem meglepő, hogy egy kocka belsejében lévő kamera nem lát mást, mint a kockát, ami jelenleg teljesen fehér.
 
A megoldás: "Rakjuk át a kamerát a kockán kívülre". Pontosabban a világot (és vele a kockát) transzformálúk úgy, hogy a kamera ne legyen a kocka belsejében. A legkényelmesebb módja: <code> gluLookAt(GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ, GLdouble centerX, GLdouble centerY, GLdouble centerZ, GLdouble upX, GLdouble upY, GLdouble upZ)</code> - hol legyen a kamera, mit nézzen, és melyik legyen a felfele vektor. Például az onInitbe <code> gluLookAt(-3, 2, -2, 0, 0, 0, 0, 1, 0); </code>
 
http://i.imgur.com/ChgkBk4.png
 
* Erről már könnyebb elhinni, hogy ez egy kocka. De színezzük át az oldalait különböző színűre. Pl. a felületi normál absz. értéke legyen a szín.
 
<br/> <syntaxhighlight lang="c">
void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  Vector normal = cross(b-a, c-a).normalize();
  glColor3f(fabs(normal.x), fabs(normal.y), fabs(normal.z));
  glVertex3f(a); glVertex3f(b); glVertex3f(c); glVertex3f(d);
}
</syntaxhighlight> <br/>
 
Az eredménye: [[Média:Grafpp4_nem_kocka.cpp‎|egy kocka?]]
 
http://i.imgur.com/o8dUcNz.png
 
Ez nem túl meggyőző... Azért nem hasonlít egy kockára, mert az oldalak láthatóságát a rajzolás sorrendje döntötte el. De nem azért szenvedtünk a projekciós transzformáció mélység értékével, mert abból az OpenGL meg tudja oldani a láthatóság problémáját? De, csak ezt be kell kapcsolni... Az ezt bekapcsoló függvényhívás: <code> glEnable(GL_DEPTH_TEST)</code>. Fontos, hogy ha ez be van kapcsolva akkor a képernyőt a <code> glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)</code> függvénnyel töröld le. Ha a GL_DEPTH_BUFFER_BIT konstanst lehagyod, akkor nem fogsz látni semmit.
 
Az eredmény: [[Média:Grafpp4_vegre_egy_kocka.cpp‎|Végre egy kocka!]]
 
http://i.imgur.com/z0QQfCD.png
 
=== A megvilágítás ===
 
A megvilágítás bekapcsolása nagyon egyszerű: <code> glEnable(GL_LIGHTING); </code>
 
Amit ez eredményez: egy majdnem teljesen fekete képernyő.
 
http://i.imgur.com/Rj0AC5p.png
 
A probléma az, hogy az OpenGL a megvilágításkor nem a glColor3f()-el megadott színeket használja fel. Hogy pontosan mit, arra majd később visszatérünk, egyelőre kérjük meg, hogy a glColor3f() színeket használja a megvilágításhoz is. Ezt a <code> glEnable(GL_COLOR_MATERIAL); </code> függvénnyel tehetjük meg.
 
Ennek hatására egy ilyen képet látunk:
 
http://i.imgur.com/FAm0t1G.png
 
Ezen még mindig nem látszódik, hogy meg lenne világítva, egyszerűen csak sötétebb lett a kocka. A gond az, hogy egyetlen fényforrásunk sincs, mindössze egy (0.2f, 0.2f, 0.2f) színű környezeti háttérvilágítás befolyásolja alapból a jelenetünket. A környezeti háttérvilágításon kívül az OpenGLben 8 darab fényforrás van. Ezek a GL_LIGHT0, GL_LIGHT1, ... GL_LIGHT7 neveket kapták. A fényforrás típusok nagyon hasonlítanak ahhoz, mint amiket a sugárkövetésnél használtunk. A fényforrás a pozíciója alapján lehet pont vagy irányfényforrás. A pozíciót 4D heterogén koordinátákkal kell megadni. Ha a negyedik koordináta nulla, akkor a pont végtelen távol van, ezt az OpenGL irányfényforrásként fogja kezelni. Ha a negyedik koordináta nem nulla (ilyenkor tipikusan 1), akkor pedig pont fényforrásról van szó. A pont fényforrás viselkedhet spotlámpaként, ehhez meg kell adni, hogy melyik irányba néz, és mekkora térszöget világít be. Továbbá az összes fényforrásnak lehet ambiens tagja is. De van egy globális ambiens fény is, ami a 8 fényforrástól független, mindig be van kapcsolva, (0.2f, 0.2f, 0.2f) a default értéke, és ehhez a házikba általában nem szabad hozzányúlni.
 
A fényforrások külön-külön ki-be kapcsolgathatóak. Például <code>glEnable(GL_LIGHT0)</code>.
Ez önmagában semmi változást nem okoz, aminek az egyik oka az, hogy mint azt a sugárkövetésnél is láttuk, a világítás számolásához szükség van a felületi normálokra.
Ezt a glNormal3f() függvénnyel adhatjuk meg, egy glVertex hívás előtt. Ez is olyan, mint a rajzolószín, ha egyszer beállítjuk, akkor az utána az összes glVertex hívásra ki fogja fejteni a hatását, amíg újra meg nem változtatjuk. Például:
 
<br/> <syntaxhighlight lang="c">
void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  Vector normal = cross(b-a, c-a).normalize();
  glColor3f(fabs(normal.x), fabs(normal.y), fabs(normal.z));
  glNormal3f(normal.x, normal.y, normal.z);
  glVertex3f(a); glVertex3f(b); glVertex3f(c); glVertex3f(d);
}
</syntaxhighlight> <br/>
 
Ezután még célszerű a fényforrás pozícióját és színét beállítani, és nem azok default értékére hagyatkozni. Ezeket a <code>glLightfv(GLenum light, GLenum pname, const GLfloat *params);</code> függvénnyel tudjuk beállítani. Pl:
 
<br/> <syntaxhighlight lang="c">
GLfloat p[4] = {-1.1f, 2.0f, -1.2f, 1};
glLightfv(GL_LIGHT0, GL_POSITION, p);
GLfloat c[4] = {0.7f, 0.8f, 0.9f, 1.0f};
glLightfv(GL_LIGHT0, GL_DIFFUSE, c);
</syntaxhighlight> <br/>
 
Az eredménye: [[Média:Grafpp4_megvilagitott_kocka.cpp‎|Megvilágított kocka]]
 
http://i.imgur.com/W7BpUW4.png <br/>
 
 
A gyakran használt megvilágítási paraméterek a <code>glLightfv()</code> függvényhez:
* <code>GL_AMBIENT</code> - a fényforráshoz tartozó környezeti megvilágítás színe. Alapértéke: (0.0, 0.0, 0.0, 1.0)
* <code>GL_DIFFUSE</code> - a fényforrás színe a diffúz megvilágítás számolásakor. Alapértéke: GL_LIGHT0 - (1.0, 1.0, 1.0, 1.0), GL_LIGHT{1..7} - (0.0, 0.0, 0.0, 1.0)
* <code>GL_SPECULAR</code> - a fényforrás színe a spekuláris megvilágítás számolásakor. Alapértéke: GL_LIGHT0 - (1.0, 1.0, 1.0, 1.0), GL_LIGHT{1..7} - (0.0, 0.0, 0.0, 1.0)
* <code>GL_POSITION</code> - a fényforrás pozíciója / iránya. Alapértéke: (0,0,1,0)
<br/>
 
A <code>glLightf(GLenum light, GLenum pname, GLfloat param);</code> is hasznos lehet, az ehhez tartozó gyakori paraméterek a pontfényforrások erősségének távolságfüggését befolyásolják:
* <code>GL_CONSTANT_ATTENUATION</code> - Alapértéke: 1
* <code>GL_LINEAR_ATTENUATION</code> - Alapértéke: 0
* <code>GL_QUADRATIC_ATTENUATION</code> - Alapértéke: 0
<br/>
 
A fényerősséget számoló képlet:
*<code> attenuated_power = power / (GL_CONSTANT_ATTENUATION + dist*GL_LINEAR_ATTENUATION + dist*dist*GL_QUADRATIC_ATTENUATION) </code>
<br/>
 
Ha valósághű távolságfüggést akarunk, akkor a konstans gyengülést vegyük le nullára, és a négyzetesre állítsuk be valami pozitív számot. Pl:
 
<br/> <syntaxhighlight lang="c">
glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 0.0f);
glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 0.5f);
</syntaxhighlight> <br/>
 
Az eredménye:
 
http://i.imgur.com/tM9VJDr.png
 
* A képpel van egy elég nagy probléma: a megvilágítás nem folytonos, látszódik, hogy hogyan bontotta háromszögekre az OpenGL a quadot amit ki akartunk rajzolni.
** A jelen esetben ez - bár rondán néz ki - de nem olyan nagy probléma. Ha viszont egy gömböt akarunk kirajzolni, ott már nagyon is zavaró, ha látszódik, hogy háromszögekből áll.
** Ennek a jelenségnek az az oka, hogy az OpenGL csak a csúcspontokba számol megvilágítást (per vertex shading), és ezeket az értékeket interpolálja a háromszög belsejében. Ez akkor amikor az OpenGL-nek az a verziója készült, amit a tárgyból használni kell (1997-ben), még nagyon jó ötletnek tűnt, ezzel teljesítményben rengeteget lehetett nyerni.
** Ez igazándiból nem olyan nagy probléma, csak az olyan objektumokat, aminek a méretei elég nagyok ahhoz, hogy azon a távon már jelentős legyen a megvilágítás megváltozása, célszerű kisebb háromszögekre bontanunk.
 
Pl ha egy oldal 2 helyett 32 db háromszögből áll, amit én így oldottam meg:
 
<br/> <syntaxhighlight lang="c">
void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  Vector normal = cross(b-a, c-a).normalize();
  glColor3f(fabs(normal.x), fabs(normal.y), fabs(normal.z));
  glNormal3f(normal.x, normal.y, normal.z);
 
  int res = 4; // resolution
  for(int i = 0; i < res; i++) {
    for(int j = 0; j < res; j++) {
      glVertex3f(a + i*(b-a)/res    + j*(d-a)/res);
      glVertex3f(a + (i+1)*(b-a)/res + j*(d-a)/res);
      glVertex3f(a + (i+1)*(b-a)/res + (j+1)*(d-a)/res);
      glVertex3f(a + i*(b-a)/res    + (j+1)*(d-a)/res);
    }
  }
  // glVertex3f(a); glVertex3f(b); glVertex3f(c); glVertex3f(d);
}
</syntaxhighlight> <br/>
 
Akkor a csúcspontokban számolt megvilágítás is egész hihetően néz ki:
 
http://i.imgur.com/dzjcMuZ.png
 
<br/>
 
És ha a felbontást felvisszük mondjuk 1024 háromszögig:
 
http://i.imgur.com/EyFQ9L0.png
 
Akkor már pixel szinten pontos képet kapunk... de sokkal sokkal drágábban. Ilyenkor rengeteg teljesen fölösleges munkát csináltatunk az OpenGLlel. Általánosságban nagyon rossz ötlet pixel szintű pontosságra törekedni a felbontás növelésével. A nem feltűnően zavaró megvilágítással általában már megelégszünk, egy összetett jelenetnél a megvilágítás pontatlansága úgy se tűnik fel.
 
=== A megvilágítás és a transzformációk ===
 
Korábban említettem, hogy egy fényforrás esetében a glLightfv függvénnyel a GL_POSITION konstans segítségével lehet állítani egy fényforrás pozícióját, ami egy négy dimenziós vektor.
 
Viszont egy vektor önmagában nem jelent semmit, amíg meg nem mondjuk, hogy melyik koordináta-rendszerben van értelmezve.
 
A rövid válaszom az, hogy a GL_MODELVIEW által definiált koordináta-rendszerben, vagyis a glLightfv-ben megadott helyzetnek a GL_MODELVIEW-val transzformált eredménye fogja meghatározni a fényforrás helyét a kamera koordináta-rendszerben. Ha érted, hogy ez mit jelent és mivel jár, akkor fojtasd a következő fejezettel.
 
Ennek az egyik legfontosabb következménye, hogy a világítás működése függ attól, hogy azt a kódban hol állítjuk be.
 
A lehetőségek bemutatásához azt feltételezem, hogy az onDisplay elején a GL_MODELVIEW egységmátrixra van állítva, majd ugyanebben a függvényben később, de még a rajzolás előtt a kamera a gluLookAt függvénnyel úgy van beállítva, hogy egy ellipszis pályán mozogjon.
 
Például:
 
<br/> <syntaxhighlight lang="c">
void setCamera() {
  float time = glutGet(GLUT_ELAPSED_TIME) / 1000.0f;
  gluLookAt(3*cos(time), 2, 2*sin(time), 0, 0, 0, 0, 1, 0);
}
 
void onDisplay() {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
 
  // ...
  setCamera();
 
  // rajzolás
}
</syntaxhighlight> <br/>
 
A három lehetőség:
* A világítást a setCamera() előtt állítjuk be. Ilyenkor az értékek kamera koordináta-rendszerben értetődnek. Ez azt jelenti, hogy ha a pozíciót (-1, 0, 0, 0)-nak állítjuk be, akkor az mindig balról fog világítani, akármerre is néz a kamera. Ezt tipikusan olyankor szoktuk használni, ha a fényforrás a nézőhöz kötődik, pl egy FPS játékban a karakterünk fegyverén a lámpa.
 
http://i.imgur.com/iZJlFr9.gif
 
* A megvilágítást közvetlenül a setCamera() függvény után állítjuk be. Ekkora a glLightfv-ben megadott pozíció a világ koordináta-rendszerben lesz értelmezve, és a statikus objektumoknak "mindig ugyanaz az oldala lesz fényes". Ezt általában akkor szoktuk használni, ha a fényforrás a jelentben egy helyben marad.
 
http://i.imgur.com/cUPVzeT.gif
 
* A megvilágítást a setCamera(), és további transzformációk használata után állítjuk be. Így tipikusan a jelentünkben valamelyik objektummal együtt mozgó fényforrásokat szoktuk kezelni.
 
http://i.imgur.com/UZT2nMr.gif
 
=== Hátsólap eldobás ===
 
Az utolsó példába, a kocka hat oldala közül egyszerre mindig csak három látszódott, de ennek ellenére mind a hatot kirajzoltuk, csak abból három a z-tárazás (GL_DEPTH_TEST) miatt nem látszódott. Ha nekünk kéne rajzolnunk, akkor a felületi normál alapján azonnal el tudnánk dönteni egy oldalról, hogy az a kamera felé néz-e vagy se. Ha ezt az ötletet az OpenGL-lel is fel tudnánk használni, a háromszögek felének fölösleges kirajzolását megspórolnánk. Viszont a rajzoláshoz használt felületi normál, amit mi adunk meg, nem mindig egyezik meg a háromszögek felületi normáljával, sőt ez a vektor a háromszög három csúcspontjára különböző is lehet. Ha pedig nem ismerjük a háromszög normál vektorát, akkor nehéz az alapján optimalizálni. A háromszög három csúcsából ki tudjuk számolni a felületi normál irányát, csak annak az előjelét nem tudjuk. Az ötlet az, hogy a háromszög pontjainak legyen egy előre meghatározott körüljárási iránya, például legyenek a normál irányából nézve az óramutató járásával ellentétes (Counter Clockwise - CCW) sorrendben felsorolva a pontok. Ekkor, ha a másik oldalról nézzük a háromszöget, akkor a körüljárási irány megfordul, és onnan az óramutató járásával megegyező (Clockwise - CW) sorrendet fogok látni. És ezt már az OpenGL is fel tudja használni.
 
Nekünk innentől csak annyi a dolgunk, hogy megadjuk, hogy a mi objektumainkra, a normál irányából nézve (ez a "front face") CW vagy CCW körüljárási irányt használunk. Pl:
 
<br/> <syntaxhighlight lang="c">
glFrontFace(GL_CCW); // Az normál irányából nézve CCW a körüljárási irány
glCullFace(GL_BACK); // A hátsó oldalt akarjuk eldobni
glEnable(GL_CULL_FACE); // És engedélyezzük a lapeldobást.
</syntaxhighlight> <br/>
 
Ugyanazt látjuk, mint a három sor nélkül, de fele annyi erőfeszítéssel.
 
http://i.imgur.com/dzjcMuZ.png
 
<br/>
 
Viszont ha az első lapokat dobjuk el a <code>glCullFace(GL_FRONT);</code> függvénnyel, akkor ezt kapjuk:
 
http://i.imgur.com/OsxWbsw.png
 
=== Színek ===
 
A színekkel kapcsolatban egy megjegyzés: a GL_COLOR_MATERIAL-ról azt írtam, hogy az csak egy átmeneti megoldás. Ami a másik fajta szín bevezetését motiválta, az az, hogy az anyagoknak lehet különböző az ambiens a diffúz és a spekuláris színe. Az OpenGL megvalósításában az anyagoknak még emisszív színe is lehet. Az emisszív színnel azt lehet elérni, hogy az objektum világítson, sötétben is látszódjon, de nem úgy, mint egy fényforrás, az emisszív anyag a környezetének a színét nem befolyásolja. A másik előnye az OpenGL implementációjának hogy az első és hátsó lapoknak külön színe is lehet.
 
Az egyes színeket állító függvény a <code>glMaterialfv(GLenum face, GLenum pname, const GLfloat *params);</code>
 
A leggyakoribb paraméterei:
* A 'face' értékei:
** GL_FRONT
** GL_BACK
** GL_FRONT_AND_BACK
* A 'pname' értékei:
** GL_AMBIENT
** GL_DIFFUSE
** GL_AMBIENT_AND_DIFFUSE
** GL_SPECULAR
** GL_EMISSION
 
Illetve egy másik hasznos függvény a <code>glMaterialf(GLenum face, GLenum pname, GLfloat param)</code>
* Ezt a GL_SHININESS állítására szoktuk használni.
 
=== Tesszelláció ===
 
Avagy hogyan bontsuk fel primitívekre (tipikusan háromszögekre) az objektumainkat.
 
Először is egy apró trükk azoknak, akik az előző példába nem látták át, hogy a kocka hogyan épül fel négyszögekre:
Az OpenGL-t meg lehet kérni rá, hogy ne töltse ki a primitíveket, csak a határukat rajzolja ki a <code>glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);</code> függvénnyel. Ez olyan, mint ha GL_LINES-ra lecseréltük volna a rajzolandó primitívet, csak így nem kell a pontok felsorolásán módosítani, elég ezt az egy sort beírni és már látjuk is az objektumunk "drótvázát".
 
http://i.imgur.com/IBsec5K.png
 
Persze a megvilágítás a vonalakra is érvényes. Ha ez zavaró, akkor kapcsoljuk ki a megvilágítást, és pl. rajzolunk mindent fehérrel.
 
A kis kitérő után a tesszellációhoz visszatérve, a primitívekre bontástól a legalapvetőbb elvárásunk, hogy visszaadja azt az alakot, amit ki akartunk rajzolni.
De ezen kívül is van pár fontos jellemzője a tesszellációknak, ami alapján az egyik primtívekre bontásra azt mondhatjuk, hogy jobb, mint a másik:
* Például, hogy nagyjából azonos területű primitívekből épül-e fel.
** Például a gömb tesszellációk tekintetében nagyon nagy különbség van az "UV sphere" és az "icosphere" között.
** Az "UV sphere" pontjait a gömb paraméteres egyenletéből tudjuk kiszámolni (gömbi koordináta rendszerből kiindulva, a sugárt fix értékűre választjuk).
 
http://upload.wikimedia.org/wikipedia/hu/2/22/Gomb-pol.jpg
 
Az egyes pontokból pedig úgy csinálunk gömböt, hogy a szomszédos pontokat (amik a paraméterekben szomszédosak), "összekötjük", azaz egy sokszöget alkotunk belőlük. Például az például az összes paraméter értékpárra kirajzolunk egy-egy négyzetet.
 
http://i.imgur.com/YGa1jJw.png
 
* Az icosphere ezzel szemben az öt szabályos test egyikéből, az ikozaéderből indul ki, és ennek növeli a polygon számát a Catmull-Clark algoritmussal. A golflabda is ilyen.
 
http://previewcf.turbosquid.com/Preview/2011/04/19__09_20_06/Perspective_normals.jpg4d727076-639f-4e38-8e93-b7b6ca4b6d52Large.jpg
 
<br/>
 
* A két gömb összehasonlítva:
 
http://i.imgur.com/qNnAKse.png
 
* Az UV sphere esetében a sarkoknál közel nulla területűek a négyszögek, ott teljesen fölöslegesen kell számolnia az OpenGL-nek. Az icosphere esetében viszont a polygonok egyenletesen vannak elhelyezve, és ugyanakkora területűek. Az icosphere tehát jobb tesszelációja a gömbnek, mint az UV sphere. De az UV sphere-t megírni sokkal egyszerűbb, és a házikhoz teljesen elegendő.
* Két tesszelláció között nyilván különbség lehet, hogy mennyire jellegzetes pontokat ragad meg az eredeti objektumból. Például vegyünk egy síkot, amibe vannak keskeny kiugróan magas pontok. A tesszellációba a kiugró pontok elhagyása az alakzat összhatását teljesen el tudja rontani, míg ha ezeket is ugyanolyan súllyal vesszük be, mint a többi sík pontot, akkor összességébe az alakzatunk dimbes-dombos lesz, mind a sík volta, mind a kiugró pontok jellegzetessége elveszik.
* Két tesszelláció között nagyon fontos különbséget okozhatnak az árnyaló normálok. De mielőtt ebbe beleszaladnánk, nézzük meg, hogy mik is azok.
 
Megjegyzés: Általában parametrikus felületeket kell tesszellálni. Itt az összes pontnak a felsorolása - az alakzattól függetlenül - két darab for loop. Az objektum kirajzolása ettől mindössze annyiban különbözik, hogy a szomszédos pontokból primitíveket is kell alkotni. De szerencsére általában a szomszédos pontok paraméterei is szomszédosak. Bár elsőre meglepőnek hangozhat, de egy gömb és egy tórusz tesszellációja csak annyiban különbözik, hogy a két futó paraméterhez hogyan rendeljük hozzá a 3D-s pontot, vagyis a két alakzatnak csak az egyenlete más, ezt leszámítva a két objektum kirajzolásának a kódja teljesen ugyanaz. Sőt, amikor a kockának az egyes oldalait, a négyzeteket bontottam fel nagyobb részletességűre a megvilágításhoz, még ott is ugyanaz az algoritmus került elő, mint ami a gömbnél vagy a tórusznál kell, egyedül az alakzat paraméteres egyenlete volt más.
 
http://i.imgur.com/KCJ9s0Q.jpg
 
=== Az árnyaló normálok ===
 
A gömb tesszelációról azért beszéltem ennyit, mert ezen keresztül meg tudom mutatni, hogy mi az az árnyalási normál, és miért van rá szükség. Az OpenGL lehetőséget ad arra, hogy a rajzoláskor ne a háromszögek normáljait használjuk, hanem - a teljesen alakzat ismeretében - minden egyes pontban külön-külön adjuk meg a normálvektort. Ezeket árnyalási normálnak hívjuk, ezek nem a tesszellált, hanem az eredeti objektumnak a normáljai. Egy gömbnél például az összes normál a gömb középpontjából az adott felületi pontba mutató egységvektor lenne. A csúcsonként megadott normálok azért jók, mert így egy háromszöget úgy tudunk árnyalni, mintha az nem sík lenne. Így háromszögekkel meglepően jól lehet közelíteni még egy gömb felületét is.
* Az OpenGL-be a <code>glShadeModel(GL_SMOOTH);</code> függvénnyel lehet bekapcsolni, hogy figyelembe vegye az árnyaló normálokat.
* Én ennek a demonstrálásához a glutSolidSphere(GLdouble radius, GLint slices, GLint stacks); függvényt használom, de ezt házikba nem lehet használni.
* A példaprogram: [[Média:Grafpp4_gomb_sima_arnyalassal.cpp|Gömb sima árnyalással]]
 
A <code>GL_FLAT</code> árnyékolás (baloldalt) összehasonlítása <code>GL_SMOOTH</code> árnyékolással (jobboldalt)
 
{|
|-
| http://i.imgur.com/9luiuQI.png || http://i.imgur.com/IWMFbU4.png
|}
 
=== A trafók vs az árnyaló normálok ===
 
Amikor a világot transzformáljuk, akkor a normálokkal mi történjen? Az eltolás kérdése egyszerű, a normál az egy irányvektor, az eltolás nem hat rá. A forgatás természetesen ugyanúgy hat rá mint a helyvektorokra. A nagyítással kapcsolatban viszont már más a helyzet.
 
Ezzel kapcsolatban módosítsuk az utolsó példaprogramot, kérjünk fele akkora gömböt a GLUT-tól, és azt nagyítsuk kétszeresére <code>glScalef()</code>-el. Az eredménye az, hogy a gömb fele olyan világos lesz:
 
http://i.imgur.com/yy8eRvZ.png
 
A világítás számításakor csak a normál változott meg, és nem nehéz kitalálni, hogy fele olyan hosszú lett. De nem kétszeresére nagyítottuk a világot? Akkor a normál miért lett fele akkora. Ha ugyanaz maradt volna a normál, akkor működne jól. De akkor az OpenGL miért rontotta el?
 
Ahhoz, hogy ezt megértsük, vegyünk nem uniform nagyítást, ami a tengelyek irányába különböző mértékben nagyít. Pl. nagyítsuk a (3, 1, 1) vektorral. Ez nyilván drasztikusabb alakváltozással, és így a normálok lényegesebb megváltozásával is jár. Ennek az eredménye egy ellipszis lesz:
 
http://i.imgur.com/RG9NlDC.png
 
Az ellipszisen egyrészt nagyon jól látszódik, hogy az a tesszellációs felbontás, ami a gömbhöz elég volt, itt már csúnya képet eredményez. De most nem ezen van a lényeg, hanem az árnyaló normálokon. Miben másak az ellipszis normáljai, mint a gömbé? Az X tengely mentén ugyanannyi változás 3-szor akkora távon következik be, ezért értelemszerűen, amíg a Y és a Z tengely mentén a egységnyit változik a felületi pont helye, addig az X tengely mentén ez az érték 1/3. Termesztésen ez a pongyola megfogalmazás a parciális deriváltak számszerű értékére vonatkozott. És a gradiens, a parc. deriváltakból álló vektor, a felületi normál amit mi kerestünk. Tehát a normálon a (3, 1, 1) nagyítás hatására (1/3, 1, 1) vektorral való skálázás történik. Általánosságban az igaz, hogy a normálokra a nagyítások inverz transzformációja hat.
 
Szerencsére ezt az OpenGL automatikusan megcsinálja helyettünk. A gond viszont ezzel az, hogy az ilyen transzformációk után a normál már nem feltétlen lesz egységvektor, mint ahogy azt a korábbi példákba is láttuk. De erre a megoldás egyszerű, a <code>glEnable(GL_NORMALIZE);</code> függvény megkéri az OpenGL-t, hogy a világítás számolásakor normalizálja a normálokat.
 
=== A textúrázás ===
 
Eddigi ismereteink szerint, ha egy objektumba egy plusz színt akartunk vinni, akkor kénytelenek voltunk egy plusz pontot is hozzáadni, ami semmi plusz információt nem hordozott, csak más volt a színe. De cserébe lényegesen többet kellett számolni.
 
A plusz színek hozzáadásának hatékonyabb megoldására lehet használni a textúrázást. Ezt használva egy háromszögön belül minden egyes pixelnek különböző színe lehet, a háromszög csúcspontjaihoz rendelt színtől függetlenül. Viszont ha a megvilágítás csúcspontonként történik, akkor a megvilágítás hogyan tudja figyelembe venni ezeket az új színeket? A rövid válasz az, hogy sehogy, de mégis használhatunk megvilágítást és textúrázást egyszerre. Ehhez egy ügyes trükkre lesz szükségünk, de erről majd bővebben akkor, amikor már tudjuk, hogy hogyan kell textúrázni. Addig kapcsoljuk ki a megvilágítást.
 
==== A textúrázás alapjai ====
 
Először egy egyszerű 6 db quadból álló kockán fogom megmutatni, hogy a textúrázás hogyan működik. A cél, hogy minden oldalra kiírjak egy számot, hogy azt az oldalt hányadikként rajzoltam ki. Ezt csak kódból fogom megoldani, nem használok hozzá külső programmal, pl. photoshoppal előállított képet, mert azt a házikhoz se lehet. Néhány szám esetleg fordítva fog állni, de ez engem nem zavar. Ilyen eredményt fogunk kapni:
 
{|
|-
| http://i.imgur.com/Bh5G5f8.png || http://i.imgur.com/0lSPkjh.png
|}
 
Először is, mi is az a textúra? A textúra egy színeket tartalmazó tömb. OpenGL 1.1-be egy vagy két dimenziós lehet, és szabvány szerint kettőhatvány méretűnek kell lennie. Annak ellenére, hogy a textúra színeket tartalmaz, és a színeket az OpenGL floatként szereti kezelni, a textúrák esetében nem annyira szeretünk floatokat használni. Itt tényleg csak (0-1) tartománybeli LDR színekre vagyunk kíváncsiak, itt float helyett elég egy fix pontos szám is, pl. komponensenként egy byte. De gyakran mindhárom komponenst le tudjuk írni mindössze egy bájtban. A float textúrák sokkal több helyet foglalnak, külön megizzasztják a memóriát, ami már enélkül is szűk keresztmetszet, ráadásul ezt nagyjából feleslegesen teszik, a float értékkészletének nagy részét nem is használják ki.
 
De hogyan állítsuk elő a számokat? Vegyünk egy unsigned char tömböt és kézzel írjuk be az összes pixelre, hogy milyen színű? Majdnem, de ez ilyen formába használhatatlan lenne. Semmi vizuális visszacsatolásunk se lenne, hogy a textúra hogy néz ki. Én ehelyett egy karakter tömbbe (magyarul stringbe) fogok ascii-art számokat rajzolni. Például a '.' karakter jelentsen fehér színt, a '*' feketét, a '+' meg szürkét. Én 8*8-as textúrákat fogok csinálni minden egyes számnak. A számokat én így álmodtam meg (ér ezeknél szebbeket csinálni):
 
<br/> <syntaxhighlight lang="c">
"........"          "........"          "........"
"...**..."          "..***+.."          "..***+.."
"....*..."          "....+*.."          "....+*.."
"....*..."          "....+*.."          "....+*.."
"....*..."          "...+*+.."          "...**+.."
"....*..."          "..+*+..."          "....+*.."
"...***.."          "..****.."          "..***+.."
"........",        "........",        "........",
 
"........"          "........"          "........"
"....*..."          "..****.."          "........"
"...+*..."          "..*....."          "..+**..."
"..+**..."          "..***+.."          "..*....."
"..+.*..."          "....+*.."          "..***+.."
"..****.."          "....+*.."          "..*..*.."
"....*..."          "..***+.."          "..+**+.."
"........",        "........",        "........"
</syntaxhighlight> <br/>
 
Technikailag már ezek a stringek is lehetnének unsigned char tömbök. És három szín van összesen, amit egy byte-on is bőven le lehetne írni. Viszont én az egyszerűség kedvéért inkább maradok a komponensenként egy byte-nál, a 3 komponensre összesen egy byte helyett, és nem kezdek el bitműveletekkel játszani a GL_R3_G3_B2-höz.
 
Termesztésen ezekből a szín előállítása egyszerűen két egymásba ágyazott for ciklus. Pl:
 
<br/> <syntaxhighlight lang="c">
GLubyte texture_data[6][64][3];
for(int t = 0; t < 6; t++) {
  for(int i = 0; i < 64; i++) {
    switch(ascii_textures[t][i]) {
      case '*':
        for(int j = 0; j < 3; j++) {
          texture_data[t][i][j] = 0;
        }
        break;
      case '+':
        for(int j = 0; j < 3; j++) {
          texture_data[t][i][j] = 127;
        }
        break;
      default:
        for(int j = 0; j < 3; j++) {
          texture_data[t][i][j] = 255;
        }
        break;
    }
  }
}
</syntaxhighlight> <br/>
 
Oké, megvan a textúra tartalma, és akkor most ezt hogyan adjuk oda az OpenGLnek?
 
Először is szükségünk van "egy textúra mutató pointerre/referenciára", egy névre, amivel hivatkozhatunk majd később a textúrára. Ez a referencia a textúrák esetében (és a modern OpenGLbe gyakorlatilag az összes objektum esetében) egy GLuint (unsigned int) lesz. A textúrázással kapcsolatos függvények dokumentációjába ez "a textúra neve"-ként lesz megemlítve.
 
Nekünk hat darab textúra kell, ezért definiáljuk egy 6 elemú GLuint tömböt:
 
<br/> <syntaxhighlight lang="c">
GLuint tex[6];
</syntaxhighlight> <br/>
 
Kérjük meg az OpenGL-t, hogy találjon ki nekünk hat darab textúra nevet. Ezek után ő ezekről a nevekről tudni fogja, hogy azok textúra vonatkoznak. Ehhez a <code> glGenTextures(GLsizei n, GLuint *textures); </code> függvényt használhatjuk.
 
<br/> <syntaxhighlight lang="c">
glGenTextures(6, tex);   
</syntaxhighlight> <br/>
 
Fontos, hogy ilyenkor még a textúra nem jön létre. Az csak akkor történik meg, amikor először használni akarjuk (bindoljuk).
 
==== A textúrák beállítása ====
 
Nyilván egy for ciklusban fogom beállítani őket, csak az 'i'-edik textúra beállítását mondom el.
 
Ahhoz, hogy egy textúrán műveletet tudjunk végezni azt bindolni kell egy targethez. Képzeljünk el egy szobát, amibe vannak képkeretek. Az összes képkeret különböző alakú, mindegyikbe másféle képet tudunk rakni. De mindig csak éppen a képkeretben lévő képeket látjuk, csak azokkal tudunk dolgozni. Ezek a képkeretek pontosan ugyanúgy viselkednek mint az OpenGL-be a targetek. Két fontos target van, az egyik az egydimenziós texturáknak (GL_TEXTURE_1D), a másik a kétdimenziósoknak (GL_TEXTURE_2D). A házikban szinte mindig csak a kétdimenziósra lesz szükséged. A hasonlatban amikor egy képet berakunk a képkeretbe, az megegyezik az OpenGL-nél a bindolással. A bindolás után az összes textúra művelet arra a képre fog vonatkozni. Bindolni a <code>glBindTexture(GLenum target, GLuint texture);</code> függvénnyel tudunk.
 
<br/> <syntaxhighlight lang="c"> 
glBindTexture(GL_TEXTURE_2D, tex[t]);   
</syntaxhighlight> <br/>
 
Ezután fel kell töltenünk a textúrát a <code> glTexImage2D(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels)</code> függvénnyel.
Elősször nézzük meg a paramétereit:
* target - Ez nálad szinte mindig GL_TEXTURE_2D lesz.
* level - Ide 0-t írj. Az OpenGL megengedi, hogy egy textúrának különböző felbontású változatait is feltöltsed, ami sokat javíthat a rendelt kép minőségén. Az alap textúránál kisebb felbontású képeket mipmap-nek hívják. Az 'i'-edik mipmap mérete minden dimenzióban fele az 'i-1'- ediknek. Ide azt írhatod be, hogy éppen hányadik mipmap-et töltöd fel (a 0. mipmap az alaptextúra). A mipmap-ek használata nagyon hasznos tud lenni, de nagyon veszélyes is. Ha ez a feature be van kapcsolva, és nem töltöd fel az összes mipmap-et, vagy valahova rossz felbontásút töltesz fel, akkor a textúra definíció szerint "incomplete" marad, és az incomplete textúrák úgy viselkednek, mintha nem is lennének ott. És ezt nagyon nehéz debuggolni, ezért a tárgyból ajánlott a mipmapek használatának kerülése.
* internalFormat - Ez legyen GL_RGB vagy GL_RGBA. Itt azt kell megadnod, hogy az OpenGL milyen formátumba tárolja a képet. Például a tömörített formátumok nagyon hasznosak, de a házikhoz nem kell használnod őket. Hatékonysági okok miatt a komponensenként egy byteos GL_RGB textúra gyakran GL_RGBA formátumba tárolódik, mert az OpenGL csak 4 byteos egységeket tud hatékonyan írni / olvasni, de ezzel nekünk explicit nem kell foglalkozni, ezt majd a driver eldönti, hogy szerinte hogy jó.
* width / height - Egyértelmű.
* border - Ez mindenképpen legyen 0.
* format - Ez is legyen GL_RGB vagy GL_RGBA. Itt adhatod meg, hogy milyen formátumban töltöd fel a képet. Ez nem feltétlen egyezik meg az internalFormat-al, más formátúmú tárolást is lehet tőle kérni, mint ahogy mi adjuk neki a textúrákat.
* type - Ez legyen GL_UNSIGNED_BYTE. Ez azt adja meg, hogy milyen típussal írtad le a színeket.
* pixels - A textúra elejére mutató pointer. Fontos megjegyezni, hogy a tömbünket úgy értelmezi, hogy a sorok mérete 4 byte-al oszható. Ha nem olyan (pl. 2*2-es RGB textúrát használtunk, akkor a sorokat ki kell egészíteni úgy nevezett "padding"-el, hogy illeszkedjenek a szóhatárra.
 
A mi esetünkben: 
 
<br/> <syntaxhighlight lang="c">
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, texture_data[t]);
</syntaxhighlight> <br/>
 
A textúrákkal kapcsolatban nagyon sok beállítási lehetőségünk van, amiket a <code> glTexParameteri(GLenum target, GLenum pname, GLint param); </code> függvénnyel tehetünk meg. Minket két fontos paraméter érdekel, a <code>GL_TEXTURE_MIN_FILTER</code>, és a <code>GL_TEXTURE_MAG_FILTER</code> és mindkettőnek a számunkra fontos lehetséges értékei: <code>GL_LINEAR</code> (ez a bilineáris szűrést bekapcsolja, amit a sugárkövetésnél is használtunk) és <code>GL_NEAREST</code> (ami a legközelebbi éréket választja). Mivel a GL_LINEAR hardwareből van megvalósítva, ezért gyakorlatilag semmivel se lassabb mint a GL_NEAREST, és általában szebb eredményt okoz (mint ahogy most is). Ami miatt azonban ezek fontosak, hogy a GL_TEXTURE_MIN_FILTER beállítása ennek a két értéknek az egyikére kikapcsolja a mipmap-ek használatát. Ennélkül a textúra úgy viselkedne, mintha nem is lenne ott.
 
A mi esetünkben:
 
<br/> <syntaxhighlight lang="c">
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
</syntaxhighlight> <br/>
 
Megjegyzés: a textúrákat "unbindolni" szoktuk, miután használtuk őket. Ez azt jelenti, hogy a 0 nevű, default textúrát használjuk, ami úgy viselkedik mint a null pointer, a rajta végzett műveletek nem csinálnak semmit. Ez nagyobb programoknál azért ''nagyon'' hasznos, mert ha valaki más hülyeséget csinál, akkor az nem a mi erőforrásunkat rontja el.
 
==== A textúrák használata ====
 
Ha azt akarjuk, hogy target befolyásolja a rajolást akkor azt a targetet engedélyezni kell pl. a <code>glEnable(GL_TEXTURE_2D);</code> és miután használtuk, ki kell kapcsolni a <code>glDisable(GL_TEXTURE_2D);</code>. Természetesen azt a képet kell bindolnunk, amit éppen használni akarunk. De hova kerüljön a kép az objektumon?
 
Ennek a specifikálásához textúrakoordinátákat is meg kell adnunk a glBegin()-glEnd() blokkban. Két dimenziós textúrák esetén a textúrakoordináták (0-1) tartománybeli float számpárok, ahol a (0, 0) jelképezi a kép bal alsó sarkát, az (1, 1) pedig a jobb felső sarkot. Azért a (0-1) tartományban használjuk őket, és nem egész pixeleket adunk meg, mert így ha a textúrát átméretezzük, a régi textúrakoordináták továbbra is jók maradnak, hiszen az a teljes mérethez képest relatív értékeket ad meg. Továbbá így használhatunk bilineáris interpolációt. A textúrakoordinátákat két dimenziós textúrák esetén a <code>glTexCoord2f(GLfloat s, GLfloat t)</code> függvénnyel adhatjuk meg.
 
A kocka kirajzolásakor annyi változtatásra szükség van, hogy most minden oldal külön glBegin()-glEnd() blobbkba kerül, mert egy ilyen blokkon belül nem lehet textúrát váltani. Az én megvalósításomban:
 
<br/> <syntaxhighlight lang="c">
void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  glTexCoord2f(0, 0); glVertex3f(a);
  glTexCoord2f(1, 0); glVertex3f(b);
  glTexCoord2f(1, 1); glVertex3f(c);
  glTexCoord2f(0, 1); glVertex3f(d);
}
 
void drawCube(const Vector& size) {
    /*      (E)-----(A)
            /|      /|
            / |    / |
          (F)-----(B) |
          | (H)---|-(D)
          | /    | /
          |/      |/
          (G)-----(C)        */
 
    Vector s = size / 2;
 
    Vector A(+s.x, +s.y, -s.z), B(+s.x, +s.y, +s.z), C(+s.x, -s.y, +s.z), D(+s.x, -s.y, -s.z),
          E(-s.x, +s.y, -s.z), F(-s.x, +s.y, +s.z), G(-s.x, -s.y, +s.z), H(-s.x, -s.y, -s.z);
 
    Vector vertices[6][4] = {
      {A, B, C, D}, {G, F, E, H}, {A, E, F, B},
      {D, C, G, H}, {B, F, G, C}, {E, A, D, H}
    };
 
    glEnable(GL_TEXTURE_2D);  
   
    for(int i = 0; i < 6; i++) {
      glBindTexture(GL_TEXTURE_2D, tex[i]); // Ezt semmiképpen se rakd a glBegin - glEnd blokk közé
 
      glBegin(GL_QUADS); {
        glQuad(vertices[i][0], vertices[i][1], vertices[i][2], vertices[i][3]);
      } glEnd();
    }
 
    glBindTexture(GL_TEXTURE_2D, 0);
    glDisable(GL_TEXTURE_2D);
}
</syntaxhighlight> <br/>
 
A teljes forráskód: [[Média:Grafpp4_texturazas.cpp‎|Textúrázott kocka]]
 
==== A textúrák és a megvilágítás ====
 
A megvilágítást nem tudjuk a textúrák színeivel számolni. Valójában a rajzolási csővezetékben a textúrázás még időben is később történik, mint a megvilágítás számolása. Amit viszont tudunk tenni, hogy fehér színt állítunk be a megvilágítás számításakor, és az eredményt - hogy az adott felületi pont mennyire világos - megszorozzuk (moduláljuk) a konkrét színnel. Ezt a <code>glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);</code> függvénnyel lehet bekapcsolni, de alapértelmezetten is be van kapcsolva. Ha nem akarunk megvilágítást, csak a textúra színét akarjuk megjeleníteni akkor a GL_MODULATE-et lecserélhetjük GL_REPLACE-re. Ezzel a függvénnyel még rengeteg hasznos dolgot lehetne csinálni, de azok a házikhoz általában nincsenek megengedve.
 
Ne feledjük, hogy ahhoz, hogy a megvilágításnak szép eredménye legyen továbbra sem elég, ha a kocka oldalai egyetlen quadból állnak. De a textúrázás bemutatásához önmagában az elég volt.
 
Példaprogram: [[Média:Graf4_megvilagitas_es_texturazas.cpp‎|Megvilágított, textúrázott kocka]]
 
{|
|-
| http://i.imgur.com/xT1d1ur.png || http://i.imgur.com/XE7dZAl.png
|}
 
==== Nem kettőhatvány méretű textúrák ====
 
Gyakran felmerülhet az igény nem kettőhatvány méretű textúrára. Például az előző példák kockájából egy dobókockát akarunk csinálni. Egy ilyet:
 
http://i.imgur.com/3benYFM.png
 
De ez a mintázat egy 7*7-es textúrát igényel, pl az öt így néz ki:
 
<br/> <syntaxhighlight lang="c">
"......."
".*...*."
"......."
"...*..."
"......."
".*...*."
"......."
</syntaxhighlight> <br/>
 
A hét köztudottan nem kettőhatvány. Ha megpróbáljuk ugyanúgy használni, mint ahogy a 8x8-as textúrával tettük, vajon működni fog?
 
Természetesen nem...
 
http://i.imgur.com/NsKm5AK.png
 
De ennek a problémának az eddigi ismereteink alapján nem szabadna meglepőnek lennie, még akkor se ha nem tudtuk, hogy csak kettőhatvány textúrákat lehet használni.
Természetesen itt azzal van a gond, hogy az OpenGL szóhatárra illeszkedő sorokat vár, viszont mi 7*3 = 21 byteos sorokat töltöttünk fel. Az OpenGL viszont úgy fogja értelmezni a textúránkat, mintha az 7*8-as lenne, aminek hatására jó pár szín elcsúszik, és egy kevés memóriaszemét is kerül a színek közé (az utolsó sorba). Ennek megoldására én három egyszerű megoldást tudok, és az egyszerű megoldásokon kívül egy olyat amit a beadón is lehet használni.
Az egyszerű megoldások:
* Írjuk be az onInitbe az alábbi sort: <code>glPixelStorei(GL_UNPACK_ALIGNMENT, 1);</code> - Ennek hatására az OpenGL egy bájtos határra ileszkedő sorokat vár.
* Minden sor végére rakjunk be egy plusz karaktert, és egy 7*8-as tömbre mutató pointer adjunk oda a glTexImage2D-nek. De fontos, hogy továbbra is azt mondjuk neki, hogy 7x7-es textúrát töltünk fel. Ez valószínűleg a legnehezebben emészthető megoldás, úgyhogy erről töltöttem fel kódot: [[Média:Grafpp4_7x8_dobokocka.cpp‎|Dobókocka 7x8-as textúrával]].
* RGB textúra helyett használjunk RGBA textúrát.
 
De várjunk csak, nem arról volt szó, hogy az OpenGL csak kettőhatvány méretű textúrákkal tud dolgozni? De.. ez szabvány szerint igaz... csak a mi gépünk nem tud róla... A 90-es években még problémát okozott a videókártyáknak a nem kettőhatvány méretű textúrák kezelése, de már több mint tíz éve tetszőleges méretű textúrával is ugyanolyan hatékonyan tud dolgozni az összes videókártya. Ez viszont sovány vigasz a beadón... ahol a nem kettőhatvány méretű textúrák nincsenek implementálva.
 
A beadón működő megoldás:
* Egészítsük ki 8x8-asra a textúrát. A szélére másoljuk be a legközelebbi valid szomszédokat, mert bilineáris szűréskor ezeknek az értékeknek is látjuk a hatását.
* Buheráljuk meg a textúrakoordinátákat, úgy, hogy 0-tól 7/8-ig menjenek. Ezzel persze elvesztjük azt az előnyt, hogy a textúrakoordináták függetlenek magának a textúrának a paramétereitől, és a textúra lecserélése a textúrakoordináták lecserélését is magával vonja.
* Ennek a megoldásnak a kódja: [[Média:Grafpp4_8x8_dobokocka.cpp‎|Dobókocka 8x8-as textúrával]]
 
{|
|-
| http://i.imgur.com/vGETS3t.png || http://i.imgur.com/3benYFM.png
|}
 
Persze a dobókocka egyeseknek jobban tetszhet GL_NEAREST szűréssel, a GL_LINEAR helyett. Ez teljesen ízlés kérdése, amúgy így néz ki bilineáris szűrés nélkül:
 
{|
|-
| http://i.imgur.com/N3uYYBc.jpg || http://i.imgur.com/Ai76YUE.jpg
|}
 
=== Egyszerű kamera ===
 
A végleges házikban a kamera általában valamilyen mozgatható objektumot követ. Ennek az implementálása nagyon egyszerű, minden rajzolás elején egységmátrixot töltünk a GL_MODELVIEW-ba, majd a gluLookAt(objektum pozíciója, objektum pozíciója + orientációja, felfele) függvényhívással megoldható.
 
Viszont ezzel nehézkes a debuggolás, nem lehet igazán jól körülnézni, és valós játékokban is ritka az ennyire egyszerű kamera.
 
Én egy egyszerű, szabadon-repülő kamera egy implementációjához adok ötletet, ami sokat segíthet a debuggoláshoz (sokszor csak egy nézetből nézve egy jelenetet nem lehet eldönteni, hogy az jó-e).
 
Kétféle input érdekel minket, a billentyűlenyomások (W,A,S,D), és az egér mozgatása (úgy, hogy közbe az egyik egérgomb le van nyomva).
 
A billentyűlenyomásokat nem kezelhetjük egyszerűen az onKeyboard-ban, ez akkor generál eseményeket, amikor egy karaktert begépelnénk, ami pl megtörténik először a billentyű lenyomásakor, aztán jelentős ideig (kb. 0.3 - 0.5 sec) nem generálódik újabb esemény, majd után másodpercenként kb. 10-20 karakterbeütés-t generál. Ez pl egy karakter irányításához teljesen használhatatlan, helyette inkább tároljuk el, hogy az egyes billentyűk lenyomott állapotban vannak-e.
 
<br/> <syntaxhighlight lang="c">
enum ControllKeys {W, A, S, D, keys_num};
bool keys_down[keys_num];  


glPushMatrix();
void onKeyboard(unsigned char key, int, int) {
glRotatef(30,0,0,1);
  switch(key) {
    case 'w': case 'W':
      keys_down[W] = true;
      break;
    case 's': case 'S':
      keys_down[S] = true;
      break;
    case 'a': case 'A':
      keys_down[A] = true;
      break;
    case 'd': case 'D':
      keys_down[D] = true;
      break;
  }
}


glBegin(GL_LINES); glVertex2f(0,0); glVertex2f(0,-0.8); glEnd(); //egyik lába
void onKeyboardUp(unsigned char key, int, int) {
    switch(key) {
    case 'w': case 'W':
      keys_down[W] = false;
      break;
    case 's': case 'S':
      keys_down[S] = false;
      break;
    case 'a': case 'A':
      keys_down[A] = false;
      break;
    case 'd': case 'D':
      keys_down[D] = false;
      break;
  }
}
</syntaxhighlight> <br/>


glPopMatrix();
Ahhoz, hogy ezt fel tudjuk használni, a kamerának tudnia kell, hogy hol van, merre néz, és milyen gyorsan tud mozogni.


glPushMatrix();
<br/> <syntaxhighlight lang="c">
glRotatef(-30,0,0,1);
  Vector fwd, pos;
  const float speed;


glBegin(GL_LINES); glVertex2f(0,0); glVertex2f(0,-0.8); glEnd(); //másik lába
  void updatePos(float dt) {
    Vector up = Vector(0, 1, 0), right = cross(fwd, up).normalize();
    up = cross(right, fwd).normalize();


glPopMatrix();
    if(keys_down[W] && !keys_down[S]) {
      pos += fwd * speed * dt;
    } else if(keys_down[S] && !keys_down[W]) {
      pos -= fwd * speed * dt;
    }


glPopMatrix();
    if(keys_down[D] && !keys_down[A]) {
//további testrészek kirajzolása
      pos += right * speed * dt;
</pre>
    } else if(keys_down[A] && !keys_down[D]) {
      pos -= right * speed * dt;
    }
  }
</syntaxhighlight> <br/>


==Görbék==
Ennek a meghívását pl az onIdle-ben tehetjük meg az alábbi módon:
* 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ó==
<br/> <syntaxhighlight lang="c">
* 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ő:
void onIdle() {
** 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.
  static float last_time = glutGet(GLUT_ELAPSED_TIME);
** 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!
  float time = glutGet(GLUT_ELAPSED_TIME);
** 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!
  float dt = (time - last_time) / 1000.0f;
** 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.
  last_time = time;


* 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.
  camera.updatePos(dt);
*  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.
  glutPostRedisplay();
* Ezek alapján az onIdle felépítése:
}
<pre>
</syntaxhighlight> <br/>
//globális változók
long previousTime;


void onIdle( ) {
Továbbá az egér mozgatást is kezelnünk kell.
long time = glutGet(GLUT_ELAPSED_TIME);
long deltaTime = previousTime - Time;


//... - mozgatás deltaTime alapján
<br/> <syntaxhighlight lang="c">
  const float mouse_speed;


previousTime = time; //akt. idő elmentése
  void updateDir(int dx, int dy) {
glutPostRedisplay();
    Vector y_axis = Vector(0, 1, 0), right = cross(fwd, y_axis).normalize();
</pre>
    Vector up = cross(right, fwd).normalize();
* '''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ó==
    // Ha teljesen felfele / lefele néznénk, akkor ne forduljon át a kamera
* 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.
    float dot_up_fwd = dot(y_axis, fwd);
* '''2D házik'''
    if(dot_up_fwd > 0.95f && dy > 0) {
** 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.
      dy = 0;
** 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.
    if(dot_up_fwd < -0.95f && dy < 0) {
* '''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.
      dy = 0;
* *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==
    // Módosítsuk az nézeti irányt
* 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á.
    fwd += mouse_speed * (right * dx + up * dy);
* 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.
    fwd = fwd.normalize();
  }
</syntaxhighlight> <br/>


===OpenGL rész===
Ennek a meghívására pedig az onMouseMotion függvény alakalmas:
* 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:
<pre>
glRasterPos2d(-1,-1);
glDrawPixels(600,600,GL_RGB,GL_FLOAT,picture);
</pre>


* A RasterPos segítségével a pixelenkénti rajzolás kezdő helyét adjuk meg, (-1,-1) a bal alsó sarok.
<br/> <syntaxhighlight lang="c">
* 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.
int last_x, last_y;
* 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.
void onMouse(int, int, int x, int y) {
* A 600*600 pixel nem véletlen, a keretben ez az ablak fixen megadott mérete.
  last_x = x;
  last_y = y;
}


===Osztályok===
void onMouseMotion(int x, int y) {
* 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:
  camera.updateDir(x-last_x, last_y-y);
** '''Vector/Coordinate/Point'''
  last_x = x;
*** 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
  last_y = y;
*** 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
</syntaxhighlight> <br/>
*** 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 -> [[#AnchorToneMap|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.


===Metszéspontok, önárnyékolás és az EPSILON===
A kamera beállítása ezek után már csak egyetlen függvényhívás:
<div id="AnchorOnarnyekolas"></div>
* TODO


===Sugár indítások, egy és kétirányú sugárkövetés===
<br/> <syntaxhighlight lang="c">
* TODO: egy/kétirányú: ...
gluLookAt(pos.x, pos.y, pos.z, pos.x+fwd.x, pos.y+fwd.y, pos.z+fwd.z, 0, 1, 0);
* 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: ...
</syntaxhighlight> <br/>


===Színek, fények, optikai modellek===
Egy ilyesfajta kamera már a közepesen komplex jelenetek összeállításban is nagyon sokat tud segíteni.  
<div id="AnchorToneMap"></div>
* 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==
Az én implementációim: [[Média:Grafpp4_kamera.cpp‎|Kamera]]
<div id="AnchorZFighting"></div>
* TODO: kamera, Z-buffer VS. clipping pane, árnyalás VS. normálvektorok, glut tesszellátorok


[[File:Graftutorial_kamera_anim.gif]]


==3D extrák==
== Utóhang ==
* TODO: fények, csillanás, textúrázás


== Hibakezelés ==
Én nagyon élveztem az összefoglaló megírását, remélem Te is hasonló élményekkel gazdagodtál a házijaid megírásakor. Amikor én a tárgyat csináltam ilyen részletes segédlet még nem volt, bár nekem hasznos lett volna. Annak ellenére, hogy már a tárgy felvétele előtt is rengeteget foglalkoztam grafikával, még a beadott házijaimba is voltak elvi hibák, amikre itt külön felhívtam a figyelmet, hogy ti ne kövessétek el. Ha ez az összefoglaló segített a házijaidban, vagy valami újat tanultál belőle, próbálj meg te is segíteni az utánad jövőknek azokból a tárgyakból, amikből Te jó vagy!


-- [[BlackGhost|BlackGhost]] - 2011.04.05.
-----
[https://wiki.sch.bme.hu/Szerkeszt%C5%91:Rohamcsiga Csala Tamás] - 2014.01.




[[Category:Infoalap]]
[[Category:Infoalap]]

A lap jelenlegi, 2021. március 22., 23:43-kori változata

Sablon:RightTOC


Előszó

Ez a segédlet a házikhoz kellő ötletek és OpenGL függvények működését hivatott elmagyarázni. Az elméletbe csak olyan mélységig megy bele, hogy a házidat meg tudd írni, de nem olyan mélységig, hogy a védésen, vagy vizsgán feltett kérdések mindegyikére válaszolni tudj. Arra ott vannak a hivatalos források: az előadásdiák, a sünis könyv, és - sokak számára meglepő - de maga az előadás is. Amiben ez a összefoglaló más az előbb említett forrásoktól, az az, hogy ez sokkal tömörebb és lényegre törőbb, mindenhol megpróbálja felhívni a figyelmed a tipikus hibákra, illetve tartalmaz több ezer sornyi kipróbálható példaprogramot. Ezek a programok azért érhetőek el forráskóddal együtt, hogy könnyen ki tudd próbálni őket, részekre tudd bontani, apró módosításokat is ki tudj rajta próbálni, egyszóval azért vannak itt, hogy a megértést segítsék. Nem azért, hogy ezekből oldd meg a feladatodat. Én megbízok bennetek, de ha ezzel visszaéltek, le fogom szedni a példaprogramokat.

Az oldalról kódot a saját házidba átemelni TILOS! Más házijába lehet, oda csak nem ajánlott. Még ha pár sornyi kódról is van szó, akkor is inkább gépeld be magadnak, addig is gyakorolsz... (nem csak gépelni). Meg persze nem is érdemes másolni, mert csak borzolod vele a plágiumkereső idegeit.

Az OpenGL és GLUT alapjai

Az OpenGL

  • Az OpenGL (Open Graphics Library) egyszerű térbeli alakzatok (primití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árszömszögek rajzolásából építkeznek. A primitíveket nagyon sokféleképpen és nagyon hatékonyan lehet az OpenGL-lel kirajzolni, de ezen kívül mást nem tud, pl. képek betöltéséhez külső könyvtárat kell használnod, nem tud á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épen 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.: glVertex3f() - 3f = 3 db float, glTexCoord2iv() - 2iv = 2 elemű int vector (azaz pointer egy tetszőleges memóriaterületre, ahol 2 db int van egymás után).
  • 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 függvényeknek lehetnek változatai különböző tulajdonságok alapján, ilyenkor ez a tulajdonság megjelenik a névben is.
  • A grafikában a lebegőpontos számokkal float alakban szeretünk dolgozni. Az esetek többségében a float és double használatával előállított kép között nem lehet szabad szemmel észrevenni a különbséget, viszont a teljesítményben elég jelentős eltérések adódhatnak. 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 double-t váró alakot, a glVertex3d-t nem.
  • Az OpenGL RGB színskálán állítja elő a képet, és neki is RGB érté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 byte-on 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ámhossza), 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 (a fotonok száma) is, ami független magától a színtől. A fényerősség tetszőlegesen nagy, vagy kicsi, sőt, grafikában akár még negatív is lehet, speciális hatások eléréséhez. 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.
  • C++-ban a beépített típusok mérete (az egy bytehoz képest) nem fix. Ezt sajnos az OpenGL nem nagyon veszi figyelembe, ő pl. int alatt mindig 4 byteos intet ért. Viszont definiál nekünk jó pár makrót, pl. a GLint-et, ami minden architektúrán garantáltan olyan méretű lesz, mint ami neki kell. Ezeknek a típusoknak az elnevezési konvenciója egyszerű: GL{u}típusnév. Az 'u' betű az unsignedra utal, például a GLushort az olyan gépeken, ahol a bájt 8 bit, megegyezik a uint16_t-vel, és általában az unsigned short-tal is. Egy kivétel van az elnevezési konvencióba: nincs GLchar, helyett GLbyte meg GLubyte van. Ezeket a típusokat nagyon ajánlott használni, főleg ha az adatunkat cím szerint adjuk át, például tömbök esetén.
  • 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 (OpenGL Utility Toolkit) 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 nagy ré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), és annak ellenére, hogy ez nálad valószínűleg működni fog, a beadón sajnos nem.
    • onInitialization() - inicializálós 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ó konstruktorában meghívunk ilyen függvényt. Így ugyanis még a main() kezdete előtt futattnánk le egy ilyen típusú függvényt, amikor még ezek a könyvtárak nincsenek inicializálva. Ennek az elkerülésére van ott az onInitialization() - ebben már nyugodtan használhatunk bármilyen függvényt.
    • onKeyboard() - Itt tudjuk kezelni egy billentyű lenyomását. Ez az esemény nem csak akkor generálódik, amikor a billentyűt éppen lenyomtuk, hanem akkor is, ha egy lenyomott betű hatására egy újabb karaktert gépelnénk be. Erre a házikban általában csak minimális szükség van.
    • onKeyboardUp() - Itt tudjuk kezelni egy billentyű felengedést. Valós játékokban ez nagyon hasznos tud lenni, de házikhoz ezt szinte soha nem kell használni.
    • onMouse() - Itt kapunk értesítést arról, ha valamelyik egérgomb állapota megváltozott, és azt is megtudjuk, hogy az ekkor az egér az ablak koordinátái szerint hol volt.
    • onMouseMotion() - Itt tudjuk meg, ha a felhasználó lenyomott egér gomb mellett mozgatta az egeret. A koordinálta értékére ugyanaz 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ó).

2D OpenGL

Rajzolás az OpenGL segítségével

Az OpenGL 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 dimenzió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áta-rendszerben 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:

http://i.imgur.com/bOSuMin.png

Minden egyes ponthoz külön színt is meg 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 képernyő bal oldalán
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();

// Színátmenetes kör, a képernyő jobb oldalán
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();

// Félkörív
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:

http://i.imgur.com/6yfh7q2.png

Eseménykezelés

A grafikus programok általában eseményvezéreltek, azaz a felhasználó által generált események (pl. egérkattintás) irányítják 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özkoordinátákkal dolgozunk (az ablak 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 megoldás:


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 töröljuk a képernyőt.
    // Szólunk, hogy az ablak tartalma megváltozott, 
    // kérjuk a GLUT-ot, hogy hívja meg az onDisplay-t.
    glutPostRedisplay(); 
  } else if(button == GLUT_LEFT_BUTTON) { // Ha a bal gomb állapota megváltozott.
    if(state == GLUT_DOWN) {
      drawing = true; // Ha lenyomtuk akkor rajzolni akarunk.
      Vector pos = convertToNdc(x, y); // Átváltjuk a pontot.
      glBegin(GL_POINTS); { // Kirajzoljuk.
        glVertex2f(pos.x, pos.y);
      } glEnd();
      last_mouse_pos = pos; // Elmentjük, hogy az első szakasz, majd ebből a pontból indul.
      glutPostRedisplay(); // Szolunk, hogy az ablak megváltozott, kérjük az újrarajzolását.
    } else if(state == GLUT_UP) {
      drawing = false; // Ha most engedtük fel, akkor mar nem akarunk rajzolni.
    }
  }
}
 
void onMouseMotion(int x, int y) {
  if(drawing) {
    Vector pos = convertToNdc(x, y); // Kiszámoljuk az egér jelenlegi helyzetet NDC-ben.
    glBegin(GL_LINES); { // Kirajzolunk egy vonalat az előző és a mostani helyzete közé.
      glVertex2f(last_mouse_pos.x, last_mouse_pos.y);
      glVertex2f(pos.x, pos.y);
    } glEnd();
    glutPostRedisplay(); // Szólunk, hogy az ablak megváltozott, kérjük az újrarajzolását.
    last_mouse_pos = pos; // Frissítjük a előző helyzetet.
  }
}


Az eredménye:

http://i.imgur.com/4WDimmL.png

Animáció

Az animáció annyit jelent, hogy az egyes objektumok állapota az időnek is a függvénye. A pillanatnyi időt a glutGet(GLUT_ELAPSED_TIME); függvényhívással tudjuk lekérdezni, célszerűen az onIdle függvényben. Az animáció alatt leggyakrabban testek mozgását, néha testek deformálódását értjük. Egy mozgó testet legjobban a fizika törvé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 bonyolulttá válni, amikor több mozgó test állapota egymástól függ (például ütköznek egymással). Ilyenkor ugyanis a korrekt szimuláció egy differenciálegyenlet megoldását jelentené. Numerikus módszerekkel viszont nem szeretünk diffegyenletet megoldani. Ennek a problémának 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ásra hatásokat. Így a képletek nagyon leegyszerűsödnek, és semmi szükség nem lesz differenciálegyenletekre. Manapság a számítógépes játékok nagy része is ezt a módszert használja.


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 milliszekundumban
  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 eltelt időt kisebb részekre, pl. max 5 milliszekundumos 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:

http://i.imgur.com/ezFQ4l4.png

Koordináta rendszerek

Az korábbi példákban 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 ugyanazt 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 foroghat is. Az OpenGL kitalálóinak az ötlete az volt erre, hogy a kamera mindig maradjon egy helyben, de ha pl. balra akarnánk forgatni, akkor helyette inkább a világ forogjon jobbra, a kamera viszont maradjon ugyanott, ezzel is ugyanazt a hatást érjük el. Első ránézésre nem látszik, hogy ez miért jó, de ez valójában egy nagyon jó ötlet. Szerencsére ha a világot el kell forgatnunk, akkor nem kell minden egyes pontot nekünk külön-külön elforgatni egy ronda trigonometrikus képlet alapján, 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 grafikában általában affin (egyenestartó) transzformációkat szoktunk használni, a vetítéseket leszámítva. Ezeket a 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átrixszal felírni. Pedig erre a műveletre biztos, hogy szükségünk lesz. Ennek kiküszöbölésére használhatunk 4D-be 'w' = 1 koordinátával beágyazott 3D-s koordináta-rendszert, ahol az eltolás is egy lineáris trafó.

Az OpenGL két mátrixot ad nekünk, amiket 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 kamera azt lássa, amit meg akarunk jeleníteni. A GL_PROJECTION pedig azt adja meg, hogy a kamerával hogyan kell fényképezni.

Két dimenzió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. Két dimenzióban 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 pozíció alapján rajzolunk. Vagy legalábbis ezt csináltuk eddig, amikor a GL_PROJECTION egységmátrix volt, de az NDC koordináta-rendszerben nem volt kényelmes dolgozni. Itt viszont lehetőséget kapunk saját koordináta-rendszer 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ünk be a jelenleg módosítható 
// mátrix helyére (ez a projekciós mátrix). 
glLoadIdentity(); 
// Ez egy olyan merőleges (ortogonális) vetítés mátrixát "írja be" a GL_PROJECTION-be, 
// aminek eredményeké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:

http://i.imgur.com/S3m5Lmv.gif


Transzformációk

A GL_MODELVIEW egyik legfontosabb használata, hogy segítségével könnyebben 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ó trigonometrikus 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. Az utóbbi megoldás 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átrixa hozzászorzódik a GL_MODELVIEW mátrixhoz (balról). Emlékeztető: a mátrix szorzás szorzás asszociatív. Ez azt jelenti, hogy két transzformációs mátrix összeszorzása után az eredmény ugyanúgy transzformál egy tetszőleges vektort, mint ha a két mátrixszal 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 hierarchiá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 egyáltalán hol van az az autó, aminek a részeiről eddig beszéltünk.

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áta-rendszerrel történik:

http://i.imgur.com/gUqk4pi.gif

- 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.

Egy másik lehetséges sorrend:


glScalef(2, 2.5f, 1); 
glRotatef(67, 0, 0, 1);
glTranslatef(2.7f, -3.1f, 0.0f);


Ami a koordinátarendszerrel történik:

http://i.imgur.com/XQcsrHs.gif

- 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.43 és 2.084. 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.

De egy probléma még felmerül, 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álni 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 egyenként vissza kell csinálni? Nincs erre jobb megoldás? A válasz természetesen az, hogy van, ez a megoldás a mátrix stack.

Matrix stack

Az OpenGL két függvényt ad amivel a matrix stack-et használhatjuk:

  • glPushMatrix(). Ez a jelenleg aktív mátrixot (GL_PROJECTION vagy GL_MODELVIEW) elmenti annak a stackjébe.
  • glPopMatrix(). Ez a jelenleg aktív mátrix stackjéből a legutóbb elmentett mátrixot visszaállítja. A következő glPopMatrix() mátrix az az előtt elmentett 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.

Például:


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 mátrix stack hierarchikusan felépülő testek rajzolását nagy mértékben megkönnyíti. Hierarchikus 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 is elmozdulnak. Minden ízü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. Ez természetesen egy fa struktúrát jelent, minden ízületnek van egy saját transzformációja, és változó számú gyereke. Ez leprogramozni nem lehetetlen, sőt nem is nehéz, de nem is kifejezetten izgalmas, viszont a mátrix stack segítségével ez nagyon egyszerűen megoldható.

Egy emberi kéz kirajzolása pszeudokóddal, hengerekből, 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:

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(GLdouble size); függvényt fogjuk használni a rajzoláshoz. Ez a függvény - nem meglepő módon - egy 'size' élhosszúságú kockát rajzol ki.

Próbáld ki nyugodtan: glutSolidCube

http://i.imgur.com/PA2A3eQ.png


A 3 dimenzió ennek a kódnak a felhasználásával csak annyiban fog különbözni a 2 dimenziótól, hogy van egy harmadik koordináta is. Az OpenGL kamera koordináta-rendszerében (és az általam definiált világ koordináta-rendszerben is) az X tengely továbbra is jobbra, az Y tengely pedig továbbra is felfele mutat, mint ahogy 2D-ben is, csak bejött egy új tengely, a Z ami felénk (a monitorból kifele) mutat.

Természetesen ez eltér attól a koordináta-rendszertől, amit matematikában szoktunk használni (X felénk, Y jobbra, Z felfele), ami valószínűleg sokakat zavarni fog.

Nekik annyi vigaszt tudok mondani, hogy a GL_MODELVIEW mátrix segítésével tetszőleges koordináta-rendszert definiálhattok magatoknak, ami akár megegyezhet azzal, amit matekórán használni szoktunk.

Én ennek ellenére a példaprogramokban nem fogok így tenni, aminek az egyik oka, hogy a példakódokban nem a konkrét vektorok értéken van a lényeg, hanem az ötleteken, és így legalább kisebb késztetést fogsz érezni, hogy rólam másolj. Másrészt meg az OpenGL-ben ezt a koordináta-rendszert "szokás" használni, ahol az Y mutat felfele. Ez főleg az árnyékoló programok világában fontos, ahol sokkal több koordináta-rendszerrel kell dolgozni, mint itt, és ott rengeteg hibalehetőséggel jár, ha mindegyik koordináta-rendszerben mást jelentenek a tengelyek.

Ezt felhasználva a példaprogram: Robot kar

http://i.imgur.com/tpAuxBa.gif

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 releváns kód, ebből a programból:


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();
}


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éldául 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 érinteni akarunk, kontrollpontnak nevezzük a görbék esetében. 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, ugyanaz a bemenet mellett.

A görbéket szinte mindig valamilyen mozgás leírásá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 paramétere, é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). Például c0 folytonos görbe az, aminek nincs szakadása. Ha egy valós útvonal nem c0 folytonos, akkor azt jelenti, hogy valahol teleportálnunk 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 kontrollpontokat 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. Ezt 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). Ezek lokálisan vezérelhetőek, nagyon kényelmes velük dolgozni, viszont az implementálásuk általában bonyolult. Ebből a görbecsaládból például a Kochanek–Bartels görbe nagyon elterjedt. Viszont sok problémához túl bonyolult implementálni, ezért létezik néhány leegyszerűsített változata is, például 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).

A görbéknek a képletét nem szokás fejből tudni, bár általában könnyen levezethetőek. Az a tapasztalat a korábbi házikból, hogy azoknak a görbéknek a képlete benne szokott lenni az előadásdiákba, amiket a házihoz használni kell. Sőt, amikor több görbét kell használni, akkor általában legalább az egyiknek pszeudokóddal meg is szokták adni az implementációját is.

Példaprogram: <Törölve, túl sokan másolták>

http://i.imgur.com/C1iKaHx.gif

Sugárkövetés

A sugárkövetős házinál a programod teljes mértékben a CPU-n fog futni, ami a videókártya segítsége nélkül nagyon meg fog izzadni, hogy a képet előállítsa neked. Az előző feladatokkal ellentétben itt az optimalizálás nagyon sokat segít, egy release build (-O3) és egy debug build (-O0) között akár több mint ötszörös sebességkülönbség is lehet. Ezért, ha éppen nem debuggolsz, mindenképpen release buildet fordíts.

A Sugárkövetés alapjai

A 2D OpenGL-es példaprogramok között volt egy 3D-s is. Most ezzel a témakörrel fogunk foglalkozni. Technikailag abban a példaprogramban a 3D rajzolást a GLUT csinálta helyettünk. De mielőtt belemennénk a részletekbe, hogy pontosan mit is csinált (ezt majd a 3D OpenGL résznél), vegyük észre, mi is ki tudunk rajzolni egy olyan kockát, mint amit a GLUT csinált, akár az OpenGL segítsége nélkül is.

Először gondoljuk át hogy a valóságban hogyan csinálnánk képet egy kockáról. Szükségünk van egy fényforrásra, enélkül garantáltan nem látnánk semmit, és szükségünk van egy ernyőre is (pl: retina), amin a képet felfoghatjuk. Továbbá nem árt, ha van egy kocka is, amit lefényképezhetünk.

Ha pontosan azt akarnánk lemodellezni, ahogy a valóságban a kép keletkezik, akkor gondba lennénk, mert a számítógép teljesítményéhez képest gyakorlatilag végtelen fotonnal kéne dolgoznunk. És ráadásul a fényforrásból kiinduló fotonok döntő többsége még csak nem is megy az ernyőnek a közelébe se. Ezt kiaknázandó, a sugárkövetés egyik alapötlete, hogy az ernyőből induljuk ki, ne a fényforrásból, és megfordított irányú fotonokat kövessünk, így csak a releváns fény részecskékkel fogunk foglalkozni.

A másik alapötlet, hogy a fotonok olyan sokan vannak, hogy a Nagy Számok Törvénye alapján gyakorlatilag teljesen pontos becsléseket kaphatunk a fotonok viselkedéséről, anélkül, hogy azokkal egyesével foglalkoznunk kellene. Ezt felhasználva nagy mennyiségű fotonból álló csomagok, úgynevezett sugarak útját követjük, és nem fotonokét. Ez talán megmagyarázza, hogy miért hívjuk a technikát sugárkövetésnek. A sugárkövetéshez szükségünk van egy képzeletbeli kamerára, és egy ernyőre (egy téglalapra). A téglalapot felosztjuk annyi egyenlő részre, ahány pixelből áll az ablakunk. Jelen esetben, 600x600-as ablak esetében ez azt jelenti, hogy a téglalap négyzet lesz(két egység élhosszúságú). Ezek után az ablak minden egyes pixelére azt a színt rajzoljuk ki, amit a képzeletbeli kamera látna az ernyőnek a pixelhez tartozó részén keresztül.

Az OpenGL használata nélkül ezt úgy kivitelezhetnénk, hogy képet mint egy színekből álló tömböt eltároljuk magunknak, abba renderelünk, majd valamilyen megfelelő kép formátumába kiírjuk ezt egy fájlba. Ezt a megoldást viszont nem lenne túl kényelmes használni. De az OpenGL-t is megkérhetjük arra, hogy jelenítse meg a képet, amit lerendereltünk a glDrawPixels(GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *pixels) függvény segítségével. A házikban tipikusan az utóbbi megoldást szoktuk használni. Például egy lehetséges megvalósítása:


 
struct Screen {
  static const int width = 600;
  static const int height = 600;
  static Color image[width * height];
  static void Draw() {
    glDrawPixels(width, height, GL_RGB, GL_FLOAT, image);
  }
};


A kamera megvalósítása már egy picit trükkösebb. Az implementálása az alábbi lépésekből áll:

  • Meg kell adnunk a képzeletbeli kamera pozícióját. Kódban pl: pos.
  • Meg kell adnunk, hogy a kamera, merrefelé néz. Kódban pl: fwd (egységvektor).
  • Azt is tudnunk kell, hogy melyik iránynak felel meg a felfele ("What's up?"). Kódban pl nevezzük up-nak.
  • Tegyük fel, hogy téglalap (vagy sík) egységnyi távolságra van a kamerától. Ekkora annak a középpontja: pos + fwd.
  • Tudnunk kell még, hogy melyik irány van jobbra. Ezt az előre és a felfele pozícióból ki tudjuk számolni: right = cross(fwd, up) // a cross-product a vektor szorzat angolul, a dot-product pedig a skalár szorzat.
  • A felfele vektor amit megadtunk nem biztos, hogy merőleges az előre vektorra, pedig nekünk olyanra van szükségünk. Pl: ha rézsútosan előre és lefele nézünk, de az 'up' vektor az ég fele mutat. Ez valójában nem baj, mert a jobbra és előre vektor ismeretében már ki tudjuk számolni a pontos felfele vektort: up = cross(right, fwd).
  • Ha ezek megvannak, akkor ki kell tudnunk számolni, hogy egy (x, y) koordinátájú pixelnek a téglalap (ami most egy 2 egység oldalhosszúságú négyzet) melyik része felel meg. Ezt így tehetjük meg:


 
Vector pos_on_plane = Vector(
  (x - Screen::width/2) / (Screen::width/2),
  (y - Screen::height/2) / (Screen::height/2), 
  0
);


  • Ezt az értéket pedig át kell számolnunk a világ koordináta rendszerébe:


Vector plane_intersection = plane_pos + pos_on_plane.x * right + pos_on_plane.y * up;


Az előadásdiának az ide vágó ábrája talán segíti a megértést (a lookat-nek a plane_pos felel meg nálam):

http://i.imgur.com/J6PfVhq.png

  • És innen már tudunk mindent a sugárról, amit követnünk kell. Ezeket az adatok célszerű egy struktúrába zárni:


struct Ray {
  Vector origin, direction;
};
Ray r = {pos, (plane_intersection - pos).normalize()};


Megjegyzések az algoritmussal kapcsolatban:

  • Azzal, hogy kijelentettük, hogy téglalap egy 2 egység élhosszúságú négyzet, és egységnyi távolságra van a kamerától, implicit kimondtuk, hogy a kamera látószöge 2 * arctg(1/1) = 90 fok. Egy nem túl arányos rajz arról, hogy ez hogy jött ki:

http://i.imgur.com/gC7f6l1.png

De nem biztos, hogy ennyit szeretnénk, úgyhogy a látószög (Field of View - Fov) is legyen inkább paraméter. Én az ernyő méretét fogom a változtatni, és az ernyő-kamera távolságot meghagyom egységnyinek. Az arány amit akarunk az a tan(fov/2).

  • Ha teljesen korrektek akarnánk lenni, akkor fél pixellel el kéne tolni az ernyőt metsző pontokat, hogy azok ne a pixelek bal alsó sarkán keresztül haladjanak át, hanem a közepükön. Bár én szabad szemmel nem látok különbséget ez a változtatás után.

Ezeket a változtatásokat is felhasználva egy lehetséges megvalósítás:


struct Camera {
  Vector pos, plane_pos, right, up;

  Camera(float fov, const Vector& eye, const Vector& target, const Vector& plane_up) 
      : pos(eye), plane_pos(eye + (target-eye).normalize()) 
   { 
      Vector fwd = (plane_pos - pos).normalize();
      float plane_half_size = tan((fov*M_PI/180)/2); 
      
      // A jobbra és felfele vektorokban is lehet tárolni a sík méretét, bár nem szép...
      right = plane_half_size * cross(fwd, plane_up).normalize();
      up = plane_half_size * cross(right, fwd).normalize();
   } 

  void takePicture() {
    for(int x = 0; x < Screen::height; ++x)
      for(int y = 0; y < Screen::width; ++y)
        capturePixel(x, y);
  }

  void capturePixel(float x, float y) {
    Vector pos_on_plane = Vector(
      (x + 0.5f - Screen::width/2) / (Screen::width/2),
      (y + 0.5f - Screen::height/2) / (Screen::height/2), 
      0
    );

    Vector plane_intersection = plane_pos + pos_on_plane.x * right + pos_on_plane.y * up;

    Ray r = {pos, (plane_intersection - pos).normalize()};
    Screen::Pixel(x, y) = scene.shootRay(r);
  }
}


Megjegyzés: én általában nem ilyen kamerát szoktam használni, de ez ugyanazt az eredményt adja, mint amit a 3D OpenGL résznél fogunk kapni, a gluLookAt() függvény segítségével.

Most már mindent tudunk a sugárkövetésről, azt leszámítva, hogy hogyan kell egy sugarat követni.

Hogyan kövessük a sugarakat?

Az ötlet az, hogy keressük meg a kamerához legközelebbi objektumot, aminek van metszéspontja a sugárral. Ha találtunk egy metszéspontot, akkor minket a metszéspontja helye és a felületi normál is érdekel (az a vektor, ami merőleges a felületre abban a pontban). Továbbá valahogy azt is jeleznünk kell, ha nem találtunk metszéspontot. Ezeknek az információknak a tárolására egy lehetséges struktúra:


 
struct Intersection {
  Vector pos, normal;
  bool is_valid;
};


Ahhoz, hogy eldöntsük, hogy egy objektumnak van-e metszéspontja a sugárral, fel kell írnunk annak az alakzatnak az egyetlenét, és meg kell oldaniuk egy 't' ismeretlenre azt az egyenletet, hogy ha a sugár kiindulási pontjából 't' egységet megyünk előre a sugár irányába, akkor ki fogjuk elégíteni az alakzat egyenletét.

  • Az ilyen egyenletek megoldásához hihetetlen sokat tud segíteni, ha egyszerű ábrákat rajzolsz hozzá.
  • Nagyon sok esetben az okoskodás, pl transzformációk használata nagyon le tud egyszerűsíteni egy ilyen problémát.
  • A síkbeli (elsőrendű) alakzatok követésekor első fokú egyenleteket fogunk kapni, míg a "görbülő" (másodrendű) alakzatok, pl kör, ellipszis, henger palást, kúp palást, hiperboloid stb... másodfokú egyenletekre vezetnek.
  • Általában véges objektumokat szoktunk rajzolni (pl. négyzet), így ha a hozzá tartozó alakzat (pl. sík) nem véges, akkor meg kell néznünk, hogy a sugár mely pontokban metszi az alakzatot, és ezekről a pontokról eldönteni, hogy azok a véges részbe is benne vannak-e. Ez utóbbi művelet lehet bonyolultabb mint az előző. Pl. egy háromszög követése lényegesen több ötletet igényel mint egy gömbbé.

Egy háromszög követésére egy lehetséges algoritmus: (Ne feledd, amit innen másolsz, az nem számít bele a saját kontribúcióba.)


 
struct Triangle {
  Vector a, b, c, normal; 

  // Az óra járásával ellentétes (CCW) körüljárási irányt feltételez ez a kód a pontok megadásakor.
  // A köröljárási irányból döntjük el a normálvektor "előjelét".
  Triangle(const Vector& a, const Vector& b, const Vector& c) 
    : a(a), b(b), c(c) {
      Vector ab = b - a;
      Vector ac = c - a;
      normal = cross(ab.normalize(), ac.normalize()).normalize();
  }

  // Ennek a függvénynek a megértéséhez rajzolj magadnak egyszerű ábrákat!
  Intersection intersectRay(Ray r) {
    // Először számoljuk ki, hogy melyen mekkora távot 
    // tesz meg a sugár, míg eléri a háromszög síkját
    // A számoláshoz tudnunk kell hogy ha egy 'v' vektort 
    // skalárisan szorzunk egy egységvektorral, akkor
    // az eredmény a 'v'-nek az egységvektorra vetített 
    // hossza lesz. Ezt felhasználva, ha a sugár kiindulási 
    // pontjából a sík egy pontjába mutató vektort levetítjük 
    // a sík normál vektorára, akkor megkapjuk, hogy milyen 
    // távol van a sugár kiindulási pontja a síktól. Továbbá,
    // ha az a sugár irányát vetítjük a normálvektorra, akkor meg
    // megtudjuk, hogy az milyen gyorsan halad a sík fele.
    // Innen a már csak a t = s / v képletet kell csak használnunk.
    float ray_travel_dist = dot(a - r.origin, normal) / dot(r.direction, normal);

    // Ha a háromszög az ellenkező irányba van, mint 
    // amerre a sugár megy, vagy ha az előző műveletben 
    // nullával osztottunk, akkor nincs metszéspont.
    if(ray_travel_dist < 0 || isnan(ray_travel_dist))
      return Intersection(); 

    // Számoljuk ki, hogy a sugár hol metszi a sugár síkját.
    Vector plane_intersection = r.origin + ray_travel_dist * r.direction;

    /* Most már csak el kell döntenünk, hogy ez a pont a háromszög
       belsejében van-e. Erre két lehetőség van: 
     
       - A háromszög összes élére megnézzük, hogy a pontot a háromszög 
       egy megfelelő pontjával összekötve a kapott szakasz, és a háromszög
       élének a vektoriális szorzata a normál irányába mutat-e.
       Pl:
     
                 a
               / |
              /  |
             /   |
            /  x |  y
           /     |
          b------c

       Nézzük meg az x és y pontra ezt az algoritmust.
       A cross(ab, ax), a cross(bc, bx), és a cross(ca, cx) és kifele mutat a 
       képernyőből, ugyanabba az irányba mint a normál vektor. Ezt amúgy a 
       dot(cross(ab, ax), normal) >= 0 összefüggéssel egyszerű ellenőrizni.
       Az algoritmus alapján az x a háromszög belsejében van.

       Míg az y esetében a cross(ca, cy) befele mutat, a normállal ellenkező irányba,
       tehát a dot(cross(ca, cy), normal) < 0 ami az algoritmus szerint azt jelenti, 
       hogy az y pont a háromszögön kívül van. 
      
       - A másik lehetőség a bary-centrikus koordinátáknak azt a tulajdonságát használja
       ki, hogy azok a háromszög belsejében lévő pontokra kivétel nélkül nem negatívak, 
       míg a háromszögön kívül lévő pontokra legalább egy koordináta negatív.
       Ennek a megoldásnak a használatához ki kell jelölnünk két tetszőleges, de egymásra 
       merőleges vektort a síkon, ezekre le kell vetítenünk a háromszög pontjait, és 
       kérdéses pontot, és az így kapott koordinátákra alkalmaznunk kell egy a Wikipédiáról
       egyszerűen kimásolható képletet: 
       http://en.wikipedia.org/wiki/Barycentric_coordinate_system#Converting_to_barycentric_coordinates
      
       Én az első lehetőséget implementálom. */

    const Vector& x = plane_intersection;

    Vector ab = b - a;
    Vector ax = x - a;

    Vector bc = c - b;
    Vector bx = x - b;

    Vector ca = a - c;
    Vector cx = x - c;

    if(dot(cross(ab, ax), normal) >= 0) 
      if(dot(cross(bc, bx), normal) >= 0) 
        if(dot(cross(ca, cx), normal) >= 0) 
          return Intersection(x, normal, true);

    return Intersection();
  }
};


A legközelebbi metszéspont kiszámolásához a legegyszerűbb (de leglassabb) megoldás, ha végigmegyünk az összes objektumon, és amikkel találtunk metszéspontot, azokra a metszéspontokra kiszámoljuk a kamerától vett távolságot, és ezeknek az értékeknek nézzük a minimumát. Ehhez persze el kell tárolni az összes objektumot egy helyre, hogy végig tudjuk iterálni rajtuk. De az objektumok különböző típusúak is lehetnek, itt sokat segít a heterogén kollekció használata. Az objektumok az én implementációmba azt is eltárolják, hogy milyen anyagból vannak.


 
struct Object {
  Material *mat;
  Object(Material* m) : mat(m) { }
  virtual ~Object() { } // Ne feletkezzünk el a virtuális destruktorról.
  virtual Intersection intersectRay(Ray) = 0;
};


És kell egy struktúra, ami tárolja ezeket, és végig tud menni rajtuk. Az én megoldásom erre:


 
struct Scene {
  static const size_t max_obj_num = 100;
  size_t obj_num;
  Object* objs[max_obj_num];

  // Dinamikus foglalt objektumokat felételezek itt
  void AddObject(Object *o) { 
    objs[obj_num++] = o;
  }
  
  Scene() : obj_num(0) { } 

  ~Scene() {
    for(int i = 0; i != obj_num; ++i) {
      delete objs[i];
    }
  }

  Intersection getClosestIntersection(Ray r) const {
    Intersection closest_intersection;
    float closest_intersection_dist;
    int closest_index = -1;

    for(int i = 0; i < obj_num; ++i) {
      Intersection inter = objs[i]->intersectRay(r);
      if(!inter.is_valid)
        continue;
      float dist = (inter.pos - r.origin).length();
      if(closest_index == -1 || dist < closest_intersection_dist) {
        closest_intersection = inter;
        closest_intersection_dist = dist;
        closest_index = i;
      }
    }
    return closest_intersection;
  }
}


Ha ennél gyorsabb algoritmusra van szükséged, akkor ajánlom egy BSP-fa implementálását, mármint konkrétan egy KD-fát csinálj, ne általános BSP-t. Ez nagyon gyors, és nem nehéz implementálni... csak meg kell írni...

Ami a kódból is látszódik, hogy még nem vagyunk készen, amikor meghatároztuk a legközelebbi metszéspontot, ugyanis nekünk egy színre van szükségünk, amit megjeleníthetünk, nem egy helyvektorra.

Megvilágítás

A hihető, valóságosnak tűnő képek hatásának kb. 90%-át a megvilágítás adja. De ahhoz, hogy ilyeneket tudjuk renderelni előbb bele kell hatolnunk a fényforrások lelki világába, és egy kis fizikára is szükségünk lesz.

Az ambiens fényforrás

A legegyszerűbb fényforrás, amit bevezethetünk, az a környezeti világítás. Ez a valóságban nem létezik, csak egy modell, ami azt hivatott utánozni, hogy nappal a tárgyaknak az a része sem teljesen fekete, amit közvetlenül nem világít meg egy fényforrás se. Ugyanis a tárgyakról a környezetében minden irányba verődik vissza fény, nem csak a szemünk irányába. Ez például egy szobában egy nagyjából konstans, iránytól független háttérvilágítást hoz létre. Ez a modell nagyon sok környezetben nem állja meg a helyét, például nagy nyílt terepen, bár vannak technikák a hibáinak kiküszöbölésére, vagy helyettesítésére (SSAO, Hemisphere lighting, Light probes stb...). Ez kódban csak annyit fog jelenteni a környezeti (ambiens) fényerőt változtatás nélkül hozzáadjuk az objektum színéhez.

Az irány fényforrás

Egy másik fontos fényforrás az irányfényforrás. Ilyen például a Nap. A Nap olyan távol van tőlünk, hogy a Föld felszínén egy adott területen nagyjából teljesen mindegy, hogy hol helyezkedik el egy objektum, a nap mindig ugyanolyan irányból és intenzitással világítja meg. Az irányfényforrás irányának természetesen fontos szerepe van, itt a számolás lényegesen bonyolultabb, mint az ambiens fényforrás esetén. Például egy felülről megvilágított szobában az asztal teteje sokkal világosabb, mint az asztal alja. Ahhoz, hogy ezt a hatást el tudjuk érni, kizárólag egyszerű fizikára van szükségünk. Tegyük fel, hogy egy anyagra két azonos erősségű fénysugár esik, az egyik merőlegesen, a másik theta szögben.

  • Ha a merőlegesen eső sugár átmérője egységnyi, akkor a theta szögben eső sugár esetében az a felület amin ugyanannyi energia eloszlik sokkal nagyobb. Könnyen levezethető, hogy az egységnyi felületre eső energia (azaz a megvilágítás ereje) cos(theta)-val arányos.
  • A beesési szög kiszámításához szükségünk van a felületi normálra. Még jó, hogy korábban gondoltunk erre. A cos(theta) kiszámításának egy egyszerű módja a skaláris szorzat használata. Ugyanis definíció szerint u * v = |u| * |v| * cos(theta). De ha u-t és v-t úgy választjuk meg, hogy egységnyi hosszúak legyenek, akkor a skaláris szorzat a cos(theta)-t adja. Ha a cos(theta) negatív, akkor a test takarásban van, és az irányfény semmit nem befolyásol a színén.

Ezzel a modellel csak olyan anyagok jeleníthetőek meg jól, amikre egy felületi pont, konstans megvilágítás esetén mindig ugyanúgy néz ki, akárhonnan is nézzük. Ilyen anyag például a legtöbb műanyag, vagy mondjuk egy szivacs. Ezek diffúz anyagok, a fényt minden irányba ugyanolyan intenzitással szórják. Ez a modell kiegészítés nélkül nem működik az olyan anyagokra, amiken a fény megtud csillanni, vagy amikben látjuk a tükörképünket, és azokra se amiken átlátunk.

Ennek a modellnek egy lehetséges implementációja:


 
struct Light {
  enum LightType {Ambient, Directional} type;
  Vector pos, dir;
  Color color;
};

struct Material {
  virtual ~Material() { }
  virtual Color getColor(Intersection, const Light[], size_t) = 0;
};

struct DiffuseMaterial : public Material {
  Color own_color;

  DiffuseMaterial(const Color& color) : own_color(color) { }

  Color getColor(Intersection inter, const Light* lgts, size_t lgt_num) {
    Color accum_color;

    for(int i = 0; i < lgt_num; ++i) {
      const Light& light = lgts[i];
      switch(light.type) {
        case Light::Ambient: {
          accum_color += light.color * own_color;
        } break;
        case Light::Directional: {
          float intensity = max(dot(inter.normal, light.dir.normalize()), 0.0f);
          accum_color += intensity * light.color * own_color;
        } break;
      }
    }

    // Negatív vagy egynél nagyobb fényerősségeket nem szabad odaadni az OpenGLnek 
    // rajzolásra. Egyelőre legyen az a megoldás, hogy az invalid részt levágjuk (szaturáljuk).
    return accum_color.saturate(); 
  }
};


Az eddigi elmélet összerakva egy programmá: <Törölve, túl sokan másolták>
Az sugárkövetés eredménye(baloldalt), összehasonlítva azzal, amit az OpenGL tud (glutSolidCube, jobboldalt), hasonló beállítások mellett:

http://i.imgur.com/ZC86LKY.jpg http://i.imgur.com/PA2A3eQ.png

A pontfényforrás

A pontfényforrás a grafikában egy nagyon gyakran használt modell, de a valóságban nem létezik. A valós fényforrásoknak egyáltalán nem elhanyagolható a kiterjedése, nem pontszerűek, viszont a kisebbeket például egy izzót közelíthetünk így, és ezzel sokkal könnyebb lesz vele számolni. A pontfényforrás az irányfényforrástól annyiban különbözik, hogy az intenzitása és az iránya se állandó.

Az intenzitásról annyit tudunk mondani, hogy az bármely, a fényforrást körülvevő zárt felületen állandó (hiszen a fotonok nem vesznek el a semmibe, és nem is születnek a semmiből). Ebből következik, hogy a fényforrás középpontú gömbök felületén is állandó az energia. Viszont erről a felületről tudjuk, hogy a távolság négyzetével arányos (A = 4*r^2*Pi), így az egy pontra jutó energia a távolság négyzetének reciprokával lesz arányos. Az arányossági tényező persze jelenettől függ, általában ez egy szabad paraméter. A fény iránya pedig egyszerűen a fényforrásból az megvilágítandó anyag felületi pontjába mutató egységvektor. A beesési szög figyelembevételéhez itt is használhatjuk az irányfényforrásoknál megismert koszinuszos képlet.

A pontfényforrás esetében fontos, hogy ne csak a színét adjuk meg, hanem az energiáját is, pl. egy húsz wattos zölden világító izzót a Color(0.0f, 20.0f, 0.0f) vektorral jellemezhetünk. Természetesen itt egy egység nem fog pontosan megfelelni egy wattnak, ez függ attól is, hogy a távolságot hogyan választottuk meg.

Egy lehetséges implementáció:


  
case Light::Point: {
  Vector pos_to_light = light.pos - inter.pos;
  float attenuation = pow(1/pos_to_light.length(), 2); 
  float intensity = max(dot(inter.normal, pos_to_light.normalize()), 0.0f);
  accum_color += attenuation * intensity * light.color * own_color;
} break;


A kamera fölül - fejlámpaként - világító pontfényforrás hatása:

http://i.imgur.com/k3hzUir.jpg

A spot lámpa

A spot lámpa a pontfényforrásnak egy változata. Majdnem mindenben ugyanúgy viselkedik, azt leszámítva, hogy csak egy bizonyos térszög alatt fejti ki hatását.

Az ötlet egyszerű, tároljuk a lámpa irányát, és a maximális még megvilágított szög koszinuszát. Azért nem magát a szöget, mert a koszinuszát a skaláris szorzatból nagyon egyszerűen ki tudjuk számolni, míg ahhoz képest az acos() függvény nagyon nagyon drága (és amúgy fölösleges). Tehát nézzük meg, hogy az adott pontot megvilágítja-e a spot lámpa, és ha igen, akkor kezeljük azt utána pont fényforrásként.

Ez egy fall-through switchel nagyon egyszerűen megírható:


  
case Light::Spot: {
  Vector light_to_pos = inter.pos - light.pos;
  if(dot(light_to_pos.normalize(), light.dir) < light.spot_cutoff) {
    break; // Ha nincs megvilágítva, akkor ne csináljuk semmit.
  } // Különben számoljuk pont fényforrással.
} // NINCS break!
case Light::Point: {
...
} break;


A spot lámpánál esetleg azzal is lehet játszani, hogy az adott térszögben a megvilágítás nem egyenletes eloszlású, hanem mondjuk kifele egyre halványodik. Ennek az implementálása plusz egy sor kódot jelentene.

A spot lámpa segítségével sokkal meggyőzőbb fejlámpát lehet csinálni. Az más kérdés, hogy nekem nem sikerült... :D

http://i.imgur.com/kW7kwHl.jpg

Árnyékok

Eddig úgy tűnik, hogy minden erőfeszítésünk ellenére az agyunk a képekről szinte gondolkodás nélkül el tudja dönteni, hogy azok nem valóságosak. Vagy az is lehet, hogy csak ritkán látunk semmi közepén lebegő kék színű diffúz kockákat, amin környezeti világítás is megjelenik, annak ellenére, hogy a kockának semmi sincs a környezetében.

Ezen változtassuk, rakjuk rá a kockát valami talajra, hogy ne lebegjen (így legalább csak a talaj lebeg, nem a kocka), és használjunk valami hihetőbb háttérszínt, mint a teljesen fekete, a megvilágítás szemléltetéséhez.

Például: <Törölve, túl sokan másolták>

http://i.imgur.com/16ohvtS.jpg

Ez máris egy fokkal jobb, de sajnos a valósághűbb környezet választása nem oldotta meg minden problémánkat, hanem inkább újakat vetett fel, például, hogy a kocka nem vet árnyékot a síkra, mint ahogy azt a valóságban tenné.

A sík azon részeinek kéne árnyékban lenni, ahol valami útban van a fényforrásból az adott pontba menő sugárnak. Ezt úgy tudjuk felhasználni, hogy ha a fényforrásból egy sugarat lövünk az adott pont felé, és megnézzük, hogy a legközelebbi tárgy, amit eltalált, az az-e, amit éppen rajzolni akarunk. Ennek egy praktikusabb alternatívája, hogy megnézzük, hogy a metszéspont fényforrástól vett távolsága megegyezik-e az adott pont távolságával a fényforrástól. Mert ha nem, akkor amit rajzolni akarunk az árnyékban van. Egy másik ezzel ekvivalens módszer, hogy a tárgyból lövünk egy sugarat a lámpa felé, és ha nem metsz semmit, vagy amit metsz, az távolabb van mint a fényforrás, csak akkor számolunk megvilágítást.

Technikailag mindkét algoritmust fogjuk használni. A második - bár bonyolultabbnak tűnhet - de irányfényforrásokra csak az módszer működik. Az irányfényforrások ugyanis végtelen távol vannak, ahonnan nem tudok elindulni, viszont a felületi pontból a fény irányába el tudunk. A pontfényforrások esetén viszont az első algoritmus egyszerűbb egy picivel.

Fontos megjegyezni, hogy például irányfényforrások esetén a sugarat nem indíthatjuk pontosan a felületi pontról, hiszen akkor az ahhoz legközelebbi metszéspont maga a felületi pont lenne amiből indítottuk. Ez persze nem mindig teljesülne, a számolási pontosság miatt, ezért néhol árnyékban lesz, néhol nem. Ezt a jelenséget "árnyék pattanás"-nak hívja a szakirodalom (shadow acne). Ez nagyon jellemző mintázatot okoz, amit általában könnyű detektálni. Az alábbi kép mutat egy példát erre:

http://i.imgur.com/TeDR81x.png

A megoldás erre, hogy az indításkor a kezdő pontot eltoljuk egy kis számmal a sugár haladási irányába. Pl. 0.0001f-el. A pontfényforrás esetén is szükség van erre, csak ott a távolság összehasonlításánál.

Egy lehetséges implementáció:


case Light::Directional: {
   // Lőjjünk egy sugarat a fényforrás irányába 
   // (Az irányfény iránya nálam a forrás felé mutat)
   Ray shadow_checker = {inter.pos + 1e-3*light.dir, light.dir}; 
   Intersection shadow_checker_int = scene.getClosestIntersection(shadow_checker);
   if(shadow_checker_int.is_valid) {
     break; // Ha bármivel is ütközik, akkor árnyékban vagyunk
   }

   /* Megvilágítás számolása */ 
}
case Light::Point: {
  // Lőjjünk egy sugarat a fényforrás felől a konkrét pont irányába
  Ray shadow_checker = {light.pos, (inter.pos - light.pos).normalize()};
  Intersection shadow_checker_int = scene.getClosestIntersection(shadow_checker);
  if(shadow_checker_int.is_valid && 
    (shadow_checker_int.pos-light.pos).length() + 1e-3 
    < (inter.pos-light.pos).length()) {
      // Ha bármivel is ütközik, ami közelebb van a  
      // fényhez, mint mi, akkor árnyékban vagyunk.
      break; 
  }

   /* Megvilágítás számolása */ 
}


Példaprogram: <Törölve, túl sokan másolták>


Pont fényforrás esetén:

http://i.imgur.com/3PNCclh.jpg

Irány fényforrás esetén:

http://i.imgur.com/1o1ose4.jpg

Irány és pont fényforrás esetén egyszerre:

http://i.imgur.com/Kkc48I9.jpg

Tonemapping

Eddig a képalkotáskor a színeknek a (0, 1) tartományon kívül eső részét levágtuk. Ez nagyon sok esetben nem túl valósághű eredményhez vezet. Például vegyünk két 20 W-os izzót és világítsunk meg vele egy kockát.

Az eddigi módszereinkkel ez így néz ki:

http://i.imgur.com/ZRgJgHx.png

A kép baloldala jól néz ki, van két egészen hihető árnyékunk. Maga a kocka is egész jó. Viszont a képnek szinte a teljes jobb oldala kiégett, és minden ugyanolyan fehér, ordít róla, hogy mű. És ezek csak 2 db 20 W-os izzó...

Vegyünk két 50 W-os izzót:

http://i.imgur.com/z4fAqu0.png

A kocka még mindig jól néz ki. Ellenben itt már árnyékok is kiégtek, és az árnyékok nagy része ugyanolyan fényes, mint az, ami közvetlenül meg van világítva. És ezek még mindig teljesen hétköznapi értékek, két db 50 W-os izzó... Ha ez nem megy, akkor a Nappal mit kezdjünk?

Egy megoldás lehetne, hogy mindent sokkal sötétebbre veszünk, úgy, hogy a Nap fénye legyen a teljesen fehér. Annál világosabbal nem nagyon szoktunk dolgozni. Igen ám, de az emberi szem egy sötét szobában se teljesen vak, ha hihető képet akarunk, akkor egy olyan algoritmus kéne, ami ilyen körülmények között is élvezhető képet ad.

A megoldás az, hogy ne lineáris színskálát használjuk. Ez azt jelenti, hogy pl. kétszer akkora fényerősségű képponthoz ne kétszer olyan fényes pixelt rajzoljunk ki, hanem csak egy picivel világosabbat. Például használjuk logaritmikus skálát, ahol ha egy fényerősség értékét négyzetre emeljük, akkor kétszer olyan fényes pixelt rajzolunk hozzá. Ez persze csak 1-nél nagyobb fényerősségekre működik, és ez mindig véges tartományt eredményez. Olyan függvény kéne, ami a (0, végtelen) tartományt (ún. High Dynamic Range - HDR) a (0, 1) tartományra (Low Dynamic Range - LDR) képzli le, de úgy, hogy még a (0, 1) tartománybeli értékekből is élvezhető képet állítson elő. A HDR színből LDR szín előállítását tonemappingnek hívjuk.

Reinhard operátor

Egy lehetséges megoldás a Reinhard operátor. Előadáson ez szokott szerepelni.

Először is kell számolnunk az adott fénynek a luminanciáját (jele: Y).

  • Ez azt adja meg, hogy az adott fény az emberi szem számára mennyire látszódik fényesnek.
  • A látható spektrum szélén lévő színek (pl a kék és a piros) adott fényerősség mellet nem látszanak nagyon fényesnek. Viszont a spektrum közepén lévő színek, főleg a zöld, ugyan akkora fényerősség mellett, mint a többi szín, sokkal világosabbnak látszik.

A kísérletileg meghatározott képlet, ami az érzékelt fényerősséget meghatározza: Y = 0.2126f*r + 0.7152f*g + 0.0722f*b

Az új luminancia értéke: Y' = Y / (Y + 1);

Az új szín pedig: color' = color * Y' / Y;

Az új szín luminanciája garantáltan a (0, 1) tartományba esik, de szín komponenseire ez nem feltétlen igaz. Pl: Color(0, 0, 10) -> Color(0, 0, 4.1928). Ebből következik, hogy az eredményt továbbra is szaturálni kell. Ez igazándiból nem zavaró. Nagyon ritkán okoz különbséget, és akkor se feltűnő.

Az eredmény viszont lényegesen sötétebb lesz mint az eredeti kép.

Megjegyzés: a tonemap-et a háttérszínen én inkább nem végzem el. Az indok emögött az, hogy a háttérszín kiválasztásakor általában abban a színben gondolkodunk, amit a képernyőn látni akarunk, nem egy olyanban, amit valamilyen műveletvégzés után, módosítva akarunk látni. Vagy például rákeresünk arra, hogy sky blue, és az első google találat által mutatott RGB értéket átváltjuk floatokká, és azt használjuk. De ha erre tonemapet alkalmaznánk, akkor egy másik színt kapnánk. Háttérszínnek amúgy is garantáltan LDR színt választunk, amit nem kell tonemap-elni.

Például 2 db 50 W-os izzó esetén:

Bal oldalt tone map nélkül, Jobb oldalt Reinhard tonemappel.

http://i.imgur.com/z4fAqu0.png http://i.imgur.com/P4cANQa.png


Nagyon fényes képek élvezhetőségén a Reinhard operator nagyon sokat javít. Viszont az alapból sötét képeken csak tovább ront.

Például két db 5 W-os izzó esetén:

Bal oldalt tonemap nélkül, Jobb oldalt Reinhard tonemap-pel.

http://i.imgur.com/2Fp5XFB.png http://i.imgur.com/rSwo1ot.png


A Reinhard operátor a semminél jobb, de nem az igazi.

Filmic tonemap operátor

Az egyik legszebb tonemap, az az a leképezés, amit Kodak filmek használnak. Ki gondolta volna, hogy Kodak ért ahhoz, hogy hogyan kell szép képet csinálni... Az algoritmus, ami ezt leutánozza a "filmic tonemap" nevet kapta, az eredetére utalva.

A pontosan algoritmus lassú, és bonyolult, egy logaritmus képzésen alapul, de 3 texture lookup is van benne. De Jim Hejl (az EA games egyik fejlesztője) talált hozzá egy nagyon egyszerű, de meglepően pontos közelítést.


  x = max(0, InputLuminance-0.004);
  OutputLuminance = (x*(6.2*x+0.5))/(x*(6.2*x+1.7)+0.06);


Először is, WTF???? Nyugi, én se ismerek ismerek olyan embert, aki értené, hogy ez miért működik. Ennek a "random képletnek" egy apró szépséghibája, hogy sRGB színtérbe állítja elő a színt, viszont nekünk sima RBG színt kell adnunk az OpenGL-nek, ezért az eredményt komponensenként 2.2-edik hatványra kell emelnünk, hogy használni tudjuk. De legalább az eredményt szaturáni már nem kell.


A tonemap nélküli kép bal oldalt, Reinhard tonemap középen, míg a Filmic tonemap jobb oldalt látható.

http://i.imgur.com/pdC31jE.png http://i.imgur.com/1dhh2lu.png http://i.imgur.com/EX4hRKs.png
http://i.imgur.com/rEABMJg.png http://i.imgur.com/o1dmQYz.png http://i.imgur.com/QBUda13.png
http://i.imgur.com/PDOptWq.png http://i.imgur.com/4I6eUH5.png http://i.imgur.com/qdFGAU1.png
http://i.imgur.com/Zy5ESZh.png http://i.imgur.com/vhRyuqI.png http://i.imgur.com/GKOhIq8.png
http://i.imgur.com/rRGFCE9.png http://i.imgur.com/fQeP0vk.png http://i.imgur.com/mR3Vmn1.png


Az előnyei:

  • A filmic operátor a túl sötét képeket világosabbá teszi.
  • A túl világos képeken továbbra is érződik, hogy ott világos van, nem úgy, mint a Reinhard operátor esetében.
  • Szinte az összes képet élvezhetőbbé teszi.
  • Csak egyszerű ALU műveleteket használ.

Spekulráis anyagok

Az anyagok amikkel eddig dolgoztunk teljesen diffúzak voltak. A teljesen diffúz anyag egy pontja, konstans megvilágítás mellett mindig ugyanúgy néz ki, akárhonnan, akármilyen szögből is nézzük. A valós anyagok viszont nem mind így viselkednek.

Most azzal fogunk foglalkozni, hogy bizonyos anyagokon egy lámpa fénye meg tud csillanni, ha megfelelő irányból nézzük. Ilyen pl. egy lakkozott fa felület. Fontos megjegyezni, hogy ez nem úgy viselkedik mint egy tükör, nem látjuk rajta a tükörképünket, csak egy megcsillanó foltot.

A viselkedésének leíráshoz meg kell értenünk, hogy ez a hatás hogy jön létre. A simának tűnő anyag felülete is valójában rücskös mikroszkóppal nézve. De a legtöbb anyagra igaz, hogy egy mérettartomány alatt már simának tekinthető mikro-lapokból (micro facet) épül fel. Ezek a kis lapok teljesen tükrözőnek tekinthetőek, és az anyagról a szemünkbe érkező fény, valójában a megfelelő irányba álló mikro-lapokról visszaverődő fényt jelenti.

http://i.imgur.com/knD4RV4.jpg

A mikro-lapok iránya normál-eloszlást követ. A várható értékük - nyilván - a felületi normál. A szórásuk viszont az anyag jellemzője, ez legyen egy paraméter. Ha a szórás nagyon nagy, akkor a lapok elhelyezkedése szinte teljesen véletlenszerű, és így minden irányba ugyanannyira tükrözőek, vagyis egy teljesen diffúz anyagot alkotnak. De ha a szórás kicsi... Akkor a lapok nagyobb része fog a normál irányába elhelyezkedni, így ha olyan szögből nézzük az objektumot, hogy ahhoz a visszaverődéshez tartozó normál, amikor a fény a szemünkbe jut, a felületi normál közelébe van, akkor abból az irányból nézve sokkal több fény a szemünkbe fog tükröződni, mint a többi irányból, ilyenkor érezzük azt, hogy az anyag megcsillan.

De mit jelent az, hogy a "közelébe"? Mennyire a közelébe? És a normál közelébe mennyivel gyengébb ez a hatás, mint pont a normálban. A lapok irányát egy harang görbe jellemzi. Hogy megtudjuk, hogy abba az irányba a lapok hányadrésze néz, ami a visszatükröződésnek kedvez, be kell helyettesítenünk a normál eloszlás sűrűségfüggvényébe. A képlet nem bonyolult, de van benne egy exponenciális függvény, aminek kiszámítása lassú. Viszont skaláris szorzattal egy koszinuszt ki tudunk számolni, ami egy picit hasonlít a haranggörbére. Ha a koszinusznak vesszük egy polinomját, pl az a*cos(x)^b, azzal nagyon jól közelíteni lehet a haranggörbét.

A leggyakoribb megoldás, a Blinn-Phong modell, a visszaverődési normál (a fényből a felületi pontba menő, és az abból a szemünk felé mutató egységvektorok átlaga) és a tényleges felületi normál által bezárt szög koszinuszát használja. A visszaverődési normál, vagy más nevén a félszög-vektor jele legyen 'H' (mint Half-angle), a normál pedig 'N'.

A koszinusz, amit használni akarunk a dot(H, N) képletből áll elő. A negatív érték nekünk nem jó, és ennek még kell vennünk egy polinómját, így végül a specular_power = pow(max(dot(H, N), 0), shininess) képletet kapjuk.

A spekuláris anyag egyben diffúz is. A spekuáris megcsillanás (specular_power * specular_color) hozzáadódik a diffúz megvilágításból származó színhez.

Például irányfényforrásokra én így implementáltam:


 
Vector L = light.dir.normalize(), V = (camera.pos-inter.pos).normalize();
Vector H = (L + V).normalize(), N = inter.normal;
float specular_power = pow(max(dot(H, N), 0.0f), shininess);
accum_color += specular_power * light.color * specular_color;


Itt se feledkezzünk el az árnyékokról. Szerencsére az árnyékszámítás itt is teljesen ugyanaz, ezért célszerű a láthatóságot egy külön függvényben eldönteni.

A 'shininess' meghatározása teljesen mértékben hasra-ütésre, próbálgatással szokott menni. Én személy szerint a kettőhatvány shinnines értékekkel szoktam először próbálkozni (8 - 16 - 32 - 64 a leggyakoribb nálam), és utána esetleg "finom hangolom" az értéket, az alapján, hogy melyik éréték néz ki jól.

A spekuláris megcsillanások nagyon sokat tudnak dobni egy kép hihetőségén.

Például: <Törölve, túl sokan másolták>

http://i.imgur.com/uCHgf9g.jpg

A tökéletes tükör

A mikroszkopikus tükrök figyelembevételével a fényforrások fénye tükröződni tud. De mi van a valódi tükrökkel? Amikben más objektumok képeit is látjuk, nem csak a fényforrások hatását? Sugárkövetéssel ilyen tükröket renderelni meglepően egyszerű. Az egyetlen dolog amit a tükör tulajdonságú anyag csinál a modellünkbe az az, hogy a tükröződés irányába továbblövi a sugarat.

Először is ki kell tudnunk számolni, hogy 'N' normálvektorral rendelkező tükör merre ver vissza egy beérkező 'I' sugarat. Azt tudjuk, hogy a három vektor egy síkban van, és a beesési szög megegyezik a visszaverődési szöggel. De mégse így fogunk számolni, egyszerű vektor műveletekkel is ki lehet fejezni.

http://i.imgur.com/RsTWJVC.jpg

Próbáld a visszavert sugarat csak a négy alapművelet és a skaláris szorzás segítségével kifejezni.

A legegyszerűbb megoldás:


inline Vector reflect(Vector I, Vector N) {
  return I - (2.0 * dot(N, I)) * N;
}


Ezt felhasználva a tükröződést implementálni már egyszerű, pl.:


 
Color getColor(Intersection inter, const Light* lgts, size_t lgt_num) {
  Ray reflected_ray;
  reflected_ray.direction = reflect(inter.ray.direction, inter.normal);
  reflected_ray.origin = inter.pos + 1e-3*reflected_ray.direction;
  return scene.shootRay(reflected_ray);
}


Az egyetlen kényelmetlenséget az okozhatja, hogy ha az eddig használt adatstruktúránk nem tárolta, hogy milyen irányból érkezett a sugár, mert az nyilván kell ahhoz, hogy tudjuk, hogy melyik irányba verődik vissza.

Nagyon fontos, hogy innentől a spekuláris megcsillanás számolásakor a nézeti vektor nem a kamerából az adott pontba mutat, hanem a sugár haladási irányának a mínusz egyszerese. Ha azt elfelejted átírni a kocka visszatükröződései olyan helyeket fognak megcsillanni, ahol nem kellene.

Például ha a padló anyagát lecserélem egy tükörre, akkor az eredmény így néz ki:

http://i.imgur.com/GLBoT7h.jpg

Ezzel akad egy érdekes probléma, ami nekem egy héten át fel se tűnt. A költői kérdés a következő: Ha tökéletes a tükrünk, és pont ugyanazt látjuk benne, mint ami felette is van, akkor miért különöl el ilyen egyértelműen a háttérszíntől? A válasz persze az, hogy 5 példaprogrammal ezelőtt hoztam egy olyan döntést, hogy a tonemap-et a háttérszínre nem alkalmazom. Ez akkor még jó ötletnek tűnt, de a tükröző anyagok bevezetésével teljesen fals eredményt okoz. Ha a tonemapet a teljes képernyőre alkalmazom, akkor ezt az eredményt kapom:

http://i.imgur.com/M09ZnHA.jpg

Ez már valóban egy tükörnek néz ki, de két apró probléma még akad vele... Az egyik, hogy lámpa fénye nem csillan meg rajta. Erre korrekt megoldást majd csak később fogunk tudni adni. A másik probléma, az az, hogy mi történik ezzel a modellel, ha két tükröt rakunk egymással szembe? A sugár a végtelenségig fog pattogni a kettő között? Nem egészen. Ugyanis ez egy rekurzív algoritmus, ahol a függvényhívásoknak a stackbe is lesz nyoma, ahol viszont a hely előbb utóbb elfogy, és ilyenkor a programunk megáll.

A sugárkövető függvényünkbe követnünk kell, hogy ez hanyadik függvényhívás volt, és ha ez a szám, meghalad valamilyen értéket, pl. 8-at, akkor a sugarat már ne lőjük tovább.


Color shootRay(Ray r, int recursion_level = 0) const {
  if(recursion_level >= 8) {
    return env_color;
  }
  //...
} 
Color getColor(Intersection inter, const Light* lgts, size_t lgt_num, int recursion_level) {
  // ...
  return scene.shootRay(reflected_ray, recursion_level + 1);
}


Példaprogram: <Törölve, túl sokan másolták>

http://i.imgur.com/jwtHLsb.jpg

A valós tükröző anyagok

A valós tükröző anyagok, nem csak a tükrök, de pl. a fényesre csiszolt fémek is, nem viselkednek tökéletes tükörként. A különbség az, hogy ezek az anyagok nem a fény 100%-át verik vissza, hanem egy részét elnyelik (az nagyrészt hővé alakul). Az elnyelt fény mértéke a hullámhossztól is függhet, ezért pl. egy sima aranyfelület elszínezi a tükörképet. Egy fürdőszobai tükör persze minden hullámhosszon nagyjából ugyanannyi fényt nyel el.

A képlet amire szükségünk lenne, az egy adott hullámhosszon a törésmutató, és a kioltási tényező függvényében megmondaná, hogy a fény hanyadrésze verődik vissza.

Az, hogy bemenet és a kimenet is hullámhossz függő elég nagy problémákat okozhat. Egy lehetséges egyszerűsítés, hogy mi csak három kitüntetett színre (a pirosra a zöldre és a kékre) számoljuk ki a képlet eredményét, és ezt ezzel megszorozzuk az RGB színskálán leírt színünket.

A képlet, ami ezt a jelenséget leírja közvetlenül a Maxwell-egyenletekből levezethető, bár az eredmény, a Fresnel-egyenletek jóval bonyolultabb, mint amit mi használni szeretnénk. Én csak a képletnek egy közelítését írom itt le, ami eltekint a polarizációtól, mert a grafikában általában ezt szokták használni. (A Schlick's approximation kiegészített alakját alakját fogom használni, lásd pl. GPU Gems 3)

  • n - törésmutató (RGB vektor)
  • k - kioltási tényező (RGB vektor)
  • F0 - egy az anyagra jellemző konstans.
    • F0 = ((n-1)*(n-1) + k*k) / ((n+1)*(n+1) + k*k);
  • theta - beesési szög
  • F(theta) - a visszaverődő relatív intenzitást adja meg, komponensenként.
    • F(theta) = F0 + (1-F0) * pow(1-cos(theta), 5)

Például:


struct ReflectiveMaterial : public Material {
  const Color F0;
 
  ReflectiveMaterial(Color n, Color k)
    : F0(((n-1)*(n-1) + k*k) /
         ((n+1)*(n+1) + k*k))
  { }
 
  Color F(float cosTheta) {
    return F0 + (1-F0) * pow(1-cosTheta, 5);
  }
 
  Color getColor(Intersection inter, const Light* lgts, size_t lgt_num, int recursion_level) {
    Ray reflected_ray;
    reflected_ray.direction = reflect(inter.ray.direction, inter.normal);
    reflected_ray.origin = inter.pos + 1e-3*reflected_ray.direction;
    return F(dot(-inter.ray.direction, inter.normal)) 
           * scene.shootRay(reflected_ray, recursion_level+1);
  }
};


Az 'n' és 'k' paraméterek az anyagra jellemzőek, általában a házi kiírásban meg vannak adva. Például ezüst esetén n = (0.14, 0.16, 0.13), k = (4.1, 2.3, 3.1)

Példaprogram: <Törölve, túl sokan másolták>

http://i.imgur.com/vJKsR53.jpg

Ez egyelőre rosszabbnak tűnik mint volt, de ennek az az oka, hogy a jelenet nagyon üres, az ezüstön kívül egy darab kocka van benne összesen. Összetettebb jelenetekben a fémek általában nagyon jól néznek ki.

Valós spekuláris anyagok

A valós tükröző anyagok részben láttuk, hogy az anyagok a különböző beesési szöggel érkező sugaraknak különböző hányadát verik vissza. A Blinn-Phong spekuláris modellünk viszont az anyagoknak ezt a tulajdonságát teljesen figyelmen kívül hagyja.

Például vegyünk két esetet, az egyikben a beesési szög sokkal nagyobb, mint a másikban.

http://i.imgur.com/SfioK3h.png http://i.imgur.com/Hv6jrKf.png


A Blinn-Phong szerint mindkét esetben a fény a maximális lehetséges mértékben verődik vissza. A Fresnel összefüggés szerint viszont minél kisebb a beesési szög (minél közelebb vagyunk a merőlegeshez), annál több fény nyelődik el.

Oké, van két ellentmondó modellünk, de mi alapján mondjuk azt a Fresnel összefüggésre, hogy az jobb? Egyáltalán melyik áll közelebb a valósághoz. Az eldöntéséhez nézzünk néhány hétköznapi anyagot. A képeket a Filmic Games weboldaláról szedtem, van ott még jó pár érdekes cikk az érdeklődőknek. A baloldali képek természtes-, míg a jobboldaliak mesterséges megvilágítással készültek.

http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_03.jpg

http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_20.jpg

http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_01.jpg

http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_23.jpg

http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_08.jpg

http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_16.jpg

http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_10.jpg

http://filmicgames.com/Images/PolarizedDiffSpec/Fresnel/shot_12.jpg

Mondanom se kell, hogy a valóság nagyon egyértelműen a Fresnel összefüggést igazolja. Szerencsére a spekulráis modellünkön csak minimális módosítást kell eszközölnünk, hogy ezt a hatást megkapjuk.

A Fresnelhez használandó beesési szög, az jelen esetben a félszög vektor és a nézeti vektor között értendő. Azért ez a két vektor kell nekünk, mert a visszaverődésben résztvevő mikro-tükröknek a félszög vektor a felületi normálja, és a nézeti irányba verődnek vissza. Tehát egyszerűen a spekuláris megcsillanás színét meg kell szoroznunk a F(max(dot(V, H), 0.0f)) vektorral.

Példaprogram: <Törölve, túl sokan másolták>

Ha az F0-ra az üvegre jellemző 0.04f-re választjuk meg, az ilyen hatást eredményez:

(Megjegyzés: a kocka spekuláris színét a két esetben úgy választottam meg, hogy nagyjából azonos legyen a spekuláris megcsillanás fényessége az első képen, de nyilván a Fresnel tag miatt ugyanaz a spekuláris szín mellet az sokkal sötétebb volt)

Fresnel (bal oldalt) összehasonlítása a Blinn-Phong-gal (jobb oldalt).

http://i.imgur.com/ps4hF5X.jpg http://i.imgur.com/cii4bVs.jpg
http://i.imgur.com/detsaXr.jpg http://i.imgur.com/wbthpam.jpg
http://i.imgur.com/NELw5EV.jpg http://i.imgur.com/xd1Sr99.jpg
http://i.imgur.com/6bOHiXF.jpg http://i.imgur.com/i4qxqJE.jpg

A spekuláris tükör

A spekulráis anyagokról alkotott modellünkbe azt használtuk ki, hogy ezek az anyagok tükrökből állnak, és ezeknek a tükröknek egy részéből a fényforrások fénye a szemünkbe verődik vissza.

A spekulráis anyagokkal ellentétben a tükröző anyagokról alkotott modellünk teljesen figyelmen kívül hagyta az elsődleges fényforrásokat, színüket csak a környezetben lévő objektumokról visszaverődő fény befolyásolta. Pedig a valóságban nem az a tapasztalat, hogy ha egy tükrön keresztül nézünk a Napba, akkor nem látjuk azt.

Az elsődleges fényforrások visszaverődésével a legfőbb problémánk az, hogy például egy pontfényforrás esetében a fényforrás tükörképe továbbra is pontszerű, ami olyan kicsi, hogy azt nem látjuk. Az irányfényforrásokkal is ugyanez az eset, csak egy végtelenül kicsi térszög alól látszódnak.

Technikailag a modellnek két részlete okozza ezt az anomáliát: ideális tükröt és ideális fényforrásokat feltételezünk egyszerre. A kettő egyszerre nem az igazi, ezért az egyikről le kell mondanunk. Az ideális tükörről sokkal könnyebb lemondani, ezért én azt választom.

A nem ideális tükrök esetében a mikro-tükrök irányának szórása nem nulla... csak majdnem nulla. Tekintsük úgy, hogy a szórás elég kicsi, hogy a tükör diffúz színe elhanyagolható, de a spekuláris megcsillanás viszont legyen látható.

A spekuláris megcsillanás számolásánál a természetesen a Fresneles kiegészített alakra van szükségünk. A shininess legyen egy nagyon nagy szám, legalább 1000.

Egy kis trükkre azonban szükségünk van. a pontfényforrások távolságfüggéséhez ugyanis követnünk kell, hogy a sugár összesen mennyi utat tett meg eddig.

Példaprogram: <Törölve, túl sokan másolták>

A spekuláris tükör fizikailag nem túl korrekt, de jól néz ki. Például ilyen hatást lehet vele elérni:

http://i.imgur.com/6WR6EXf.jpg

A látható fényforrások

Az előző példában láttuk, hogy hihetőbb képet kapunk, ha egy tükrön keresztül látjuk a fényforrásokat is. De mi van azzal az esettel, ha közvetlenül nézzük azokat? Tükörből látszanak, de direktbe nem?

A spekuláris tükör analógiájára itt használhatunk egy olyan modellt, hogy minden fényforrás, ami nincsen takarásban a képernyő egy bizonyos térszögét világosabbá teszi. Kb 10 sor, és semmi új ötlet nem kell hozzá, de legalább nem azt látjuk, hogy a fény a semmiből jön.

http://i.imgur.com/rL6DMNy.jpg

A pontfényforrásokra persze ez nem néz ki olyan jól, főleg azért, mert a képen nem látunk mélységet.

De irányfényforrásokra sokkal hihetőbb hatást ér el az ötlet.

Például: <Törölve, túl sokan másolták>

http://i.imgur.com/BxeEztT.jpg

Nem mondom, hogy a Nap a valóságban így néz ki, de ez nagyságrendekkel hihetőbb, mint egy láthatatlan Nap.

A fényt megtörő anyagok

A Fresnel egyenlet eddigi felhasználásakor azt feltételeztük, hogy a fénynek az a része, ami nem verődik vissza, az megpróbál továbbmenni, de pl. egy fém belsejében ezt nem tudja megtenni, ezért elnyelődik, energiává alakul. De nem minden anyag viselkedik így.

Például az üveg esetében a fény, ha nem verődik vissza, akkor továbbmegy az üvegben, de egy picit el is térül. Az irányának a megváltozását a Snelius-Descart törvény írja le sin(Alpha1) / sin(Alpha2) = n1 / n2 . Az irány kiszámoláshoz jobb lenne egy képlet, ami egyszerű vektorműveleteket használ. Továbbá a kiszámolásnál figyelnünk kell a teljes visszaverődés esetére is. Ilyenkor gyakorlatilag a fény 100%-ka visszaverődik, még az a rész is, ami a Fresnel egyenlet szerint továbbmenne. És persze a teljes visszaverődés esetén a spekuláris megcsillanás számolásakor se szabad megszorozni az eredményt a Fresneles taggal.

Én az irány kiszámolásához az alábbi képletet fogom használni. A képlet másolása helyett inkább próbáld meg levezetni magadnak.


inline Vector refract(Vector I, Vector N, double n) {
  double k = 1.0 - n * n * (1.0 - dot(N, I) * dot(N, I));
  if (k < 0.0) {
    return Vector();
  } else {
    return n * I - (n * dot(N, I) + sqrt(k)) * N;
  }
}


Fontos megjegyezni, hogy ebben a képletben az 'n' a relatív törésmutató. Pl. ha a sugár levegőből üvegbe megy, akkor - mivel a levegő törésmutatója 1-nek tekinthető - így a relatív törésmutató az üveg törésmutatója: 1.5 / 1 = 1.5. Viszont amikor a sugár az üvegből távozik, akkor a relatív törésmutató 1 / 1.5 = 0.666. Gyakori hiba ennek a reciprok képzésnek a lehagyása.

Egy másik hibalehetőség ezzel a függvénnyel kapcsolatban, hogy ha normálvektor ellentettjét használjuk, akkor rossz eredményt ad. Erre fontos figyelni, pl. amikor a kocka belsejéből kifele jön a sugár, hiszen ilyenkor a befele mutató normállal kell számolni, nem a kifele mutatóval. Ugyanígy a Fresnel egyenlet is rossz eredményre vezet, ha a normál ellentettjével számolunk, ezért célszerű a számolások legelején megfordítani a normált, ha arra szükség van:


if(dot(inter.ray.direction, inter.normal) > 0) {
  inter.normal = -inter.normal;
}


Fontos még, hogy a Fresnel egyenletben kifele menet is ugyanazt a törésmutatót kell használni. Az F0 kiszámolásakor az egyesek a levegő törésmutatója helyén állnak, általános esetben a képlet a ((n1 - n2)^2 + k*k) / ((n1 + n2)^2 + k*k) alakot veszi fel. Ez a képlet viszont n1 és n2 szempontjából szimmetrikus, így nem kell két F0-t számolni, egyet a befele, egyet meg a kifele menő sugarakhoz.

Csak olyan törő anyagokkal foglalkozunk, amiknek a törésmutatója a hullámhossztól független. A nem így viselkedő anyagok, a prizmák nagyon látványos képeket tudnak eredményezni, de sokkal számításigényesebbek és bonyolultabbak, ezért most nem fogok velük foglalkozni.

Az én implementációm leegyszerűsítve:


struct RefractiveMaterial : public ReflectiveMaterial {
  float n, n_rec;
 
  RefractiveMaterial(float n, Color k, Color specular_color, float shininess)
    : ReflectiveMaterial(n, k, specular_color, shininess), n(n), n_rec(1 / n)
  { }
 
  Color getColor(Intersection inter, const Light* lgts, size_t lgt_num, int recursion_level) {
    if(dot(inter.ray.direction, inter.normal) > 0) {
      inter.normal = -inter.normal;
    }

    Ray reflected;
    reflected.direction = reflect(inter.ray.direction, inter.normal);
    reflected.origin = inter.pos + 1e-3*reflected.direction;
 
    Color reflectedColor, refractedColor;
 
    Ray refracted;
    refracted.direction = refract(inter.ray.direction, inter.normal, inter.ray.in_air ? n : n_rec);
    if(!refracted.direction.isNull()) {
      refracted.origin = inter.pos + 1e-3 * refracted.direction;
      refracted.in_air = !inter.ray.in_air;
 
      Color F_vec =  F(dot(-inter.ray.direction, inter.normal));
      reflectedColor = F_vec * scene.shootRay(reflected, recursion_level+1) 
                       + getSpecularHighlight(inter, lgts, lgt_num, reflected.travelled_dist, false);
      refractedColor = (1 - F_vec) * scene.shootRay(refracted, recursion_level+1);
    } else {
      reflectedColor = scene.shootRay(reflected, recursion_level+1) 
                       + getSpecularHighlight(inter, lgts, lgt_num, reflected.travelled_dist, true);
    }
 
    return reflectedColor + refractedColor;
  }
};


Példaprogram: <Törölve, túl sokan másolták>

http://i.imgur.com/ULyutJC.jpg http://i.imgur.com/bO4qAbx.jpg

A kocka egészen hihetően néz ki, a bal oldali képen, meredek szögből nézve, nagyrészt tükörként viselkedik, a jobb oldali képen pedig nagyrészt átlátszó. Viszont a talaj megvilágítása teljesen rossz. Az árnyékszámító algoritmus azt feltételezte, hogy fény a nem megy át a - jelenleg átlátszó - kockán. De ha az árnyékokat elhagynánk, akkor is teljesen rossz képet kapnánk. Az üveg kocka megtöri a fényt, de néhol tükröz is, esetleg sok fénysugarat ugyanabba a pontba fókuszál... ezeknek a jelenségeknek a hatását az eddigi megvilágítási modellünk egyáltalán nem vette figyelembe.

A klasszikus megvilágítási modell (ahol az anyagok színe az ambiens, diffúz és spekuláris tagok összege) azt feltételezte, hogy a fény, a fényforrásból a jelenet bármely pontjába csak egyenes úton juthat el. Ennek az a nagy előnye, hogy egy felületi pont színéhez nem kell tudnunk a többi pont színéről semmit. Az ezzel a tulajdonsággal rendelkező világításszámoló algoritmusokat lokális illuminációnak nevezzük. Ha a jelenetben van tükröző vagy törő anyag akkor ez értelemszerűen nem működik. Az ilyen jelenteknél másképp kell megvilágítást számolnunk. Ilyenkor globális illuminációra van szükségünk.

A globális illumináció

A lokális illuminációt azért szerettük, mert nagyon lecsökkenti a probléma számításigényét. A ténylegesen jelen lévő fotonok számától függetlenül, pixelenként egyetlen sugár követésével megtudtuk oldani a feladatot. Azonban a törő vagy tükröző anyagok ezt az algoritmust reménytelenül elbonyolítják. Ilyenkor a valóság utánzása nyers erővel a leginkább járható út.

Az ötlet, hogy a fényforrásból lőjünk nagyon sok fotont random irányba, és nézzük meg, hogy miután a törő / tükröző anyagok továbblőtték a fotonokat, hova csapódnak be.

A diffúz anyagokat osszuk fel részekre, és egy textúrába tároljuk el az egyes részeibe becsapódó fotonok színeinek összegét. A rendereléskor ezt a színt jelenítsük meg az adott részhez. Így nagyon szép eredményeket érhetünk el - de nagyságrendekkel lassabban...

Először szükségünk van egy függvényre, ami a diffúz felület térbeli pontjaihoz hozzárendel egy pontot a textúrán.

  • A textúra koordináták x komponensét 'u'-nak az y komponenst pedig 'v'-nek szokás hívni.
  • Célszerű a textúrán belüli koordinátát is valós értéknek tekinteni, nem egészeknek, és bilineáris elérést használni. Ez azt jelenti, hogy ha egy 'c' színű fotonnak 4.7 az 'u' koordinátája, akkor 0.3*c-t írjuk a 4-es 'u' koordinátára és 0.7*c-t az 5-ös koordinátára. Két dimenzióban analóg módon 4 helyre is írunk.
  • A kiolvasáshoz is használjuk bilineáris elérést.
  • Profiknak célszerű textúra helyett KD-fába tárolni a fotonok becsapódását. Sokkal szebb eredményt lehet így elérni, és ráadásul még a spekuláris megcsillanások is implementálhatóak, hiszen a fotonok iránya is könnyen eltárolható.

Pont fényforrás esetén nem kell külön számolni azzal, hogy a foton mekkora távolságot tett meg. A foton nem lesz gyengébb, attól, hogy x utat megtett, csak minél távolabb vagyunk a fényforrástól, egy adott pontra annál kevesebb foton fog esni. De ez nem a foton tulajdonságából adódik, ez a fotontérképből implicit ki fog jönni.

A fotontérkép méretének és a fotonok számának megválasztása teljesen ad-hoc. De nem érdemes túlzásba vinni, hiszen a globális illumináció nagyon számításigényes. A házihoz reális értékek pl: fotontérkép mérete: 2^5 * 2^5 - 2^7 * 2^7, fotonok száma: 10^4 - 10^6.

A "foton" igazándiból foton csomagot jelent, pl. a törő anyagok kétfelé választhatnak egy ilyen csomagot. A foton tudja magáról, hogy milyen színű. A kölcsönhatásokkor a Fresnel egyenlet következtében a foton színe megváltozhat. Az egyes fotonok energiája legyen fordítottan arányos a fotonok számával. A fényforrás energiája az összes fotonra egyenletesen oszlik szét. Ha megkétszerezzük a fotonok számát, akkor is ugyanolyan fényes jelentet akarunk kapni.

A törő és tükröző anyagok pont ugyanúgy lépnek kölcsönhatásba a fotonokkal, mint ahogy a sugarakkal is. Ennek az implementáláshoz semmi új ötlet nem kell.

Például egy felülről megvilágított üvegkocka így szórja a fényt: <Törölve, túl sokan másolták>

http://i.imgur.com/1YvdpgA.jpg http://i.imgur.com/4NYiFSE.jpg

A kétirányú sugárkövetés

A globális illumináció implementálásakor sokak fájó szívvel válnak meg a kódban a lokális illumináció résztől, lévén, hogy hiába írták meg, ha nem jó semmire. De ez nem így van. Egyrészt a 3D OpenGL-es házikhoz nagyon nagy segítséget fog nyújtani, hogy érted, hogy hogyan működik a lokális illumináció, hiszen az OpenGL is ezt fogja használni. Másrészt még a sugárkövetés házi végleges formájába is hasznos lehet az a kód.

A kétirányú sugárkövetés ötlete, hogy használjuk a lokális és a globális illuminációt egyszerre. A diffúz anyagot világítsuk meg lokálisan, és csak azok a fotonok keltsenek rajta kausztikát, amik nem triviális úton (egyenes vonalon, végig a levegőben, kölcsönhatás nélkül) jutottak el a fényforrásból az anyagig. Tehát ha a foton ütközésekor a rekurziós szint 0, akkor az közvetlenül a fényforrásból jutott el hozzánk, azt ne mentsük el.

A kétirányú sugárkövetés előnyei:

  • Rengeteg erőforrást meg tud spórolni. Az üvegkocka esetében sokkal kevesebb foton elég egy ugyanolyan minőségű kép előállításához.
  • A spekuláris hatást is figyelembe tudjuk venni.

A kétirányú sugárkövetés hátrányai:

  • Nem triviális a konstansokat úgy beállítani, hogy a lokális és a globális illumináció konzisztens legyen.
  • Az árnyékok széle recésebb lesz. A bilineáris szűrés miatt a globális illumináció pontosabban határozza meg az árnyékok szélét, mint az a módszer, amit a lokális illuminációnál használtunk.

Példaprogram: <Törölve, túl sokan másolták>

A korábbi jelenet kétirányú (bal oldalt) és csak globális (jobb oldalt) megvilágítással, mindkét esetben 500 000 fotonnal

http://i.imgur.com/fU0yrlg.jpg http://i.imgur.com/1YvdpgA.jpg


Ennél a példaprogramnál egy kicsivel összetettebb jelentnél már rengeteget tud dobni a kétirányú sugárkövetés. Viszont ezeknél meg sokkal jobban fáj a szép árnyék hiánya. Például:

http://i.imgur.com/hPu5FGv.jpg http://i.imgur.com/nNVSlsW.jpg


Két lehetséges megoldás a recés árnyékok problémára:

  • A triviális utakon is vegyük figyelembe a globális illuminációt, csak ott sokkal kisebb súllyal, úgy, hogy ott továbbra is a lokális illumináció domináljon. Például a fotonokat szorozzuk be a recursion_level == 0 ? 0.25f : 1.0f számmal. Ez a megoldás ingyen van, és egy picivel jobb eredményhez vezet. Ennek a hatása:

http://i.imgur.com/OkgFCZh.jpg

  • A másik megoldás a multi-sampling használata. Itt az az ötlet, hogy minden pixelhez több képet is fényképezzünk le, és ezeket mossuk össze. Ez az megoldás nagyon drága, a beadón ezt nem illik használni viszont rengeteget javít a képminőségen és a triviálisan lehet vele mélység-élességet is implementálni.

Mivel a tárgyból ezt úgy se nagyon kéne használnod, ezért csak érdekességképpen mutatom meg a legegyszerűbb multi-sampling algoritmust, ami random mintavételezést használ:


void takePicture() {
  for(int x = 0; x < Screen::height; ++x) {
    for(int y = 0; y < Screen::width; ++y) {
      Color sum;
      const int sample_num = 16;
      for(int i = 0; i < sample_num; ++i) {
        float x_ = x + float(rand())/RAND_MAX - 0.5f;
        float y_ = y + float(rand())/RAND_MAX - 0.5f;

        sum += capturePixel(x_, y_);
      }
      Screen::Pixel(x, y) = sum / sample_num;
    }
  }
}


http://i.imgur.com/hFG6KmA.jpg

Természetesen egy állókép messze nem olyan izgalmas, mint egy animáció. De sajnos a sugárkövetéssel real-time animációt nagyon nehéz elérni, az utolsó példaprogrammal (minimális módosítás után) 8 másodpercnyi animáció lerenderelése 3 perc volt, az eredménye a bal oldali képen látszódik.

Persze ez még messze nem olyan szép, mint amit te a házid eredményeként kaphatsz, aminek az egyik legfőbb oka, hogy a jelenet nagyon egyszerű. Ha ezt egy tükörszobába rakom be, akkor a jobb oldali kép lesz az eredmény. De ez még mindig csak egy darab kocka. Több másodrendű alakzat ennél sokkal izgalmasabb eredményhez vezet.


http://oi61.tinypic.com/14d22s4.jpg http://oi57.tinypic.com/25rm1yf.jpg


3D OpenGL

Kedvcsináló

Sugárkövetéssel nagyon látványos képeket tudunk elérni, ha vesszük a fáradtságot, hogy érdekes objektumokat helyezzünk el a világban. Főleg a másodrendű felületek tudnak nagyon szép képeket adni. Az árnyékszámításhoz gyakorlatilag ölünkbe hullott egy algoritmus, amihez pont ugyanazt kellett csinálni, mint amit rajzoláskor is csináltunk. A globális illuminációval ráadásul olyan hatásokat is implementálni tudtunk, amikkel a mai játékokba szinte sehol nem lehet találkozni. Akkor mi a gond a sugárkövetéssel, miért nem ezt használjuk mindenhol, miért kell akkor egyáltalán OpenGL? A probléma az vele, hogy lassú. Az általam mutatott programok mindössze 14 darab háromszögből álltak, messze elmaradva a mai játékok komplexitásától, és így is, a globális illuminációval kb. 5 másodperc volt, mire a kép előállt. Ez nem tűnik soknak, de optimális esetben egy játékhoz másodpercenként legalább 60szor kéne képet alkotnunk, a sugárkövető ettől messze elmarad. Ha a kódot két héten át optimalizáltam volna, akkor akár 0.1 másodperc is lehetne a renderelési idő. Igen ám, de ez csak a képalkotásra szánt idő! A játék logika, főleg ütközés detektálások, vagy ruha-, víz szimuláció stb. egyáltalán nincsenek ingyen, és még azoknak is bele kéne férnie az időbe. És ez még mindig csak 14 darab háromszög.

A általunk használt sugárkövetéssel a legnagyobb gond az, hogy nem használja a videókártyát. Hiába van a gépünkbe egy szuperszámítógép teljesítményű hardware, ha nem használjuk semmire. A mai videókártyák nem csak előre meghatározott műveletek tudnak végrehajtani, hanem már programozhatóak is. Az ezt kihasználó grafikus programok hihetetlen hatékonyak. Például az én grafika nagyházim egy 72 millió háromszögből álló jelenetet tudott valós időben (kb. 20 FPS-el) kirajzolni (fizikával együtt).


Csak emlékeztetőül, sugárkövetéssel 14 háromszög nem ment valós időben... A videókártya segítségével a 72 millió háromszögből álló jelenet real-time kirajzolása még messze nem a maximum amit el lehet érni.

A videókártyát használhatnánk arra, hogy gyors raytracert írjunk. Ehhez viszont meg kellene tanulni, hogy hogyan kell a videókártyát programozni... Ehelyett mi a videókártya kezelését az OpenGLre bízzuk, és az inkrementális elvet használva fogunk rajzolni (ugyanúgy, mint a 2D OpenGL-es résznél, csak 3D-ben).

A 3D-s kocka

A 2D OpenGL-es résznél mutattam egy glut függvényt, ami egy kockát rajzol ki. Ezt a házikhoz nem lehet használni, inkább írjunk meg magunknak.


void glVertex3f(const Vector& v) {
  glVertex3f(v.x, v.y, v.z);
}

void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  glVertex3f(a); glVertex3f(b); glVertex3f(c); glVertex3f(d);
}

void drawCube(const Vector& size) {
  glBegin(GL_QUADS); {
    /*       (E)-----(A)
             /|      /|
            / |     / |
          (F)-----(B) |
           | (H)---|-(D)
           | /     | /
           |/      |/
          (G)-----(C)        */

    Vector s = size / 2;

    Vector A(+s.x, +s.y, -s.z), B(+s.x, +s.y, +s.z), C(+s.x, -s.y, +s.z), D(+s.x, -s.y, -s.z), 
           E(-s.x, +s.y, -s.z), F(-s.x, +s.y, +s.z), G(-s.x, -s.y, +s.z), H(-s.x, -s.y, -s.z);

    glQuad(A, B, C, D); glQuad(E, H, G, F); glQuad(A, E, F, B);
    glQuad(D, C, G, H); glQuad(B, F, G, C); glQuad(A, D, H, E);

  } glEnd();
}


Az eredménye: Egy négyzet

http://i.imgur.com/iWkEuQj.png

Na várjunk csak, mi egy kockát rajzoltattunk ki, akkor miért egy négyzetet látunk? Azért mert kameránál a "fényképezést", mint transzformációt leíró mátrix (a GL_PROJECTION) egy egységmátrix, így a fényképezés egyszerűen a z koordináta eldobására korlátozódik. A kockának mindössze két oldala van, aminek az XY síkra vett vetülete nem nulla területű, de mindkét vetület egy-egy négyzet. Mi a kettő közül a később kirajzolt oldalnak a vetületét látjuk.

A megoldás: állítsuk be a projekciós mátrixot. Első közelítésként próbálkozhatnánk a sugárkövetésnél használt fényképezés leírásával. Ez részben működne, az algoritmus bemenete a látószög (FoV), és a képernyő szélességének és magasságának az aránya, és ezekből meghatározza a képen az X és Y koordinátákat. Ez a transzformáció önmagában viszont nekünk nem elég, mert a láthatóság eldöntése miatt szükségünk van egy 'z' koordinátára is, amibe azt akarjuk eltárolni, hogy az adott objektum milyen távol van a kamerától (előjelesen, a kamera nézeti iránya a pozitív). De ez az érték a jelenetben a (-végtelen, végtelen) tartományon mozog, amit mi a (-1, 1) tartományra akarunk transzformálni.

Ilyen problémával már találkoztunk, a tonemappingnél, és két lehetséges megoldást is mutattam rá. De itt egyik se működik. Az ötlet ami itt szoktunk használni, az az, hogy egy bizonyos távolságnál (zNear) közelebb lévő és egy másik távolságnál (zFar) pedig a távolabb lévő objektumokat nem rajzoljuk ki. Ez gyakorlatban egy elég jól alátámasztható döntés, ha egy objektumokkal teli zsúfolt világban végtelen távolra ellátnánk, akkor végtelen erőforrásra is szükségünk lenne ahhoz, hogy arról képet alkossunk. A szemünkhöz nagyon közeli objektumok pedig olyan nagynak látszódnának, hogy nem látnánk tőlük semmi mást. A két távolság két vágósíkot határoz meg. Egy transzformáció kell nekünk, amire egy a közeli vágósíkon lévő objektum 'z' koordinátája 0 lesz, míg a távoli vágósík pontjai 1 mélység koordinátákkal rendelkeznek. Kb. 10 sor lenne levezetni az ezt megvalósító transzformáció mátrixát, de ettől megkíméllek titeket, mert a házihoz úgy sincs rá szükség, ugyanis egy glu függvény ezt megcsinálja helyettünk.

A függvény amit használhatunk az a gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar). Megjegyzések a paramétérek megválasztásához:

  • A fov-ot szögben, nem radiánba kell megadnunk. A reális értéke 40 - 150 fok között mozog.
  • Az ascpect a képernyő szélessége / képernyő magassága. A házikba ennek az értéke egy.
  • A zNear-nél sokan nagy késztetést éreznek, hogy egy nagyon kicsi értéket állítsanak be, hogy semmi se kerüljön az első vágósík elé. A gond viszont ezzel az, hogy az mélység számító algoritmus a természeténél fogva több különböző ponthoz is ugyanazt az értéket rendeli, hiszen csak egy véges tartományt használhat (általában egy 24 bites fixpontos számot). Amiért ez zavaró, az az, hogy minél nagyobb a zFar / zNear értéke, annál nagyobb tartományt kell ugyanarra a (0..1) intervallumra transzformálni. Ez pedig azzal jár hogy az egyre távolabb lévő pontokhoz is ugyanazt a mélységet fogja használni. Ilyenkor pedig nem tudjuk eldönteni, hogy mi látszódik, és mi nem, aminek szinte mindig nagyon ronda eredménye szokott lenni. Egy ökölszabály, hogy a zFar / zNear értéke ne legyen (sokkal) nagyobb 1000-nél. A házikhoz tipikusan nincs szükség 100-nál nagyobb zFar-ra, ilyenkor a zNear ne legyen 0.1-nél kisebb.

Például:


void onInitialization() {
  glMatrixMode(GL_PROJECTION);
  gluPerspective(60, 1, 0.1, 10);
  glMatrixMode(GL_MODELVIEW);
}


A kockát kirajzoló (kirajzolni próbáló) program eredménye ezzel a pár sorral kibővítve:

Egy teljesen fehér kép

http://i.imgur.com/2B4efRi.png

Igen, én képes voltam képes erről feltölteni egy screenshotot. Én mondtam, hogy sugárkövetéssel látványosabb eredményeket lehet elérni :D

Igazándiból annyira nem meglepő, hogy egy kocka belsejében lévő kamera nem lát mást, mint a kockát, ami jelenleg teljesen fehér.

A megoldás: "Rakjuk át a kamerát a kockán kívülre". Pontosabban a világot (és vele a kockát) transzformálúk úgy, hogy a kamera ne legyen a kocka belsejében. A legkényelmesebb módja: gluLookAt(GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ, GLdouble centerX, GLdouble centerY, GLdouble centerZ, GLdouble upX, GLdouble upY, GLdouble upZ) - hol legyen a kamera, mit nézzen, és melyik legyen a felfele vektor. Például az onInitbe gluLookAt(-3, 2, -2, 0, 0, 0, 0, 1, 0);

http://i.imgur.com/ChgkBk4.png

  • Erről már könnyebb elhinni, hogy ez egy kocka. De színezzük át az oldalait különböző színűre. Pl. a felületi normál absz. értéke legyen a szín.


void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  Vector normal = cross(b-a, c-a).normalize();
  glColor3f(fabs(normal.x), fabs(normal.y), fabs(normal.z));
  glVertex3f(a); glVertex3f(b); glVertex3f(c); glVertex3f(d);
}


Az eredménye: egy kocka?

http://i.imgur.com/o8dUcNz.png

Ez nem túl meggyőző... Azért nem hasonlít egy kockára, mert az oldalak láthatóságát a rajzolás sorrendje döntötte el. De nem azért szenvedtünk a projekciós transzformáció mélység értékével, mert abból az OpenGL meg tudja oldani a láthatóság problémáját? De, csak ezt be kell kapcsolni... Az ezt bekapcsoló függvényhívás: glEnable(GL_DEPTH_TEST). Fontos, hogy ha ez be van kapcsolva akkor a képernyőt a glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) függvénnyel töröld le. Ha a GL_DEPTH_BUFFER_BIT konstanst lehagyod, akkor nem fogsz látni semmit.

Az eredmény: Végre egy kocka!

http://i.imgur.com/z0QQfCD.png

A megvilágítás

A megvilágítás bekapcsolása nagyon egyszerű: glEnable(GL_LIGHTING);

Amit ez eredményez: egy majdnem teljesen fekete képernyő.

http://i.imgur.com/Rj0AC5p.png

A probléma az, hogy az OpenGL a megvilágításkor nem a glColor3f()-el megadott színeket használja fel. Hogy pontosan mit, arra majd később visszatérünk, egyelőre kérjük meg, hogy a glColor3f() színeket használja a megvilágításhoz is. Ezt a glEnable(GL_COLOR_MATERIAL); függvénnyel tehetjük meg.

Ennek hatására egy ilyen képet látunk:

http://i.imgur.com/FAm0t1G.png

Ezen még mindig nem látszódik, hogy meg lenne világítva, egyszerűen csak sötétebb lett a kocka. A gond az, hogy egyetlen fényforrásunk sincs, mindössze egy (0.2f, 0.2f, 0.2f) színű környezeti háttérvilágítás befolyásolja alapból a jelenetünket. A környezeti háttérvilágításon kívül az OpenGLben 8 darab fényforrás van. Ezek a GL_LIGHT0, GL_LIGHT1, ... GL_LIGHT7 neveket kapták. A fényforrás típusok nagyon hasonlítanak ahhoz, mint amiket a sugárkövetésnél használtunk. A fényforrás a pozíciója alapján lehet pont vagy irányfényforrás. A pozíciót 4D heterogén koordinátákkal kell megadni. Ha a negyedik koordináta nulla, akkor a pont végtelen távol van, ezt az OpenGL irányfényforrásként fogja kezelni. Ha a negyedik koordináta nem nulla (ilyenkor tipikusan 1), akkor pedig pont fényforrásról van szó. A pont fényforrás viselkedhet spotlámpaként, ehhez meg kell adni, hogy melyik irányba néz, és mekkora térszöget világít be. Továbbá az összes fényforrásnak lehet ambiens tagja is. De van egy globális ambiens fény is, ami a 8 fényforrástól független, mindig be van kapcsolva, (0.2f, 0.2f, 0.2f) a default értéke, és ehhez a házikba általában nem szabad hozzányúlni.

A fényforrások külön-külön ki-be kapcsolgathatóak. Például glEnable(GL_LIGHT0). Ez önmagában semmi változást nem okoz, aminek az egyik oka az, hogy mint azt a sugárkövetésnél is láttuk, a világítás számolásához szükség van a felületi normálokra. Ezt a glNormal3f() függvénnyel adhatjuk meg, egy glVertex hívás előtt. Ez is olyan, mint a rajzolószín, ha egyszer beállítjuk, akkor az utána az összes glVertex hívásra ki fogja fejteni a hatását, amíg újra meg nem változtatjuk. Például:


void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  Vector normal = cross(b-a, c-a).normalize();
  glColor3f(fabs(normal.x), fabs(normal.y), fabs(normal.z));
  glNormal3f(normal.x, normal.y, normal.z);
  glVertex3f(a); glVertex3f(b); glVertex3f(c); glVertex3f(d);
}


Ezután még célszerű a fényforrás pozícióját és színét beállítani, és nem azok default értékére hagyatkozni. Ezeket a glLightfv(GLenum light, GLenum pname, const GLfloat *params); függvénnyel tudjuk beállítani. Pl:


GLfloat p[4] = {-1.1f, 2.0f, -1.2f, 1};
glLightfv(GL_LIGHT0, GL_POSITION, p);
GLfloat c[4] = {0.7f, 0.8f, 0.9f, 1.0f};
glLightfv(GL_LIGHT0, GL_DIFFUSE, c);


Az eredménye: Megvilágított kocka

http://i.imgur.com/W7BpUW4.png


A gyakran használt megvilágítási paraméterek a glLightfv() függvényhez:

  • GL_AMBIENT - a fényforráshoz tartozó környezeti megvilágítás színe. Alapértéke: (0.0, 0.0, 0.0, 1.0)
  • GL_DIFFUSE - a fényforrás színe a diffúz megvilágítás számolásakor. Alapértéke: GL_LIGHT0 - (1.0, 1.0, 1.0, 1.0), GL_LIGHT{1..7} - (0.0, 0.0, 0.0, 1.0)
  • GL_SPECULAR - a fényforrás színe a spekuláris megvilágítás számolásakor. Alapértéke: GL_LIGHT0 - (1.0, 1.0, 1.0, 1.0), GL_LIGHT{1..7} - (0.0, 0.0, 0.0, 1.0)
  • GL_POSITION - a fényforrás pozíciója / iránya. Alapértéke: (0,0,1,0)


A glLightf(GLenum light, GLenum pname, GLfloat param); is hasznos lehet, az ehhez tartozó gyakori paraméterek a pontfényforrások erősségének távolságfüggését befolyásolják:

  • GL_CONSTANT_ATTENUATION - Alapértéke: 1
  • GL_LINEAR_ATTENUATION - Alapértéke: 0
  • GL_QUADRATIC_ATTENUATION - Alapértéke: 0


A fényerősséget számoló képlet:

  • attenuated_power = power / (GL_CONSTANT_ATTENUATION + dist*GL_LINEAR_ATTENUATION + dist*dist*GL_QUADRATIC_ATTENUATION)


Ha valósághű távolságfüggést akarunk, akkor a konstans gyengülést vegyük le nullára, és a négyzetesre állítsuk be valami pozitív számot. Pl:


glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 0.0f);
glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 0.5f);


Az eredménye:

http://i.imgur.com/tM9VJDr.png

  • A képpel van egy elég nagy probléma: a megvilágítás nem folytonos, látszódik, hogy hogyan bontotta háromszögekre az OpenGL a quadot amit ki akartunk rajzolni.
    • A jelen esetben ez - bár rondán néz ki - de nem olyan nagy probléma. Ha viszont egy gömböt akarunk kirajzolni, ott már nagyon is zavaró, ha látszódik, hogy háromszögekből áll.
    • Ennek a jelenségnek az az oka, hogy az OpenGL csak a csúcspontokba számol megvilágítást (per vertex shading), és ezeket az értékeket interpolálja a háromszög belsejében. Ez akkor amikor az OpenGL-nek az a verziója készült, amit a tárgyból használni kell (1997-ben), még nagyon jó ötletnek tűnt, ezzel teljesítményben rengeteget lehetett nyerni.
    • Ez igazándiból nem olyan nagy probléma, csak az olyan objektumokat, aminek a méretei elég nagyok ahhoz, hogy azon a távon már jelentős legyen a megvilágítás megváltozása, célszerű kisebb háromszögekre bontanunk.

Pl ha egy oldal 2 helyett 32 db háromszögből áll, amit én így oldottam meg:


void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  Vector normal = cross(b-a, c-a).normalize();
  glColor3f(fabs(normal.x), fabs(normal.y), fabs(normal.z));
  glNormal3f(normal.x, normal.y, normal.z);

  int res = 4; // resolution
  for(int i = 0; i < res; i++) {
    for(int j = 0; j < res; j++) {
      glVertex3f(a + i*(b-a)/res     + j*(d-a)/res); 
      glVertex3f(a + (i+1)*(b-a)/res + j*(d-a)/res); 
      glVertex3f(a + (i+1)*(b-a)/res + (j+1)*(d-a)/res); 
      glVertex3f(a + i*(b-a)/res     + (j+1)*(d-a)/res); 
    }
  }
  // glVertex3f(a); glVertex3f(b); glVertex3f(c); glVertex3f(d);
}


Akkor a csúcspontokban számolt megvilágítás is egész hihetően néz ki:

http://i.imgur.com/dzjcMuZ.png


És ha a felbontást felvisszük mondjuk 1024 háromszögig:

http://i.imgur.com/EyFQ9L0.png

Akkor már pixel szinten pontos képet kapunk... de sokkal sokkal drágábban. Ilyenkor rengeteg teljesen fölösleges munkát csináltatunk az OpenGLlel. Általánosságban nagyon rossz ötlet pixel szintű pontosságra törekedni a felbontás növelésével. A nem feltűnően zavaró megvilágítással általában már megelégszünk, egy összetett jelenetnél a megvilágítás pontatlansága úgy se tűnik fel.

A megvilágítás és a transzformációk

Korábban említettem, hogy egy fényforrás esetében a glLightfv függvénnyel a GL_POSITION konstans segítségével lehet állítani egy fényforrás pozícióját, ami egy négy dimenziós vektor.

Viszont egy vektor önmagában nem jelent semmit, amíg meg nem mondjuk, hogy melyik koordináta-rendszerben van értelmezve.

A rövid válaszom az, hogy a GL_MODELVIEW által definiált koordináta-rendszerben, vagyis a glLightfv-ben megadott helyzetnek a GL_MODELVIEW-val transzformált eredménye fogja meghatározni a fényforrás helyét a kamera koordináta-rendszerben. Ha érted, hogy ez mit jelent és mivel jár, akkor fojtasd a következő fejezettel.

Ennek az egyik legfontosabb következménye, hogy a világítás működése függ attól, hogy azt a kódban hol állítjuk be.

A lehetőségek bemutatásához azt feltételezem, hogy az onDisplay elején a GL_MODELVIEW egységmátrixra van állítva, majd ugyanebben a függvényben később, de még a rajzolás előtt a kamera a gluLookAt függvénnyel úgy van beállítva, hogy egy ellipszis pályán mozogjon.

Például:


void setCamera() {
  float time = glutGet(GLUT_ELAPSED_TIME) / 1000.0f;
  gluLookAt(3*cos(time), 2, 2*sin(time), 0, 0, 0, 0, 1, 0);
}

void onDisplay() {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  
  // ...
  setCamera();
   
  // rajzolás
}


A három lehetőség:

  • A világítást a setCamera() előtt állítjuk be. Ilyenkor az értékek kamera koordináta-rendszerben értetődnek. Ez azt jelenti, hogy ha a pozíciót (-1, 0, 0, 0)-nak állítjuk be, akkor az mindig balról fog világítani, akármerre is néz a kamera. Ezt tipikusan olyankor szoktuk használni, ha a fényforrás a nézőhöz kötődik, pl egy FPS játékban a karakterünk fegyverén a lámpa.

http://i.imgur.com/iZJlFr9.gif

  • A megvilágítást közvetlenül a setCamera() függvény után állítjuk be. Ekkora a glLightfv-ben megadott pozíció a világ koordináta-rendszerben lesz értelmezve, és a statikus objektumoknak "mindig ugyanaz az oldala lesz fényes". Ezt általában akkor szoktuk használni, ha a fényforrás a jelentben egy helyben marad.

http://i.imgur.com/cUPVzeT.gif

  • A megvilágítást a setCamera(), és további transzformációk használata után állítjuk be. Így tipikusan a jelentünkben valamelyik objektummal együtt mozgó fényforrásokat szoktuk kezelni.

http://i.imgur.com/UZT2nMr.gif

Hátsólap eldobás

Az utolsó példába, a kocka hat oldala közül egyszerre mindig csak három látszódott, de ennek ellenére mind a hatot kirajzoltuk, csak abból három a z-tárazás (GL_DEPTH_TEST) miatt nem látszódott. Ha nekünk kéne rajzolnunk, akkor a felületi normál alapján azonnal el tudnánk dönteni egy oldalról, hogy az a kamera felé néz-e vagy se. Ha ezt az ötletet az OpenGL-lel is fel tudnánk használni, a háromszögek felének fölösleges kirajzolását megspórolnánk. Viszont a rajzoláshoz használt felületi normál, amit mi adunk meg, nem mindig egyezik meg a háromszögek felületi normáljával, sőt ez a vektor a háromszög három csúcspontjára különböző is lehet. Ha pedig nem ismerjük a háromszög normál vektorát, akkor nehéz az alapján optimalizálni. A háromszög három csúcsából ki tudjuk számolni a felületi normál irányát, csak annak az előjelét nem tudjuk. Az ötlet az, hogy a háromszög pontjainak legyen egy előre meghatározott körüljárási iránya, például legyenek a normál irányából nézve az óramutató járásával ellentétes (Counter Clockwise - CCW) sorrendben felsorolva a pontok. Ekkor, ha a másik oldalról nézzük a háromszöget, akkor a körüljárási irány megfordul, és onnan az óramutató járásával megegyező (Clockwise - CW) sorrendet fogok látni. És ezt már az OpenGL is fel tudja használni.

Nekünk innentől csak annyi a dolgunk, hogy megadjuk, hogy a mi objektumainkra, a normál irányából nézve (ez a "front face") CW vagy CCW körüljárási irányt használunk. Pl:


glFrontFace(GL_CCW); // Az normál irányából nézve CCW a körüljárási irány
glCullFace(GL_BACK); // A hátsó oldalt akarjuk eldobni
glEnable(GL_CULL_FACE); // És engedélyezzük a lapeldobást.


Ugyanazt látjuk, mint a három sor nélkül, de fele annyi erőfeszítéssel.

http://i.imgur.com/dzjcMuZ.png


Viszont ha az első lapokat dobjuk el a glCullFace(GL_FRONT); függvénnyel, akkor ezt kapjuk:

http://i.imgur.com/OsxWbsw.png

Színek

A színekkel kapcsolatban egy megjegyzés: a GL_COLOR_MATERIAL-ról azt írtam, hogy az csak egy átmeneti megoldás. Ami a másik fajta szín bevezetését motiválta, az az, hogy az anyagoknak lehet különböző az ambiens a diffúz és a spekuláris színe. Az OpenGL megvalósításában az anyagoknak még emisszív színe is lehet. Az emisszív színnel azt lehet elérni, hogy az objektum világítson, sötétben is látszódjon, de nem úgy, mint egy fényforrás, az emisszív anyag a környezetének a színét nem befolyásolja. A másik előnye az OpenGL implementációjának hogy az első és hátsó lapoknak külön színe is lehet.

Az egyes színeket állító függvény a glMaterialfv(GLenum face, GLenum pname, const GLfloat *params);

A leggyakoribb paraméterei:

  • A 'face' értékei:
    • GL_FRONT
    • GL_BACK
    • GL_FRONT_AND_BACK
  • A 'pname' értékei:
    • GL_AMBIENT
    • GL_DIFFUSE
    • GL_AMBIENT_AND_DIFFUSE
    • GL_SPECULAR
    • GL_EMISSION

Illetve egy másik hasznos függvény a glMaterialf(GLenum face, GLenum pname, GLfloat param)

  • Ezt a GL_SHININESS állítására szoktuk használni.

Tesszelláció

Avagy hogyan bontsuk fel primitívekre (tipikusan háromszögekre) az objektumainkat.

Először is egy apró trükk azoknak, akik az előző példába nem látták át, hogy a kocka hogyan épül fel négyszögekre: Az OpenGL-t meg lehet kérni rá, hogy ne töltse ki a primitíveket, csak a határukat rajzolja ki a glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); függvénnyel. Ez olyan, mint ha GL_LINES-ra lecseréltük volna a rajzolandó primitívet, csak így nem kell a pontok felsorolásán módosítani, elég ezt az egy sort beírni és már látjuk is az objektumunk "drótvázát".

http://i.imgur.com/IBsec5K.png

Persze a megvilágítás a vonalakra is érvényes. Ha ez zavaró, akkor kapcsoljuk ki a megvilágítást, és pl. rajzolunk mindent fehérrel.

A kis kitérő után a tesszellációhoz visszatérve, a primitívekre bontástól a legalapvetőbb elvárásunk, hogy visszaadja azt az alakot, amit ki akartunk rajzolni. De ezen kívül is van pár fontos jellemzője a tesszellációknak, ami alapján az egyik primtívekre bontásra azt mondhatjuk, hogy jobb, mint a másik:

  • Például, hogy nagyjából azonos területű primitívekből épül-e fel.
    • Például a gömb tesszellációk tekintetében nagyon nagy különbség van az "UV sphere" és az "icosphere" között.
    • Az "UV sphere" pontjait a gömb paraméteres egyenletéből tudjuk kiszámolni (gömbi koordináta rendszerből kiindulva, a sugárt fix értékűre választjuk).

http://upload.wikimedia.org/wikipedia/hu/2/22/Gomb-pol.jpg

Az egyes pontokból pedig úgy csinálunk gömböt, hogy a szomszédos pontokat (amik a paraméterekben szomszédosak), "összekötjük", azaz egy sokszöget alkotunk belőlük. Például az például az összes paraméter értékpárra kirajzolunk egy-egy négyzetet.

http://i.imgur.com/YGa1jJw.png

  • Az icosphere ezzel szemben az öt szabályos test egyikéből, az ikozaéderből indul ki, és ennek növeli a polygon számát a Catmull-Clark algoritmussal. A golflabda is ilyen.

http://previewcf.turbosquid.com/Preview/2011/04/19__09_20_06/Perspective_normals.jpg4d727076-639f-4e38-8e93-b7b6ca4b6d52Large.jpg


  • A két gömb összehasonlítva:

http://i.imgur.com/qNnAKse.png

  • Az UV sphere esetében a sarkoknál közel nulla területűek a négyszögek, ott teljesen fölöslegesen kell számolnia az OpenGL-nek. Az icosphere esetében viszont a polygonok egyenletesen vannak elhelyezve, és ugyanakkora területűek. Az icosphere tehát jobb tesszelációja a gömbnek, mint az UV sphere. De az UV sphere-t megírni sokkal egyszerűbb, és a házikhoz teljesen elegendő.
  • Két tesszelláció között nyilván különbség lehet, hogy mennyire jellegzetes pontokat ragad meg az eredeti objektumból. Például vegyünk egy síkot, amibe vannak keskeny kiugróan magas pontok. A tesszellációba a kiugró pontok elhagyása az alakzat összhatását teljesen el tudja rontani, míg ha ezeket is ugyanolyan súllyal vesszük be, mint a többi sík pontot, akkor összességébe az alakzatunk dimbes-dombos lesz, mind a sík volta, mind a kiugró pontok jellegzetessége elveszik.
  • Két tesszelláció között nagyon fontos különbséget okozhatnak az árnyaló normálok. De mielőtt ebbe beleszaladnánk, nézzük meg, hogy mik is azok.

Megjegyzés: Általában parametrikus felületeket kell tesszellálni. Itt az összes pontnak a felsorolása - az alakzattól függetlenül - két darab for loop. Az objektum kirajzolása ettől mindössze annyiban különbözik, hogy a szomszédos pontokból primitíveket is kell alkotni. De szerencsére általában a szomszédos pontok paraméterei is szomszédosak. Bár elsőre meglepőnek hangozhat, de egy gömb és egy tórusz tesszellációja csak annyiban különbözik, hogy a két futó paraméterhez hogyan rendeljük hozzá a 3D-s pontot, vagyis a két alakzatnak csak az egyenlete más, ezt leszámítva a két objektum kirajzolásának a kódja teljesen ugyanaz. Sőt, amikor a kockának az egyes oldalait, a négyzeteket bontottam fel nagyobb részletességűre a megvilágításhoz, még ott is ugyanaz az algoritmus került elő, mint ami a gömbnél vagy a tórusznál kell, egyedül az alakzat paraméteres egyenlete volt más.

http://i.imgur.com/KCJ9s0Q.jpg

Az árnyaló normálok

A gömb tesszelációról azért beszéltem ennyit, mert ezen keresztül meg tudom mutatni, hogy mi az az árnyalási normál, és miért van rá szükség. Az OpenGL lehetőséget ad arra, hogy a rajzoláskor ne a háromszögek normáljait használjuk, hanem - a teljesen alakzat ismeretében - minden egyes pontban külön-külön adjuk meg a normálvektort. Ezeket árnyalási normálnak hívjuk, ezek nem a tesszellált, hanem az eredeti objektumnak a normáljai. Egy gömbnél például az összes normál a gömb középpontjából az adott felületi pontba mutató egységvektor lenne. A csúcsonként megadott normálok azért jók, mert így egy háromszöget úgy tudunk árnyalni, mintha az nem sík lenne. Így háromszögekkel meglepően jól lehet közelíteni még egy gömb felületét is.

  • Az OpenGL-be a glShadeModel(GL_SMOOTH); függvénnyel lehet bekapcsolni, hogy figyelembe vegye az árnyaló normálokat.
  • Én ennek a demonstrálásához a glutSolidSphere(GLdouble radius, GLint slices, GLint stacks); függvényt használom, de ezt házikba nem lehet használni.
  • A példaprogram: Gömb sima árnyalással

A GL_FLAT árnyékolás (baloldalt) összehasonlítása GL_SMOOTH árnyékolással (jobboldalt)

http://i.imgur.com/9luiuQI.png http://i.imgur.com/IWMFbU4.png

A trafók vs az árnyaló normálok

Amikor a világot transzformáljuk, akkor a normálokkal mi történjen? Az eltolás kérdése egyszerű, a normál az egy irányvektor, az eltolás nem hat rá. A forgatás természetesen ugyanúgy hat rá mint a helyvektorokra. A nagyítással kapcsolatban viszont már más a helyzet.

Ezzel kapcsolatban módosítsuk az utolsó példaprogramot, kérjünk fele akkora gömböt a GLUT-tól, és azt nagyítsuk kétszeresére glScalef()-el. Az eredménye az, hogy a gömb fele olyan világos lesz:

http://i.imgur.com/yy8eRvZ.png

A világítás számításakor csak a normál változott meg, és nem nehéz kitalálni, hogy fele olyan hosszú lett. De nem kétszeresére nagyítottuk a világot? Akkor a normál miért lett fele akkora. Ha ugyanaz maradt volna a normál, akkor működne jól. De akkor az OpenGL miért rontotta el?

Ahhoz, hogy ezt megértsük, vegyünk nem uniform nagyítást, ami a tengelyek irányába különböző mértékben nagyít. Pl. nagyítsuk a (3, 1, 1) vektorral. Ez nyilván drasztikusabb alakváltozással, és így a normálok lényegesebb megváltozásával is jár. Ennek az eredménye egy ellipszis lesz:

http://i.imgur.com/RG9NlDC.png

Az ellipszisen egyrészt nagyon jól látszódik, hogy az a tesszellációs felbontás, ami a gömbhöz elég volt, itt már csúnya képet eredményez. De most nem ezen van a lényeg, hanem az árnyaló normálokon. Miben másak az ellipszis normáljai, mint a gömbé? Az X tengely mentén ugyanannyi változás 3-szor akkora távon következik be, ezért értelemszerűen, amíg a Y és a Z tengely mentén a egységnyit változik a felületi pont helye, addig az X tengely mentén ez az érték 1/3. Termesztésen ez a pongyola megfogalmazás a parciális deriváltak számszerű értékére vonatkozott. És a gradiens, a parc. deriváltakból álló vektor, a felületi normál amit mi kerestünk. Tehát a normálon a (3, 1, 1) nagyítás hatására (1/3, 1, 1) vektorral való skálázás történik. Általánosságban az igaz, hogy a normálokra a nagyítások inverz transzformációja hat.

Szerencsére ezt az OpenGL automatikusan megcsinálja helyettünk. A gond viszont ezzel az, hogy az ilyen transzformációk után a normál már nem feltétlen lesz egységvektor, mint ahogy azt a korábbi példákba is láttuk. De erre a megoldás egyszerű, a glEnable(GL_NORMALIZE); függvény megkéri az OpenGL-t, hogy a világítás számolásakor normalizálja a normálokat.

A textúrázás

Eddigi ismereteink szerint, ha egy objektumba egy plusz színt akartunk vinni, akkor kénytelenek voltunk egy plusz pontot is hozzáadni, ami semmi plusz információt nem hordozott, csak más volt a színe. De cserébe lényegesen többet kellett számolni.

A plusz színek hozzáadásának hatékonyabb megoldására lehet használni a textúrázást. Ezt használva egy háromszögön belül minden egyes pixelnek különböző színe lehet, a háromszög csúcspontjaihoz rendelt színtől függetlenül. Viszont ha a megvilágítás csúcspontonként történik, akkor a megvilágítás hogyan tudja figyelembe venni ezeket az új színeket? A rövid válasz az, hogy sehogy, de mégis használhatunk megvilágítást és textúrázást egyszerre. Ehhez egy ügyes trükkre lesz szükségünk, de erről majd bővebben akkor, amikor már tudjuk, hogy hogyan kell textúrázni. Addig kapcsoljuk ki a megvilágítást.

A textúrázás alapjai

Először egy egyszerű 6 db quadból álló kockán fogom megmutatni, hogy a textúrázás hogyan működik. A cél, hogy minden oldalra kiírjak egy számot, hogy azt az oldalt hányadikként rajzoltam ki. Ezt csak kódból fogom megoldani, nem használok hozzá külső programmal, pl. photoshoppal előállított képet, mert azt a házikhoz se lehet. Néhány szám esetleg fordítva fog állni, de ez engem nem zavar. Ilyen eredményt fogunk kapni:

http://i.imgur.com/Bh5G5f8.png http://i.imgur.com/0lSPkjh.png

Először is, mi is az a textúra? A textúra egy színeket tartalmazó tömb. OpenGL 1.1-be egy vagy két dimenziós lehet, és szabvány szerint kettőhatvány méretűnek kell lennie. Annak ellenére, hogy a textúra színeket tartalmaz, és a színeket az OpenGL floatként szereti kezelni, a textúrák esetében nem annyira szeretünk floatokat használni. Itt tényleg csak (0-1) tartománybeli LDR színekre vagyunk kíváncsiak, itt float helyett elég egy fix pontos szám is, pl. komponensenként egy byte. De gyakran mindhárom komponenst le tudjuk írni mindössze egy bájtban. A float textúrák sokkal több helyet foglalnak, külön megizzasztják a memóriát, ami már enélkül is szűk keresztmetszet, ráadásul ezt nagyjából feleslegesen teszik, a float értékkészletének nagy részét nem is használják ki.

De hogyan állítsuk elő a számokat? Vegyünk egy unsigned char tömböt és kézzel írjuk be az összes pixelre, hogy milyen színű? Majdnem, de ez ilyen formába használhatatlan lenne. Semmi vizuális visszacsatolásunk se lenne, hogy a textúra hogy néz ki. Én ehelyett egy karakter tömbbe (magyarul stringbe) fogok ascii-art számokat rajzolni. Például a '.' karakter jelentsen fehér színt, a '*' feketét, a '+' meg szürkét. Én 8*8-as textúrákat fogok csinálni minden egyes számnak. A számokat én így álmodtam meg (ér ezeknél szebbeket csinálni):


"........"          "........"          "........"
"...**..."          "..***+.."          "..***+.."
"....*..."          "....+*.."          "....+*.."
"....*..."          "....+*.."          "....+*.."
"....*..."          "...+*+.."          "...**+.."
"....*..."          "..+*+..."          "....+*.."
"...***.."          "..****.."          "..***+.."
"........",         "........",         "........",

"........"          "........"          "........"
"....*..."          "..****.."          "........"
"...+*..."          "..*....."          "..+**..."
"..+**..."          "..***+.."          "..*....."
"..+.*..."          "....+*.."          "..***+.."
"..****.."          "....+*.."          "..*..*.."
"....*..."          "..***+.."          "..+**+.."
"........",         "........",         "........"


Technikailag már ezek a stringek is lehetnének unsigned char tömbök. És három szín van összesen, amit egy byte-on is bőven le lehetne írni. Viszont én az egyszerűség kedvéért inkább maradok a komponensenként egy byte-nál, a 3 komponensre összesen egy byte helyett, és nem kezdek el bitműveletekkel játszani a GL_R3_G3_B2-höz.

Termesztésen ezekből a szín előállítása egyszerűen két egymásba ágyazott for ciklus. Pl:


GLubyte texture_data[6][64][3];
for(int t = 0; t < 6; t++) {
  for(int i = 0; i < 64; i++) {
    switch(ascii_textures[t][i]) {
      case '*':
        for(int j = 0; j < 3; j++) {
          texture_data[t][i][j] = 0;
        }
        break;
      case '+':
        for(int j = 0; j < 3; j++) {
          texture_data[t][i][j] = 127;
        }
        break;
      default:
        for(int j = 0; j < 3; j++) {
          texture_data[t][i][j] = 255;
        }
        break;
    }
  }
}


Oké, megvan a textúra tartalma, és akkor most ezt hogyan adjuk oda az OpenGLnek?

Először is szükségünk van "egy textúra mutató pointerre/referenciára", egy névre, amivel hivatkozhatunk majd később a textúrára. Ez a referencia a textúrák esetében (és a modern OpenGLbe gyakorlatilag az összes objektum esetében) egy GLuint (unsigned int) lesz. A textúrázással kapcsolatos függvények dokumentációjába ez "a textúra neve"-ként lesz megemlítve.

Nekünk hat darab textúra kell, ezért definiáljuk egy 6 elemú GLuint tömböt:


GLuint tex[6];


Kérjük meg az OpenGL-t, hogy találjon ki nekünk hat darab textúra nevet. Ezek után ő ezekről a nevekről tudni fogja, hogy azok textúra vonatkoznak. Ehhez a glGenTextures(GLsizei n, GLuint *textures); függvényt használhatjuk.


glGenTextures(6, tex);


Fontos, hogy ilyenkor még a textúra nem jön létre. Az csak akkor történik meg, amikor először használni akarjuk (bindoljuk).

A textúrák beállítása

Nyilván egy for ciklusban fogom beállítani őket, csak az 'i'-edik textúra beállítását mondom el.

Ahhoz, hogy egy textúrán műveletet tudjunk végezni azt bindolni kell egy targethez. Képzeljünk el egy szobát, amibe vannak képkeretek. Az összes képkeret különböző alakú, mindegyikbe másféle képet tudunk rakni. De mindig csak éppen a képkeretben lévő képeket látjuk, csak azokkal tudunk dolgozni. Ezek a képkeretek pontosan ugyanúgy viselkednek mint az OpenGL-be a targetek. Két fontos target van, az egyik az egydimenziós texturáknak (GL_TEXTURE_1D), a másik a kétdimenziósoknak (GL_TEXTURE_2D). A házikban szinte mindig csak a kétdimenziósra lesz szükséged. A hasonlatban amikor egy képet berakunk a képkeretbe, az megegyezik az OpenGL-nél a bindolással. A bindolás után az összes textúra művelet arra a képre fog vonatkozni. Bindolni a glBindTexture(GLenum target, GLuint texture); függvénnyel tudunk.


  
glBindTexture(GL_TEXTURE_2D, tex[t]);


Ezután fel kell töltenünk a textúrát a glTexImage2D(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels) függvénnyel.

Elősször nézzük meg a paramétereit:

  • target - Ez nálad szinte mindig GL_TEXTURE_2D lesz.
  • level - Ide 0-t írj. Az OpenGL megengedi, hogy egy textúrának különböző felbontású változatait is feltöltsed, ami sokat javíthat a rendelt kép minőségén. Az alap textúránál kisebb felbontású képeket mipmap-nek hívják. Az 'i'-edik mipmap mérete minden dimenzióban fele az 'i-1'- ediknek. Ide azt írhatod be, hogy éppen hányadik mipmap-et töltöd fel (a 0. mipmap az alaptextúra). A mipmap-ek használata nagyon hasznos tud lenni, de nagyon veszélyes is. Ha ez a feature be van kapcsolva, és nem töltöd fel az összes mipmap-et, vagy valahova rossz felbontásút töltesz fel, akkor a textúra definíció szerint "incomplete" marad, és az incomplete textúrák úgy viselkednek, mintha nem is lennének ott. És ezt nagyon nehéz debuggolni, ezért a tárgyból ajánlott a mipmapek használatának kerülése.
  • internalFormat - Ez legyen GL_RGB vagy GL_RGBA. Itt azt kell megadnod, hogy az OpenGL milyen formátumba tárolja a képet. Például a tömörített formátumok nagyon hasznosak, de a házikhoz nem kell használnod őket. Hatékonysági okok miatt a komponensenként egy byteos GL_RGB textúra gyakran GL_RGBA formátumba tárolódik, mert az OpenGL csak 4 byteos egységeket tud hatékonyan írni / olvasni, de ezzel nekünk explicit nem kell foglalkozni, ezt majd a driver eldönti, hogy szerinte hogy jó.
  • width / height - Egyértelmű.
  • border - Ez mindenképpen legyen 0.
  • format - Ez is legyen GL_RGB vagy GL_RGBA. Itt adhatod meg, hogy milyen formátumban töltöd fel a képet. Ez nem feltétlen egyezik meg az internalFormat-al, más formátúmú tárolást is lehet tőle kérni, mint ahogy mi adjuk neki a textúrákat.
  • type - Ez legyen GL_UNSIGNED_BYTE. Ez azt adja meg, hogy milyen típussal írtad le a színeket.
  • pixels - A textúra elejére mutató pointer. Fontos megjegyezni, hogy a tömbünket úgy értelmezi, hogy a sorok mérete 4 byte-al oszható. Ha nem olyan (pl. 2*2-es RGB textúrát használtunk, akkor a sorokat ki kell egészíteni úgy nevezett "padding"-el, hogy illeszkedjenek a szóhatárra.

A mi esetünkben:


 
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 8, 8, 0, GL_RGB, GL_UNSIGNED_BYTE, texture_data[t]);


A textúrákkal kapcsolatban nagyon sok beállítási lehetőségünk van, amiket a glTexParameteri(GLenum target, GLenum pname, GLint param); függvénnyel tehetünk meg. Minket két fontos paraméter érdekel, a GL_TEXTURE_MIN_FILTER, és a GL_TEXTURE_MAG_FILTER és mindkettőnek a számunkra fontos lehetséges értékei: GL_LINEAR (ez a bilineáris szűrést bekapcsolja, amit a sugárkövetésnél is használtunk) és GL_NEAREST (ami a legközelebbi éréket választja). Mivel a GL_LINEAR hardwareből van megvalósítva, ezért gyakorlatilag semmivel se lassabb mint a GL_NEAREST, és általában szebb eredményt okoz (mint ahogy most is). Ami miatt azonban ezek fontosak, hogy a GL_TEXTURE_MIN_FILTER beállítása ennek a két értéknek az egyikére kikapcsolja a mipmap-ek használatát. Ennélkül a textúra úgy viselkedne, mintha nem is lenne ott.

A mi esetünkben:


glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);


Megjegyzés: a textúrákat "unbindolni" szoktuk, miután használtuk őket. Ez azt jelenti, hogy a 0 nevű, default textúrát használjuk, ami úgy viselkedik mint a null pointer, a rajta végzett műveletek nem csinálnak semmit. Ez nagyobb programoknál azért nagyon hasznos, mert ha valaki más hülyeséget csinál, akkor az nem a mi erőforrásunkat rontja el.

A textúrák használata

Ha azt akarjuk, hogy target befolyásolja a rajolást akkor azt a targetet engedélyezni kell pl. a glEnable(GL_TEXTURE_2D); és miután használtuk, ki kell kapcsolni a glDisable(GL_TEXTURE_2D);. Természetesen azt a képet kell bindolnunk, amit éppen használni akarunk. De hova kerüljön a kép az objektumon?

Ennek a specifikálásához textúrakoordinátákat is meg kell adnunk a glBegin()-glEnd() blokkban. Két dimenziós textúrák esetén a textúrakoordináták (0-1) tartománybeli float számpárok, ahol a (0, 0) jelképezi a kép bal alsó sarkát, az (1, 1) pedig a jobb felső sarkot. Azért a (0-1) tartományban használjuk őket, és nem egész pixeleket adunk meg, mert így ha a textúrát átméretezzük, a régi textúrakoordináták továbbra is jók maradnak, hiszen az a teljes mérethez képest relatív értékeket ad meg. Továbbá így használhatunk bilineáris interpolációt. A textúrakoordinátákat két dimenziós textúrák esetén a glTexCoord2f(GLfloat s, GLfloat t) függvénnyel adhatjuk meg.

A kocka kirajzolásakor annyi változtatásra szükség van, hogy most minden oldal külön glBegin()-glEnd() blobbkba kerül, mert egy ilyen blokkon belül nem lehet textúrát váltani. Az én megvalósításomban:


void glQuad(const Vector& a, const Vector& b, const Vector& c, const Vector& d) {
  glTexCoord2f(0, 0); glVertex3f(a);
  glTexCoord2f(1, 0); glVertex3f(b);
  glTexCoord2f(1, 1); glVertex3f(c);
  glTexCoord2f(0, 1); glVertex3f(d);
}

void drawCube(const Vector& size) {
    /*       (E)-----(A)
             /|      /|
            / |     / |
          (F)-----(B) |
           | (H)---|-(D)
           | /     | /
           |/      |/
          (G)-----(C)        */

    Vector s = size / 2;

    Vector A(+s.x, +s.y, -s.z), B(+s.x, +s.y, +s.z), C(+s.x, -s.y, +s.z), D(+s.x, -s.y, -s.z), 
           E(-s.x, +s.y, -s.z), F(-s.x, +s.y, +s.z), G(-s.x, -s.y, +s.z), H(-s.x, -s.y, -s.z);

    Vector vertices[6][4] = {
      {A, B, C, D}, {G, F, E, H}, {A, E, F, B},
      {D, C, G, H}, {B, F, G, C}, {E, A, D, H}
    };

    glEnable(GL_TEXTURE_2D); 
    
    for(int i = 0; i < 6; i++) {
      glBindTexture(GL_TEXTURE_2D, tex[i]); // Ezt semmiképpen se rakd a glBegin - glEnd blokk közé

      glBegin(GL_QUADS); {
        glQuad(vertices[i][0], vertices[i][1], vertices[i][2], vertices[i][3]);
      } glEnd();
    }

    glBindTexture(GL_TEXTURE_2D, 0);
    glDisable(GL_TEXTURE_2D);
}


A teljes forráskód: Textúrázott kocka

A textúrák és a megvilágítás

A megvilágítást nem tudjuk a textúrák színeivel számolni. Valójában a rajzolási csővezetékben a textúrázás még időben is később történik, mint a megvilágítás számolása. Amit viszont tudunk tenni, hogy fehér színt állítunk be a megvilágítás számításakor, és az eredményt - hogy az adott felületi pont mennyire világos - megszorozzuk (moduláljuk) a konkrét színnel. Ezt a glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); függvénnyel lehet bekapcsolni, de alapértelmezetten is be van kapcsolva. Ha nem akarunk megvilágítást, csak a textúra színét akarjuk megjeleníteni akkor a GL_MODULATE-et lecserélhetjük GL_REPLACE-re. Ezzel a függvénnyel még rengeteg hasznos dolgot lehetne csinálni, de azok a házikhoz általában nincsenek megengedve.

Ne feledjük, hogy ahhoz, hogy a megvilágításnak szép eredménye legyen továbbra sem elég, ha a kocka oldalai egyetlen quadból állnak. De a textúrázás bemutatásához önmagában az elég volt.

Példaprogram: Megvilágított, textúrázott kocka

http://i.imgur.com/xT1d1ur.png http://i.imgur.com/XE7dZAl.png

Nem kettőhatvány méretű textúrák

Gyakran felmerülhet az igény nem kettőhatvány méretű textúrára. Például az előző példák kockájából egy dobókockát akarunk csinálni. Egy ilyet:

http://i.imgur.com/3benYFM.png

De ez a mintázat egy 7*7-es textúrát igényel, pl az öt így néz ki:


"......."
".*...*."
"......."
"...*..."
"......."
".*...*."
"......."


A hét köztudottan nem kettőhatvány. Ha megpróbáljuk ugyanúgy használni, mint ahogy a 8x8-as textúrával tettük, vajon működni fog?

Természetesen nem...

http://i.imgur.com/NsKm5AK.png

De ennek a problémának az eddigi ismereteink alapján nem szabadna meglepőnek lennie, még akkor se ha nem tudtuk, hogy csak kettőhatvány textúrákat lehet használni. Természetesen itt azzal van a gond, hogy az OpenGL szóhatárra illeszkedő sorokat vár, viszont mi 7*3 = 21 byteos sorokat töltöttünk fel. Az OpenGL viszont úgy fogja értelmezni a textúránkat, mintha az 7*8-as lenne, aminek hatására jó pár szín elcsúszik, és egy kevés memóriaszemét is kerül a színek közé (az utolsó sorba). Ennek megoldására én három egyszerű megoldást tudok, és az egyszerű megoldásokon kívül egy olyat amit a beadón is lehet használni. Az egyszerű megoldások:

  • Írjuk be az onInitbe az alábbi sort: glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - Ennek hatására az OpenGL egy bájtos határra ileszkedő sorokat vár.
  • Minden sor végére rakjunk be egy plusz karaktert, és egy 7*8-as tömbre mutató pointer adjunk oda a glTexImage2D-nek. De fontos, hogy továbbra is azt mondjuk neki, hogy 7x7-es textúrát töltünk fel. Ez valószínűleg a legnehezebben emészthető megoldás, úgyhogy erről töltöttem fel kódot: Dobókocka 7x8-as textúrával.
  • RGB textúra helyett használjunk RGBA textúrát.

De várjunk csak, nem arról volt szó, hogy az OpenGL csak kettőhatvány méretű textúrákkal tud dolgozni? De.. ez szabvány szerint igaz... csak a mi gépünk nem tud róla... A 90-es években még problémát okozott a videókártyáknak a nem kettőhatvány méretű textúrák kezelése, de már több mint tíz éve tetszőleges méretű textúrával is ugyanolyan hatékonyan tud dolgozni az összes videókártya. Ez viszont sovány vigasz a beadón... ahol a nem kettőhatvány méretű textúrák nincsenek implementálva.

A beadón működő megoldás:

  • Egészítsük ki 8x8-asra a textúrát. A szélére másoljuk be a legközelebbi valid szomszédokat, mert bilineáris szűréskor ezeknek az értékeknek is látjuk a hatását.
  • Buheráljuk meg a textúrakoordinátákat, úgy, hogy 0-tól 7/8-ig menjenek. Ezzel persze elvesztjük azt az előnyt, hogy a textúrakoordináták függetlenek magának a textúrának a paramétereitől, és a textúra lecserélése a textúrakoordináták lecserélését is magával vonja.
  • Ennek a megoldásnak a kódja: Dobókocka 8x8-as textúrával
http://i.imgur.com/vGETS3t.png http://i.imgur.com/3benYFM.png

Persze a dobókocka egyeseknek jobban tetszhet GL_NEAREST szűréssel, a GL_LINEAR helyett. Ez teljesen ízlés kérdése, amúgy így néz ki bilineáris szűrés nélkül:

http://i.imgur.com/N3uYYBc.jpg http://i.imgur.com/Ai76YUE.jpg

Egyszerű kamera

A végleges házikban a kamera általában valamilyen mozgatható objektumot követ. Ennek az implementálása nagyon egyszerű, minden rajzolás elején egységmátrixot töltünk a GL_MODELVIEW-ba, majd a gluLookAt(objektum pozíciója, objektum pozíciója + orientációja, felfele) függvényhívással megoldható.

Viszont ezzel nehézkes a debuggolás, nem lehet igazán jól körülnézni, és valós játékokban is ritka az ennyire egyszerű kamera.

Én egy egyszerű, szabadon-repülő kamera egy implementációjához adok ötletet, ami sokat segíthet a debuggoláshoz (sokszor csak egy nézetből nézve egy jelenetet nem lehet eldönteni, hogy az jó-e).

Kétféle input érdekel minket, a billentyűlenyomások (W,A,S,D), és az egér mozgatása (úgy, hogy közbe az egyik egérgomb le van nyomva).

A billentyűlenyomásokat nem kezelhetjük egyszerűen az onKeyboard-ban, ez akkor generál eseményeket, amikor egy karaktert begépelnénk, ami pl megtörténik először a billentyű lenyomásakor, aztán jelentős ideig (kb. 0.3 - 0.5 sec) nem generálódik újabb esemény, majd után másodpercenként kb. 10-20 karakterbeütés-t generál. Ez pl egy karakter irányításához teljesen használhatatlan, helyette inkább tároljuk el, hogy az egyes billentyűk lenyomott állapotban vannak-e.


enum ControllKeys {W, A, S, D, keys_num};
bool keys_down[keys_num]; 

void onKeyboard(unsigned char key, int, int) {
  switch(key) {
    case 'w': case 'W':
      keys_down[W] = true;
      break;
    case 's': case 'S':
      keys_down[S] = true;
      break;
    case 'a': case 'A':
      keys_down[A] = true;
      break;
    case 'd': case 'D':
      keys_down[D] = true;
      break;
  }
}

void onKeyboardUp(unsigned char key, int, int) {
    switch(key) {
    case 'w': case 'W':
      keys_down[W] = false;
      break;
    case 's': case 'S':
      keys_down[S] = false;
      break;
    case 'a': case 'A':
      keys_down[A] = false;
      break;
    case 'd': case 'D':
      keys_down[D] = false;
      break;
  }
}


Ahhoz, hogy ezt fel tudjuk használni, a kamerának tudnia kell, hogy hol van, merre néz, és milyen gyorsan tud mozogni.


  Vector fwd, pos;
  const float speed;

  void updatePos(float dt) {
    Vector up = Vector(0, 1, 0), right = cross(fwd, up).normalize();
    up = cross(right, fwd).normalize();

    if(keys_down[W] && !keys_down[S]) {
      pos += fwd * speed * dt;
    } else if(keys_down[S] && !keys_down[W]) {
      pos -= fwd * speed * dt;
    }

    if(keys_down[D] && !keys_down[A]) {
      pos += right * speed * dt;
    } else if(keys_down[A] && !keys_down[D]) {
      pos -= right * speed * dt;
    }
  }


Ennek a meghívását pl az onIdle-ben tehetjük meg az alábbi módon:


void onIdle() {
  static float last_time = glutGet(GLUT_ELAPSED_TIME);
  float time = glutGet(GLUT_ELAPSED_TIME);
  float dt = (time - last_time) / 1000.0f;
  last_time = time;

  camera.updatePos(dt);
  glutPostRedisplay(); 
}


Továbbá az egér mozgatást is kezelnünk kell.


  const float mouse_speed;

  void updateDir(int dx, int dy) {
    Vector y_axis = Vector(0, 1, 0), right = cross(fwd, y_axis).normalize();
    Vector up = cross(right, fwd).normalize();

    // Ha teljesen felfele / lefele néznénk, akkor ne forduljon át a kamera
    float dot_up_fwd = dot(y_axis, fwd);
    if(dot_up_fwd > 0.95f && dy > 0) {
      dy = 0;
    } 
    if(dot_up_fwd < -0.95f && dy < 0) {
      dy = 0;
    }

    // Módosítsuk az nézeti irányt
    fwd += mouse_speed * (right * dx + up * dy);
    fwd = fwd.normalize();
  }


Ennek a meghívására pedig az onMouseMotion függvény alakalmas:


int last_x, last_y;
void onMouse(int, int, int x, int y) {
  last_x = x;
  last_y = y;
}

void onMouseMotion(int x, int y) {
  camera.updateDir(x-last_x, last_y-y);
  last_x = x;
  last_y = y;
}


A kamera beállítása ezek után már csak egyetlen függvényhívás:


gluLookAt(pos.x, pos.y, pos.z, pos.x+fwd.x, pos.y+fwd.y, pos.z+fwd.z, 0, 1, 0);


Egy ilyesfajta kamera már a közepesen komplex jelenetek összeállításban is nagyon sokat tud segíteni.

Az én implementációim: Kamera

Graftutorial kamera anim.gif

Utóhang

Én nagyon élveztem az összefoglaló megírását, remélem Te is hasonló élményekkel gazdagodtál a házijaid megírásakor. Amikor én a tárgyat csináltam ilyen részletes segédlet még nem volt, bár nekem hasznos lett volna. Annak ellenére, hogy már a tárgy felvétele előtt is rengeteget foglalkoztam grafikával, még a beadott házijaimba is voltak elvi hibák, amikre itt külön felhívtam a figyelmet, hogy ti ne kövessétek el. Ha ez az összefoglaló segített a házijaidban, vagy valami újat tanultál belőle, próbálj meg te is segíteni az utánad jövőknek azokból a tárgyakból, amikből Te jó vagy!


Csala Tamás - 2014.01.