Számítógépes grafika házi feladat tutorial
Ez az oldal a korábbi SCH wikiről lett áthozva.
Ha úgy érzed, hogy bármilyen formázási vagy tartalmi probléma van vele, akkor, kérlek, javíts rajta egy rövid szerkesztéssel!
Ha nem tudod, hogyan indulj el, olvasd el a migrálási útmutatót.
Ez a tutorial 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.
Az oldalról kódot a saját házidba átemelni TILOS! Még ha pár sornyi kódról is van szó, gépeld be szépen magadtól, addig is gyakorlod a dolgot. Meg persze nem is érdemes másolgatni, mert csak borzolod vele a plágiumkereső idegeit, és sokban ronthat az esélyeiden az aláírás megszerzésére
Ez a lap még erősen fejlesztés alatt áll, ha van ötleted akkor írd bele nyugodtan, vagy egészítsd ki az egyik hiányzó TODO részt
FAQ (avagy a grafika listán a legtöbb levelet generáló "misztikus" hibák)
- Fordításkor ilyen hibát kapok: error: stray ‘\357’ in program... - UTF-8 BOM hiba
- Az osztást tartalmazó számításom rossz eredményt ad - Egészosztási hiba
- A 3D-s kép zajos, az élek cikk-cakkosak - Z-Fighting
- Sugárkövetéskor a testen fura fekete részek jelennek meg - Önárnyékolás
- Sugárkövetéskor az egész képem fekete / fehér - Tone Map használata
- glQuad használatakor négyszög helyett két egymásba csúszott háromszög jelenik meg - QUAD
Tipikus C++ hibák
- Ha stabil C++ tudással rendelkezel, ezt a részt nyugodtan ugord át
- UTF8 BOM problémák:
- Ha a kódodat UTF-8 kódolással mented, sok szerkesztő a fájl elejére biggyeszt három nem látható karaktert, az úgy nevezett UTF-8 Byte Order Mark-ot. Ez a Gordiuson fordítási hibát fog eredményezni, ezért a kódunkat feltöltés előtt mindenképpen konvertáljuk sima ASCII/ANSI szövegfájllá (Még a Jegyzettömb is képes erre, Mentés másként - ANSI)
- Lokális változók scope-on kívüli használata:
- Figyeljünk rá hogy a lokális változók csak addig léteznek, amíg a scope-on belül járunk
- Ennek megfelelően ne adjuk át őket cím szerint, ha mégis referenciát akarunk készíteni ilyen változóról, használjunk const referenciát!
- Egészosztás véletlen használata:
- A C++-ban az operandusok típusától függ, hogy egészosztás (div) vagy rendes osztás történik
- Ez a működés nehezen detektálható hibákat okozhat, ezért mindig figyeljünk rá! Például ha egész típusú változót osztunk egy számmal, az eredmény egész lesz, hiába akarjuk egy lebegőpontos változóban tárolni:
int a=5; float b=a/10; //értéke nem 0.5 lesz, hanem 0!
- A hiba elkerülése végett használjunk cast-olást vagy típusos konstansokat:
float b=(float)a/10; float b=a/10.0f;
- Hiányzó break a switch szerkezetben
- A C++-ban a switch szerkezetének egyszerre több ága is lefuthat! Ha ezt nem szeretnénk, figyeljünk rá hogy minden ágat zárjunk le a break; paranccsal!
- TODO: rossz konstruktor hívások
Az OpenGL és GLUT alapjai
- Az OpenGL alapja egy állapotgép. Ha valamit beállítasz (pl. rajzolószínt) az úgy is marad, amíg nem módosítod.
- Az OpenGL alacsony szinten programozható, sebesség-orientált rendszer. Nem képes olyan "extrákra" mint árnyékolás, 3D tesszelláció stb., ezek megoldását a programozóra bízza.
- A függvények neve a Hungarian Notation elveit követi, azaz:
- 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, a szavak első betűje nagy
- a függvény végén gyakran megadjuk a paraméterek típusát, így megkülönböztetve a különböző meghívásokat (pl.: 3f - 3 db float, v - vector)
- pl.: glVertex3f()
- 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!)
- onInitialization() - inicializáló rész, pl. globális változók inicializálására. Figyeljünk rá hogy bizonyos GLUT függvények itt még nem használhatók, hisz pl. még nem jött létre az ablak amire rajzolunk.
- onKeyboard() - billentyűzet kezelés, a keret példája alapján eléggé egyértelmű, ne feledjük az újrarajzolást (glutPostRedisplay)
- onMouse() - egér esemény kezelő, a keret példája alapján érthető. Figyeljünk rá, hogy a kapott koordináták nem a mi világkoordinátáink, hanem az oprendszer által átadott ablak koordináták! Ezek jellemzően bal fentről jobb lefelé nőnek, 2D-ben némi matekozással átszámolhatjuk világkoordinátákra.
- onIdle() - Az időzítést és animációt megoldó függvény, akkor hívódik meg ha a gépnek éppen nincs jobb dolga. Részletesen lásd az animációról szóló részben.
- Az openGL-ben a színek RGB értékekkel adhatók meg, de a megszokottól eltérően nem a 0-255 tartományban, hanem a 0-1 tartományban. A (0,0,0) tehát a fekete, az (1,0,0) a piros, míg az (1,1,1) szín a fehéret jelenti.
Koordináta-rendszerek és 2D/3D projekció
- Az alapértelmezett koordináta-rendszer X, Y (és Z) koordinátái -1 és +1 között mozognak. 2D-ben (-1,-1) a bal alsó, (+1,+1) a jobb felső sarok. Ezeket ha nem muszáj, ne bolygassuk!
- Ha mégis módosítani kell, ez 2D-ben a gluOrtho2D(), 3D-ben a glFrustum() függvényekkel lehetséges.
- Az OpenGL 3D-s koordináta-rendszere jobb-sodrású, azaz ha az Y-tengely felfelé, míg az X-tengely jobbra mutat, akkor a Z-tengely felénk mutat kifelé a képernyőből
- Az alapértelmezett projekció a 2D-s orthogonális vetítés, 2D-s rajzoláshoz tökéletesen megfelel. 3D-ben általában perspektivikus vetítést használunk, ezt a gluPerspective() függvénnyel állíthatjuk be. Ennek paraméterezése nem triviális, erről bővebben a 3D-ről szóló részben olvashatsz.
- Minden projekció módosító függvény valójában az OpenGL projekciós mátrixait állítja át. Erről bővebben a projekcióról szóló részben olvashatsz. Elöljáróban annyit, hogy ezeket a függvényeket csak egyszer hívd meg (pl.: onInitialization-ben), mert többszöri meghívásuk hatása összeadódik!
2D rajzolás
- A rajzoláshoz válasszunk egy primitívet, majd adjuk meg annak pontjait. A pontok megadása a glBegin() és a glEnd() parancsok között történik. A primitívek neve és használata a következő képen láthatók:
- A glBegin paraméterében kell megadni a rajzolandó primitívet (csupa nagybetűvel, mert ezek beépített konstansok)
- A primitívek egyes pontjait 2-dimenzióban a glVertex2f() illetve 3-dimenzióban a glVertex3f() függvényekkel célszerű megadni. A primitívek ugyanúgy használhatók 2 és 3 dimenzióban is.
- Ha különálló háromszögeket/négyszögeket szeretnénk rajzolni, nem kell a 3 vagy 4 koordinátából álló blokkokat külön-külön glBegin-nel és glEnd-el körülvenni, írhatjuk folyamatosan őket, az openGL eleve 3-asával/4-esével fogja őket értelmezni.
- Pl.:
glBegin(GL_TRIANGLES); glVertex2f(0,0); glVertex2f(0.2,0.2); glVertex2f(0.2,0); glVertex2f(0.6,0.8); glVertex2f(0.9,0.3); glVertex2f(0.5,0.9); glEnd();
- QUAD-ok használatakor figyeljünk rá, hogy a 4 pontot a négyszög körüljárása szerint adjuk meg. Pl. Bal fölső, jobb fölső, jobb alsó, bal alsó. Ha itt az utolsó két pontot felcserélnénk, akkor két egymásba csúsztatott háromszöget látnánk.
Projekciós mátrixok, transzformációk
- Az OpenGL a megjelenítő csővezetékben két transzformációs mátrixot használ, ezek a ModelView és a Projection mátrixok. A keretrendszer nem módosítható részében mindkét mátrixba az egység mátrixot töltik a glLoadIdentity() függvény segítségével. Ez alkotja a már említett 2D orthogonális vetítést (-1,+1) méretű koordináta-rendszerben.
- A két mátrix között csak elvi különbség van. A keretrendszerben is használt glMatrixMode(GL_MODELVIEW) ill. glMatrixMode(GL_PROJECTION) paranccsal állíthatjuk be, hogy melyiket szeretnénk módosítani. Mivel a keretben utoljára a GL_PROJECTION kerül beállításra, ezért innentől minden függvényhívás azt módosítja. Ezt hagyhatjuk így, a feladat egy mátrixal is tökéletesen megoldható.
- Ezek a mátrixok három módon módosíthatók:
- Projekció módosító függvénnyek: glOrtho2D, gluPerspective... - lásd a Koordináta-rendszerek részben
- Transzformációs függvénnyek: glRotatef, glTranslatef és glScalef
- a függvények végén szereplő f a float paramétereket jelenti
- *glRotatef(angle,x,y,z)*: forgatás angle fokkal az X, Y, Z irányú tengely körül. Ha pl. a Z tengely mentén szeretnénk forgatni 90 fokkal, akkor a következő paramétereket adjuk meg: (90,0,0,1)
- *glTranslatef(x,y,z)*: eltolás a 3 tengely mentén megadott mértékkel
- *glScalef(x,y,z)*: nagyítás a 3 tengely mentén megadott szorzóval. Egynél kisebb értékkel nyilván kicsinyítünk
- a függvények magát a koordináta rendszert változtatják, így ha valamit két egységgel el szeretnénk tolni az X tengelyen, használjuk a glTranslatef(2,0,0) függvényt, majd rajzoljunk az origóba, ami ugye most már két egységgel arrébb került
- a Z tengelyre vonatkozó paramétert is mindig meg kell adni, bár 2D rajzolás esetén nincs hatása
- Az első házikhoz fölösleges ilyen transzformációs függvényeket használni, egyszerű 2D elforgatás kiválóan megoldható sin és cos függvények használatával. Arra azonban figyeljünk, hogy ezek a függvények radiánban várják a szög értékét!
- Közvetlen mátrix-beállító függvények: glLoadMatrix, glMultMatrix - ezekre jó eséllyel nem lesz szükség
- Az OpenGL állapotgép jellege miatt a mátrixok módosítása maradandó, így meg kell oldanunk a mátrixok visszaállítását:
- A mátrixokba az alapértelmezett egységmátrix tölthető a glLoadIdentity paranccsal. Ha ezt a módszert használjuk, az onDisplay elején mindig be kell töltenünk az egységmátrixot, majd az esetleges projekciót. Nem szép megoldás.
- glPushMatrix, glPopMatrix - Az openGL beépített mátrix-vermét hívjuk segítségül. A glPushMatrix() függvénnyel eltároljuk az aktuális mátrixot, míg a glPopMatrix() meghívásával visszatöltjük. Ha például szeretnénk egy rajzolást eltolni, de utána nem szeretnénk a módosult koordináta-rendszerrel bajlódni, tegyük a következőt:
glPushMatrix(); glTranslatef(0.5,0,0); //rajzolás az eltolt helyen glPopMatrix(); //rajzolás az eredeti helyen
- A Push és Pop nem véletlenül egy vermet használ a mátrixok tárolására. Egymásba ágyazott Push-Pop párok segítségével hierarchikus objektumok kirajzolását könnyedén megoldhatjuk:
Vegyünk példának egy pálcikaember kirajzolását. Az origó legyen a pálcikaember testének közepe. Rajzoljunk egy vonalat (0,-0.5) és (0,+0.5) pontok között. Tároljuk el az aktuális koordináta-rendszert a glPushMatrix meghívásával. A lábszár kirajzolásához először toljuk el az origót a glTranslatef(0,-0.5,0) paranccsal a test aljához, hívjunk újabb Push-t, majd forgassuk el a koordináta-rendszert 30 fokkal a Z tengely mentén: glRotatef(30,0,0,1). Ekkor a 0.8 hosszú lábat egyszerűen megrajzolhatjuk a (0,0) és a (0,-0.8) közötti vonallal, mely már elforgatva és megfelelően eltolva jelenik meg. Ha vissza szeretnénk térni az elforgatás nélküli koordináta-rendszerhez, csak hívjuk meg a glPopMatrix függvényt. Ez után az ellenkező irányba forgatva (-30 fok) megrajzolhatjuk a másik lábat. Két glPopMatrix hívás után visszatértünk az eredeti origóba a test közepéhez, és folytathatjuk a test többi részének kirajzolását.
Ugyanez kódban:
glBegin(GL_LINES); glVertex2f(0,-0.5); glVertex2f(0,+0.5); glEnd(); //test kirajzolása glPushMatrix(); glTranslatef(0,-0.5,0); glPushMatrix(); glRotatef(30,0,0,1); glBegin(GL_LINES); glVertex2f(0,0); glVertex2f(0,-0.8); glEnd(); //egyik lába glPopMatrix(); glPushMatrix(); glRotatef(-30,0,0,1); glBegin(GL_LINES); glVertex2f(0,0); glVertex2f(0,-0.8); glEnd(); //másik lába glPopMatrix(); glPopMatrix(); //további testrészek kirajzolása
Görbék
- A görbékről igen sok anyag található a könyvben, diákon, még itt a wiki-n is, így csak néhány hasznos tippet és programozás-technikai dolgot írok itt le
- A görbe egy elvont matematikai fogalom, a kirajzoláskor nem használjuk! A görbéket vektorizáljuk (vonalakra bontjuk), és ezeket a törött-vonalakat rajzoltatjuk ki az openGL segítségével.
- A program elején hozzuk létre a görbéket, vektorizáljuk őket, és a vonalak kezdő és végpontjait tároljuk el. Semmiképpen se az onDisplay-ben számoljuk ki minden egyes kirajzoláskor a görbe pontjait (ha csak nem változó alakú görbéről van szó)! A görbére a vektorizálás után már nincs is szükségünk, csak a vonalakat rajzoljuk ki.
- A görbék egyik közös vonása, hogy az egész görbét (vagy annak egy szegmensét) egy folytonos t változóval járhatjuk végig. Vektorizáláskor ezt a t változót léptetjük végig 0 és 1 között adott lépésközzel, így megkapjuk a görbe egyes pontjait, amit utána egyenessel összekötve közelítünk. A t végigjárásának lépésköze lesz a törött-vonal felbontása.
- A görbék kontrollpontjainak megadása nem egyszerű, a rajzolás és a tesztelés egyszerűbbé tehető, ha a görbénket egy másik program segítségével "megtervezzük". Ilyen Java-applet-eket könnyen találhatunk az Interneten, elég rákeresni a "<görbenév> applet" kifejezésre, és máris grafikus felületen kattingathatjuk össze a görbénket.
Időzítés, animáció
- Az animáció kezelésére és az újrarajzolás elhelyezésére (a Gordiuson történő hibás működések miatt) sokféle megoldás született. Én a szerintem leglogikusabb elrendezést írom itt le, amely garantáltan működik otthon és a Gordiuson is. Az animáció elve a következő:
- Létezik a világ modellje, gyakorlatilag a globális változóink. Azért globálisak, hogy elérhetőek legyenek a különböző GLUT függvényekből. A modellt csak az események, azaz az onKeyboard, onMouse és onIdle függvények módosíthatják.
- Van egy jól megírt onDisplay részünk, ami a modell alapján kirajzolja a világ aktuális állapotát. Az onDisplay nem módosítja a modellt. Az onDisplay-ben ne hívjuk meg saját magát, illetve ne hívjunk glutPostRedisplay-t!
- A keretrendszer időnként úgy érzi, hogy ráérne újrarajzolni a képet, ilyenkor meghívja az onIdle függvényt. Ez a függvény nem rendszeres időközönként hívódik meg, meghívását tekintsük véletlenszerűnek!
- Az onIdle függvényben kiszámítjuk az eltelt időt, ez alapján módosítjuk a modellt, majd meghívjuk a glutPostRedisplay-t a világ újrarajzoltatásához.
- Nem gond ha minden onIdle hívásra meghívjuk a glutPostRedisplay-t, bár kicsit pazaroljuk az erőforrásokat, de legalább garantáltan minden módosítás érvényesül a képen is.
- Az eltelt idő számításához kérjük le a program indítása óta eltelt időt. Ezt a _glutGet(GLUT_ELAPSED_TIME)_ függvény adja vissza egy long típusú változóban. Ha az onIdle végén egy globális változóba eltároljuk az így kapott értéket, akkor az onIdle következő hívásakor már kiszámíthatjuk a két hívás között eltelt időt. Ezt a globális változót az onInitialization-ben állítsuk 0-ba, nehogy az esetleg benne lévő memóriaszemét az első onIdle híváskor indokolatlanul elmozdítsa a modellünket.
- Ezek alapján az onIdle felépítése:
//globális változók long previousTime; void onIdle( ) { long time = glutGet(GLUT_ELAPSED_TIME); long deltaTime = previousTime - Time; //... - mozgatás deltaTime alapján previousTime = time; //akt. idő elmentése glutPostRedisplay();
- Saját gép VS. Gordius
- A saját gépünkön teszteléskor az onIdle igen gyakran meghívódik. A program szabad idejében folyamatosan hívogatja, és minden onIdle végén újrarajzolja a képet. Ennek eredménye a folyamatos CPU használat és több száz FPS-en futó program. (A játékprogramokba azért tesznek Limit Framerate kapcsolót, hogy elkerüljék hogy az indokolatlanul magas FPS generálás teljesen elfogyassza a CPU-t)
- Az ilyen sűrű hívás rendkívül rövid eltelt időket eredményez, ezért készüljünk fel, hogy két hívás között alig mozdul az animációnk. Ha az elmozdulást tároljuk, mindenképpen float vagy double változóban tegyük, hiszen könnyen lehet hogy az elmozdulás nem éri el az egy egész egységet.
- A Gordius ezzel szemben összesen 3-4-szer hívja meg az onIdle-t, pontosan annyiszor, ahány képet generál. Ezen meghívások között viszont több másodperc is eltelhet, ezért készítsük fel a programot az igen nagy eltelt idők lekezelésére is!
- A ritka onIdle hívások tesztelésére jó módszer, ha az ablakunkat fejlécénél fogva megfogjuk, majd csak pár másodperc utána engedjük el. Ekkor ugyanis az ablak mozgatása közben nem rajzolódik újra a kép, az onIdle csak az elengedés után fog legközelebb meghívódni.
- Animáció:
- Az animálás alapja a két onIdle hívás között eltelt idő, amit ezredmásodpercben kapunk meg. Ha elmozdulást, forgást vagy hasonló mozgást animálunk, használjuk a jól ismert u = v*t képletet
- Ha ciklikusan ismétlődő cselekvést akarunk animálni, figyeljünk rá hogy az eltelt idő lehet nagyobb mint a ciklusidő. Ha pl. 2 másodpercenként újrainduló animációnk van, ki kell tudnunk számolni, hogy 6.8 másodperc múlva hogy áll a modellünk. Ennek számítása a következő módon történhet:
- Használhatunk pl. modulo osztást valós számokkal. (Vigyázat, a % operátor csak egészekkel működik, így itt nem használható!) A példánál maradva: hogy megtudjuk, hogy az eltelt 6.8 mp után az aktuális cikluson belül hol tartunk, számoljunk a következő módon: 6.8 - ( floor(6.8/2.0) * 2.0 ) = 0.8
- Ha az ilyen matematikázás már túl bonyolultnak tűnik, használjunk egy egyszerű while ciklust: while (deltat > 2.0) { deltat-=2.0; } Lassabb, de biztosabb megoldás, legalább nem egy elrontott osztáson múlik az animáció működőképessége.
Tesszelláció
- A tesszelláció a világban található objektumok felbontása háromszögekre. Hogy miért van erre szükség? Mert az openGL csak vonalakat és háromszögeket képes kirajzolni, minden test ezekből épül fel.
- 2D házik
- Már itt is megjelenhet tesszelláció. Ha egy görbe vonallal körülvett kitöltött síkidomot várnak tőlünk, azt nyilván háromszögekre bontva tudjuk kirajzolni.
- Ha a síkidomunk konvex, vagy "majdnem konvex" akkor szerencsénk van. Vegyünk egy olyan pontot, mellyel a görbe összes másik pontja összeköthető anélkül hogy metszenénk az éleket. Konvex síkidom esetén ez nyilván bármelyik pont lehet. Ha a választott ponttól kezdve körüljárjuk a síkidomot, és a kapott pontokat egy TRIANGLE_FAN pontjainak adjuk meg, akkor már kész is a kirajzolás.
- Ha a síkidom konkáv, akkor sajnos egy bonyolultabb megoldásra lesz szükségünk. Ez a fülvágó algoritmus. Ennek megvalósításáról olvashatunk a diákon vagy akár internetes tutorialokban is, erre most nem térnék ki.
- Sugárkövetéses házik - Ha a házi háromszöghálót vagy tesszellált testet említ, akkor bizony nekünk kell leprogramozni a bonyolult test háromszögekre bontását. Ez a 3D-s házikhoz hasonlóan megoldható, kicsit lejjebb olvashatsz róla. A kész háromszögeket tároljuk el, hiszen ezekre egyesével meg kell majd hívnunk a sugár ütköztetést.
- *3D házik
- Bár a glu és glut könyvtárak sokféle könnyen használható objektumot tartalmaznak, ezek a házikban *nem használhatóak*!
- TODO: árnyaló normálok
- TODO: tipikus testek
Sugárkövetés
- A sugárkövetés házinak nem sok köze van az openGL-hez, inkább sok 3D-s koordináta-geometria és némi fizika szükséges hozzá.
- A sugárkövetés nagy hátránya, hogy mivel mi írjuk a "renderelő motort", így igen nehéz a tesztelés és debuggolás, hiszen egy apró hiba miatt is előfordulhat hogy a képen semmit sem látunk. Könnyítsük meg a saját dolgunkat azzal, hogy egy papírra lerajzoljuk a testek elhelyezkedését, többször is ellenőrizzük a tengelyek irányát. Ha sehogy nem azt látjuk amit szeretnénk, írjuk ki a változóink értékét a konzolra, és úgymond szövegesen teszteljük a programot.
OpenGL rész
- Sugárkövetésnél OpenGL függvényt csak a kész kép kirajzolásához használunk, a következő módon:
glRasterPos2d(-1,-1); glDrawPixels(600,600,GL_RGB,GL_FLOAT,picture);
- A RasterPos segítségével a pixelenkénti rajzolás kezdő helyét adjuk meg, (-1,-1) a bal alsó sarok.
- A DrawPixels hívásal egy 600*600-as képet rajzolunk pixelenként a rasztertárba. A képet a picture tömb tárolja, melynek minden eleme RGB adatokat tárol float típussal. Az egész sugárkövetés célja, hogy ezt a szín-táblázatot feltöltsük az oda érkező fény színével és intenzitásával.
- Egy ilyen tömböt a következő módon definiálhatunk: Color picture[600][600]; ahol Color egy 3 float értéket tartalmazó osztály. A színkomponensek természetesen itt is 0-1 skálán értendők.
- A 600*600 pixel nem véletlen, a keretben ez az ablak fixen megadott mérete.
Osztályok
- Megkönnyítjük a saját dolgunkat, ha az alábbi osztályokat létrehozzuk, és azokhoz megfelelő operátorokat készítünk:
- Vector/Coordinate/Point
- Elnevezés ahogy tetszik, a lényeg hogy egy 3D-s vektor X,Y,Z koordinátáit tárolja double vagy float típusú változókban
- Ugye tudjuk hogy a vektor lehet helyvektor és irányvektor is, erre nem kell két külön osztály
- Hasznos operátorok: +=, *=, +, *, skaláris szorzat, vektoriális szorzat
- Hossz lekérdezés (egyszerű Pitagorasz tétel 3D-ben), illetve normalizálás (a koordinátákat külön-külön leosztjuk a hosszal, így a vektor hossza 1 lesz)
- Az irányvektorokat mindig normalizáljuk, ezzel megkönnyítjük a későbbi számításokat
- Color - szigorúan float típusú adattagok a DrawPixels-el való kompatibilitás végett. A színkomponensek 0 és 1 között mozognak, ám a sugárkövetéskor sokszor a színen valójában intenzitást értünk, így 1-nél nagyobb értékeket is felvehet (pl.: a sugarak színe "összeadódik") Figyeljünk rá hogy ekkor a végső kép pixeleit 0 és 1 közé hozzuk -> Tone Mapping
- Ray - sugár, melynek van kiinduló pontja és iránya (normalizálva)
- Triangle
- A tesszellált testek alapvető építőeleme, három pontja és egy normál-vektora van, mely a síkjára merőleges két irány egyikébe mutat. Ez határozza meg, hogy melyik a háromszög "előlapja" és melyik a "hátlapja".
- Létfontosságú függvénye a metszéspont keresés, amely egy sugárra visszaadja, hogy az metszi-e, és ha igen, hol metszi a háromszöget. Lényeges lesz majd a sugár kiindulópontjának valamint a metszéspontnak a távolsága is. Ha ez az érték negatív, a háromszög a sugár "mögött" van, így nem találja el.
- Mesh - háromszögekből álló test, mely egy közös metszéspont kereső függvényt definiál, az összes háromszög metszéspontjai közül azt adja vissza, amelyiknek legkisebb a távolsága (de még pozitív), tehát amit legelőször talál el a sugár.
- Vector/Coordinate/Point
Metszéspontok, önárnyékolás és az EPSILON
- TODO
Sugár indítások, egy és kétirányú sugárkövetés
- TODO: egy/kétirányú: ...
- Az indítandó sugarak létrehozásához nekünk kell definiálnunk a perspektivikus kamerát, hasonlóan ahhoz, ahogy az openGL 3D perspektivikus leképzését definiáljuk: TODO: ...
Színek, fények, optikai modellek
- Tone Map
- A megkapott nagy intenzitáskülönbségeket tartalmazó képünket kirajzolás előtt a 0-1 tartományba hozzuk
- Legegyszerűbb módja az egyszerű osztás: megkeressük a legnagyobb intenzitású képpontot, és annak intenzitásával leosztunk mindent. Hátránya hogy a kép nagyon sötét lehet.
- Egy fokkal jobb megoldás, ha legnagyobb intenzitású képpont intenzitásának kb. 80%-val osztunk le, és az így kapott 1-nél nagyobb színértékeket 1-el helyettesítjük (kvázi levágjuk a tartomány tetejét) Kis kísérletezéssel egész jó dinamikatartományú képet kaphatunk, kevés beégett fehér résszel.
- Az igazi professzionális Tone Map ennél bonyolultabb, részletesen olvashatunk róla a diákon vagy az interneten.
- Egyik fontos eleme, hogy figyelembe veszi a három színkomponens fényességének eltérését, és a következő kísérleti alapon megállapított intenzitásképletet használja: Y = 0.2126 R + 0.7152 G + 0.0722 B
3D alapok
- TODO: kamera, Z-buffer VS. clipping pane, árnyalás VS. normálvektorok, glut tesszellátorok
3D extrák
- TODO: fények, csillanás, textúrázás
-- BlackGhost - 2011.04.05.