原開源書籍在此:computer-graphics-from-scratch
可以理解為讀後感或翻譯。
RayTracying最關鍵的點是:
??看出去的世界
而非世界如何達到??
這也是這個演算法叫做ray tracying - 光線跟蹤的原因。
一般來說屏幕坐標都是始於左上角,然後朝右和下延伸。
不過這裡為了作圖方便,我們把坐標系按照數學中更常見的方式把原點放於屏幕中央,x和y的延伸方向按照平時的習慣來放。
這樣可以知道屏幕坐標系 和畫布坐標系 的變換:
1.2 畫布 vs 窗戶
因為我們屏幕是二維的,無論我們怎樣模擬,實際上都是要把物體畫在一個二維的平面(畫布)上,我們這裡就假設我們把??放在原點上,而有一扇窗戶在坐標軸 z = d 處 ,我們眼睛能看到的也就是窗戶出去的世界。
那麼對於畫布上的任意一點,在窗戶上都有一個對應的位置,因為它們是中心相同平面平行。
所以對畫布上的每一個點 , 我們都能找到窗戶上的對應點 , ,加上上一段話中的對應關係,實際上就是一個比例的問題,所以我們可以繼續知道畫布坐標系 在窗戶上的對應坐標 為:
同時因為窗戶放在 z = d 處,我們知道:
這就有了如何將畫布上的每一點轉化為窗戶上的每一點的坐標變換。
從眼睛??射出的光線,我們都可以看成是 。
同時P點位置也可以寫成: , 令 為 ,也就是其方向的向量,如下圖:
這裡需要注意的是: O,P,V是位置, 是向量。
同時知道:
這裡我們在空間中放置一個球體
球心為C,那麼球上點P需要滿足方程:
假設OP就是我們看出去的光線,來追蹤它,當看向球體時,它會與球體產生交互,圖中這條光線就是和球體相遇了:
球上的屬於這條光線的P點應該滿足:
代入1式進2式:
來解方程:
展開:
令
這就變成解關於t的一元二次方程:
會出現:
對應的就是下圖的狀況:
所以問題就變簡單了,如果我們有交互,那麼我們應該展示的是近的點 的顏色,如果我們沒有交互,那麼我們展示的就是背景色。
以上就是光線追蹤的根本原理。
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(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這個實際上之前已經寫到,就是從O到V的向量,那麼我們的V又由最早的畫布到窗戶可以得知,所以我們可以有函數:
CanvasToViewport(x, y){ return (x * Vw/Cw, y * Vh/Ch, d) }
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,很簡單,因為我們並沒有考慮光的作用。
代碼
推薦閱讀: