Własny silnik graficzny. Część I: podstawy śledzenia promieni.

30.11.2010 - Robert Kraus
TrudnośćTrudnośćTrudnośćTrudność

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:
$ primatyRay $ $ = $ $ front $ $ + $ $ xA*right $ $ + $ $ yA*up $,
gdzie $ xA $ oraz $ yA $ 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 $ a' $ oraz $ b' $ są równe. Wzór na wektor kierunku odbicia to:

$$ out = N \cdot dotProd(N, -in) \cdot 2 + in. $$
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:

$$ out =  N \cdot (\eta \cdot cos(a') - \sqrt{1 - \eta^2 \cdot (1 - cos(a'))}) -in \cdot \eta . $$
Gdzie $ \eta $ jest stosunkiem indeksów refrakcji (ośrodek opuszczany / ośrodek docelowy).

Gdy promień przechodzi z powietrza do wnętrza obiektu (trafiamy w punkt $ X $) nasz wektor normalny jest zorientowany odpowiednio, a $ \eta = \frac{ior1}{ior2} $. Gdy promień wydostaje się z wnętrza obiektu na powietrze (trafiamy w punkt $ Y $) wektor normalny skierowany jest w przeciwną stronę (musimy go odwrócić dla celów obliczenia refrakcji), a $ \eta = \frac{ior2}{ior1} $.

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 $ traceRay $ 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 $ t $ 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 $ lambert $ skalując wynik przez współczynnik odbicia rozproszonego ($ dr $) i kolor z tekstury ($ texMapped $).
W wierszu nr 16 obliczamy odbicie zwierciadlane (lustrzane). Wynikiem wyrażenia jest przeskalowany przez współczynnik odbicia zwierciadlanego wynik funkcji $ traceRay $ wywołanej na promieniu odbitym.
W wierszu nr 17 obliczamy transmisję z refrakcją. Wynikiem wyrażenia jest przeskalowany przez współczynnik transmisji wynik funkcji $ traceRay $ 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ę $ traceRay $ 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 $ \{0..255\} $, w której trzy kolejne elementy odpowiadają kanałom koloru dla pewnego piksela. Kolor każdego piksela obliczamy wywołując funkcję $ splicer $ zamieniając jej wynik na kolor w postaci liczb z przedziału $ \{0..255\} $ 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   

5
Twoja ocena: Brak Ocena: 5 (5 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com