[光線追蹤] 05 -- 相機(jī) & 球體 & 啞光材質(zhì)
在上一篇專欄里介紹了光追的程序結(jié)果,? 并且實(shí)現(xiàn)了幾個(gè)重要的模板類.? 這篇專欄就對(duì)模板進(jìn)行實(shí)現(xiàn),? 做出第一個(gè)可以跑的光追程序.

針孔相機(jī)
所有相機(jī)實(shí)類都繼承自 Camera 模板類,? 并且主要是實(shí)現(xiàn)其中的 get_ray 虛函數(shù).
在眾多相機(jī)類型里最常見(jiàn)到的就是針孔相機(jī)了.? 針孔相機(jī)的準(zhǔn)確模型如下圖所示

針孔可以看作一個(gè)無(wú)限小的洞,? 而在后方的傳感器上出現(xiàn)了前方景物的倒像.? 而為了方便計(jì)算機(jī)建模,? 利用相似三角形可以把模型轉(zhuǎn)化為

這樣,? 針孔相機(jī)就可以使用一個(gè)視點(diǎn)位置 (針孔位置) 和一個(gè)焦平面 (傳感器位置) 表示.? 視點(diǎn)位置可以使用一個(gè) Vec3 表示,? 而焦平面則需要3個(gè) Vec3 表示,? 如下圖所示

其中,? d 表示焦平面中心與視點(diǎn)的相對(duì)位置,? u 和 v 是焦平面的紋理坐標(biāo)系.? 值得注意的是,? {u, v, d} 構(gòu)成了左手系,? 在光柵化渲染里,? 為了轉(zhuǎn)成右手系習(xí)慣上把 d 或 v 逆轉(zhuǎn) (opengl 或?dx),? 但光追不涉及仿射變換所以其實(shí)也不在意左右手的問(wèn)題了,? 這里就繼續(xù)用上圖所示的 {u, v, d} 進(jìn)行建模了.
為了進(jìn)行 MC 積分,? 光追需要對(duì)單像素進(jìn)行多次采樣,? 也就是說(shuō)相機(jī)類里還要包含 Sampler,? 并且在焦平面上的采樣應(yīng)該是與 figure 互相綁定的,? 所以 Sampler 可以直接寫(xiě)在基類 Camera 里,? 改寫(xiě) Camera 為

另外需要注意圖像的 y 軸正方向是從上到下,? 而不是紋理坐標(biāo)里的從下到上.? 那么最終針孔相機(jī)實(shí)現(xiàn)為:

雖然實(shí)現(xiàn)了針孔相機(jī),? 但是很明顯這種形式不太像是常用的相機(jī).? 在通常相機(jī)參數(shù)里應(yīng)該提供 視點(diǎn)位置,? 視方向,? 視野寬度,? 長(zhǎng)寬比,? 視上方向.? 實(shí)際上,? 這里實(shí)現(xiàn)的相機(jī)是通用的,? 而通常的相機(jī)參數(shù)可以通過(guò)計(jì)算得到相應(yīng)的紋理坐標(biāo) u, v.? 給針孔相機(jī)定義一個(gè)初始化方法:

里面的數(shù)學(xué)就不過(guò)多敘述了,? 都是簡(jiǎn)單的幾何.? 需要注意的是這個(gè) viewup (視上方向),? 表示的是在視野內(nèi)的上方向,? 而不是世界上方向.? 當(dāng)視上方向與世界上方向相等時(shí),? 圖像是這樣的:

但當(dāng)視上方向與世界上方向形成一定角度時(shí)可以產(chǎn)生這樣的圖像:

也就是說(shuō)視上方向可以起到控制傾角的作用,? 不過(guò)一般來(lái)說(shuō)不怎么經(jīng)常調(diào)整傾角,? 所以可以給 viewup 一個(gè)默認(rèn)的世界上方向.? 但特別需要注意的是,? 計(jì)算會(huì)在視方向和視上方向互相平行時(shí)出錯(cuò),? 所以在給出視方向和傾角時(shí),? 應(yīng)該有一個(gè)方法計(jì)算正確的視上方向:

其中,? d 是視方向,? alpha 是傾角,? 并且這個(gè)公式是由矩陣變換得出的?(說(shuō)過(guò)這里不會(huì)討論線代).
最后稍微提到一點(diǎn),? 在通常的相機(jī)里,? u, v, d 是互相垂直的,? 但如果 u, v 互相垂直,? 但 d 不都與 u, v 垂直,? 即可以得到移軸相機(jī).? 移軸相機(jī)這里也不實(shí)現(xiàn)了,? 感興趣的可以自己做做.

球體
幾何物體實(shí)類全部都繼承自 Object 模板類,? 并實(shí)現(xiàn) hit 和 hit_record 虛函數(shù) (這里不討論光源,? 其他虛函數(shù)統(tǒng)統(tǒng)返回零值).
在幾何物體里,? 除了平行四邊形,? 最簡(jiǎn)單的物體就是球體了.? 可以給出兩個(gè)參數(shù)確定一個(gè)球體:? 球心位置 p 和球體半徑 r,? 空間上所有符合??的點(diǎn)構(gòu)成一個(gè)球體.
之前說(shuō)過(guò),? 光線上任意一點(diǎn)可以表示為?,? 把光線方程代入球體方程里可以得到
,? 求解 t 就可以得到:? 光線經(jīng)過(guò)路程?t 后與球體碰撞.? 因?yàn)槟iL(zhǎng)有關(guān)系:?
,? 所以將上式兩邊平方,? 應(yīng)用關(guān)系,? 開(kāi)括號(hào),? 合并同類項(xiàng),? 可以得到:
,? 這樣就變成了普通的一元二次方程,? 解就完事了.? 在解出 t 后,? 在確保值大于 0 的情況下,? 值較小的那個(gè)解就是光線與球體最近的碰撞點(diǎn).? 下面給出 Sphere類和相應(yīng)的 hit 函數(shù)實(shí)現(xiàn):


在 hit 里判別 " a_t 是否大于 0" 的時(shí)候使用了 1e-8 是因?yàn)榇嬖诟↑c(diǎn)數(shù)誤差,? 需要選取比 0 大的數(shù)值避免計(jì)算錯(cuò)誤?如下圖所示,? 盡管這里直接選定 1e-8 作為判別值,? 但實(shí)際上對(duì)于不同大小的球體,? 判別值是不一樣的 (這個(gè)就不是這里的討論范圍了).

類似地,? 可以實(shí)現(xiàn) hit_record 方法:

在 hit_record 里,? ray 即是 rec.ray,? tmax 即是 rec.t,? 所以在構(gòu)建 HitRecord 時(shí)說(shuō)到初始化 t 可以起到限制 Ray 最遠(yuǎn)距離的作用,? 這在判斷兩個(gè)不同物體的遠(yuǎn)近時(shí)非常有用.? 另外這里不計(jì)算紋理坐標(biāo)的原因是,? 打算以后專門開(kāi)一篇紋理采樣的專欄,? 到時(shí)候再說(shuō)吧.

啞光材質(zhì)
材質(zhì)實(shí)類全部都繼承自 Material 模板類,? 并且實(shí)現(xiàn) render 虛函數(shù) (這里不討論光源, 所以不考慮 render_emissive 虛函數(shù)).
當(dāng)光線照射到理想的啞光材質(zhì)上時(shí),? 能量將會(huì)均勻分布在所有出射方向上.? 現(xiàn)實(shí)里是不存在理想的啞光材質(zhì)的,? 最貼近的例子就是啞光紙.? 啞光材質(zhì)對(duì)應(yīng)的 BRDF 模型稱為 Lambertian 反射模型,? 之前說(shuō)過(guò)描述反射的式子為?,? 既然啞光材質(zhì)會(huì)均勻地分布能量,? 也就是 f? 與出射方向 ω? 無(wú)關(guān),? 并且由光路可逆條件知道,? f? 也與入射方向 ω? 無(wú)關(guān),? 也就是說(shuō) Lambertian 的?f? 是一個(gè)常數(shù).? 之前說(shuō)過(guò)在測(cè)試函數(shù)空間里,? 反射率定義為?
,? 那么對(duì)于 Lambertian 來(lái)說(shuō)就是:
.? 對(duì)于真實(shí)物體來(lái)說(shuō),? 反射率必定小于 1,? 在這里的程序里,? 出射光線的顏色可以由紋理指定,? 所以可以強(qiáng)制指定 BRDF 的反射率等于 1,? 即是
,? 解得
.
于是得到 Lambertian 的半球模型為?,? 應(yīng)用之前說(shuō)過(guò)的 MC 積分可以得到模型的解為
.? 那么啞光材質(zhì)可以實(shí)現(xiàn)為

可能就會(huì)有人好奇說(shuō)好的累加去哪里了,? 忽略 L? 時(shí)的符號(hào)渲染模型為?,? 迭代一次即為
,? 因?yàn)?K 是一個(gè)積分,? 所以 K2 也是一個(gè)積分,? 只不過(guò)需要積分的變量更多了,? 并且 MC 積分是可以用采樣求解高維積分的,? 那么把 K2 看作單個(gè)高維積分,? 那么相應(yīng)的 MC 積分為
.? 所以累加可以放在積分的最頂層進(jìn)行, 亦即在焦平面上的像素采樣.
或者寫(xiě)成一行也可以:

世界類
世界類 World 含有光追所需所有對(duì)象,? 包括但不限于:? 光追器,? 相機(jī),? 物體s 和 光線迭代的最大深度.? 所以世界類為

當(dāng)然隨著光追程序的完善,? World 里面的東西也會(huì)越來(lái)越多,? 但是在這里的話這樣就足夠了.

渲染部分
到這里為止,? 場(chǎng)景里一個(gè)光源都沒(méi)有,? 所以渲染出來(lái)的場(chǎng)景肯定是漆黑一片.? 但是可以改寫(xiě) RayTracer 類使得部分光線返回不為 0 的顏色值.
在 RayTracer 里有兩部分是直接返回顏色值的:? 迭代過(guò)深 和 光線不與物體碰撞.? 當(dāng)光線不與物體碰撞時(shí),? 可以看作光線與無(wú)限遠(yuǎn)處的天空發(fā)生碰撞,? 并且返回天空的顏色值.? 當(dāng)光線迭代過(guò)深時(shí),? 可以看作光線能量在反射傳播時(shí)耗盡,? 亦即必定返回 0 值.? 那么繼承 RayTracer 類實(shí)現(xiàn)一個(gè)返回天空顏色的光追器:

到此,? 光追所需的類型已經(jīng)全部準(zhǔn)備好了,? 剩下的就是排列組合了 (
渲染過(guò)程也是非常直觀明了的:? 歷遍圖像像素,? 并且每個(gè)像素內(nèi)歷遍采樣集:


到此整個(gè)渲染邏輯已經(jīng)全部實(shí)現(xiàn)了,? 就可以進(jìn)行一個(gè)例子的跑.
這里使用一個(gè)簡(jiǎn)單的場(chǎng)景:? 兩個(gè)球體,? 其中一個(gè)非常大的球體充當(dāng)場(chǎng)景地板:

make_sampler 是自己做的糖,? 可以在 samples.h 里找到定義.
那么在 main 里可以這樣實(shí)現(xiàn)渲染:

輸出的圖像如下:


可以留意到,? Lambertian 的實(shí)現(xiàn)完全只依賴于輸入的采樣集:
當(dāng)采樣集分布不為 cosθ 時(shí)可以做到其他不同的 BRDF.? 比如說(shuō)更改采樣在半球上的集中度:
渲染結(jié)果將會(huì)改變:

可以看到兩球相切的陰影變得更加深色了.? 盡管這里的程序把采樣生成直接暴露在外部,? 從而調(diào)整采樣可以做到不同的效果,? 但必須記住:? 只有集中度為 1 的半球采樣才表示 Lambertian 模型.
另外不得不注意的是,? 在構(gòu)建 world 時(shí)創(chuàng)建了幾個(gè) Sampler,? 為了可以正確地計(jì)算 MC 積分,? 必須要求 Sampler 之間每個(gè)采樣集里的樣本數(shù) samples_per_set?都是一致的.

這里簡(jiǎn)單實(shí)現(xiàn)了第一個(gè)可以運(yùn)行的光追程序,? 但代碼里仍有很多地方還沒(méi)細(xì)化 (特別是 Material).? 在下一篇專欄里將會(huì)實(shí)現(xiàn)幾種不同的簡(jiǎn)單光源以及環(huán)境光.
項(xiàng)目倉(cāng)庫(kù):?https://github.com/nyasyamorina/nyasRT
來(lái)點(diǎn)不正經(jīng)的澀弔圖扣扣群:?274767696
封面pid:?94213121