原開源書籍在此:computer-graphics-from-scratch

可以理解為讀後感或翻譯。

RayTracying最關鍵的點是:

??看出去的世界

而非世界如何達到??

這也是這個演算法叫做ray tracying - 光線跟蹤的原因。

例子

1.預備知識

1.1 屏幕坐標 vs 畫布坐標

一般來說屏幕坐標都是始於左上角,然後朝右和下延伸。

不過這裡為了作圖方便,我們把坐標系按照數學中更常見的方式把原點放於屏幕中央,x和y的延伸方向按照平時的習慣來放。

這樣可以知道屏幕坐標系 S_x 和畫布坐標系 C_x 的變換:

S_x = frac{C_w}{2} + C_x

S_y = frac{C_h}{2} - C_y

1.2 畫布 vs 窗戶

因為我們屏幕是二維的,無論我們怎樣模擬,實際上都是要把物體畫在一個二維的平面(畫布)上,我們這裡就假設我們把??放在原點上,而有一扇窗戶在坐標軸 z = d 處 ,我們眼睛能看到的也就是窗戶出去的世界。

那麼對於畫布上的任意一點,在窗戶上都有一個對應的位置,因為它們是中心相同平面平行。

所以對畫布上的每一個點 C_x , C_y 我們都能找到窗戶上的對應點 V_x , V_y ,加上上一段話中的對應關係,實際上就是一個比例的問題,所以我們可以繼續知道畫布坐標系 C_x, C_y 在窗戶上的對應坐標 V_x, V_y 為:

V_x = C_xfrac{V_w}{C_w}

V_y = C_yfrac{V_h}{C_h}

同時因為窗戶放在 z = d 處,我們知道:

V_z = d

這就有了如何將畫布上的每一點轉化為窗戶上的每一點的坐標變換。

1.3 窗戶 vs 空間

從眼睛??射出的光線,我們都可以看成是 overrightarrow{OP}

同時P點位置也可以寫成: P = O + t(V-O) , 令 overrightarrow{D}V - O ,也就是其方向的向量,如下圖:

P = O + toverrightarrow{D}

這裡需要注意的是: O,P,V是位置, overrightarrow{OP}, overrightarrow{D} 是向量。

同時知道:

  • t < 0: 逆向於光線上的點
  • t = 0: 原點
  • t > 0 && t < 1: 原點到V點之間的點,也就是原點到窗戶之間的點
  • t = 1: V點
  • t > 1: V點之後,依舊在射出的光線之上,當t取某個值為P點

2.空間放物體

這裡我們在空間中放置一個球體

球心為C,那麼球上點P需要滿足方程:

|P - C| = r

3. 處理交互 跟蹤光線

假設OP就是我們看出去的光線,來追蹤它,當看向球體時,它會與球體產生交互,圖中這條光線就是和球體相遇了:

球上的屬於這條光線的P點應該滿足:

P = O + toverrightarrow{D}

|P - C| = r

代入1式進2式:

| O + toverrightarrow{D} - C| = r

| toverrightarrow{D} + overrightarrow{OC}| = r

來解方程:

(toverrightarrow{OD} + overrightarrow{OC})(toverrightarrow{OD} + overrightarrow{OC}) = r^2

展開:

 t^2|overrightarrow{OD}|^2 + 2 toverrightarrow{OD}cdotoverrightarrow{OC} + |overrightarrow{OC}|^2 -r^2 = 0

 k_1 = |overrightarrow{OD}|^2 ?

k_2 = 2 overrightarrow{OD}cdotoverrightarrow{OC}

k_3 = |overrightarrow{OC}|^2 -r^2

這就變成解關於t的一元二次方程:

 {t_1, t_2}  = egin{equation} frac{ -k_2 pm sqrt{k_2^2 - 4k_1k_3} } {2k_1} end{equation}

會出現:

  • k_2^2 - 4k_1k_3 > 0 : 兩個解
  • k_2^2 - 4k_1k_3 = 0? : 一個解
  • k_2^2 - 4k_1k_3 < 0 : 無解

對應的就是下圖的狀況:

所以問題就變簡單了,如果我們有交互,那麼我們應該展示的是近的點 t_1 的顏色,如果我們沒有交互,那麼我們展示的就是背景色。

以上就是光線追蹤的根本原理。

至此の偽碼

跟蹤光線與球相交 IntersectRaySphere

IntersectRaySphere(O, D, sphere){
C = sphere.center
r = sphere.radius
oc = O - C

k1 = dot(OD, OD)
k2 = 2 * dot(OC, OD)
k3 = dot(OC,OC) - r*r

discriminant = k2 * k2 - 4 * k1 * k3
if discriminant < 0:
return inf, inf

t1 = (-k2 + sqrt(discriminant))/(2*k1)
t2 = (-k2 - sqrt(discriminant))/(2*k1)
return t1, t2
}

t1 的顏色TraceRay

這裡我們在空間里放入好幾個球體,然後計算t1處的顏色偽碼如下:

TraceRay(O, D, t_min, t_max){
closest_t = inf
closest_sphere = NULL
for sphere in scene.Spheres {
t1, t2 = IntersectRaySphere(O, D, sphere)
if t1 in [t_min, t_max] and t1 < closest_t
closest_t = t1
closest_sphere = sphere
if t2 in [t_min, t_max] and t2 < closest_t
closest_t = t2
closest_sphere = sphere
}

if closest_sphere == NULL
return BACKGROUND_COLOR

return closest_sphere.color
}

我們在空間中放入三個小球:

畫到畫布上

在上述三個偽碼函數中,最終是TraceRay這個函數調用了其餘兩個函數,那麼我們現在需要來設定它的參數。

D

D這個實際上之前已經寫到,就是從O到V的向量,那麼我們的V又由最早的畫布到窗戶可以得知,所以我們可以有函數:

CanvasToViewport(x, y){
return (x * Vw/Cw, y * Vh/Ch, d)
}

t_min, t_max

t = 1 是V,是在窗戶上,t > 1 是窗戶之後,是場景,所以我們需要取的值是 t_min = 1,我們並不需要攝像頭和窗戶之間的顏色,因為我們也沒有放任何東西在那裡,我們需要的是窗戶之後的景色,所以 t_min = 1, t_max = inf

組裝

最後,我們需要的是來做循環,把所有的代碼組裝在一起,放在屏幕上,所以偽碼如下:

O = <0,0,0>

for x in [-Cw/2, Cw/2]{
for y in [-Ch/2, Ch/2]{
D = CanvasToViewport(x, y)
color = TraceRay(O, D, t_min, t_max)
canvas.putPixel(x, y, color)
}
}

對應的窗戶大小和距離都可以在代碼中看到,看結果:

這根本看起來不3d,很簡單,因為我們並沒有考慮的作用。

代碼

推薦閱讀:

相关文章