簡單陰影分析

滑稽樹發表於2020-12-27

ShadowMap實現方式

正常ShadowMap

實現的Shadowmap的方法就是,在光源位置放置一個相機,朝著光照方向,繪製一遍場景,得到的深度圖就是Shadowmap。而在真正繪製場景的時候則可以將場景的世界空間下的點P使用燈光的proj*view矩陣轉換到Shadowmap座標系下,取樣shadowmap,把得到的shadow值與場景點的深度值進行比較,如果當前點的深度值比shadow值更大,那麼該點就處於陰影中,從而不做對應點的光照計算。

螢幕空間ShadowMap

具體參考螢幕空間深度陰影貼圖(Screen Space Deep Shadow Maps)
正常繪製陰影肯定會有OverDraw,為了減少OverDraw,可以先把整個場景的深度圖繪製出來,可以使用EarlyZ。

  1. 繪製得到深度圖了
  2. 再回到光源位置,將光源位置處將會繪製ShadowMap的每個畫素位置轉換到世界空間,然後再轉換到真正的相機投影到的depth空間,取得深度值。
  3. 將得到的深度值轉換回光源的depth空間,並存到當前的ShadowMap對應的畫素位置上。

避免了OverDraw,場景以及每個光源都只需要渲染一次

潛在問題

本節參考
1、Shadow Acne
主要表現是在不該產生陰影的位置產生間隔的波浪形陰影,如圖所示,球面 出現了自陰影,
在這裡插入圖片描述增大陰影紋理解析度並不能消除
生成的原因如圖所示(參考自https://www.zhihu.com/question/49090321
在這裡插入圖片描述光源處得到的shadowmap的一個畫素對應於場景中一片區域,如果光源與場景面的夾角很大,那麼一個shadowmap的畫素對應的區域會更大,導致的artifact會更明顯。
ShadowBias就是做如下操作
在這裡插入圖片描述

解決方法是使用ShadowBias

  1. Depth Bias:計算時把場景畫素超光源方向移動一定偏差距離
  2. Slope-Scaled Depth Bias: 跟Depth Bias一樣,但是移動的距離跟物體表面與光源的傾斜程度有關,表面傾斜程度越大,移動的偏差就越大
  3. Normal Offset:畫素朝著頂點表面法線方向移動,也要考慮表面傾角
  4. View Direction Offset:跟NormalOffset一樣,但是是朝著相機方向移動
  5. Receiver Plane Depth Bias:The receiver plane is calculated to analytically find the ideal bias.

軟陰影

本節參考知乎回答
由於陰影的邊緣是硬邊緣,所以需要軟邊緣來模擬軟陰影。方便分析,參考ESM的paper,定義一個Shadow Test Function
s ( x ) = f ( d ( x ) , z ( p ) ) s(x)=f(d(x),z(p)) s(x)=f(d(x),z(p))
x x x 是攝像機看到的場景中某一點, d ( x ) d(x) d(x)是這一點到光源的距離, p p p是在陰影紋理中遮擋物的位置, z z z則是從shadowmap中讀取出來的值。
對於標準陰影貼圖技術
f ( d , z ) = { 1 , d ( x ) ≥ z ( p ) 0 , d ( x ) < z ( p ) f(d,z)=\left\{\begin{matrix} 1, & d(x) \ge z(p)\\ 0, & d(x) \lt z(p) \end{matrix}\right. f(d,z)={1,0,d(x)z(p)d(x)<z(p)

PCF

PCF就是正常的計算ShadowMap,但是在計算場景表面一個點的陰影的時候會多重取樣,並將差值的結果作為 d ( x ) d(x) d(x)的值,從而得到軟陰影,想要要過更好就取樣更大的取樣核函式5x5等。Unity預設使用的就是PCF

  • 優點:效果好
  • 缺點:計算量大,需要在計算陰影的時候對每個計算陰影的點都做多重取樣。可以參考元神,通過計算出一個螢幕的陰影邊緣mask,只對半影區域(即陰影邊緣)做PCF,這樣既利用了PCF的優點,同時也避免了在陰影內和陰影外的無意義取樣。

參考
在這裡插入圖片描述在這裡插入圖片描述在這裡插入圖片描述
這張神奇的Mask貼圖是怎麼生成的呢?這張Mask貼圖的解析度是螢幕解析度的1/4×1/4,也就是說一個Mask值對應的是一個4×4的block。然後我們對4×4的block裡面的每一個畫素,來判斷它是不是在陰影中,最後彙總成一個陰影、半影和非陰影的三個狀態,儲存到Mask貼圖裡。這樣我們能夠得到一個準確的半影資訊,但是它不夠快,所以我們做了進一步的優化,只選擇4×4這個block裡面很少的幾個畫素,來判斷是不是在陰影當中。
這幾個畫素的判斷結果,就代表了整個block的資訊,顯然這樣會出現一些誤差,因為我們是拿幾個少數幾個畫素的結果來代表整個block,所以我們把這樣計算得到了Mask貼圖做了模糊處理,讓半影的區域稍微擴散出去。整個Mask貼圖的生成,包括模糊處理大概的開銷是在0.3毫秒左右。大家可以看一下對比圖。

在這裡插入圖片描述在這裡插入圖片描述通過計算Mask的方法確實非常值得借鑑。而且就使用經驗而言,近景軟陰影就只有PCF是完美的選擇

VSM

VSM的實現關鍵在於渲染到深度紋理的同時渲染深度的平方,並在shading pass計算期望和方差。這個深度貼圖會佔用比之前多一倍的記憶體

實現需要注意幾個地方:一是在shadow pass 的時候就利用mipmap,高斯模糊等技術做好預處理,減少鋸齒和突變。二是在shading 的時候,注意只有深度大於期望的時候才利用切比雪夫公式進行近似計算(這是由於計算的深度是一個上界)

ESM

PCF需要在計算每個點的時候都進行多重取樣,這也是它的最致命缺點,而ESM則是通過通過對ShadowMap進行預濾波,然後在計算的陰影的時候取樣ESM的計算方法。

ShadowTest可以被線性化為
f ( d , z ) = ∑ i = 1 ∞ a i ( d ) B i ( z ) f(d, z)=\sum_{i=1}^{\infty} a_{i}(d) B_{i}(z) f(d,z)=i=1ai(d)Bi(z)
假設 d ≥ z d \ge z dz,即場景所有的點的深度值都比shadowmap的值更大,那麼把 f f f 重新定義成如下
f ( d , z ) = lim ⁡ α → ∞ e − α ( d − z ) f(d, z)=\lim _{\alpha \rightarrow \infty} e^{-\alpha(d-z)} f(d,z)=αlimeα(dz)
則可以使用一個大的 c c c 來逼近 α \alpha α ,則指數形式可以寫成
f ( d , z ) = e − c ( d − z ) = e − c d e c z \begin{aligned} f(d, z) &=e^{-c(d-z)} \\ &=e^{-c d} e^{c z} \end{aligned} f(d,z)=ec(dz)=ecdecz
接下來,我們可以對shadowmaping函式進行處理
s f ( x ) = [ w ∗ f ( d ( x ) , z ) ] ( p ) = [ w ∗ ( e − c d ( x ) e c z ) ] ( p ) = e − c d ( x ) [ w ∗ e c z ] ( p ) \begin{aligned} s_{f}(\mathbf{x}) &=[w * f(d(\mathbf{x}), z)](\mathbf{p}) \\ &=\left[w *\left(e^{-c d(\mathbf{x})} e^{c z}\right)\right](\mathbf{p}) \\ &=e^{-c d(\mathbf{x})}\left[w * e^{c z}\right](\mathbf{p}) \end{aligned} sf(x)=[wf(d(x),z)](p)=[w(ecd(x)ecz)](p)=ecd(x)[wecz](p)
我們可以看到,可以直接把預濾波放到shadowmap上做了。但是如圖所示, c c c 必須要大,才能讓ESM的效果更好,但是 c c c 太大,又會導致超越深度圖值最大值。

在這裡插入圖片描述參考龔大的部落格

簡單來說,它就是用 exp ⁡ ( k ∗ ( z − d ) ) = exp ⁡ ( k ∗ z ) ∗ exp ⁡ ( − k ∗ d ) \exp^{(k*(z-d))} = \exp^{(k*z)} * \exp^{(-k*d)} exp(k(zd))=exp(kz)exp(kd)來近似(z-d>0)。生成了ESM之後,照樣可以和VSM一樣用gaussian blur或者box blur。

SSMVSMESM
生成return d;float2 dxdy = float2(ddx(d),
ddy(d));
return float2(d, d* d
+ 0.25f * dot(dxdy, dxdy));
return exp(c * d);
使用return z < d;float p = (z < moments.x);float variance = moments.y
– moments.x * moments.x;
variance = max(variance, min_variance);
float m_d = moments.x – z;
float p_max = variance
/ (variance + m_d * m_d);
p_max = linstep(bleeding_reduce,
1, p_max);return max(p, p_max);
return saturate(occluder
* exp(-c * z));
引數min_variance和bleeding_reduce,場景相關c,場景無關,越大越好
優點簡單- 可以blur
- 無bias問題
- 簡單
- 可以blur
- 無bias問題
- blur kernel較小
- 1個32F通道
- 效果和深度複雜度無關
缺點- 無法blur
- bias問題
- 需要較大的blur kernel
- 2個32F通道
- 效果和深度複雜度有關
- 精度
- 非線性depth

改進,使用 SIGGRAPH 2009的Advances in Real-Time Rendering in 3D Graphics and Games裡,Lighting Research at Bungie提到的logarithmic space filtering。這裡正是利用d-z遠小於d或z的原理,把取值範圍縮小了,精度也因此提高。filtering本身就是完成這個:
∑ i = 0 N w i e c d i \sum_{i=0}^N w_i e^{cd_i} i=0Nwiecdi
其中w來自於gaussian filter的kernel。如果進一步推這個公式,就能得到:
∑ i = 0 N w i e c d i = e c d 0 e ln ⁡ ( w 0 + ∑ i = 1 N w i e c ( d i − d 0 ) ) \sum_{i=0}^N w_ie^{cd_i} = e^{cd_0}e^{\ln\left(w_0+\sum_{i=1}^N w_ie^{c(d_i-d_0)}\right)} i=0Nwiecdi=ecd0eln(w0+i=1Nwiec(did0))
這個被稱為log space filtering。最終filter的結果是個不會溢位的量。
d = c d 0 + ln ⁡ ( w 0 + ∑ i = 1 N w i e c ( d i − d 0 ) ) d = cd_0 + \ln\left(w_0+\sum_{i=1}^N w_ie^{c(d_i-d_0)}\right) d=cd0+ln(w0+i=1Nwiec(did0))
用這個修改過的公式去做gaussian blur,那麼即便是16F也可以承受高達300的c。
最後改進後的形式為

SSMESM改進的ESM
生成return d;return exp(c * d);return d;
使用return z < d;return saturate(occluder
* exp(-c * z));
return saturate(exp(occluder
– c * z));
引數c,場景無關,越大越好c,場景無關,越大越好
優點簡單- 簡單
- 可以blur
- 無bias問題
- blur kernel較小
- 1個32F通道
- 效果和深度複雜度無關
- 簡單
- 可以blur
- 無bias問題
- blur kernel較小
- 1個16F通道
- 效果和深度複雜度無關
- 很大的c
- 線性depth
缺點- 無法blur
-bias問題
- 精度
- 非線性depth
必須使用修改過的blur

從實際表現上來看,ESM最大的問題還是漏光
在這裡插入圖片描述根據原理,預濾波的方法必然會導致漏光的現象。
因為在陰影邊緣,陰影的變化非常大,而做了指數為c的運算後,其值會變得非常的大。c值很大的時候,陰影會變得非常的硬,但是c值不大的時候又會因為做了指數運算,在陰影邊緣稍微有點差別都會被指數倍放大,做filter的時候回汙染陰影值,所以會導致漏光。所以其實並不好用。

但是如果是用在靜態軟陰影上,我們生成靜態軟陰影的時候其實是可以知道哪些地方是相交的,所以可以提前把相交的面上做的filter減弱,那麼最終得到的預濾波的shadowmap就不會有漏光的問題。

ESM和VSM是實現

我用Unity的SRP實現了VSM和ESM,有興趣的可以拉下來看看
VSM和ESM實現

引用

[1] https://digitalrune.github.io/DigitalRune-Documentation/html/3f4d959e-9c98-4a97-8d85-7a73c26145d7.htm

[2] https://zhuanlan.zhihu.com/p/26853641

[3] http://www.gamelook.com.cn/2020/11/404063

[4] Exponential Shadow Maps

[5] 切換到esm

相關文章