版權聲明
- 本文為“優夢創客”原創文章,您可以自由轉載,但必須加入完整的版權聲明
- 文章內容不得刪減、修改、演繹
- 本文視頻版本:見文末
- 各位同學大家好,今天我要給大家分享的是光線追蹤的原理和實現
- 大家知道在過往很多年里面,光線追蹤技術一般只能用于離線渲染
- 但是由于硬件發展的速度非常快,大家知道摩爾定律是每一年半硬件的性能會增長一倍
- 但是現在像英偉達顯卡,可能半年或者一年就會推出性能翻倍的顯卡
- 所以已經完全打破了摩爾定律
- 那按照這個規律,可能也過不了多久,光線追蹤技術就可以用在實時游戲當中
- 實際上現在有一些游戲像賽博朋克 2077 已經用上了支持光追技術
- 所以今天我就要給大家分享光線追蹤的原理和實現,幫助大家能跟上未來圖形學的發展
今天我們課程的議題主要有這樣一些:
- 基本光線追蹤的實現方式
- 包括基本光錐代碼架構
- 光錐性能優化
- 光追的性能開銷主要來源于跟蹤的射線數量以及射線反彈的次數,那么如何對光錐進行性能優化呢?
- 光錐效果優化
- 光錐在實現效果的時候,算法特別簡單,不需要去用各種各樣的trick來做優化。但是由于光追的性能限制,如何在保證性能的前提下去優化效果實現,是一個問題
- 基于基本形體的光錐介紹
- 在前面,我會以一些簡單的基本形體,比如:球形立方體去介紹基本光錐算法,但是如何基于任意物體表面來進行光線追蹤呢?
- 光錐的鏡面反射和后處理
- 我會講到如何給光錐去添加鏡面反射效果,而不僅僅是擁有自發光或者是漫反射
- 光追后處理
- 最后一步我們會講到如果要做后處理,那么運用光錐應該怎么做。光錐做后處理跟前面我們講的一樣,就是它做起來特別的簡單,不需要特殊的trick。
光追基本原理
- 光線追蹤認為:我們屏幕上面每一個的像素,是我們在光線追蹤時的目標方向
- 我們從攝像機所在的位置向屏幕的每一個像素點發射一條射線,并且沿著這條射線的方向一直行進
- 然后根據這個射線碰到了哪些物體以及碰到的這個物體反彈的光線信息,去決定最終我們在這個像素點的位置究竟應該如何著色
- 下面我們先從一個最物理的角度去理解一下光照的情形
- 大家看現在這個圖,其實從原理上來說,從物理正確的角度來說,光是由于光源向360度角,沒有特定方向性發射光線照射在物體表面
- 比如說這邊這個光源,那么我從光源位置發射出一束光線
- 然后光線經過無數次的反彈,比如說它可能反彈到這個位置,然后就反彈到這個位置
- 經過無數次的反彈,最后假設有一束的光線最終射入到了我們的眼睛,那么我們就看見了光
- 大致的原理是這樣
- 但是實際上如果我們這樣去進行光線追蹤的話,那么它的性能實在是太低了
- 因為你想一下,你的這個燈光要往360度角無死角的去發出光線
- 然后去追蹤每一束光線它的一個反射再反射再反射的情況
- 即使不考慮反彈,由于它是向全方向去發射光線,它的計算開銷仍然很大
- 所以實際上圖形學當中,我們并不是這樣去進行光對的
- 而是從我們的攝像機,也就是我們的眼睛去發射光線,然后計算光線經過的物體表面的顏色
- 比如說我們的眼睛看到了這樣一塊地板,這個地板是紫色的,那么好,我就知道在這個位置是可以獲得一個紫色的光
- 那么類似的假設我的眼睛是照在這個位置,但是由于這個光線它的來源可能是另外一個發光的物體,所以我還需要把這個發光物體也考慮進去
- 我需要考慮燈光的多次反彈的組合的結果
- 這種時候地圖圖形學當中去計算光的方式,把這些光照信息記錄在攝像機的投影面上面
- 這就是我們圖形學當中進行光追的方式
- 好,現在我們具體分析一下光錐的技術實現
- 當我們從攝像機發射出一束光線,在進行光錐的時候,怎么樣去獲得光呢?
- 其實有 3 種可能性
- 第一種可能性是從光源位置發射出一束光,這束光沒有照到任何物體的表面
- 這種情況就不要考慮,因為這個時候沒有獲得光照,也不需要去計算
- 第二種情況是射線剛好命中物體的表面,留在我們圖上面這個紅點
- 那這個時候我們的射線跟我們的物體表面會有一個焦點,我需要獲取這個焦點的光照顏色信息,把它記錄在這一條射線路徑的光照的結果上面
- 第三種情況更普遍,就是從光源位置發出的射線經過物體,但是它不是剛好是照在物體的邊緣上面,而是命中物體的內部
- 這個時候會有兩個命中點,一個點叫入點,另外一個叫輸點
- 這個時候光照計算的結果就有兩個點在這里,我們不需要入點和出點 2 個解
- 為什么呢?因為在光追的時候,我追蹤到入點的時候,由于我們的觸點是不能夠被我們的眼睛所看見的,所以觸點對我來說是沒有計算價值的
- 第一種可能性是從光源位置發射出一束光,這束光沒有照到任何物體的表面
- 到這里,大家就已經完成了光追的第一步
- 在這里我們只繪制一個球體,不考慮光線的反彈
- 那么我們的代碼結構大致上會是這樣子的
- 首先我會去獲得攝像機的試點,這個地方是獲得我們觀察的射線
- 假設我們的眼睛在這兒,我要觀察球體的表面的像素
- 通過這句話我會把這個像素轉換到 3D 空間
- 然后根據這個目標點以及我們的攝像機的原點,我們構造射線的方向
- 最后一行是我用來判斷射線是不是跟球體發生了碰撞
- 我是把它封裝到這個函數里面,叫做 CalculateRayCollision
- 如果要是發生了碰撞,那么我就取得發生碰撞的這個小球的顏色
- 如果是綠色,最后我們在這個像素點位置就渲染一個綠色出來
- 對于我們的藍球和紅球是同理的
- 首先我會去獲得攝像機的試點,這個地方是獲得我們觀察的射線
光線反彈方向的確定
- 現在我們如果僅僅把一個小球以一種無光照或者說以一種固定顏色的方式去渲染出來,那很顯然不是光追希望獲得的結果
- 我們希望獲得的結果是計算一束射線,就是白色這個射線照射在一個物體表面以后所獲得的一個漫反射的信息,diffuse reflection
- 那么怎么樣去獲得這個漫反射信息呢?在這里是有一個難點的
- 對于我們知道漫反射是在物理上來說,它是朝各個方向不規則的光照反射結果的疊加
- 所以,如果你想真正的物理真實的去計算一個光錐漫反射,那么你需要把照射在物體的某一個點上面的所有反射光全部計算一遍,這樣的話顯然是不現實,它的計算代價太高了!
- 在這里,我們的光錐是運用了一個技巧,就是我隨機的選擇某一個方向,用這個反射的結果來代表漫反射的結果,那這樣的話性能開銷就會低得多!
- 雖然它的結果可能并不是那么令人信服,但是至少它是一個合理的解決方案
- 并且我在后面還會講到如何對它進行優化
- 好,那么我們如何隨機生成一個光照反射的方向呢?
- 在這里我們就需要在 shader 里面去編寫一個產生隨機數的算法
- 這個算法細節我們先忽略
- 那么來看一下,假設這個算法我已經寫好了,那么我的算法大概會生成一個什么樣的圖像
- 那么我就會在屏幕上面的每一個像素點上面都去生成一個隨機值
- 大家注意在這里因為反射的向量是一個單位向量,所以這個隨機值介于 0 到 1 之間
- 如果每一個像素點是隨機值用顏色輸出,那么對應的顏色就說是在黑白灰之間
- 并且當我們隨機生成反射方向的時候,我們可以傳入不同的隨機數種子
- 不同的隨機數種子就會生成不同的反射圖案
- 大家知道隨機數種子是什么意思嗎?
- 通俗來說,我們在計算機當中生成的隨機數,其實并不是真正的隨機的,它是通過一個表達式去計算出來的
- 你可以把隨機數種子理解成數學函數 Y 等于 F(X) 里面的 X,你傳入不同的 X 就得到不同的隨機數 Y,這個 X 就是種子
- 因此比如說當種子值是1的時候,那么產生的序列可能是 ABCDEFG
- 而種子如果是3的時候,它會產生另外一個序列,可能是 CDGZRTX
- 這就是隨機數種子的意義所在,就是輸入不同的種子產生不同的結果
- 所以如果你想讓光線追蹤的結果是隨機的,那么你可以輸入不同的隨機數種子
- 大家看左邊和右邊這兩幅圖像就是用不同的種子所生成的圖像
- 但是大家知道光線反射的一個方向應該是一個三維向量
- 而我們這個圖里面只是為每一個像素生成了一個值
- 所以實際上我們通常需要一次生成三個隨機數,并且要對它進行單位化
- 就像我們下面這個圖表示的一樣,在這里每一個像素其實就代表了每一束光線照在物體表面以后的反射方向
- 左邊這個圖是在分辨率比較低的情況下生成的
- 這個像素的分辨率比較低,其實就是反射的方向比較少
- 右邊這個圖是在整個場景的分辨率調高以后生成的
- 你會注意到它輸出結果就是一個密密麻麻的彩色的噪點圖
- 每一個噪點就代表了每一個像素點的反射方向
- 這個就是我們在隨機生成漫反射方向時候所需要的結果
- 這個圖就是我們生成的小球反射方向的可視化表示
- 右邊這個圖我等一下解釋,我們先看左邊截圖里這個算法:
- 剛才我們說因為需要介于 0 到 1 之間的值,并且需要三個,所以在這里我生成第一個值
- 這個地方是生成第二個值作為反射方向,X 方向這是 Y 方向,還有一個 Z 方向
- 但是因為這個隨機值它是介于 0 到 1 之間的,而我們的反射方向應該是在-1到+1之間
- 所以在這里有一個乘以 2 減 1 的操作
- 并且雖然三個分量都是介于-1到+1之間,但是它們合起來以后形成的向量并不一定是一個單位向量
- 所以在這里我還要需要進行單位化
- 右邊這個圖輸出的是反射向量的分布信息:
- 我前面講過了,漫反射是隨機選擇一個反射方向
- 所以你生成反射向量的那個方向就會能獲得更多的光線
- 因為你會去跟蹤這個方向,結果就是這個位置比較亮,因為它照射的這個方向的光線反彈次數比較多
- 在這里 diffuse reflection 是隨機的,這張圖實際上就描述了反射向量生成的隨機值的分布信息
- 大家會注意到這個分布是不均勻分布的
- 你可以在這里看到一個星星的標志,或者是一個奔馳的一個車標
- 這就說明我生成的反射向量并不是完全隨機的
- 這也是光錐需要考慮的問題:
- 如果生成的反射向量不是隨機的,剛才那么更多的獲得反射向量的位置,就會產生更多的光照,就會更明亮
- 這個結果顯然不是我們所希望的
- 所以在這里我們就需要對這個隨機數的生成方式進行調整
- 那么怎么調整呢?
- 常見的隨機數分布概率有兩種:
- 一種是:均勻分布
- 像我們之前之所以生成了一個像外星人標志的反射方向的力度,是因為我們采用的隨機數的生成方式是均勻零分布
- 當它作為球體的反射方向的時候,就得到了上面這種分布不均的結果
- 另一種是:正態分布
- 正態分布是說大部分的隨機數都是分布在這個隨機數范圍的中心區域,而隨機數范圍的邊緣區域獲得的結果比較少
- 這個是我們要去做的一個調整
- 一種是:均勻分布
- 限于時間關系,我就不去一點點的去展示這個正態分布的算法了
- 相關內容的學習可以參考文末資料
好,我們來看一下這個最終的效果。如果你采用的是正態分布,那么你生成的結果是一個沒有接縫的結果。那么學到這里,大家掌握光線追蹤算法的基本框架已經有 50%了!
- 在這里咱們還有一個問題要處理:
- 就是假設我有一束光照在物體的表面的這個點上面,那么它反彈的方向不應該指向物體內部
- 除非這是一個半透明物體,它對光照具有一定的穿透性
- 對于一個普通材質的物體,如果不考慮透明性,光照就應該不會反彈到物體的內部
- 所以這個時候我們就要對這個光線反彈的方向進行校正,那怎么校正呢?
- 在這里告訴大家一個 trick,就是你直接取反取向量的賦值就可以了
- 如左圖所示,白色表示入射光,綠色表示物體正面方向,黃色半球表示可反彈區域,如果隨機生成的反彈向量為紅色,則應調整為綠色方向
- 這里是我們在進行光線追蹤的時候的一個算法的坑點
- 也希望大家通過我們這節課的學習能夠有所了解,避免在具體實現這個光錐算法的時候遇到困難
- 恭喜大家又近了一步!
光追基本代碼框架
- 最后我們來看一下光錐的基本代碼框架,它代碼框架大概是這樣子的:(文字表現力有限,需要實時講解請參考文末視頻版本)
- 假設現在有一束入射光作為我們光錐函數的參數,傳入到 Trace 函數里面去了(Trace 是跟蹤的意思)
- 然后我們再傳入一個隨機數,這個隨機數用來生成反彈向量
- 然后在這里我們會寫一個 for 循環
- 為什么是一個循環呢?
- 因為剛才我講過,一束光照射到一個物體的表面,它可能會一次反彈以后又碰到一個物體,然后還需要進行二次反彈
- 但是我們在真實的計算機世界當中,我不可能這樣一個物體去無限的反彈下去,無限的去計算反彈,那這個性能開銷是無限大的
- 所以說在這里我們可以設置一個最大計算的反彈次數
- 在這里如果是實時渲染,不是離線渲染的話,不會把這個值設的很高
- 為什么是一個循環呢?
- 在for循環里,我們通過調用下面這行 CalculateRayCollision 去傳入射線,獲得跟小球的碰撞信息
- 如果我們碰撞到某一個物體了,那么我就去計算它的下一次的碰撞
- 這個時候我就去構造我的這個反彈向量
- 在生成反彈向量的時候,我不會去直接找他碰撞了哪個東西,而是隨機的去瞎碰
- 也就是函數 RandomHemisphereDirection(隨機方向生成請參考《TA全棧》工程)
- 如果我們碰撞到某一個物體了,那么我就去計算它的下一次的碰撞
- 然后我們會進入到下一次循環
- 下一次循環的時候,如果沒有碰到任何物體,也就是if條件不成立以后,那這次光錐就結束了
- 如果反射向量繼續與場景物體碰撞,那就會再次進入if語句,計算下一次反彈
- 當然前提是它在你的設定的反彈次數以內
- 所以大家會發現光追的一個坑點:
- 如果反彈向量剛好命中物體,那在這個點上面你計算出來的反射就會更強烈
- 如果沒有,則反射就會更弱
- 所以,光錐為什么產生的結果是隨機的,是有瑕疵的!
- 下面我們來看下渲染效果(詳細版看文末視頻參考)
這是場景的基本設置(非光追渲染效果)
這是根據基本設置,將中間的小球設置為發光體的光追渲染效果
這是不同光線反彈次數情況下,光追效果的變化
可以看到,反彈次數越多,小球之間互相收到光照影響就越明顯,但性能開銷也越大
光追的8個進階要點
上面主要分享的是“小結”中的第一點“基本光線追蹤》
文字表現力較弱,其余內容可以看我的視頻版本分享
這里也給大家概括了一些光追進階點,供大家進一步參考:
參考
- 完整視頻請點擊本鏈接觀看:【TA技術美術進階】光線追蹤:原理和實現_嗶哩嗶哩_bilibili
- 更多技術干貨請加本人主頁頭像