Uvod u raytracing
U ovom tekstu pokušaću da kroz jednostavan primer pokažem osnove funkcionisanja raytracing algoritma. Rejtrejsing algoritam generiše fotorealistične slike objekata u prostoru, zbog čega se koristi u programima za modelovanje i u (novijim) igricama.
Već na samom početku teksta ćemo, uz malo koda, dobiti program koji može da renderuje sliku sfere u prostoru. Zatim ćemo taj program postepeno proširivati implementirajući razne tehnike koje doprinose realističnosti slike. Ako ispratite ceo tekst, na samom kraju imaćete rejtrejser (raytracer) koji je sposoban da izrenderuje slike poput sledeće:

Za praćenje ovog teksta biće vam potrebne minimalne programerske sposobnosti. Za potpuno razumevanje teksta biće vam potrebna i neka osnovna znanja iz srednjoškolske matematike i fizike. Kako znam da mnogi zaborave takve informacije nakon srednje škole, potrudio sam se da ispišem detaljnija pojašnjena za sve one stvari koje mogu biti problem. Ta objašnjenja se nalaze u sivim pravougaonicima koji se mogu otvarati ili zatvarati pritiskom na naslov.
O objašnjenjima
Objašnjena nisu neophodna za praćenje daljeg izlaganja ali mogu produbiti razumevanje materije. Za neke čitaoce će objašnjenja biti nepotrebna, te bih njima savetovao da ne troše čitalačku pažnju na njih. Za druge čitaoce će možda biti preobimna, pa bih i njima savetovao da ih privremeno preskoče, a da se vrate na njih kada se materija malo “slegne”.
Sva teorijska razmatranja u tekstu ispraćena su konkretnim kodom. Kompletan kôd rejtrejsera posle svakog implementiranog poboljšanja dat je u git istoriji repozitorijuma Zrak, a u samom tekstu su postavljeni linkovi (označeni sa 🔷) ka odgovarajućim git unosima.
Kôd koji prati ovaj tekst napisan je u Python jeziku, ali vi možete pratiti tekst kucajući kod u svom omiljenom jeziku. Pajton jezik je zbog svoje neefikasnosti verovatno poslednji jezik koji želite da koristite za pisanje rejtrejsera. Međutim, Pajton jezik je veoma čitljiv i mogu ga razumeti svi koji poznaju barem jedan imperativni jezik. Čak i ako niste nikad pisali Pajton, razumećete kodove koje ću u nastavku navesti.
Svakako vam savetujem da ovaj tekst ispratite baš tako što ćete paralelno sa čitanjem implementirati sami svoj rejtrejser. Na taj način ćete biti sigurni da zaista razumete materiju. Ipak, ne morate se slepo držati algoritama koji su izneti ovde. Dobro je ako eksperimentišete, istražujete, menjate parametre i sl. i na taj način dođete do nekih zanimljivih rezultata ili efikasnijih algoritama. Ako poslušate moj savet, uvidećete da je programiranje rejtrejsera veoma zabavno, čak i kada taj rejtrejser ne funkcioniše kako ste zamislili.
Suština algoritma
Rejtrejsing algoritam funkcioniše veoma slično kao što funkcioniše ljudsko oko ili kamera.
Zamislimo da se u prostoru nalazi jedna sfera, tačkasti izvor svetlosti i posmatrač, tj. vi. Sfera i izvor svetlosti su postavljeni tako da vidite sferu ispred sebe. Vi vidite sferu jer se zraci svetlosti odbijaju od sfere i ulaze u vaše oko gde prave sliku na mrežnjači. Ono što vi vidite može se predstaviti kao slika na jednom prozirnom platnu koje se nalazi između vas i sfere. Ta slika je dobijena tako što je svaka tačka slike “obojena” u skladu sa bojom zraka koji prolazi kroz nju i upada u oko. Stručno rečeno, slika je dobijena centralnom projekcijom prostora na platno, pri čemu je vaše oko centar te projekcije.
Rejtrejsing algoritam simulira goreopisani proces, s jednom malom razlikom: umesto da se putanja svetlosti prati od izvora ka oku, u rejtrejsing algoritmu se putanja svetlosti prati od oka ka izvoru svetlosti. Razlog za to je sasvim jednostavan: većina svetlosnih zrakova nikada i ne završi u oku, već odluta u prostor. Zbog toga, nema ni svrhe pratiti putanje tih zrakova. Sasvim je dovoljno pratiti samo one zrakove koji su upali u oko.
Ono što nam predstoji u narednim redovima jeste konstrukcija programa koji izračunava putanje svetlosnih zrakova na osnovu pozicije izvora svetlosti, objekata i kamere (vodeći pritom računa o fizičkim fenomenima kao što su rasipanje, odbijanje i prelamanje svetlosti…).
Kreiranje slike
Na samom početku napisaćemo kôd koji pravi samu sliku (png
fajl). Za to je potrebno da se opredelite za neku od biblioteka koje su dostupne za vaš jezik1. Ja sam se odlučio za pyplot
biblioteku.
Koju god biblioteku da nameravate da koristite, postupak kreiranja slike je gotovo isti. Prvo se u memoriji konstruiše jedna matrica piksela, koja se zatim popuni informacijama o bojama. Na samom kraju, pozivom odgovarajuće funkcije podaci iz te matrice se zapisuju u fajl. U Pajtonu, kôd izgleda ovako:
import numpy as np
import matplotlib.pyplot
sirina = 1000
visina = 600
slika = np.zeros((visina, sirina, 3))
for i in range(visina):
for j in range(sirina):
slika[i, j] = np.array([0, i/visina, j/sirina])
matplotlib.pyplot.imsave('slika.png', slika)
Pokretanje Python programa
Da biste pokrenuli navedeni Pajton kod, potrebno je da imate instalirane biblioteke numpy
i matplotlib
. Ove biblioteke možete instalirati pokretanjem komande
$ pip install numpy matplotlib
Nakon instaliranja navedenih biblioteka program jednostavno pokrenite komandom
$ python3 ime_datoteke.py
Pokretanjem gornjeg koda dobijamo narednu sliku:

np.array([0, i/visina, j/sirina])
- mešavina plave i zelene boje koja zavisi od pozicije piksela.Postavka kamere
Za nastavak teksta potrebno je poznavati termine kao što su vektor, koordinate vektora i skalarni proizvod. Ako imate barem malo znanja o navedenim pojmovima, osećajte se slobodnim da preskočite naredna dva pojašnjenja.
Vektori
U matematici vektori se definišu kao elementi algebarske strukture koju nazivamo vektorski prostor. Za naše potrebe, ovako apstraktna definicija nije mnogo korisna, pa ćemo mi vektor definisati na intuitivan, geometrijski, način. Za nas će vektor biti jedna strelica (usmerena duž) u prostoru (u našem slučaju dvodimenzionalnom ili trodimenzionalnom). Pritom, dve strelice koje se paralelnim pomeranjem mogu “preklopiti”, smatraćemo za isti vektor (zato se ponekad vektori nazivaju i slobodni vektori).
Vekore ćemo u ovom tekstu označavati sa strelicom iznad imena kao na primer ili . Kada se ime vektora sastoji od dva velika slova, onda ta slova označavaju početnu i krajnju tačku vektora.
Od svih vektora ističe se nula vektor . On predstavlja usmerenu duž koja počinje i završava se u istoj tački (bilo kojoj, npr. ).
Svaki nenula vektor možemo okarakterisati sa tri veličine: pravac, smer i intenzitet. Pravac predstavlja pravu na kojoj vektor “leži”, smer predstavlja usmerenje vektora na toj pravi, dok intenzitet predstavlja dužinu vektora. Intenzitet vektora označavamo sa . Broj nazivamo još i norma vektora.
Nula vektor nema definisan pravac i smer, a intenzitet mu je (i jedini je vektor sa intenzitetom ).
Ključna osobina vektora je ta što se oni mogu međusobno sabirati (ako pripadaju istom prostoru) i množiti skalarima tj. realnim brojevima.
Sabiranje vektora je veoma jednostavna operacija: da bismo našli zbir vektora i potrebno je samo da početak vektora prenesemo na kraj vektora . Njihov zbir, vektor , biće vektor koji počinje u početku vektora i završava se na kraju vektora . Može se lako dokazati da prilikom sabiranja redosled vektora nije bitan, odnosno da važi . Takođe, sabiranje vektora sa nula vektorom, daje isti vektor, baš kao i kod sabiranja brojeva.
Druga operacija koja je karakteristična za vektore je množenje s skalarima. Neformalno govoreći, proizvod skalara (broja) i vektora je vektor koji je nastao izduživanjem (odnosno sabijanjem) vektora … puta. Množenjem negativnog broja sa vektorom menjamo smer vektoru, a množenjem bilo kog vektora sa nulom dobijamo nula vektor .
Sada kada smo uveli pojmove sabiranja vektora i množenja skalarom, možemo uvesti i pojam linearne kombinacije. Linearana kombinacija vektora je svaki vektor koji se može dobiti od sabiranjem i skalarnim množenjem. Na primer, , zatim kao i jesu linearne kombinacije vektora .
Iako je geometrijska definicija vektora jasna, ipak nam ona na prvi pogled ne daje način da implementiramo pojam vektora na računaru. Zbog toga je potrebno uvesti pojam baze i koordinata. Baza je kolekcija vektora, takva da se svaki vektor iz prostora može izraziti na jedinstven način kao linearna kombinacija elemenata te kolekcije. Ostavićemo za neke naprednije matematičke tekstove detaljnu analizu ove definicije, a mi ćemo na primeru dvodimenzionalnog i trodimenzionalnog prostora videti šta baza predstavlja.
Nije teško uveriti se da u dvodimenzionalnom prostoru svaka kolekcija od dva nenula vektora koji pritom nemaju isti pravac, čini jednu bazu. Svaki drugi vektor može se izraziti kao linearna kombinacija ova dva vektora, odnosno važi gde su i neki realni brojevi. Uređeni par nazivamo koordinate vektora u odnosu na bazu
U trodimenzionalnom vektorskom prostoru, svaka kolekcija od tri nenula vektora koji po parovima nemaju isti pravac niti sva tri pripadaju istoj ravni, čini bazu. U trodimenzionalnom prostoru, svaki vektor se može predstaviti kao i pritom su koordinate vektora u bazi .
Iako možda nikad niste čuli za pojam baze, verovatno ste čuli za pojam koordinate. To je tako, jer se u geometriji gotovo uvek za bazu uzima skup vektora koji su jedinične dužine i međusobno normalni. U tom slučaju, pojam koordinata je sasvim jasan i prirodan, jer one predstavljaju udaljenosti kraja vektora od koordinatnih osi odnosno koordinatnih ravni. I mi ćemo u nastavku teksta uvek uzimati takve baze. Inače, takve baze nazivamo ortonormirane.
Koordinate su korisne jer nam omogućavaju da lako predstavljamo vektore u računaru, ali i da računamo sa tim predstavama. Na primer ako vektor ima koordinate a vektor ima koordinate onda zbir ima koordinate zbira . Kratko rečeno, zbir koordinata je koordinata zbira. Takođe, proizvod skalara i vektora ima koordinate . Isti principi važe i kod trodimenzionalnih vektora.Skalarni proizvod
Skalarni proizvod je operacija pri kojoj se svakom paru vektora dodeljuje jedan skalar. Skalarni proizvod vektora i , u oznaci se definiše kao gde je ugao između vektora i .
Nije teško uverti se da skalarni proizvod poseduje naredne osobine:
- Komutativnost. Za sve vektore i važi .
- Homogenost. Za svaki skalar i vektore i važi .
- Distributivnost. Za sve vektore i važi .
Zbog svojstva homogenosti i distributivnosti, skalarni proizvod je moguće izračunati samo na osnovu koordinata kad god su dati skalarni proizvodi elemenata baze. Zaista, ako je i , tada se odmah dobija da je
Ako je baza ortonormirana (što će kod nas uvek biti slučaj) tada je dok su ostali skalarni proizvodi nule, pa dobijamo dobro poznati izraz za skalarni proizvod (1)
Jednačina (1) nam omogućuje da računamo skalarne proizvode na računaru. Takođe, kako za svaki vektor važi , sledi da na računaru možemo računati i dužine vektora po formuli Štaviše, sada kada znamo kako da računamo skalarne proizvode i dužine, možemo računati i kosinuse uglova između nenula vektora po formuliKoordinatni sistem ćemo postaviti u centar kamere (oka) tako da je pravac gledanja u pravcu negativne poluose. Slika (platno iz prethodne sekcije) na koju ćemo projektovati scenu je pravougaonik širine postavljen paralelno sa ravni tako da njegov centar seče osu u tački . Ova postavka je prikazana na narednoj slici.
Plavi vektor na prethodnoj slici označava vektor koji povezuje centar koordinatnog početka sa gornjim levim uglom piksela na platnu2. Nije teško uvideti da su koordinate tog vektora date sa
(2)
gde je (i,j)
položaj piksela u matrici (indeks), a format slike tj. količnik širine i visine slike.
Izvođenje vektora pravca
Platno na koje projektujemo sliku je postavljeno u ravni . Stoga je i koordinata vektora pravca .
Dalje, primetimo da je platno podelejno na sirina
kolona i visina
vrsta. Svaka od ćelija te podele predstavlja jedan piksel slike. Kako je platno široko , sledi da je svaka od kolona, a samim tim i svaki piksel, široka tačno , gde je broj kolona. Kako leva ivica platna ima koordinatu sledi da je i
-ti piksel ima koordinatu kao što je napisano u (2).
Sličnim postpkom dobijamo i koordinatu, s tim što izraz delimo sa jer je platno visoko tačno .
Vektor dat sa (2) je vektor pravca zraka svetlosti kojeg ćemo da “pustimo” iz kamere ka prostoru (u prvoj sekciji sam napomenuo da ćemo program konstruisati tako da putanja svetlosti prati od kamere ka izvoru svetlosti). Sam zrak ćemo predstaviti kao strukturu koja sadrži vektor pravca kretanja (to će biti (2)), kao i početnu tačku zraka (to će biti koordinatni početak).
Vektor dat sa (2) nema jediničnu dužinu, a nama će kasnije biti korisno da baš bude jedinične norme. Zbog toga ćemo implementirati i funkciju normiraj
koja normira dati nenula vektor3.
Kompletan program za sada izgleda ovako:
import numpy as np
import matplotlib.pyplot
import math
sirina = 1000
visina = 600
def duzina(vektor):
return math.sqrt(np.dot(vektor, vektor))
def normiraj(vektor):
d = duzina(vektor)
if d == 0:
return vektor
else:
return vektor/d
formatSlike = sirina/visina
slika = np.zeros((visina, sirina, 3))
for i in range(visina):
y = (1 - 2 * i/visina) / formatSlike
for j in range(sirina):
x = -1 + 2 * j/sirina
zrak = {
'pravac': normiraj(np.array([x, y, -1])),
'tacka': np.array([0, 0, 0])
}
slika[i, j] = np.array([0, 0, 0])
matplotlib.pyplot.imsave('slika.png', slika)
Prvi zraci
Jednačina prave
Svaka prava je određena sa jednim pravcem i jednom tačkom koja pripada toj pravoj. Ovu karakterizaciju možemo iskoristiti za pronalaženje jednačine koja opisuje pravu. Neka je jedna tačka koja se nalazi na pravoj i neka je vektor pravca te prave. Tada za svaku tačku sa te prave važi da je kolinearan sa vektorom , odnosno postoji realan broj takav da je . Međutim, znamo da je i . Kombinujući ove dve jednakosti dobijamo (3) što predstavlja jednačinu prave. Za naše potrebe ovu jednačinu ćemo koristiti u koordinatnom obliku koji glasi gde su koordinate tačke , koordinate tačke a koordinate vektora
Jednačina sfere i presek prave i sfere
Sfera je skup tačaka koje se nalaze na istom odstojanju od neke tačke . Prema tome neka tačka pripada sferi ako i samo ako važi . Kako je dobijamo da je (4) Ako tačka ima koordinate a tačka ima koordinate , tada iz gornje jednakosti i definicije skalarnog proizvoda dobijamo jednačinu sfereSada ćemo da implementiramo renderovanje jedne sfere. Definišimo sferu kao strukturu u kojoj polje 'pprecnik'
označava poluprečnik sfere.
sfera1 = {
'centar': np.array([0, 0, -4]),
'pprecnik': 1
}
Da bismo odredili da li zrak seče sferu, posmatrajmo narednu sliku:
Na slici je sa označen centar sfere, sa poluprečnik, sa vektor pravca zraka, sa normalna projekcija tačke na pravu određenu vektorom . Očigledno, zrak seče sferu ako i samo ako je , te je prema tome dovoljno samo odrediti To možemo lako uraditi koristeći Pitagorinu teoremu primenjenu na trougao sa pravim uglom kod temena . Kako je jedinične dužine, važi , pa je
U Pajtonu je jednostavno implementirati navedeni kriterijum:
def presekZrakaISfere(zrak, sfera):
ss = np.dot(sfera['centar'], sfera['centar'])
sz = np.dot(sfera['centar'], zrak['pravac'])
if sz > 0 and ss - sz * sz < sfera['pprecnik']:
return 1
return 0
sz > 0
je postavljen da bismo renderovali samo sfere koje se nalaze “ispred” kamere.U dvostrukoj petlji iskoristićemo funkciju presekZrakaISfere
da bismo za svaki zrak proverili da li seče sferu. Ako zrak seče sferu, odgovarajući piksel ćemo obojiti u belo (RGB = [1, 1, 1]
), dok ćemo u suprotnom odgovarajući piksel obojiti u crno.
for i in range(visina):
y = (1 - 2 * i/visina) / formatSlike
for j in range(sirina):
x = -1 + 2 * j/sirina
zrak = {
'pravac': normiraj(np.array([x, y, -1])),
'tacka': np.array([0, 0, 0])
}
if presekZrakaISfere(zrak, sfera1):
slika[i, j] = np.array([1, 1, 1])
else:
slika[i, j] = np.array([0, 0, 0])
Pokretanjem programa dobijamo prvu sliku generisanu rejtrejsingom:

Scena sa jednim objektom nije mnogo interesantna, te ćemo stoga implementirati renderovanje niza sfera. Sve sfere ćemo predstaviti kao niz sfere
čiji elementi sadrže i polje 'boja'
:
sfere = [
{'centar': np.array([0, 0, -4]), 'pprecnik': 1, 'boja': np.array([1, 0, 0])},
{'centar': np.array([-1, 0, -3]), 'pprecnik': 0.5, 'boja': np.array([0, 1, 0])},
{'centar': np.array([0.8, 1, -4]), 'pprecnik': 0.3, 'boja': np.array([0, 0, 1])}
]
Jasno je da sada moramo ponoviti prethodni postupak za svaku sferu pojedinačno. Međutim, više nije dovoljno samo odrediti da li zrak seče neku sferu ili ne, već je potrebno odrediti i udaljenost tog preseka. Naime, kako neki zrak može seći više sfera istovremeno, moramo pažljivo odrediti koju sferu je zrak prvu presekao.
Kako nam funkcija presekZrakaISfere
za sada ne daje informaciju o udaljenosti presečne tačke, izmenićemo je tako da funkcija sada vraća i samu presečnu tačku zraka i sfere (ako takva tačka postoji).
Presečne tačke sfere određene jednačinom i prave određene jednačinom dobijamo tako što nepoznate , i zamenimo u jednačinu sfere koristeći jednakosti date jednačinom prave. Na taj način dobijamo gde je , , i . Koristeći osobine skalarnog proizvoda, dobijamo kvadratnu jednačinu po :
Iako gornja jednačina sadrži vektorske veličine u sebi, ona je zapravo skalarna jednačina u kojoj su pritom poznati svi koeficijenti uz nepoznatu . Pri rešavanju ove jednačine izdvajaju se tri slučaja:
- Diskriminanta jednačine je strogo negativna. Tada su rešenja kvadratne jednačine strogo kompleksni brojevi koji nemaju fizički smisao, i prava i sfera se ne seku.
- Diskriminanta jednačine je 0. Kvadratna jednačina ima jedno rešenje (za koje kažemo da je dvostruko). U ovom slučaju prava dodiruje sferu (i tada kažemo da je prava tangenta na sferu).
- Diskriminanta jednačine je strogo pozitivna. Tada postoje dva različita rešenja kvadratne jednačine koja odgovaraju dvema presečnim tačkama prave i sfere.
Navedenu računicu je lako implementirati u Pajtonu, što je i prikazano u narednom kodu. Potrebno je samo povesti računa o sledećem: u slučaju kada postoje dva različita preseka prave i sfere, biramo onaj presek koji je bliži kameri, a to je upravo presek koji odgovara manjem rešenju kvadratne jednačine.
def resenjaKvadratneJednacine(a, b, c):
D = b**2 - 4 * a * c
if D < 0:
return (None, None)
else:
return ((-b - math.sqrt(D))/(2 * a), (-b + math.sqrt(D))/(2 * a))
def presekZrakaISfere(zrak, sfera):
sz = np.dot(sfera['centar'] - zrak['tacka'], zrak['pravac'])
if sz > 0:
a = np.dot(zrak['pravac'], zrak['pravac'])
b = 2 * np.dot(zrak['pravac'], zrak['tacka'] - sfera['centar'])
c = np.dot(zrak['tacka'] - sfera['centar'], zrak['tacka'] - sfera['centar']) - sfera['pprecnik']**2
t1, _ = resenjaKvadratneJednacine(a, b, c)
if t1 is None:
return (0, None)
return (1, zrak['tacka'] + t1 * zrak['pravac'])
return (0, None)
Sada funkciju presekZrakaISfere
možemo iskoristiti za pronalaženje sfere koju zrak prvu seče. Zrak ćemo obojiti bojom te sfere.
udaljenostPrvogPreseka = math.inf
boja = np.array([0, 0, 0])
for sfera in sfere:
zrakSeceSferu, tackaPreseka = presekZrakaISfere(zrak, sfera)
if not zrakSeceSferu:
continue
duzinaZraka = duzina(tackaPreseka - zrak['tacka'])
if duzinaZraka < udaljenostPrvogPreseka:
udaljenostPrvogPreseka = duzinaZraka
boja = sfera['boja']
slika[i, j] = boja
Pokretanjem programa dobijamo:

Senčenje
Rezultat našeg programa sada deluje malo zanimljivije, ali i dalje ne odaje utisak trodimenzionalne scene. Zato ćemo implementirati senčenje sfera, tehniku koja će odmah dati trodimenzionalni izgled generisanim slikama.
Tačkasti izvor svetlosti možemo predstaviti jednostavnim objektom:
izvorSvetlosti = {
'centar': np.array([ -2, 5, 0]),
'boja': np.array([30, 30, 30])
}
Primetimo da u ovom slučaju vektor boje ima koordinate koje su mnogo veće od Kako intenzitet svetlosti opada s kvadratom rastojanja od izvora svetlosti, neophodno je definisati “malo veći” vektor boje da bi se scena dovoljno osvetlila.
Za senčenje sfera koristićemo Lambertov4 zakon koji veoma dobro opisuje odbijanje svetlosti od mat materijala. Po Lambertovom zakonu zrak svetlosti se odbija od mat materijala5 podjednako u svim pravcima, intezitet odbijenog zraka je proporcionalan veličini gde je intenzitet upadnog zraka, a ugao između normale površi i upadnog zraka.
Za izvođenje Lambertovog zakona potrebno je poznavati principe radiometrije, što je svakako van domašaja ovog teksta. Ipak, naredna slika bi možda mogla da vam dâ intuitivnu ideju zašto zakon važi.
Senčenje pomoću Lambertovog zakona je jednostavno implementirati. Prvo izračunamo intenzitet svetlosti koja pada na površ. Taj intenzitet je, naravno, boja iz strukture izvorSvetlosti
umanjena za kvadrat rastojanja između izvora svetlosti i tačke koju posmatramo. Zatim, na osnovu tog intenziteta izračunamo koeficijent osvetljenje
po Lambertovom zakonu. Koeficijent osvetljene
množimo dalje vektorom boje sfere.
normalaSfere = normiraj(tackaPrvogPreseka - najblizaSfera['centar'])
pravacKaSvetlu = normiraj(izvorSvetlosti['centar'] - tackaPrvogPreseka)
kosinus = kosinusUgla(pravacKaSvetlu, normalaSfere)
rastojanje = duzina(izvorSvetlosti['centar'] - tackaPrvogPreseka)
osvetljenje = kosinus * izvorSvetlosti['boja'] / rastojanje**2
boja = najblizaSfera['boja'] * osvetljenje
Pri opisanom računu dobijamo vektor boja
kojim je potrebno obojiti piksel. Međutim, prilikom kreiranja slike, vektor boje mora imati koordinate koje leže u intervalu . Da bismo osigurali da ovo važi, iskoristićemo funkciju clip
iz Numpy biblioteke. Stoga, umesto
slika[i, j] = boja
stavljamo
slika[i, j] = np.clip(boja, 0, 1)
Pokretanjem programa dobijamo scenu koja podseća na one iz filma 2001: Odiseja u svemiru

Bilo bi lepo da u našu scenu unesemo jednu ravan, koja će delovati kao podloga za ostale sfere. Da bismo to uradili možemo postupiti na dva načina:
- Prvi način podrazumeva da izvedemo formulu za presek zraka i ravni, i implementiramo je u kodu, baš kao što smo to učinili s formulom za presek zraka i sfere. Iako je ta formula jednostavnija nego ona koju smo izveli za presek zraka i sfere, ipak bi implementacija zahtevala da barem malo apstrahujemo kôd6.
- Drugi način je dosta kraći. Zapravo, radi se o jednostavnom triku: umesto ravni dodaćemo jednu sferu izuzetno velikog prečnika. Ako se nalazimo “blizu” te sfere, tada će nam ona delovati kao ravan7.
Ja se odlučujem za drugi način jer je dosta kraći i stoga pogodniji za ovaj tekst. Ipak, ako želite da napravite nešto više nego što ovaj tekst nudi, onda svakako vredi da krenete prvim putem. Ako dobro dizajnirate program, moći ćete da lako dodajete nove geometrijske objekte u svoj rejtrejser. Nakon ravni, možete implementirati trouglove, a nakon toga praktično imate sve što vam je potrebno za uvoz 3D modela u program8.
Elem, dodavanjem ogromne sive sfere dobijamo narednu sliku:

Na prethodnoj slici uočava se jedan veliki problem: sfere ne bacaju senku.
Ovaj problem rešavamo na najjednostavniji mogući način: kada odredimo tačku koju želimo da renderujemo (to je ona koja je najbliža kameri od svih preseka datog zraka i sfera), tada ćemo prvo proveriti da li između te tačke i izvora svetlosti postoji neka druga sfera. Ako takva sfera postoji, onda tu tačku bojimo u crno, a u suprotnom je bojimo u skladu s Lambertovim zakonom.
zrakSenke = {
'pravac': pravacKaSvetlu,
'tacka': tackaPrvogPreseka
}
for sfera in sfere:
zrakSenkeSeceSferu, presekZrakaSenke = presekZrakaISfere(zrakSenke, sfera)
if zrakSenkeSeceSferu and
duzina(presekZrakaSenke - tackaPrvogPreseka) < duzina(izvorSvetlosti['centar'] - tackaPrvogPreseka):
uSenci = 1
break
else:
uSenci = 0

Višestruko osvetljenje
Scena bi bila mnogo zanimljivija ako bi bila istovremeno osvetljena sa više izvora svetlosti. Upravo ćemo to sada i implementirati.
Kao i u slučaju sfera, izvor svetlosti ćemo zameniti nizom izvora svetlosti
izvoriSvetlosti = [
{'centar': np.array([-2, 5, 0]), 'boja': np.array([20, 20, 20])},
{'centar': np.array([3, 0, -6]), 'boja': np.array([ 0, 0, 1])},
{'centar': np.array([2.7, 0, -4]), 'boja': np.array([ 1, 0, 0])},
{'centar': np.array([4, 0, -5]), 'boja': np.array([ 0, 1, 0])}
]
Sada je dovoljno da kôd koji je zadužen za senčenje sfere, postavimo u petlju koja prolazi kroz sve izvore svetlosti. Pritom, krajnju boju tačke sfere formiramo kao zbir svih boja koje se dobijaju osvetljivanjem pojedinačnih izvora svetlosti:
if tackaPrvogPreseka is not None:
for izvorSvetlosti in izvoriSvetlosti:
# ...
# Kod koji smo do sada imali za izračunavanje
# osvetljenja na osnovu Lamertovog senčenja
# ...
if not uSenci:
boja = boja + najblizaSfera['boja'] * osvetljenje
Pokretanjem koda dobijamo narednu sliku:

Odmah se uočava da bi bilo dobro kada bi i sami izvori svetlosti bili vidljivi. Ovo ćemo kasnije implementirati.
Reflektivni materijali
Lambertovo senčenje je sasvim dobro za prikazivanje mat površina9. Ipak, znamo da u prirodi nisu sve površine mat već ima i onih sjajnih. Za modelovanje sjajnih površi u računarskoj grafici često se koristi Blin–Fongov model. Ovaj model je neznatno komplikovaniji od Lambertovog modela, te ga ja neću ovde prikazati. Umesto toga, implementiraću savršene reflektujuće površine, tj. ogledala.
Sve što je potrebno za modelovanje ogledala jeste zakon odbijanja svetlosti, koji kaže da se svetlost od glatke reflektujuće površi odbija tako da je upadni ugao jednak odbojnom uglu, kao i da se upadni zrak, normala na površ ogledala i odbijeni zrak nalaze u istoj ravni.
Izvođenje zakona odbijanja svetlosti
Zakon odbijanja svetlosti možemo izvesti iz Fermaovog10 principa najmanjeg vremena koji glasi: putanja svetlosti izmеđu dve tačke je takva da je vreme puta, tokom kog je pređena putanja, najkraće. Uz malo analitičke geometrije i diferencijalnog računa iz navedenog principa direktno sledi zakon odbijanja svetlosti (jedno takvo izvođenje će biti kasnije prikazano).
Ogledala formiraju virtuelnu sliku na svojoj površini, tj. u ogledalu vidimo tačno onu sliku koju bi video neko ko se nalazi naspram nas, simetrično u odnosu na ogledalo. Dakle, za implementaciju ogledala u rejtrejseru potrebno je formirati sliku unutar naše slike. Ovo je mnogo jednostavnije nego što zvuči: dovoljno je samo izdvojiti logiku koja je određivala boju zraka (tj. boju kojom bojimo odgovarajući piksel) u zasebnu funkciju, a zatim tu funkciju pozvati rekurzivno kad god je u pitanju reflektujuća površina. Da bismo osigurali da se rekurzija završava, koristićemo promenljivu dubina
koja označava koliko još rekurzivnih koraka možemo da napravimo.
def bojaZraka(zrak, dubina):
udaljenostPrvogPreseka = math.inf
tackaPrvogPreseka = None
najblizaSfera = None
boja = np.array([0, 0, 0])
if dubina == 0:
return boja
# Kôd koji određuje presek s najbližom sferom
if tackaPrvogPreseka is not None:
# Kôd koji određuje Lambertovo senčenje i smešta boju tačke u promeljivu 'boja'
# ...
if 'reflektivnost' in najblizaSfera:
reflektovanZrak = {
'pravac': zrak['pravac'] - 2 * np.dot(zrak['pravac'], normalaSfere) * normalaSfere,
'tacka': tackaPrvogPreseka
}
r = najblizaSfera['reflektivnost']
boja = (1 - r) * boja + r * bojaZraka(reflektovanZrak, dubina - 1)
return boja
Sve što je prikazano u gornjem kodu već smo imali. Jedini dodatak je poslednji blok, koji se poziva kad god sfera ima polje 'reflektivnost'
u sebi. U tom slučaju, boja tačke se određuje tako što se po zakonu odbijanja svetlosti odgovarajući zrak plasira u prostor, a zatim se njegova boja pomeša s bojom sfere u zavisnosti od samog parametra 'reflektivnost'
. Na primer, ako bi promenljiva 'reflektivnost'
imala vrednost , tada bi boja upadnog zraka bila tačno jednaka boji odbojnog zraka, tj. imali bismo savršeno ogledalo. U slučaju da promenljiva 'reflektivnost'
ima vrednost , tada bi se boja odbijenog zraka u potpunosti ignorisala, i imali bismo obično Lambertovo senčenje.
Naravno, sada je petlja po svim pikselima mnogo jednostavnija:
for i in range(visina):
y = format * (1 - 2 * i/visina)
for j in range(sirina):
x = -1 + 2 * j/sirina
zrak = {
'pravac': normiraj(np.array([x, y, -1])),
'tacka': np.array([0, 0, 0])
}
slika[i, j] = np.clip(bojaZraka(zrak, 8), 0, 1)

Izdvajanje funkcije bojaZraka
omogućava nam još neka interesantna poboljšanja. Prvo ćemo izmeniti rejtrejser tako da i izvori svetlosti postanu vidljivi. Izvore svetlosti ćemo prikazati kao sfere, ali za razliku od drugih sfera, izvori svetlosti će imati “sjaj”. To će doprineti realističnosti krajnje slike.
Dodajmo pprecnik
polje za svaki izvor svetlosti, kao i polje emisija
koje će označavati da se radi o izvoru svetlosti.
izvoriSvetlosti = [
{
'centar': np.array([-2, 5, 0]),
'boja': np.array([20, 20, 20]),
'emisija': True,
'pprecnik': 1
},
# ...
]
Sada ćemo dodati petlju koja će određivati najbliži presek zraka s nekim izvorom svetlosti (ovakvu već petlju imamo za ostale sfere):
for izvorSvetlosti in izvoriSvetlosti:
zrakSeceSferu, tackaPreseka = presekZrakaISfere(zrak, izvorSvetlosti)
if not zrakSeceSferu:
continue
duzinaZraka = duzina(tackaPreseka - zrak['tacka'])
if duzinaZraka < udaljenostPrvogPreseka:
udaljenostPrvogPreseka = duzinaZraka
tackaPrvogPreseka = tackaPreseka
najblizaSfera = izvorSvetlosti
Dakle, nakon završetka ove petlje, u promenljivoj najblizaSfera
imamo ili emisionu (svetleću) ili neemisionu sferu. Već smo implementirali logiku za neemisione sfere, pa nam stoga ostaje da implementiramo logiku koja određuje boju zraka kad taj zrak pogodi emisionu sferu. Emisione sfere svakako ne bi trebalo da budu osenčene niti reflektujuće. Takođe, rekli smo da želimo da vidimo sjaj oko izvora svetlosti.
U sofisticiranim rejtrejsing programima, sjaj oko izvora svetlosti se dobija implementacijom fenomena rasejanja svetlosti11. Mi ćemo se za potrebe ovog teksta poslužiti (jeftinim) trikom12.
Ideja trika je jednostavna: što je ugao pod kojim zrak pada na sferu manji, to zrak pada bliže “obodu” sfere (gledano iz pravca tačke iz koje je zrak pušten). Da bismo videli sjaj oko sfere dovoljno je da propustimo zrak kroz sferu, a zatim “pomešamo” boju tog zraka s bojom te sfere (tj. s bojom tog izvora svetlosti).
if 'emisija' in najblizaSfera:
k = np.clip(1.5 * pow(abs(kosinusUgla(zrak['pravac'], normalaSfere)), 5), 0, 1)
propustenZrak = {
'pravac': zrak['pravac'],
'tacka': tackaPrvogPreseka - 2 * np.dot(zrak['pravac'], normalaSfere) * najblizaSfera['pprecnik'] * zrak['pravac']
}
return k * najblizaSfera['boja'] + (1 - k) * bojaZraka(propustenZrak, dubina - 1)
k
koji određuje odnos mešanja boja, konstruisan je tako da je centar sfere neproziran, dok sjaj oko sfere opada sa petim stepenom kosinusa upadnog ugla. Ovaj izbor je bio umetnička sloboda, vi svakako eksperimentišite sa različitim parametrima.
Meke senke
Sada kada su izvori svetlosti vidljivi, uočljiviji je jedan problem sa senkama. Naime, obodi senke nikad nisu oštri prelazi iz osvetljenog u neosvetljeni deo, već postepeni prelazi. Razlog tome je što ni izvori svetlosti nisu idealne tačke, već zauzimaju neki prostor, te stoga mogu delimično osvetljavati površine (u zavisnosti od procenta izvora svetlosti koji je vidljiv iz te tačke).
U rajtrejsingu ovaj problem možemo prevazići na dva načina:
- Umesto da proveravamo da li je samo centar izvora svetlosti zaklonjen nekim objektom, ovaj test ćemo izvršiti za desetine tačaka koje se nalaze na izvoru svetlosti (te tačke mogu biti odabrane deterministički ili nasumično). Ovo zaista rešava problem sa senkama, i neznatno komplikuje kôd.
- Drugi metod podrazumeva potpunu modifikaciju Lambertovog senčenja. Umesto da tačku mat objekta senčimo samo na osnovu ugla između normale i pravca ka svetlu (i pritom provaravamo da li je ta tačka u senci), možemo da odbijemo zrak od mat površi u nasumičnom pravcu, a zatim da formiramo boju te tačke na osnovu boje odbijenog zraka (slično kao kod ogledala). Ovaj proces moramo učiniti za desetine zrakova (svaki od njih odbijen u različitom nasumičnom pravcu) a zatim uzeti prosek dobijenih boja.
Jasno je da je drugi način značajno vremenski zahtevniji. Ipak taj način nam automatski rešava i druge suptilnije probleme (kao što je npr. indirektno osvetljenje).
Prozirni materijali
Sem reflektivnosti, postoji još jedan svetlosni fenomen koji je jednostavan za implementaciju: refrakcija ili prelamanje svetlosti.
Prelamanje svetlosti se dešava kada svetlost prelazi iz jedne u drugu sredinu sa različitim optičkim indeksom13. Prelamanje svetlosnog zraka je opisano Snelovim14 zakonom po kom za upadni ugao i ugao prelamanja važi jednakost
(5)
gde su i respektivno brzine prostiranja svetlosti u prvoj i drugoj sredini.
Dokaz Snelovog zakona
Snelov zakon se takođe može izvesti iz Fermaovog principa najmanjeg vremena, baš kao i zakon odbijanja svetlosti.
Posmatrajmo jedan zrak svetlosti koji prolazi kroz tačke i koje se nalaze u različitim optičkim sredinama. Neka su i normalne projekcije tačaka i na zajedničku granicu ovih sredina, i neka se svetlost kreće brzinom u sredini u kojoj se nalazi tačka , a brzinom u sredini u kojoj se nalazi tačka . Ovaj zrak svetlosti u nekoj tački mora preseći granicu sredina. U ovakvoj postavci problema, Snelov zakon je ekvivalentan određivanju pozicije tačke u odnosu na tačke i .
Mi ćemo na osnovu Snelovog zakona računati ugao prelamanja . Za neke vrednosti veličina , i dobija se da je što je nemoguće. U ovom slučaju, zrak se ne prelama u drugu sredinu već se odbija od površi po zakonu koji je već spomenut u tekstu.
Prvo ćemo u listu sfera dodati sferu koja ima polje 'transparentnost'
kao i 'indeks'
:
sfere = [
# ....
{
'centar': np.array([-1.05, 0, -2]),
'pprecnik': 0.3,
'boja': np.array([1, 1, 1]),
'reflektivnost': 1,
'transparentnost': 1,
'indeks': 1.3
},
]
Za sfere koje imaju polje 'transparentnost'
primenićemo sledeći postupak: kada zrak svetlosti pogodi sferu, vektor pravca zraka ćemo izmeniti u skladu sa Snelovim zakonom a zatim pustiti novi zrak. Taj zrak će proći kroz istu sferu da bi se ponovo prelomio pri izlasku iz nje.
Ipak, sa opisanom idejom postoji jedan problem: funkcija presekZrakaISfere
konstruiše tačku preseka na osnovu manjeg od rešenja kvadratne jednačine. To može predstavljati problem kada se zrak već nalazi u sferi, jer je tada taj presek upravo ona tačka sfere u kojoj se zrak prelomio. Zato ćemo dodati uslov koji obebeđuje da se uvek konstruiše presek zraka i sfere koje se nalazi “ispred” tačke iz koje plasiramo zrak:
def presekZrakaISfere(zrak, sfera):
# ...
t1, t2 = resenjaKvadratneJednacine(a, b, c)
if t1 < 0:
t1 = t2
return (1, zrak['tacka'] + t1 * zrak['pravac'])
Implementacija prelemanja svetlosti je neznatno komplikovanija od implementacije ogledala: potrebno je samo povesti računa da li zrak ulazi ili izlazi iz transprentnog materijala, kao i da li je došlo totalne refleksije.
if 'transparentnost' in najblizaSfera:
if kosinusUgla(normalaSfere, zrak['pravac']) > 0:
normalaSfere = - normalaSfere
indeks = najblizaSfera['indeks']
else:
indeks = 1 / najblizaSfera['indeks']
kosinus = min([np.dot(normalaSfere, -1 * zrak['pravac']), 1])
sinus = math.sqrt(1 - kosinus**2)
if sinus <= 1:
vNormalan = indeks * (zrak['pravac'] + kosinus * normalaSfere)
vTransvezalan = -1 * math.sqrt(abs(1 - np.dot(vNormalan, vNormalan))) * normalaSfere
propustenZrak = {
'pravac': vNormalan + vTransvezalan,
'tacka': tackaPrvogPreseka - 0.0001 * najblizaSfera['pprecnik'] * normalaSfere
}
t = najblizaSfera['transparentnost']
boja = (1 - t) * boja + t * bojaZraka(propustenZrak, dubina - 1)
else:
reflektovanZrak = {
'pravac': zrak['pravac'] - 2 * np.dot(zrak['pravac'], normalaSfere) * normalaSfere,
'tacka': tackaPrvogPreseka
}
boja = bojaZraka(reflektovanZrak, dubina - 1)
Pokretanjem programa dobijamo veoma zanimljivu sliku:

Pozicioniranje kamere
Do sada je kamera uvek bila pozicionirana u koordinatnom početku, i bila usmerena ka negativnoj osi. Veoma je lako implementirati logiku koja dozvoljava prozvoljno postavljanje kamere u prostoru ako znamo da koristimo vektorski proizvod.
Vektorski proizvod
Vektorski proizvod je operacija koja svakom paru vektora i pridružuje vektor tako da važi:
- vektor je normalan na vektore i
- dužina vektora je jednaka površini paralelograma razapetog sa vektorima i
- sistem je pozitivno orijentisan, odnosno poštuje pravilo desne ruke.
Za razliku od skalarnog proizvoda, vektorski proizvod nije ni komutativan ni ascocijativan, ali poseduje analogne osobine:
Na osnovu iznetih osobina, lako se može proveriti da je skalarni proizvod vektora i dat sa
što se skraćeno često zapisuje u obliku determinante
Kameru ćemo pozicionirati tako što ćemo zadati tri parametra:
- tačku u kojoj se nalazi kamera:
gledajOd
, - tačku ka kojoj kamera gleda:
gledajKa
, - ugao rotacije oko ose određene s prethodne dve tačke.
Na osnovu zadatih tačaka gledajOd
i gledajKa
, konstruišemo jedinični vektor pravacKamere
. Množeći vektor pravacKamere
vektorom gore = [0, 1, 0]
, dobijamo vektor i_
. Množeći vektorski vektore i_
i pravacKamere
, dobijamo vektor j_
. Po konstrukciji, vektori i_
, j_
i pravacKamere
su jedinični i međusobno ortogonalni.
Sada bismo mogli da konstruišemo zrak koji puštamo iz kamere kao
zrak = {
'pravac': normiraj(pravacKamere + x * i_ + y * j_),
'tacka': gledajOd
}
Da bismo omogućili takozvani camera roll, tj. rotaciju kamere oko vektora pravacKamere
, dovoljno je da zarotiramo vektore i_
i j_
oko vektora pravacKamere
, što je i prikazano na narednoj slici:
Posmatrajući prethodnu sliku, gde je sa označen ugao nagibKamere
, dolazimo do koda
i2 = math.cos(nagibKamere) * i_ + math.sin(nagibKamere) * j_
j2 = -math.sin(nagibKamere) * i_ + math.cos(nagibKamere) * j_
Kôd koji smo do sada napisali omogućuje potpunu kontolu položaja i usmerenja kamere. Ipak, jednom malom izmenom koda možemo dobiti i kontrolu faktora uvećanja kamere tj. zoom. Ako vektor pravacKamere
skaliramo, tada ćemo u suštini menjati udaljenost ravni na koju projektujemo sliku od centra kamere. Takva promena će se odraziti na sliku tako što će slika biti umanjena ili uvećana.
pravacKamere
duži, to će objekti na slici biti veći. U suštini, skaliranje vektora pravacKamere
je menjanje žižine daljine.
Gama korekcija
Gama korekcija je tehnika koja se može implementirati izmenom jedne jedine linije. Vi možete uzeti tu izmenu zdravo-za-gotovo, ali bih za kraj teksta želeo da napišem par reči o tome zašto je potrebno izvršiti takvu korekciju. Ako vas to ne interesuje, osećajte se slobodno da pređete na narednu sekciju.
Ljudski vid, kao i ostala čula, ne reaguje linearno na nadražaje15. Umesto toga, percepcija nadražaja je zavisna od logaritma intenziteta nadražaja. Ova činjenica je poznata kao Fehnerov16 zakon i odnosi se na gotovo sva ljudska čula.
Posledica ovog fenomena jeste to da je vid osetljiviji na promene nadražaja niskog inteziteta. Konkretnije rečeno, oko bolje razlikuje tamne nijanse od svetlijih nijansi.
Kada govorimo o digitalnim slikama, važno je znati da postoji samo ograničen broj nijansi kojim je opisan svaki od piksela. Tačan broj zavisi od formata u kom čuvamo sliku, a svakako je iz mnogih razloga pogodno koristiti što manji broj nijansi. Zato se prilikom konverzije analognog signala (svetla) u digitalni signal koristi gama kompresija kojom se obezbeđuje da se nijanse kodiraju neravnomerno, dajući prednost tamnijim nijansama.
Dakle, u .jpg
, .png
i sličnim fajlovima nalaze se vrednosti koje su dobijene gama kompresijom. Kada se navedeni fajlovi reprodukuju, tada se vrši gama dekompresija (ili gama ekspanzija), kojom se poništava efekat gama kompresije. Proces gama dekopresije vrše zajedničkim snagama korisnički programi, operativni sistem, grafička karta i monitor.
Za nas nije važno kako se tačno vrši gama dekompresija, ali je važno da znamo da se prilikom reprodukcije slike očekuje da fajl sadrži gama kompresovane vrednosti! Zbog toga, mi moramo izvršiti gama kompresiju pre nego što sačuvamo vrednosti fajl. Srećom, to se može veoma lako uraditi primenom neke konkavne (i bijektivne) funkcije na svaku od komponenata boje. Najjednostavniji izbor je gde je neki broj iz intervala . U Pajtonu za stepenovanje možemo iskoristiti pow
funkciju koja, primenjena na vektor, stepenuje svaku od komponenti posebno:
slika[i, j] = np.clip(pow(bojaZraka(zrak, 8), 0.7), 0, 1)

Kraj
Naše putovanje se ovde završava. Nadam se da Vam se ovaj tekst svideo, i nadam se da ste ga ispratili programirajući vaš rejtrejser. Svakao, postoji još mnogo prostora za poboljšanje programa koji je konstruisan u ovom tekstu. Ako imate želju da nastavite sa kontrukcijom rejtrejsera preporučio bih vam da pokušate neki od slećih izazova:
- Implementacija drugih geometrisjkih objekata, poput ravni, trougla, cilindra…
- Implementacija Blin-Fongovog modela senčenja
- Implementacija mekih senki
- Animiranje objekata na sceni
- Implementacija tekstura (učitane iz spoljnog izvora ili generisane)
- Implementacija dubinske oštrine
Osećajte se slobodno da mi pošaljete zanimljive slike koje izrenderujete :)
Preporuke za dalje čitanje
Ovaj tekst predstavlja samo delić oblasti koja se naziva računarska grafika. Ovde ću navesti neke besplatne resurse koji vam mogu poslužiti u daljem istraživanju.
Za više informacija o rejtrejsingu preporučio bih:
- Ray Tracing in One Weekend – serijal od tri kratke knjige namenjen početnicima u rejtrejsingu. Ovaj materijal predstavlja prirodan nastavak ovog članka.
- Physically Based Rendering: From Theory To Implementation – knjiga u kojoj možete saznati mnogo o modernim tehnikama rejtrejsinga. Namenjena pre svega naprednijim čitaocima.
Za više o matematici koja se koristi u računarskoj grafici preporučio bih:
- 3D Math Primer for Graphics and Game Development
- Immersive Linear Algebra
- Discrete Differential Geometry: An Applied Introduction – uvod u napredniju matematiku koja se koristi u računarskoj grafici
Rejtrejsing je samo jedna od tehnika koja se koristi u računarskoj grafici. U 3D igricama se do skoro isključivo koristila tehnika rasterizacije. Nešto više o ovoj tehnici možete pročitati u knjizi
Ili eventualno da napišete drajver za slike, što i nije tako teško kao što zvuči.↩︎
Nije mnogo važno ka kojoj tački piksela je usmeren zrak. Važno je da je vektor usmeren unutar odgovarajućeg piksela.↩︎
Pogledati objašnjenje o skalarnom proizvodu za više informacija.↩︎
Johann Heinrich Lambert (1728–1777).↩︎
Mat materijali se u stručnoj literaturi nazivaju difuzni.↩︎
Ovo je idealno mesto da počnete da koristite klase za prezentovanje različitih vrsta objekata na sceni.↩︎
Ako ste pristalica teorije ravne zemlje, definitivno bi trebalo da probate da implementirate ovaj način.↩︎
Većina objekata koje vidimo u 3D animacijama i igricama, sačinjena je od velikog broja malih trouglova, baš kao što su slike sačinjene od piksela.↩︎
Postoje i drugi modeli difuznih materijala. Za nas će biti dovoljan samo Lambertov model.↩︎
Pierre de Fermat (1607–1665).↩︎
Rasejanje je fenomen pri kom svetlosni zraci odstupaju od pravolinijske putanje zbog osobina sredine kroz koju prolaze. Jedna od posledica ovog fenomena jeste pojava sjaja oko svetlećih objekata.↩︎
Trik sam ad hoc smislio za potrebe ovog teksta. Svestan sam da ima ograničenja, ali slike deluju sada realističnije te ću ga zadržati.↩︎
Optički indeks sredine predstavlja odnos brzine svetlosti u toj sredini i brzine svetlosti u vakuumu.↩︎
Willebrord Snellius (1580–1626).↩︎
Ako bi ljudski vid reagovao linearno, tada bi svako udvostručenje intenziteta nadražaja uzrokovalo udvostručenje intenziteta percepcije tog nadražaja.↩︎
Gustav Theodor Fechner (1801–1887).↩︎