Kamera i promienie pierwotne
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| struct camera {
vec3 location; // położenie kamery
vec3 up, front, right; // wektory kierunków: do góry, do przodu, w prawo
nat xRes, yRes; // rozdzielczość obrazu
camera(vec3 location, vec3 up, vec3 front, nat xRes, nat yRes) :
location(location),
up(up),
front(front),
right(uCrossProd(front,up)),
xRes(xRes),
yRes(yRes)
{ }
};
// odchylenie poziome od środka obrazu/obiektywu
inline float xA(camera& c, float x)
{ return (2.0f * x / fl(c.xRes) -1.0f); }
// odchylenie pionowe od środka obrazu/obiektywu
inline float yA(camera& c, float y)
{ return (2.0f * y / fl(c.yRes) -1.0f); } |
Wyobrażamy sobie, że nasz obraz ma formę kraty złożonej z małych kwadratów, która jest umieszczna tak,
że wektor kierunku, na który skierowana jest kamera (front), celuje w sam środek tej kraty.
Mając dodatkowo wektory wskazujące kierek pionowy w górę (up) i poziomy w prawo (right) względem naszej kraty
jesteśmy w stanie obliczyć dowolny promień pierwotny celujący w pewien kwadrat kraty (odpowiadający pewnemu pikselowi)
urzywając wzoru:
,
gdzie oraz są pewnymi liczbami skalującymi wektory,
określającymi odchylenie od środka ekranu.
1
2
3
4
5
6
7
| // promień z kamery przez współrzędne na płaszczyźnie ekranu (x,y)
inline ray primaryRay(camera& c, float x, float y) {
return ray(
c.location, // źródłem jest położenie kamery
unitise(c.up * yA(c,y) + c.right * xA(c,x) + c.front)
);
} |
Zwierciadlane odbijanie promienia
Obsługujemy jedynie idealnie zwierciadlane odbicie, czyli kąty oraz są równe.
Wzór na wektor kierunku odbicia to:
Poniżej funkcja generująca promień odbity przy pomocy powyższego wzoru.
1
2
3
4
5
6
7
| // odbijanie zwierciadlane
inline ray reflect(ray::i r, hit& h) {
return ray(
h.ip, // źródłem jest punkt trafiony przez promień odbijany 'r'
unitise(h.N * (dotProd(h.N, r.abackDir)*2.0f) + r.aheadDir)
);
} |
Przepuszczanie i załamywanie promienia
Wzór na wektor kierunku transmisji z refrakcją to:
Gdzie
jest stosunkiem indeksów refrakcji (ośrodek opuszczany / ośrodek docelowy).
Gdy promień przechodzi z powietrza do wnętrza obiektu (trafiamy w punkt ) nasz wektor normalny
jest zorientowany odpowiednio, a . Gdy promień wydostaje się z wnętrza
obiektu na powietrze (trafiamy w punkt ) wektor normalny skierowany jest w przeciwną stronę
(musimy go odwrócić dla celów obliczenia refrakcji), a .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // transmisja z refrakcją
inline ray transmit(ray::i r, hit& h) {
float ior = h.mat -> ior;
float d = dotProd(h.N, r.abackDir);
float ir; // stosunek ior_wchodzący / ior_wychodzący
float a;
if (d < 0.0f) { ir = ior; a = -1.0f; d *= -1.0f; }
else { ir = 1.0f / ior; a = 1.0f; }
float i = 1.0f - ir*ir*(1.0f - d*d);
return ray(
h.ip, // źródłem jest punkt trafiony przez promień przepuszczany 'r'
unitise( h.N*a*(ir*d - sqrt(i)) - r.abackDir*ir )
);
} |
Promienie wtórne i rekurencyjne śledzenie
Promienie emitowane z kamery trafiają w różne obiekty, a natępnie odbijają się od nich lub są przepuszczane.
Dodatkowo jeśli powierzchnia odbija w sposób rozproszony emitujemy tzw. promienie ciena do świateł punktowych
obliczając przy ich pomocy informację o tym czy obiekt jest w cieniu, czy jest oświetlany bezpośrednio
przez źródła światła.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // r - śledzony promień
// s - scena, w której śledzony jest promień
// d - głębokość rekursji
color traceRay(ray::i r, scene& s, nat d) {
if (d == 0) return black; // koniec rekursji
hit h = nearestHit(r, s); // informacje o najbliższym przecięciu
if (h.t == undefined) return black; // brak przecięcia
material& m = *h.mat; // materiał trafionego obiektu
color texMapped = notNull(m.drMap) ? (*m.drMap)(h.uv.u, h.uv.v) : white;
if (m.hasNormalMap) // perturbacja wektora normalnego zgodnie z teksturą
h.N = uChangeSpace((*m.normalMap)(h.uv.u, h.uv.v), h.Ex, h.N, h.Ez);
color final; // sumaryczna energia podróżująca przez promień 'r'
if (m.isDiffuse) final += m.dr * lambert(h, s) * texMapped;
if (m.isSpecular) final += m.sr * traceRay(reflect(r, h), s, d-1);
if (m.isTransmittive) final += m.st * traceRay(transmit(r, h), s, d-1);
return final;
} |
W wierszu nr 5 funkcja osiąga dno rekursji, a więc nie oblicza dalszych odbić promieni
dając w wyniku kolor czarny (brak energii).
Wierszu nr 6 szukamy najbliższego obiektu, który zderza się z promieniem, wyliczając różne przydatne
informacje potrzebne do obliczenia oświetlenia, ewentualnego odbicia/załamania, czy też odczytania
danych z tekstury.
W wierszu nr 7 sprawdzamy, czy parametr jest określony, jeśli nie, to znaczy, że żaden obiekt
nie zderza się z promieniem, a więc dajemy w wyniku kolor czarny.
W wierszach 9-12 dokonujemy teksturowania.
W wierszu nr 15 obliczamy oświetlenie bezpośrednie ze źródeł światła (dzięki niemu powstaje informacja o kolorze
powierzchni rozpraszającej świało). A więc obliczamy oświetlenie bezpośrednio przy pomocy funkcji
skalując wynik przez współczynnik odbicia rozproszonego () i kolor z tekstury ().
W wierszu nr 16 obliczamy odbicie zwierciadlane (lustrzane). Wynikiem wyrażenia jest przeskalowany
przez współczynnik odbicia zwierciadlanego wynik funkcji wywołanej na promieniu odbitym.
W wierszu nr 17 obliczamy transmisję z refrakcją. Wynikiem wyrażenia jest przeskalowany
przez współczynnik transmisji wynik funkcji wywołanej na promieniu załamanym.
Antyaliasing
Każdy piksel traktujemy jako kwadrat o polu 1. Losujemy w tym kwadracie kilka punktów
strzelając przez nie promieniami pierwotnymi (ang. primary rays).
Na każdym z tych promieni wywołujemy funkcję i obliczamy średnią arytmetyczną
uzyskanych wyników.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // s - scena, w której śledzimy promienie
// (x,y) - współrzędne piksela
// maxD - maksymalna głębokość rekursji dla śledzenia promieni
// numS - ilość próbek (tj. ilość promieni pierwotnych przypadających na 1 piksel)
color splicer(scene& s, float x, float y, nat maxD, nat numS) {
color f;
for (nat i = 0; i < numS; i++)
f += traceRay(
primaryRay( s.cam, x + 0.5f -uRand(), y + 0.5f -uRand() ),
s,
maxD
);
return f / numS;
} |
Mapowanie tonów
Może się zdarzyć, że energia padająca na piksel jest za duża,
czyli przynajmniej jeden kanał koloru ma wartość większą od 1.
Sytuacja taka wymaga mapowania tonów. My posłużymy się
prymitywną metodą polegającą na obcięciu do jedynki wartości
większy od jedynki.
1
2
3
4
5
6
7
| #define FILTER(x) ( x > 1.0f ? 1.0f : x )
void toneMapping(color::io c) {
c.r = FILTER(c.r);
c.g = FILTER(c.g);
c.b = FILTER(c.b);
} |
Generowanie obrazu, czyli rendering
Obraz to tablica liczb z przedziału
, w której trzy kolejne
elementy odpowiadają kanałom koloru dla pewnego piksela.
Kolor każdego piksela obliczamy wywołując funkcję
zamieniając jej wynik na kolor w postaci liczb z przedziału
wpisując je wdpowienie miejsca talbicy przechowującej obraz.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // maksymalna głębokość śledzenia promieni
nat maxTraceDepth = 5;
// ilość próbek (promieni pierwotnych) na 1 piksel
// im większa wartość tym lepsza jakość obrazu
nat numSamples = 1;
void render(scene& s, GLubyte* picture) {
camera& cam(s.cam);
color pixel;
for (nat y = 0; y < cam.yRes; y++)
for (nat x = 0; x < cam.xRes; x++) {
pixel = splicer(s, fl(x), fl(y), maxTraceDepth, numSamples);
toneMapping(pixel);
picture[3*(y*cam.xRes + x) +0] = bt(pixel.r * 255.0f);
picture[3*(y*cam.xRes + x) +1] = bt(pixel.g * 255.0f);
picture[3*(y*cam.xRes + x) +2] = bt(pixel.b * 255.0f);
}
} |
Materiały do artykułu
Tutaj możesz ściągnać pełny kod źródłowy programu, który pisaliśmy w tym artykule.
Część I
Część II
Część III
Część IV