[光線追蹤] 04 -- 程序結(jié)構(gòu)
經(jīng)過之前幾篇專欄的討論,? 已經(jīng)確定了光追渲染模型為?,? 那么下面就需要確定程序結(jié)構(gòu).? 為了方便快捷,? 這里的程序?qū)?huì)使用最新的?C++20,? 盡管很多特性并不會(huì)用上.

顏色值
在之前的專欄里,? 一直都是以輻射度表示光線的輻射強(qiáng)弱,? 也就是單個(gè)浮點(diǎn)數(shù)數(shù)值.? 為了產(chǎn)生彩色圖像,? 則需要對(duì)每種需要的色彩進(jìn)行光追計(jì)算.? 為了計(jì)算多種色彩,? 需要對(duì)輻射度引入波長微元,? 那么在波長 λ?處的輻射度變?yōu)??.? 人類可見光的波長范圍大約在 380nm~780nm 內(nèi),? 在這個(gè)范圍內(nèi)進(jìn)行渲染即可得到準(zhǔn)確的彩色圖像.
實(shí)際上,? 人類可以感知的色彩僅有 3 種: 紅綠藍(lán),? 那么整個(gè)可見光范圍的圖像可以通過加權(quán)累加得到在人眼里準(zhǔn)確的紅綠藍(lán)圖像 (請(qǐng)自行wiki: CIE-XYZ 色度空間).? 既然最終圖像僅有 3 種顏色,? 那么可以假設(shè)僅對(duì)紅綠藍(lán)進(jìn)行光追就可以得到足夠接近的圖像 (實(shí)際上某些條件下紅綠藍(lán)光追會(huì)產(chǎn)生極其錯(cuò)誤的圖像),? 從而降低了需要計(jì)算的數(shù)值的數(shù)量.? 另外假設(shè)所有改變光線的事件對(duì)所有顏色都是完全相等的?(這至少意味著折射是錯(cuò)誤的, 即無法復(fù)現(xiàn)透鏡色散之類的現(xiàn)象),? 這樣假設(shè)可以在單次光追里同時(shí)計(jì)算紅綠藍(lán)三種顏色的輻射度,? 而不是對(duì)每種顏色都做一次光追.
不得不說的是,? 如果改變光線的事件如果對(duì)每種顏色都有差異的話,? 那么在可見光范圍里對(duì)每個(gè)顏色進(jìn)行單獨(dú)光追是非常合理的,? 但這種合理性在從可見光映射到紅綠藍(lán)時(shí)就已經(jīng)消失了.? 為了在紅綠藍(lán)光追里準(zhǔn)確還原可見光光追,? 性能消耗最多是可見光光追的三倍.
另外,? 在實(shí)際程序里將會(huì)使用 RGB 值進(jìn)行光追.? 上面說到的紅綠藍(lán)實(shí)際上是指 XYZ 值而不是 RGB 值,? RGB 渲染比 XYZ 渲染產(chǎn)生的色彩更加局限 (wiki: CIE-XYZ色度空間).? 使用 RGB 的理由是:? RGB 可以直接由計(jì)算機(jī)顯示,? 而 XYZ 需要映射到 RGB 后才可以顯示.? 程序定義的 RGB 類幾乎與 Vec3 類相等,? 但浮點(diǎn)數(shù)精度是單精度,? 這是因?yàn)轭伾⒉恍枰浅?zhǔn)確的數(shù)值來計(jì)算.? 在常見的圖像格式里,? RGB 分量為范圍在 [0, 255] 的整數(shù),? 但在渲染緩沖區(qū)里,? RGB 的分量為范圍在?[0, 1] 的浮點(diǎn)數(shù),? 使用浮點(diǎn)數(shù)是因?yàn)檎麛?shù)還沒達(dá)到色彩計(jì)算的精度要求,? 但使用 [0, 1] 內(nèi)的數(shù)值是因?yàn)轱@卡計(jì)算的要求,? 雖然這里設(shè)計(jì)的程序是使用 CPU運(yùn)行的,? 但出于習(xí)慣還是會(huì)使用 [0, 1] 的浮點(diǎn)數(shù)表示 RGB.

相機(jī)模板
既然光追是渲染程序,? 那么肯定需要輸出一個(gè)二維的 RGB 圖像.? 另外需要知道渲染圖像上每個(gè)像素的光線,? 當(dāng)從像素處獲得光線后,? 就可以進(jìn)入光追流程了.? 那么相機(jī)模板為

其中虛函數(shù) get_ray 由相機(jī)的子類來實(shí)現(xiàn).

智能指針
為了不讓內(nèi)存管理成為一件痛苦的事,? 并且考慮到類型之間的多態(tài)行為,? 這里程序內(nèi)將會(huì)使用大量的智能指針.? 為了避免類型名過長,? 將會(huì)使用類型名后接 -p 代表相應(yīng)的智能指針類型,? 相應(yīng)的成員名將會(huì)以?-_p 結(jié)尾:
智能指針定義在 <memory> 里.? 下面是一段智能指針的使用例子:

可以看到,??使用智能指針的 C++ 非常像自帶垃圾回收的其他語言 (如 python, C# 等).? 至于智能指針的回收垃圾性能問題就不是這里的討論范圍了,? 因?yàn)檫@里程序創(chuàng)建和清除智能指針對(duì)象只會(huì)發(fā)生在 build_world 函數(shù)內(nèi),? 而光追內(nèi)部是不涉及創(chuàng)建/清除智能指針對(duì)象的.

材質(zhì),? 物體和碰撞記錄
在渲染模型的專欄里介紹半球模型時(shí),? 提到了一個(gè)返回與光線最近碰撞點(diǎn)的的函數(shù) h,? 這個(gè)函數(shù)在 World 類里以方法?hit_object?實(shí)現(xiàn).? 盡管數(shù)學(xué)上是返回最近碰撞點(diǎn),? 但在實(shí)際實(shí)現(xiàn)里可以通過返回多個(gè)數(shù)值達(dá)到化簡(jiǎn)計(jì)算,? 比如可以返回碰撞點(diǎn)處的法線供后續(xù)渲染.? 所以這里提出 HitRecord 類來記錄光線與物體碰撞時(shí)產(chǎn)生的數(shù)據(jù),? 下面是這個(gè)類的實(shí)現(xiàn):

其中 World 引用和內(nèi)置的 Ray 是為了減少后面方法的參數(shù)數(shù)量;? depth 表示當(dāng)前光線的深度,? 避免出現(xiàn)無限遞歸;? hit 表示光線是否有與物體碰撞;? object_p 表示與光線碰撞的物體指針,? 因?yàn)楣饩€可能不與物體碰撞,? 所以使用 null 進(jìn)行初始化;? t 為光線類里 at(t) 中的 t,? 而初始化 HitRecord 時(shí)輸入 t 是為了限制光線的最遠(yuǎn)距離;? point, normal, tex 則是碰撞點(diǎn)和碰撞點(diǎn)處的法線和紋理坐標(biāo).? 那么 hit_object?可以用下面的方法實(shí)現(xiàn):

另外,? 在面積模型里判斷兩點(diǎn)是否有物體阻擋的函數(shù),? 可以這樣實(shí)現(xiàn) (absnorm 與之前的定義不一樣了, 請(qǐng)與 github 上代碼定義為準(zhǔn)):

在代碼里可以看到 Object 類定義了兩個(gè)函數(shù):? hit 和 hit_record,? hit 僅計(jì)算光線是否與物體碰撞,? 而 hit_record 除了計(jì)算光線是否與物體碰撞,? 還會(huì)計(jì)算碰撞時(shí)需要用到的數(shù)據(jù).
當(dāng)從 hit_object 里獲得 HitRecord 后,? 就可以進(jìn)行光線的渲染,? 渲染光線由材質(zhì)?Material 類下的 render 方法進(jìn)行.? 因?yàn)?HitRecord 內(nèi)包含渲染光線所需的所有數(shù)據(jù),? 所以 render 方法僅需輸入 HitRecord 就可以進(jìn)行渲染.? 材質(zhì)包括但不限于:? 不透明材質(zhì),? 透明材質(zhì),? 發(fā)光材質(zhì).? 在上一篇專欄里有說過,? 為了避免重復(fù)計(jì)算光源,? 光源的 L?,obj 應(yīng)該為 0,? 所以為了保證接口的通用性,??Material 里應(yīng)該有兩個(gè)渲染方法:

自發(fā)光的材質(zhì)類重載 render_emissive,? 而其他材質(zhì)重載 render.
因?yàn)椴馁|(zhì)和物體是綁定的,? 所以 Object 類里還需要包含材質(zhì).? 而對(duì)于自發(fā)光物體,? 需要實(shí)現(xiàn)在物體表面上進(jìn)行均勻采樣,? 這里使用一個(gè)函數(shù) get_sample 返回物體表面上的采樣和相應(yīng)的法線,? 那么最后 Object 定義為

這里直接偷懶把返回的采樣點(diǎn)和法線打包成 Ray 返回了 (point 是采樣點(diǎn), direction 是法線).? 其中 is_light 方法是判斷材質(zhì)是否為自發(fā)光材質(zhì).
另外,? 為了實(shí)現(xiàn)在物體表面采樣,? Object 的子類里必定含有 Sampler,? 但一個(gè) Sampler 是比較龐大的,? 并且通常場(chǎng)景里發(fā)光物體是少數(shù),? 所以為了節(jié)省內(nèi)存,? Object 子類里應(yīng)該存放 Sampler 的指針,? 而不是 Sampler 本身,? 為此可以定義 Sampler 的智能指針類:?

光線追蹤流程
從上面討論的幾個(gè)類中,? 不難知道光追的大概流程為:? 1) 使用 Camera::get_ray 得到初始光線 Ray;? 2) 使用 World::hit_object?獲得 HitRecord;? 3) 使用 Material::render 渲染光線并返回 RGB 值.? 不過從光追的數(shù)學(xué)模型知道,? Material::render 的具體實(shí)現(xiàn)會(huì)比較復(fù)雜,? 很可能需要計(jì)算多條次級(jí)光線,? 而計(jì)算光線的顏色又需要從上述第二步開始形成遞歸.
為了控制遞歸,? 這里提出 RayTracer 類.? 下面是 RayTracer 類的簡(jiǎn)單實(shí)現(xiàn):

而 Material::render 內(nèi)的次級(jí)光線可以這樣渲染:

另外需要留意到,? RayTracer::trace 是調(diào)用 Material::render 方法,? 這意味著從相機(jī)出發(fā)的光線如果與自發(fā)光物體碰撞將會(huì)返回黑色,? 從而造成自發(fā)光物體不可見.? 所以可以額外寫一個(gè)方法區(qū)分自發(fā)光物體與普通物體的 trace 方法:

那么從相機(jī)直接出發(fā)的光線將調(diào)用 RayTracer::trace_from_camera 而不是 RayTracer::trace.

在這里介紹了光追的流程亦即實(shí)現(xiàn)了幾個(gè)比較關(guān)鍵的模板類.? 接下來的專欄將會(huì)逐個(gè)實(shí)現(xiàn)模板類的實(shí)類.? 上面的代碼可能存在性能或者邏輯問題,? 所以絕對(duì)不是一成不變的,? 代碼的最終結(jié)果請(qǐng)以 gayhub 上的為準(zhǔn).
但就算是 gayhub 上的代碼也只能確保編譯通過,? 隨著代碼的修改,? 可能會(huì)使某些之前可以運(yùn)行的特性在新更新的版本里失效 (一個(gè)人維護(hù)這么大的項(xiàng)目屬于是有點(diǎn)累了).? 如果有什么意見或者問題提出的話,? 歡迎加q群討論.
gayhub倉庫:??https://github.com/nyasyamorina/nyasRT
扣扣群:??274767696
封面pid:?97290687