所謂基於屏幕,就是指利用的信息來源於“屏幕”,例如:frame buffer、depth buffer、G-buffer 都記錄着屏幕所看到的各 pixel 的信息。
Reflective Shadow Maps(RSM)
Reflective Shadow Maps(RSM):主要是利用了類似 shadow map 思想的GI技術,但 shadow map 嚴格意義上不屬於用戶的“屏幕”信息,而是屬於光源的“屏幕”信息,為了懶得再寫多一篇博客分類,我還是將其歸納為 screen space 的技術。
RSM的思路:將受到直接光照的地方都視為次級光源,那么 shading point x 所受的次級光照便是來源於各個次級光源的反射。
次級光照 = bounce 為1的間接光照,RSM算法只能支持 bounce 為 1 的間接光照效果
然后,假定次級光源均是 diffuse 物體,那么一小塊次級光源 patch(這塊次級光源面積位於點 \(x_p\) )對 shading point x 的 irradiance 貢獻是:
\(\Phi\) 是次級光源 patch 的 radiant flux,\(\mathbf{n_p}\) 是 \(x_p\) 的法線 ,\(\mathbf{n}\) 是 \(x\) 的法線
所有的次級光源 patch 對 \(x\) 的貢獻加起來便是 \(x\) 的間接光照 irradiance:
那么,怎么找到這些次級光源呢?這就用到了 shadow map 的思想:
- 陰影生成 pass:在光源攝像機渲染 shadow map (往往只記錄了深度)的時候,順便額外記錄 世界坐標 \(x_p\) 、法線 \(n_p\)、 接受的直接光源 radiant flux \(\Phi_p\)。那么就可以認為 shadow map 的一個 texel 對應一塊patch ,從而這張 shadow map 就包含了所有次級光照 patch 的信息了 。
實際上,世界坐標也可以通過uv坐標、遮擋深度來推算得到,好處是可以節省空間,壞處那自然是沒那么精確了。
此外,計算一個 texel (或者說一塊patch)的 \(\Phi_p\) 時,無論光源是directional light還是spot light,都不必計算 cosine 或者距離衰減,而直接用光源強度與物體 albedo 相乘
\(u_p、v_p\) 為 \(x_p\) 在 shadow map 上的紋理坐標。
- 主渲染pass:在 pixel shader 階段,計算出 \(x\) 對應的 shadow map uv坐標,並取該坐標周圍若干個 texel (這些正是我們要采樣的次級光源點)對應的 世界坐標 \(x_p\) 、法線 \(n_p\)、 接受的直接光源radiant flux \(\Phi_p\) ,它們將對 \(x\) 的渲染造成間接光照影響:
RSM效果圖:
RSM 重要性采樣
理論上,為了實現最好的RSM效果,應當取整張 shadow map 的所有 texel 作為次級光源點,因為整張shadow map 意味着包含了整個光源照到的信息。但這樣所需的采樣數就相當於 shadow map 的分辨率,代價太高。
因此我們應當使用少量的采樣數來保證性能,同時也要保證RSM的間接光源質量能夠接受,那么就容易想到用 Importance Sampling 來加速采樣的收斂。那么哪些地方的次級光源點比較重要呢?
RSM 假定,離 shading point x 近的點更可能給 x 的光照貢獻大,而遠的點給 x 的光照貢獻小。
因此這個用於RSM的 Importance Sampling 將給近的的地方更多的采樣點(當然權重更小),遠的地方更少的采樣點(權重更大),用可視化采樣點數量和權重大概就是這個樣子:
因此,選取一個隨機采樣點坐標 \((u,v)\) 和對應的權重 \(importance\):
其中,\(s、t\) 為 shading point x 在 shadow map 的紋理坐標,\(\xi_{1}、\xi_{2}\) 為隨機數
RSM 的應用與缺陷
缺陷:
- 性能開銷與燈光數量成正比,有點昂貴(意味着需要同樣數量的 shadow map、在多張 shadow map 采樣等...)
- 由於 shadow map 記錄的是光源攝像機屏幕上的表面幾何信息,因此在計算 patch 對 shading point 的貢獻時很難做到檢查 visibility:

- 僅支持 one-bounce 間接光照效果
- RSM 假設次級光源面均是 diffuse 的,這會影響圖像的正確性(當然大部分情況下還是可以接受的)
應用:
- 作為廉價的GI方法,常被用於做單個重要光源的GI效果(例如手電筒)

Screen Space Ambient Occulsion(SSAO)
屏幕空間環境光遮蔽(Screen Space Ambient Occulusion,SSAO):是一類游戲工業界很常用且廉價的屏幕空間 GI 方法。
所謂環境光遮蔽(AO),就是某個 shading point 因為被其它幾何表面所遮擋 ,從而降低了接受環境光的比例(這種遮蔽常常發生在凹處表面):

AO 的基本公式:
AO 往往只代表一個簡單的光線入射遮擋比例,乘於它意味着需要把 shading point 當作 diffuse 的表面來看待(即與觀察方向無關,出射到哪都是 \(\frac{1}{\pi}\) 的 irradiance)。
一種計算 AO 的經典方法就是通過蒙特卡洛 + ray casting 去預計算一個模型上各處的 AO,然后做成該模型的 AO texture 后就可以在運行時采樣並與環境光照值相乘(AO texture 存的是 visibility 值)。
而 SSAO 不需要預計算過程,而只需要通過屏幕的 depth 信息就能做到還算不錯的 AO 效果。
SSAO 的算法流程:對於某個 shading point ,
-
在該點一定半徑內的球型范圍內隨機采樣 N 個點,然后這些采樣點將與 depth buffer 對應的深度作比較:若采樣點的深度小於 depth buffer 對應位置的深度,則說明該采樣點被遮蔽了
-
根據所有被遮蔽得到采樣點數量 \(Occ\),計算出 AO 為 \(A(p) = \frac{Occ}{N}\)
-
那么該 shading point 的環境光照即為
\[L_{indirect}(x) = \frac{1-A(x)}{\pi} \int_{\Omega^{+}} L_{\mathrm{environment}}\left(\mathrm{x}, \omega_{i}\right) {\rho} \cos \theta_{i} \mathrm{~d} \omega_{i} \]
\(\rho\) 為 albedo。
SSAO 效果圖(左為關閉SSAO效果,右為開啟SSAO效果,可以看到物體交界處等地方多了更多的暗部細節):

SSAO Blur
實踐中由於性能限制,SSAO 一般僅使用16個采樣點,那么 AO 的結果將會是 noisy 的:

這時候就稍微修改下 SSAO 的算法流程,在計算 shading point 的 AO 時,不再直接乘於 color。而是先寫入到一個 AO buffer 上,之后用一個屏幕后處理 pass 對 AO buffer 信息進行邊緣保留濾波算法(其實就是保持邊緣感的模糊操作,例如雙邊濾波算法),那么得到將是不那么 noisy 的 AO 結果:

SSAO 半球采樣
實際上,渲染方程本就是上半球的積分,下半球的光線不會照到 shading point,因此 SSAO 采樣范圍不應該是一個球型,而應當是基於該點的法線為中心的半球形采樣范圍。
采樣上半球采樣范圍的 SSAO 改進方法,得到該范圍的采樣點算法也很簡單:
vec3 rand; // 在球形上的隨機坐標
vec3 n; // shading point法線
rand = sign(dot(n,rand))*rand; // 在半球上的隨機坐標
SSAO 的應用與缺陷
缺陷:
- 僅包含屏幕表面的幾何信息不能表示完全正確的 visibility,因此 AO 效果不那么准確(相對於預計算AO貼圖)
例如,下圖中間點的采樣,有個紅色采樣點實際上沒有被遮蔽。但是該采樣點的深度小於depth buffer的對應深度,因此被 SSAO 判定為遮蔽了。
- 僅支持短距離的物體遮蔽效果
應用:
- 廉價的GI效果,提升畫面的暗部細節,大部分游戲都會將其納入一種畫面增強選項,雖然之后有性能和效果更好的 HBAO 算法作為取代。
Screen Space Directional Occlusion(SSDO)
Screen Space Directional Occlusion(SSDO) 也是一類與 SSAO 極其相似的屏幕空間 GI 方法,區別在於它們看待光線遮蔽的角度是相反的:
- AO 認為 shading point 朝外的光線打到物體幾何表面時,相當於外部環境光被這個表面遮擋了,因此(對於下面這幅圖) AO 將紅色部分視為間接光照來源,黃色部分視為損失的間接光照
- 而 DO 認為 shading point 朝外的光線打到物體幾何表面時,相當於受到了表面的間接光照,因此(對於下面這幅圖) DO 會將黃色部分視為間接光照來源,紅色部分視為損失的間接光照
也因此,SSAO 往往增加的是明暗細節,而 SSDO 往往增加的是周圍表面的顏色影響(或者說增加 color bleeding 效果)。
SSDO 需要依賴屏幕的 color, depth 信息來完成。
SSDO 算法流程的思路也和 SSAO 相似:對於某個 shading point,

- 先在 shading point 一定半徑內的半球型范圍內隨機采樣 N 個點,然后這些采樣點將與 depth buffer 對應的深度作比較:若采樣點的深度小於 depth buffer 對應位置的深度,則說明該采樣點被遮蔽了
- 對於每個被遮蔽的采樣點,將該采樣點對應的 pixel 視為次級光源 patch,對 shading point 造成間接光照的貢獻:
SSDO 計算該 GI 的時候會把物體幾何表面所有 pixels(也包括 shading point 本身)都將假設為 diffuse 表面
\(Area(p)\) 即為 pixel p 的片元面積,可以通過 p 的深度算出(p越遠,對應的片元面積越大)
- 累積所有被遮蔽采樣點的間接光照,得到該 shading point 的間接光照 irradiance,並以 diffuse 形式反射到眼睛里:
SSDO 效果圖:
SSDO 的應用與缺陷
缺陷:
- 僅包含屏幕表面的幾何信息仍然不能表示完全正確的 visibility,即會缺失屏幕看不到的平面信息(對於有顏色的GI效果很容易看出artifact)
- 僅支持短距離GI效果,而無法展示長距離的GI

- 僅支持 one-bounce 間接光照
Horizon Based Ambient Occlusion(HBAO)
前面 SSAO 計算 AO 的方式太過耗費性能,因為它是基於估量面積覆蓋率去計算 AO 的(樣本是三維空間分布的),而為什么我們不能基於方向角覆蓋率去計算 AO (樣本是二維空間分布)呢?
HBAO 就是基於方向角覆蓋率的思想出發:在 shading point 上往各個方向進行 ray casting,找到與 shading point 切平面夾角最大且 hit success(意味着遇到遮擋物)的 ray 方向,通過該 ray 方向和切平面的夾角 \(\alpha\) 就可以得到遮蔽率 :
不過,直接計算 \(\alpha\) 是挺耗的,需要將 ray 向量和切向量點積后進行反三角函數,因此原論文采取了一種更容易計算的近似方式:將 \(\alpha\) 拆成兩部分,一部分是 ray 方向與 view 平面的角度差 \(h\) ,另一部分是切平面與 view 平面的角度差 \(t\),其關系是 \(\alpha = h - t\)
在計算 ao cosine 貢獻時,shading point 沒有使用原來的 n 而是用 view 向量作為法向量,這樣計算 AO 便可以:

HBAO 的具體算法流程為:
- 對於 shading point p,我們先屏幕空間上采樣四個方向,它們對應 \(\theta_1\),\(\theta_2\),\(\theta_3\),\(\theta_4\) :
- 初始的四個方向都是軸對齊的十字(虛線),然后對整個十字進行一個隨機旋轉角度,就可以得到當前幀的四個采樣方向(實線)

-
對於某個采樣方向 \(\theta\) :
- shading point 上沿 \(\theta\) 方向進行 ray marching,最終找到可遇到遮蔽物與 view 平面最高仰角的 sin 值:
\[tan(h(\theta)) = \frac{RayDir.z}{\sqrt{(RayDir.x)^2+(RayDir.y)^2}} \]\[\sin(h(\theta)) = \frac{\tan(h(\theta))}{\sqrt{1+\tan^2(h(\theta))}} \]如下圖例子,找到的最高仰角為 \(S_3\) 方向上的。
- 也順便根據 ray tracing 結果對應的 hit dist \(r(\theta)\),計算出本次 \(\theta\) 角切面的樣本權重為:
\[W(\theta) = max(0,1-\frac{r(\theta)}{R_{max}}) \]
- 根據 shading point 的 normal 算出其切線 tangent,算出切線與 view 平面的夾角的 sin 值,同理可算出 \(sin(t(\theta))\)
- 最終計算 AO 為:
- 最后對 AO 圖像進行一個空間濾波(或者說模糊)來降低噪聲
HBAO 的 Normal 問題
shading point 在利用 normal 計算切線 tangent 的時候,最好使用 face normal(即三角面法線)而非 interpolated normal(通過頂點插值得到的法線):如果使用 interpolated normal ,那么在模型凹角處容易出現 artifacts。
如下例,face normal 比起 interpolated normal 才更能做到 AO 的正確性:

然而使用 face normal 后,對於低精度(low-tessellation)的曲面來說又會出現 artifacts:可能會出現黑白相間的現象,類似 shadow map 的 shadow acne 現象。
究其原因,如下圖假設銜接處的 shading point 本應該是曲面,然而由於使用了低精度的 mesh,由 face normal 計算出來的 tangent plane 是偏下的,因此銜接處的 AO 過度偏暗。
這種情況反而用 interpolated normal 更能做到 AO 的正確性,不過由於大部分情況都不是曲面,因此還是建議算法以 face normal 為核心。

那么干脆借鑒 shadow map bias 的做法,我們也給 HBAO 加 bias(例如可以讓 tangent plane 往上抬高 30 度角來):

HBAO 的采樣
HBAO 在選擇屏幕空間采樣方向和在 ray marching 的時候可以使用開銷極其低廉的預定義樣本(可通過數組定義):每幀隨機選擇一種十字采樣方向(共有5種),選定后每個采樣方向 ray marching 也只步進兩次。

所以這種方式下 HBAO 的樣本只需要 4個方向×2個步長 = 8個。其相比於 SSAO,樣本數大大減少但卻能同時能保持較好質量的 AO。
HBAO 的不連續問題
有些情況下,相鄰的 shading point 可能 AO 是相差較大導致畫面,如下圖:

其核心原因在於,\(P_0\) 本來 AO = 0.7,而 \(P_1\) 的遮擋物超出 ray marching 的最遠距離,於是 AO 突變為 0。我們當然也可以采用更長距離的 ray marching,代價是要不犧牲性能采樣更多樣本,要不降低質量使用間隔較大的步長。

為了減少這種不連續的情況,論文在 HBAO 的流程中引入了混合權重:
也可以采用更平滑的混合權重:
\[W(\theta) = max(0,1-(\frac{r(\theta)}{R_{max}})^2) \]
雖然總體 AO 會減輕,但是卻能夠很好地解決不連續問題,並且不引入新的性能開銷或者造成 AO 質量降低。
HBAO 的應用與缺陷
應用:
- HBAO 比 SSAO 開銷往往更低:由於只需要更低於 SSAO 的樣本數,甚至還能達到質量更高的 AO 效果。
缺陷:
- 對於有中空的 AO 情況,HBAO 會把 hit success 的最高仰角以下的方向都認為是被遮擋的,這是與 Ground Truth AO 不符(SSAO 反而更能適應這種中空情形)。
如圖左,hit success 的是橙色射線 ,hit fail 的是綠色射線,而圖右的 HBAO 統統認為最高仰角以下的射線都是 hit fail。
有空再補一補 HBAO+ 和 GTAO 的檔
Screen Space Reflection(SSR)/Screen Space Ray Tracing(SSRT)
Screen Space Reflection(SSR),一類與 ray tracing 思路非常相似的屏幕空間GI方法,因此也有被叫為 Screen Space Ray Tracing(SSRT)。
它的想法是,將屏幕所看到的表面幾何信息當成一個場景,然后計算間接光照時,往半球范圍若干個方向投射射線,看看能和這個場景的哪個屏幕像素點相交,這些便可以相交的像素點便是提供間接光照的來源。

SSR 需要用到的屏幕信息:color、normal、depth
SSR 的算法流程:
-
在第一個 pass 只渲染整個場景的直接光照,得到包含直接光照結果的 color buffer 、normal buffer、 depth buffer。
-
在第二個 pass 對整個屏幕渲染,對於某個 shading point ,在該點往半球隨機方向投射若干條射線(使用 ray marching算法),然后將與射線相交的點 \(\mathrm{p'}\) 將對 shading point 的間接光照做出貢獻(這與渲染方程是一致的):
\[L_{\mathrm{indirect}}\left(\mathrm{p}, \omega_{o}\right) = \int_{\Omega^{+},V=1} L_{}\left(\mathrm{p'}, \omega_{i}\right) f_{r}\left(\mathrm{p}, \omega_{i}, \omega_{o}\right) \cos \theta_{i} \mathrm{~d} \omega_{i} \]其中當射線命中時, \(V = 1\) ;否則,\(V = 0\)
為了減少計算,這里仍然假設次級光源點是 diffuse 的,這樣式子實際可以寫成:
\[L_{\mathrm{indirect}}\left(\mathrm{p}, \omega_{o}\right) = \int_{\Omega^{+},V=1} \frac{E(\mathrm{p'})}{\pi} f_{r}\left(\mathrm{p}, \omega_{i}, \omega_{o}\right) \cos \theta_{i} \mathrm{~d} \omega_{i} \]

此外,SSR 還可以通過使用不同的 brdf 來實現不同的反射效果:

SSR 效果圖:

Ray Marching
得益於帶 depth buffer,SSR 可以實現比較廉價的 Ray Marching 效果。Ray Marching 的精度和性能之間的平衡將取決於 march 的步長。
算法先從 start point 開始,
- 每次往射線方向走一個步長得到一個測試點,將該測試點變換成屏幕坐標 \((u,v,z)\)
- 根據uv坐標取 depth buffer 對應的深度 \(d\) 與 \(z\) 比較:若 \(z>d\) ,則說明射線碰到該uv位置上像素點的“柱條”,返還該測試點;否則,重復上述步驟

bool RayMarch(vec3 ori, vec3 dir, out vec3 hitPos) {
float step = 1.0;
vec3 lastPoint = ori;
for(int i=0;i<10;++i){
// 往射線方向走一步得到測試點深度
vec3 testPoint = lastPoint + step * dir;
float testDepth = GetDepth(testPoint);
// 測試點的uv位置對應在depth buffer的深度
vec2 testScreenUV = GetScreenCoordinate(testPoint);
float bufferDepth = GetGBufferDepth(testScreenUV);
// 若測試點深度 > depth buffer深度,則說明光線相交於該測試點位置所在的像素柱條
if(testDepth-bufferDepth > -1e-6){
hitPos = testPoint;
return true;
}
// 繼續下一次 March
lastPoint = testPoint;
}
return false;
}
Ray Marching + Hi-z
在 SSR 的 ray marching 中,步長短了會導致要走很多步,消耗很多性能;而步長長了則可能會導致越過原本應該相交的地方后面,導致錯誤的相交。
為了優化這一過程,我們可以對 depth buffer 生成 hi-z(若干層 depth buffer,底層級比高層級的分辨率低),原理類似於 mimap,但不是傳統 mimap 所取的平均值,而是換成了取最大值。這樣我們在低層級的 depth buffer 進行大步的 marching 時碰到 texel,那么說明可能與在這塊 texel 里的某個子像素相交,因此需要降低層級,進行更小步的 marching;若沒碰到,則說明不在當前這塊 texel 的任何子像素,可以繼續下一大步(而且是更激進)。
這個 hi-z 加速方法實際上和 BVH 方法是相似的,hi-z 每個 texel 相當於每個 AABB 包圍盒,層級越低則包圍盒越大。
mip = 0;
while(level>-1)
step through current cell;
if(above Z plane) ++level;
if(below Z plane) --level;
Edge Fading
由於 screen space 的方法天生丟失了屏幕以外的信息,在某些時候的渲染可能會看到反射物比較突兀的斷掉了屏幕外的信息:

為了掩蓋這一突兀的 artifact,可以使用基於像素uv坐標的間接光照權重貢獻,即uv坐標越接近邊界(例如接近u=0、u=1、v=0、v=1),則權重貢獻應當越小:

優化
BRDF 重要性采樣
為了讓 SSR 的采樣更容易收斂,我們可以根據不同的 BRDF lobe 在進行 importance sampling:

射線結果重用(Radiance Filtering)
當 pixel 的 ray marching 得出一個相交點時,不僅計算出對該 pixel 的間接光照貢獻,還可以將計算該點與原 pixel 附近的 pixel 的間接光照貢獻並賦給相應的 pixel :

預過濾采樣結果
每個方向采樣得到的結果將根據不同的 BRDF lobe 來決定這個結果的權重,從而最終綜合得到一個過濾后的間接光照結果,減少了采樣的 noise 問題:

SSR/SSRT 的應用與缺陷
缺陷:
- screen space 方法仍然缺失了屏幕所看不到的幾何信息

- diffuse 情況下,由於要往半球范圍均勻采樣(不能像specular/glossy那樣用importance sampling極大優化采樣),容易造成nosiy結果,這時候可能需要犧牲更多的性能來采樣更多
應用:
- SSR 的渲染效果非常好(更前面的方案看起來總像是增強部分的圖像效果)
- 通過不同的 brdf 函數,可以自由調成各種反射效果(specular/glossy/diffuse)
Screen Space Pixel-projected Reflections
Pixel-projected Reflections(PPR)是用於 planar reflections(平面反射,常見於水面、光滑地板)情況下的一種聰明且低廉的算法,其核心思路是將場景的像素投影到反射平面的像素上,從而避免了從反射平面像素出發投射光線的操作(避免了光追的調用)。

Projection Pass
首先,需要創建一個屏幕大小且元素類型為 uint32 的 intermediate buffer,其將用於存儲屏幕位置 offset。場景像素需要根據平面位置和法線來算出對應的目標反射平面像素,並在該目標像素中寫入本場景像素的屏幕位置 offset,從而在后續的 reflection pass 中可以讓每個反射平面像素找到自己對應的場景像素並獲取像素顏色。
具體實現上,在繪制場景物體時和繪制反射平面物體時可采取不同的 stencil flag 寫入;隨后可以通過一個全屏 PS(開啟 stencil test)或者 CS 來完成 projection pass,這樣就可以實現對所有場景像素做 projection 操作。
Hash Resolve
在將場景像素投影到反射平面像素時,會存在寫入沖突問題:可能會有多個場景像素正好投影到同一個反射平面像素,導致每幀都在隨機競爭寫入,其錯誤效果大致如下:

這時候正確的結果應當是只取離反射平面最近的那個場景像素,因為更遠的場景像素相當於是被遮蔽了。

那如何判斷哪個場景像素最近?只需要看誰在反射平面法線方向軸上的 offset 最小就取誰,那么可以通過 InterlockedMin
原子操作來解決寫入沖突,還能保留正確的寫入結果。我們可以對場景像素的位移編碼成如下 hash 值:

-
最后兩位用於表示如圖的 4 種坐標系,從而覆蓋了所有反射平面法線方向的情況:法線落在哪個坐標系的藍色扇形范圍,便會使用該坐標系。這樣以對應的坐標系為基准,其 y 軸上的值便可以視為 offset。
-
12位 y 整數值(無符號)和12位 x 整數值(有符號)均為基准坐標系上的 offset。只不過場景像素必定在平面之上,因此 y 值必定不是負數,也就不需要符號位。
-
3位 y 小數值(有符號),3位 x 小數值(有符號)為補充整數值精度用的,可以在后續步驟中利用到。
對編碼后的 hash 值,就可以通過 InterlockedMin
原子操作對 intermediate buffer 進行寫入。
uint originalValue = 0;
uint valueToWrite = PPR_EncodeIntermediateBufferValue( offset );
InterlockedMin( uavIntermediateBuffer[ targetPixelIndex ], valueToWrite, originalValue );
Edge Stretch
對於橫跨屏幕的平面(尤其常見於水面),由於透視投影的緣故,可能在屏幕邊緣處產生反射信息丟失的問題。

Edge fading 可以解決類似問題,而另一種可選的方法是將投影出去的像素往屏幕邊緣處拉伸:

可以在寫入 intermediate buffer 前,先對投影到的目標像素位置做一個偏移:
float heightStretch = (posWS.z – waterHeight);
float angleStretch = saturate(- cameraDirection.z);
float screenStretch = saturate(abs(reflPosUV.x * 2 - 1) – threshold);
targetPixelPos.x *= 1 + heightStretch * angleStretch * screenStretch * intensity;
Reflection Pass
和 projection pass 恰恰相反,reflection 對所有反射平面像素(而非場景像素)做 reflection 操作:訪問 intermediate buffer 上對應的 offset,從而往該 offset 后的屏幕位置獲取 scene color。
在具體實現,也需要一個屏幕 PS 或 CS,也同樣需要利用 stencil buffer 來保證只對反射平面像素處理。
Holes Filling
然而實際並不是每個反射平面像素都被寫入過 offset,總是會出現 holes,主要有兩類情況:
- 反射平面像素對應的 projection 位置雖然存在場景像素,但是剛好該場景像素投影到了隔壁的反射平面像素,擦邊而過了;往往呈現出黑色點狀或線狀。
- 反射平面像素對應的 projection 位置不存在正確的場景像素(屏幕信息不能表達完整的場景信息);往往呈現出大塊黑色。
第一類問題可以通過復用鄰近 4 個平面像素的 offset 來解決。具體就是在 intermediate buffer 中讀取周圍 4 個 pixel 的 offset,看哪個 offset 離自己最近就當成自己的 offset,這時候 hash 值中的小數部分就能派上用場了(如果僅看整數部分大概率 4 個 offset 都會一樣近)。
uint v0 = srvIntermediateBuffer[ vpos.x + vpos.y * int(globalConstants.resolution.x) ];
// read neighbors 'intermediate buffer' data
const int2 holeOffset1 = int2( 1, 0 );
const int2 holeOffset2 = int2( 0, 1 );
const int2 holeOffset3 = int2( 1, 1 );
const int2 holeOffset4 = int2(-1, 0 );
const uint v1 = srvIntermediateBuffer[ (vpos.x + holeOffset1.x) + (vpos.y + holeOffset1.y) * int(globalConstants.resolution.x) ];
const uint v2 = srvIntermediateBuffer[ (vpos.x + holeOffset2.x) + (vpos.y + holeOffset2.y) * int(globalConstants.resolution.x) ];
const uint v3 = srvIntermediateBuffer[ (vpos.x + holeOffset3.x) + (vpos.y + holeOffset3.y) * int(globalConstants.resolution.x) ];
const uint v4 = srvIntermediateBuffer[ (vpos.x + holeOffset4.x) + (vpos.y + holeOffset4.y) * int(globalConstants.resolution.x) ];
// get neighbor closest reflection distance
const uint minv = min( min( min( v0, v1 ), min( v2, v3 ) ), v4 );
第二類問題則比較難解決,將留到后面的優化部分。
優化
Fallback 成其它 Ray Tracing 方法
為了解決第二類 holes filling 問題,可以對 holes filling 后仍然缺失 offset 信息的反射平面像素所在的 tile 進行標記;接着,用額外一個 CS pass 來對這些被標記 tiles 的 pixels 進行 radiance 的計算(例如使用 SSRT 或 reflection probe 采樣)。
Glossy Reflection & Normal Map
對於 PPR 技術來說,一個平面反射像素就只有唯一一個鏡面反射方向的樣本,也因此只適合做 mirror reflection(鏡面反射)。然而只要擴展一下,也還是可以為平面反射引入 glossy reflection,甚至應用上 normal map 來讓每個平面反射像素都有不同的法線。首先,我們需要根據 normap map 算出平面反射像素的法線后,再根據 BRDF importance sampling 來生成一個 ray。
在 SIGGRAPH 2017 Optimized pixel-projected reflections for planar reflectors 的做法中,這個 ray 將使用 SSRT 來獲取 radiance,如果 SSRT miss 了才使用 mirror reflection 的結果。然而這種做法實際上和 SSR 沒太大區別。
雖然 mirror ray 和 generated ray 的射線方向往往不是同的,但是它們依然比較相似。那么我們為什么不充分利用 mirror ray 信息來指導 generated ray 呢?因此一個思路是牛頓迭代法:
- 將 mirror ray 的長度 scale 一下 generated ray 得到一個預測點。
- 攝像機往預測點看,訪問對應的 depth,重建出 p'' 的世界坐標,並同時計算 p'' 與平面反射像素的距離,並再次用該距離 scale 一下 generated ray 得到下一個預測點(進行了一次迭代)。
- 攝像機往第二個預測點看,訪問對應的 color 作為本次 generated ray 的 radiance 結果返回。
也就是說,如果需要追求更高的精度,可以通過 n 次訪問 depth buffer,來獲取 n + 1 次的迭代效果,並且迭代的初始值為 mirror ray 的長度,已經算是一種非常接近真實解了(取決於 mirror ray 和 generated ray 的相似度)。
當然牛頓迭代法在表面連續的情況下能夠完美工作,在突變的地方可能有一定效果問題,但是整體效果仍然是可接受的。
PPR 的應用與缺陷
- 在傳統 planar reflections 中,需要把場景多繪制一遍到反射平面上。而 PPR 只需要利用屏幕信息,就可以實現 planar reflections 的效果,無需額外多一次繪制場景;當然 PPR 仍然會存在屏幕信息不完整的問題,需要通過 edge fading 等方法減輕邊緣信息丟失問題。
- PPR 與 SSR 利用屏幕信息的思路是相似的,然而在實現的思路上卻是相反的;區別在於 PPR 是從場景像素傳播到反射平面像素的(data scattering),而 SSR 是反射平面像素從場景像素中獲取顏色的(data fetching)。在性能上,PPR 往往比 SSR 更加開銷低廉,因為避免了需要很多步采樣深度圖的 ray marching。只是 PPR 的使用場景只適合用於水平的反射平面上(如平地板、水面),而不能用於形狀復雜的任意表面上。
參考
- [1] GAMES202-高質量實時渲染-閆令琪
- [2] RSM paper | Reflective Shadow Maps [2005]
- [3] SSDO paper | Approximating Dynamic Global Illumination in Image Space [2009]
- [4] HBAO paper|Image-space horizon-based ambient occlusion[2008]
- [5] SIGGRAPH 2008 | Image-space horizon-based ambient occlusion | Nvidia
- [6] SIGGRAPH 2017 | Optimized pixel-projected reflections for planar reflectors
- [7] Screen Space Planar Reflections in Ghost Recon Wildlands :: Rémi Génin — Graphics & coding (remi-genin.github.io)