前言
這周無疑是屬於《黑神話:悟空》的一週,不過由於上週已經寫了一篇關於魔獸世界的流水賬了,這周還是回頭更技術文章——而且話題是有關虛幻5的lumen的,所以也算和《黑神話:悟空》有點關係。
近年來虛幻引擎的高質量渲染方面確實已經把Unity甩開很遠了。之前更過兩篇是介紹引擎整體提升的,其中很重要的一部分就是網格管線的設計。而今天介紹的這篇主要是介紹一箇中間層面的技術——光照數據緩存(Radiance Caching)。
高清引擎中的光照計算是遵循物理法則和公式的,整體思路沒有脫離輻射度量學(Radiometry)的範圍。簡單來說它研究的就是:光能量的傳遞過程中微表面吸收和發出的能量——由於發射出的能量是球體範圍的,因此基於距離的衰減也可以表達為“單位面積可以接受到的光能量的分佈變少了”;材質的性質也可以表達為不透明介質對能量吸收和發射的程度(散射介質是另一個複雜話題了,比如之前介紹的雲)。最終整體的能量獲得可以表達為對微表面能量的積分,這套體系中發出的能量就被稱為Radiance,接收的能量則是Irradiance——前者常被翻譯為輻射度,後者常被翻譯為輻照度。(這兩個詞後面儘量保留英文原文,以避免過於類似而混淆)
我個人對此的瞭解雖然也僅停留在Games101和202課程的程度,不過我覺得以此為基礎概念性的來理解工業化過程中的一些光照渲染設計已經足夠了。所以這裡提到的緩存其實是對中間狀態光能量的緩存。
為什麼要做這個緩存其實不難理解——為了以空間換精度的方式進一步提高畫質。一起研學這篇分享主要是理解“如何緩存”的過程。
原文檔是以PPT為主,每頁下面搭配了一行左右的解說稿。本文還是以翻譯原文PPT頁及解說稿為主,打星號的部分則是我個人的補充。由於篇幅原因還是拆分成上下兩篇(反正留點業餘時間玩黑猴),這是其中的上篇。
1 設計背景介紹——Background
在光照渲染中,最終像素點的光照可以通過對接收的光輻射代入BRDF,並積分得出。
*BRDF——雙向反射分佈函數(Bidirectional Reflectance Distribution Function)是用來定義給定入射方向上的Irradiance如何影響給定出射方向上的Radiance。
*例如表面越光滑,越接近鏡面反射,則出射能量越集中在與入射對稱的角度;越粗糙則越接近漫反射,出射能量會均勻分佈在半球甚至球形範圍的任意角度;介於兩者之間的材質有時候也可以用“各項異性”或“各項同性”以及更多其它的複雜模型來表達。
- 這種積分一般是通過蒙特卡洛積分方法,以離散的樣本來儘量近似估計微表面接收的光輻射。
- 對某一個方向的光輻射的估計也是需要光線追蹤的(因為要考慮多次彈射的情況)。
即使有著兩層BVH等加速結構,光線追蹤仍然是相對較慢的。
*BVH之前有文章介紹過,這裡不展開了。
- 通常來說每像素只能接收2幀發出一根射線(用分時緩衝方式和插值等方式來提高精度)。
- 但高質量的全局光照需要(每像素每幀)上百根射線才有比較好的採樣效果。
*圖中展示了室外100根射線的效果,而室內則需要更多射線。
已有的實時渲染方案——輻照度場
在實時渲染領域,之前的研究主要分成了2類。其中一類是輻照度場(Irradiance Fields)。
- 從一小組探針中翻出射線——通過Tatarchuk 2012提供的世界空間網格的方案來分佈。
- 預計算Irradiance
- (以半分辨率甚至更低分辨率來渲染光照)通過插值的方式來達成全分辨率精度
- 探針剔除以避免漏光(主要是排除一些不應該受光照的採樣位置)
輻照度場的問題
- (由於採樣步長等原因)漏光或過度剔除
- 探針分佈不能一概而論
- 光照更新較慢(不能適應瞬間的變化)
- 這套方案特有的顯得很“平”的觀感——比起空間光探針方案,遮擋物附近的Irradiance相對更高頻一些(整體會更亮)
*簡單來說這套方案就是,在採樣數有限的情況下,想辦法通過更好的近似光場來減少質量損失。所謂“場”可以理解成是空間中一些有規律的點。
已有的實時渲染方案——屏幕空間降噪
另一類方案是屏幕空間降噪(Screen Space Denoiser)。
- 逐像素射線追蹤——(採樣)cosine分佈、每像素1射線
- 通過空間數據和分時重用的方式來降噪(後面介紹了2種對應方案及出處)
*基於分時的方案在Games202系列課中也有介紹,有興趣的可以去看看。這套方案簡單來說就是通過空間插值和分幀緩衝及插值的方式,來保證1幀內的採樣數符合性能需求。
屏幕空間降噪的問題
即使通過固定採樣率運行了幾幀,得到的輸入數據還是有很多噪點(需要進一步進行圖像降噪)。
Noise is not constant. Near a bright light source there’s less noise, and further from a bright light source the noise increases as fewer of the rays actually hit that light source.
噪點的形式也不是一成不變的。靠近亮光源處,噪點就較少;遠離亮光源處早點就增加,因為會有更少的採樣射線命中光源。
*其實還有光源大小、數量等等很多因素。針對如何更高效地命中光源也有很多優化設計,本文介紹的也是其中之一。
2 方案概述——Our Approach
Instead of tracing from every single pixel on the screen, we bundle up our rays and we trace from a much smaller set of pixels. This is effectively Screen Space Radiance Caching.
相比於從每個像素髮出射線,我們從一個更小的像素點集發出射線——這是屏幕空間光照緩存提升性能的核心(減少一次運算的像素點數量)。
圖中是以較低採樣率接收光照的結果,這也能有不錯的基礎效果——因為入射的radiance是連續的,儘管幾何體的法線不是連續的。
儘管光照採樣是低分辨率的,我們仍在帶入BRDF時使用全分辨率,這樣也能得到細緻的間接光照效果。
*左邊下面一張僅展示了GBuffer中的法線緩衝,這僅僅是示意。計算材質的BRDF一般還至少需要表面顏色(Albedo)、粗糙度(Roughness 或高光度 Specular)之類的信息。
在光照緩衝探針之間,我們採用了一定的過濾(filter)方式——後續會介紹到這樣做的一些優點。
在重點方向我們會有更多采樣射線。我們有一定的估計入射光來源的策略,會針對光源方向發出更多射線(後面提到的重要性採樣)。
針對遠距離光源我們有一套獨立的採樣方案被稱為世界空間光照緩存(World Space Radiance Cache),這能為我們帶來更穩定的遠距離光照表現。
*這部分介紹會留在下半部分。
*圖中展示了兩種算法得到的結果差距,左邊是每像素2個射線的屏幕空間降噪算法;右邊是本文中的光照緩存算法。可以看到右側的效果在一定的屏幕空間(或圖像)降噪策略後是可以接收的,而左側則完全不行。
*屏幕空間和圖像空間的區別在於,屏幕空間還可以使用GBuffer中的數據作為插值或過濾的依據。
3 管線集成
*這部分開始介紹了這個方案的工業化管線集成及一些要處理的細節。
最終集成到管線中的核心是屏幕空間光照緩存(Screen Space Radiance Cache),以及作為(遠距離)後備選擇的世界空間光照緩存(World Space Radiance Cache)。
兩者都是在相對最終分辨率較低的像素分辨率上執行,集成到其它部分時則是全分辨率——例如法線計算、插值和集成、基於分時的過濾器等(如圖)。
接下來介紹一些屏幕空間光照緩存的具體步驟:
- 在GBuffer中放置探針
- 生成射線——追蹤——在探針空間中執行過濾
屏幕探針結構
屏幕空間探針(的採樣結果)被排布成一個圖集的形式,每一個探針採用八面體展開的佈局。
每個探針是8x8的分辨率,並採用64根射線追蹤。
八面體的佈局中提供了標準化分佈的世界空間方向。
一種八面體的示意圖
It’s important that we use World Space directions here because it means that neighbors have matching directions, and we can find a matching direction in a neighbor very quickly, and I’ll talk about why that’s important later.
很重要的是我們採用世界空間的方向,這意味著相鄰的探針也有對應的該方向,並且我們可以很快查找到相鄰的同方向的結果——後續會介紹為何這個結構如此重要。
After we do our tracing from the probes, we have Radiance and HitDistance and we store those in the atlas for further processing.
在從探針進行射線追蹤後,我們可以得到Radiance(這裡是接收到的光照能量)和HitDistance (射線命中距離)——將它們存儲到緩衝圖集中以作為後續過程的數據。
屏幕探針放置
在實際放置屏幕探針時,我們採用統一化的網格——每16像素放置一個探針。
可以看到圖中橙色部分是初步執行後插值失敗的像素。(*插值失敗的依據原文沒有給出,應該是基於一些容差維度)
之後我們把格子的分辨率加倍,並在之前插值失敗的格子中間加入一個新探針,再執行插值測試。
繼續執行直到餘留很少的插值失敗的像素。
*這個步驟的核心思想參考了被稱為 Hierarchical Refinement (層級精進)的方案。
屏幕探針放置
最終在像素層級我們採用氾濫填充(Flood fill)的方式處理最後的少量像素——而不是還為之分配採樣探針。
自適應採樣
這個算法是一個自適應的採樣過程,為了能保證它總能在實時渲染中有效,我們需要設置一個上限(以避免極端情況過度消耗性能)。
與此同時,我們也不希望把(不同分辨率放置的)探針分開處理,因此我們把自適應的探針排列在圖集的底部(例如8像素格子的就排在16像素格子的下方,如圖)。當我們達到圖集空間的上限時,我們無法放置新的探針,則相對的就採用氾濫式填充算法以處理仍然插值失敗的像素點。
屏幕探針抖動
由於我們僅是每隔N個像素放置了一個探針,因此我們也需要分幀抖動放置探針的網格(進行微小的偏移),以及八面體內射線的方向。
我們的探針直接分佈在屏幕像素上(而不是基於世界空間換算的更小單位),以確保沒有漏光問題或是像素和探針之間的色差。
不過由於探針是放置在屏幕空間的,我們必須通過一個分時過濾器(temporal filter)以隱藏遮擋計算上的差異(明暗邊緣不能有變化)。
後續我也會提到一些這套方案的其它副作用。
插值
要通過屏幕空間光照緩存通過插值計算出屏幕像素(光照值),我們以像素平面的距離作為光照探針的權重項——像素平面通過像素點的法線和位置推導得出。
這能避免前景的射線運算錯誤地“漏光”至背景層。
插值
在從光照緩存中插值的偏移量中我們也做了抖動處理——不過抖動的範圍還是被限制在原始像素的同一平面上——確保抖動不會導致插值失敗是很重要的。
抖動能使探針之間的空間上的光照差異相對被分散開一些,這也有助於提高最終光照的分時穩定性——通過擴展TAA(Temporal Anti Aliasing 分時抗鋸齒)的鄰接裁剪步驟。
管線驗證
*在管線組裝起來後,得到的結果與離線路徑追蹤結果一致。
但是當降低到我們的性能預算(1/2射線每像素)時,在室內空間的噪點會比較多。
*後面的部分也主要是關於一些進一步的改進和補充。
4 重要性採樣
*在射線數量給定的前提下,這個部分解釋瞭如何提高檢測的預判準確度。
回到渲染方程中的蒙特卡洛部分,我們想要將射線以更符合函數中積分的分佈方式來分配。
但是如何預估呢?尤其考慮到是入射光線(能量、方向)是首要需要得出的項。
在屏幕空間光照緩存中,我們可以通過查找上一幀的方式來對光照有一個很好的預估。
.相比於執行昂貴的屏幕空間查找方案,我們可以將當前屏幕位置映射到上一幀(高清渲染中的常見方案,基於空間變換的插值預判),並對相鄰4個探針結果做平均。
光照緩存數據中的射線已經基於位置和方向進行了索引化,而這是使查找能很快速的原因。
對於重映射失敗的點,例如(上一幀)屏幕外或被遮擋的點,我們有世界空間的光照緩存作為備選(相應的是一種精度較低的補充方案)。
本頁底部右側的圖片展示了八面體數據佈局中的入射光線數據,對於這個探針來說,光線主要從三個方向而來。
對於BRDF來說,我們可以使用插值後的屏幕光照探針數據來計算全部像素的BRDF——其它項都能從GBuffer中得出(結合光線方向)。
對於放置在平的牆面上的探針來說,大約一半的射線會得到0值的BRDF(背面),我們也不需要這些方向的射線——由此我們可以將它們重分配到更重要的方向。
不過比起單獨做每一項(光照、BRDF)的重要性採樣,更好的方式是可以對整體的乘積做重要性採樣。
結構化重要性採樣
這就是引入結構化的重要性採樣的原因。結構化的重要性採樣分配了相對少量的層級結構的區域——基於概率密度函數(Probability Density Function)。
這實現了很好的全局分層效果,不過本頁提到的採樣點放置算法需要離線預處理一些數據。儘管如此,我們仍可以採用這種“層級結構+閾值”的思想。
*關於概率密度函數可以去看看Games101。
並且我們發現這種方式能很好的與我們的八面體探針mip四叉樹完美的映射。
On the left we have calculated the product between the incoming lighting and the BRDF, and on the right, we’ve subdivided Octahedral texels where the PDF was highest.
圖中左側的是計算好的入射光和BRDF的乘積,右側則是在高PDF(這裡是概率密度函數的縮寫)部分再分割後的八面體柵格。
集成到管線
為了將它集成到我們的八面體探針管線中,我們需要為射線追蹤步驟加入一些中間存儲和計算步驟,使得每個追蹤能各自計算射線的方向。
在進行追蹤並計算radiance之後,我們需要將非統一化網格得到的radiance與統一化探針的佈局進行最終整合(如圖)。
射線生成算法
這裡是一個實際的計算著色器(compute shader)中實現結構化重要性採樣的射線生成算法:
- 首先,我們對於每個八面體柵格計算光照和BRDF的乘積。
- 我們從統一分佈的射線方向開始,以確保一開始追蹤時線程組所有線程都能處於繁忙狀態。
- 之後我們基於射線的PDF進行排序。對於每3個低於剔除閾值的PDF數據,我們將其去除(refine 這裡指釋放出一些射線供更需要精度的地方)並轉而超採樣(分成4個)的高PDF射線方向。
可以看到圖中左側的一部分BRDF係數是0,我們剔除了這些射線並把這些數量重新分配到光照和BRDF值都最高的一些方向(超採樣分成了更小的格子,以提升精度)。
圖中示意了這一算法在世界空間生效的過程。
左側圖中是統一化發出的射線方向,一半都浪費在與牆壁相交且與最終光照結果無關。
右側是重分配這些射線到重要的光照方向後的結果(白線)。
從圖中的視圖模式已經能看出這樣做帶來的明顯噪聲減少。
改進方向
我們還可以對這個算法做一些改進。
相比於剔除光照中PDF較低的射線,我們僅剔除BRDF較低的射線——這是因為本身光照的PDF這裡就是一個估計值,且受影響於噪聲。
(某一幀中)我們可能錯失了上一幀中的某個微小光源,但我們仍應該追蹤那個方向。(*這其實就是基於離散方向射線採樣的其中一個問題)
BRDF相對而言就是更精確的,它的數據都來自GBuffer(全分辨率),因此是無噪聲的。
在學習了空間過濾技術(spatial filter 下篇中會提到)後我們能更激進地進行剔除。我們能剔除BRDF值遠大於0的值,減少它們的權重以把射線分配到更多較暗的角落(以提高精度)。
這給我們帶來了另一種把不重要的射線分配到重要方向的方案。
*圖中展示了結果對比——每幀每像素1/2射線。
這裡是這套重要性採樣方案的簡單總結:
- 我們通過上一幀的光照數據指導這一幀的射線方向,並以遠距離光照指導這一幀的射線方向(下一篇介紹的世界空間光照緩衝)。
- 我們把射線集成為探針(包含前面提到的那種數據結構),這使我們能實現更智能化更高效的採樣過程。
結語
下一篇會包含Spatial Filtering 、World Space Radiance Cache、Full Resolution Steps等幾個部分,以及集成後的性能表現。
這裡面展示的一些工業化細節雖然是以經驗性為主的,但也是很重要的。其實從卡馬克那個時候開始,尖端的遊戲引擎要考慮的一直是如何優化管線結構,讓本來實時跑不動的圖形學理論或離線渲染方案能運行起來。
回到光照緩存這個功能來說,逐像素的光追採樣就是質量不如這個方案的,而這個方案中的1/16的分辨率、八面體探針(的圖集)、探針8x8的分辨率(以及基於這個分辨率的64根射線)、重要性採樣中的中間層(光照與BRDF的乘積)等——這些算法和數據設計才是虛幻引擎的開發者們在特定的硬件條件下通過實驗摸索出的性能和效果的平衡點——這個過程是無關物理理論的,它是屬於引擎獨有的“工業化”過程。
而由於硬件情況一直在變化,因此可以說引擎的“工業化”是一個永不過時的過程。任何時候要打造最頂尖的高質量畫面,都需要很多引擎開發者不懈的努力。不過總的來說,由於對分時策略比較依賴,因此各種高畫質遊戲在畫面激烈變化時都不可避免有一個從“糊”到清晰的過程(這也是有時候用動態模糊遮一下的原因)。
下週會更這篇分享的後半部分。
最後是資料鏈接:
Radiance Caching for Real-Time Global Illumination 的PPT文件