[工作積累] shadow map問題匯總


1.基本問題和相關

Common Techniques to Improve Shadow Depth Maps:

https://msdn.microsoft.com/en-us/library/windows/desktop/ee416324(v=vs.85).aspx

 

Cascaded Shadow Maps

https://msdn.microsoft.com/en-us/library/windows/desktop/ee416307(v=vs.85).aspx

 

Soft shadow

PCSS: http://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf

PCSS shader sample: http://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf

 

Translucent shadow

http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/S2008-Filion-McNaughton-StarCraftII.pdf

http://www.crytek.com/download/Playing%20with%20Real-Time%20Shadows.pdf

Shadows & Transparency

Translucency map generation:

  • Depth testing using depth buffer from a regular opaque shadow map to avoid back projection/leaking
  • Transparency alpha is accumulated only for objects that are not in “opaque” shadows
  • Alpha blended shadow generation pass to accumulate translucency alpha (sorted back to front)
  •  In case of cascaded shadow maps, generate translucency map for each cascade
  • Shadow terms from shadow map and translucency map are both combined during deferred shadow passes with max() operation S

 

2.實現細節和問題

 

Hardware shadow map:

D16/ D32 / D24S8 作為rendertarget.關閉color wirte以加速渲染.

 

Hardware PCF:

  • 紋理采樣開啟雙線性過濾
  • d3d9用tex2Dproj, d3d11用SampleCmpLevelZero

這兩個指令跟普通雙線性采樣不同的是, 會先執行比較(深度比較),然后基於比較后的結果(陰影強度)進行雙線性插值

 

Depth Bias

常規的shadow acne通過depth bias就可以修復, 如果是自陰影, slope scaled bias也很重要.

如果需要用RFloat來繪制深度, color buffer沒辦法用bias, 那么就需要模擬slope scaled bias. 根據 https://msdn.microsoft.com/en-us/library/windows/desktop/cc308048(v=vs.85).aspx

對於dx可以這樣模擬: 

 (shadow slope scaled depth pass, rendering to a RFloat)

float dx = ddx(depth);
float dy = ddy(depth);
const float bias = max(abs(dx), abs(dy)) * YOUR_SLOPE_SCALED_BIAS;
return depth + bias;

OpenGL 也有類似的功能 https://www.opengl.org/sdk/docs/man/html/glPolygonOffset.xhtml

原理都一樣,spec沒有說具體公式,但是通常有個偏移就可以正確顯示了,不需要精確的一致

 

最小視錐(minimal frustum)

在shadow map貼圖大小固定的情況下, 視錐越小, shadow map上的內容越少(有效內容不變), 所以利用率和分辨率越高.

 

naive 實現:

1.計算場景包圍盒

2.根據場景包圍盒計算最小視錐

3.渲染shadow map

 

更好的實現:

1.計算場景包圍盒

2.用場景相機裁剪這個包圍盒

3.用裁剪后的凸包體,計算最小視錐

4.渲染shadow map

 

由於場景包圍盒被相機裁剪(求交), 所以包圍盒變小, 那么生成的視錐也變得更小.

因為視錐是不規則六面體, 視錐和AABB求交得到的是一個凸包.求交方法可以看Ogre, Ogre有convex實現.

我用的另外一個方法: 用aabb 的12條邊和視錐的6個面求交, 視錐的12個邊和aabb的6個面求交, 得到的交點如果同時在aabb和視錐內就是凸包體的頂點.

如果要計算凸包在Light space的包圍盒, 那么應該先將每個頂點變換到light space, 再求包圍盒.

如果先求包圍盒, 在把包圍盒變換到light space, 因為AABB的軸對齊特性, 變換以后通常會變大.

 

我嘗試過的其他的實現:

將場景bounding (minz, maxz)渲染到小紋理(比如32x32), 再download到CPU, 最后得到硬件裁剪后的最后可見的bounding box.

特點: shadow map利用率高, 對於自陰影, 拉得非常近時, 效果仍然非常清晰. 這種方式一般情況下沒有必要使用.

這個就是類似 SDSM (Sample Distribution Shadow Maps) (一開始不知道叫這個名字,自己想的方法,后來在龔敏敏大大的文章里才知道), 這里有個例子,是將深度讀回並down sample得到minz和maxz: https://github.com/TheRealMJP/Shadows

 

Depth bias based on frustum depth range

因為使用的是Hardware shadowmap, 所以需要指定depth bias和slope scaled bias.

由於frustum根據根據convex的頂點計算來. 如果frustum的深度范圍(znear, zfar)不固定的話, 那么相同的bias值, 對應的誤差會有浮動.

所以可以根據視錐的深度范圍來計算bias, 這樣的誤差值是固定的. 方法:

渲染shadow map depth的時候只指定slope scaled bias, 不指定depth bias.

渲染物體的時候, shadow matrix = M * light_projection * light_view , 也就是再乘以一個參數矩陣 M (DepthBias):

        float offset = isD3D9 ? 0.5f / (float)ShadowMapSize : 0f;   //half texel offset
        float bias = -0.0015f / (DepthRange) * adjustScale;
        DepthBias[0] = Vector4(1, 0.0f, 0.0f, offset));
        DepthBias[1] = Vector4(0.0f, 1, 0.0f, offset));
        DepthBias[2] = Vector4(0.0f, 0.0f, 1.0f, bias));
        DepthBias[3] = Vector4(0.0f, 0.0f, 0.0f, 1.0f));

同時由於計算出的uv要采樣shadowmap, 對於dx9來說, 同時可以預先應用half pixel offset.

 

PCSS:

PCSS的原理比較簡單, 也有很多變種. 目前用的是標准的實現. 遇到的問題:

PCSS的半影采樣范圍是根據相似三角形計算出來的

float PenumbraSize(float zReceiver, float zBlocker) //Parallel plane estimation
{
     return (zReceiver - zBlocker) / zBlocker; 
}  

如果zblocker的深度太小(接近0), 那么半影采樣范圍就變得非常大, 難以接受.

這種情況在做Self shadow的時候會遇到, 因為視錐是很小的. 解決辦法:

1.用戶指定半影的大小范圍(shader constant, lightWidthMin, lightWidthMax),  然后根據深度(距離)做線性插值.

或者 2.放大depth range, 這樣最小的depth也不會接近0

 

如果產生陰影的物體和接受陰影的物體靠得太近, zReceiver - zBlocker 太小, 導致半影范圍接近於0, 導致鋸齒:

解決辦法可以用上面的線性插值, 因為線性插值最小值是lightWidthMin, 保證有最小的半影范圍.

或者: 在半影范圍上加一個常量值, 比如1.0/shadowMapSize
個人使用lightwidth_min和max線性插值, 這樣也方便美術調控參數.

 

Translucent shadow(not alpha test):

上面已經貼出的Crytek和StarCraftII的方法了, 方式比較類似.

我這里的簡單實現: 

R8 + Depth16, alpha和depth同時繪制, 一個color buffer保存alpha, 一個depth buffer保存深度.

opaque: Depth test - less, disable color write.

transparency: Depth test - less, disable Z write, output alpha, enable color blending(addative)

這種方式比較簡單, 一次繪制沒有render target切換, 先畫不透明物體再畫半透明物體. shader中對於alpha的陰影判斷也比較hacky: 如果alpha值(R8.color.r)不為0, 則認為有陰影, 不需要比較深度. 因為能寫alpha值的時候, 說明深度測試less通過了. 對於PCSS需要深度的, 可以模擬一個深度值.

問題: 不支持自陰影(doesn't support self shadow). 產生陰影的半透明物體本身, 如果要計算陰影, 根據alpha!=0這個判斷, 也是有陰影的...

 

改進方式:

基於上面的方式, 給transparent objects再加上一個depth pass, 繪制陰影時采樣兩張深度圖. 或者將前面的R8改為RGBA, A保存透明度, RGB打包深度, 單獨混合alpha通道, 這樣不用切換render target.
兩個depth pass的話多一張D16的貼圖, 顯存占用要比RGBA小.

另外, alpha值越大, 光線穿透越少, shadow值應該越小, 我用crytek的max()結果是不對的,我用的是min(sample(ShadowMap), 1-alpha), 懷疑crytek使用的是 1- max(1-sample(ShadowMap), alpha).

即crytek paper所說的shadow term是(1-sample(shadowMap)), 改值越大,陰影就越大。

 

問題: 自陰影錯誤

|        |          |

a1      a2       O

如上, O是不透明物體, a1和a2是透明物體.

當出現多層透明物體的時候, O的陰影是對的, 因為a1和a2的alpha 會混合.

基於上面的改進, 因為有了深度信息, 再加上bias, 所以a1不會有陰影, 也是對的.

但是a2的陰影和O的陰影是一樣的, 都是基於同一個alpha計算出來的.由於a2是半透明物體, 陰影表現沒有那么明顯, 這里的問題可以忽略.

或者: 用深度來做線性插值進行:

shadow(uv.xyz) = lerp(1-alpha_a1a2, 1, saturate( (depth_O  - uv.z) / (deoth_O -  depth_a1) ) ); 

其中uv.z是shader中當前物體的深度. alpha_a1a2是alpha混合的結果. depth_O是opaque shadow map采樣出的深度. depth_a1是transparent shadow map采樣出來的深度.

這其實還是不對的, 因為a2的陰影透明度是1-alpha_a1, 跟距離無關, 但是可以解決透明物體和非透明物體靠的太近時的z fighting和shadow acne.

 

上面是工作中主要遇到的問題. 另外簡單記錄一下其他東西.

CSM

如果所有陰影都產生在一張shadowmap上,那么近處的分辨率也會比較低.CSM主要是通過多個cascade來提高近處的陰影分辨率.

之前工作中也做過PSSM, 一般會把多個shadowmap合到一張上, 比如4張1024x1024的shadowmap, 可以用2048x2048的貼圖, 通過viewport來繪制四個區域. 上面Crytek也提到了.

還有CSM邊界分割處也需要blend處理, 不然會有縫隙.

 

PSM

perspect最大的特點是近大遠小, 所以使用pserspective 投影, 來提高近處陰影的分辨率.

但一般方向光都是平行光, 需要用orthographic (parallel)投影, 但由於一般場景相機都是perspective, 視錐不是box, 但在投影以后(post perspective space)是一個box, D3D是z[0,1]的扁盒子, OGL是一個cube. 在這個空間下, 因為方向會有切變(比如世界空間下的視錐四條射線, 到了這個空間是4條平行線), 所以原本世界空間的方向光到了post perspective space就變成了點光源, 可以用perspective projection了.

原理大致是這樣, 實現的話會比較tricky.

另外一個Light space perspective shadow map (LiSPSM), 也是一種PSM, 還是利用perspective 投影來提高近處投影的精度. 主要的改變是不在用場景相機的post perspective space. 因為perspective投影下大部分方向都會有形變, 但是垂直於視方向(平行於xy平面)的方向不會有改變. LiSPSM就是利用這一點, 使用一個垂直於光照方向的透視投影, 來渲染深度. 如果直接用這個視錐渲染的話, y值就是沿着光照方向的深度值. 所以先把光空間變換到垂直於光照方向, 應用透視投影, 再變換回來, 得到的z就是深度了. 這個實現上要比PSM簡單得多. 我也嘗試了一下, 但是和一般的shadow map差別不大(比LiSPM的demo視頻和代碼效果差多了), 可能是實現上有點問題, 或者是只渲染了一個角色的自陰影的問題.

 


更新:關於自陰影的問題,如果要支持全方向的光源,遇到了一些問題。

1.一般陰影用depth bias效果已經不算差, 自陰影需要slope scaled bias 才能更好的去掉acne

2.自陰影的projective aliasing(投影鋸齒,最前面第一個鏈接有)非常嚴重:

projective aliasing是因為光照方向和表面切線平行(與法線垂直)的時候,這個面上的的所有點只共享了一個投影點造成的陰影精度不夠。shadowmap分辨率和利用率提高,以及好的sampling比如NxN的PCF可能會緩解這種問題。
我目前的處理方式,是根據它產生的特點,做一個糾正

#define SELFSHADOW_COS_MAX 0.00872653549837393496488821397358 //cos 89.5 degree
shadow = sampleSahdowMap(...);
shdaow = min( saturate(NdotL-SELFSHADOW_COS_MAX ), shadow);

即在接近垂直的時候,clamp為0,省得各種黑白交錯。這么做的結果是,0.5 degree precision loss,但是效果好了很多。

注意在計算spot/point light 的NdotL時, L = normlalize(lightPos.xyz - worldPos.xyz * lightPos.w); 是光源位置到當前點位置的向量, 這跟繪制shadowmap的direction並不一樣,需要使用實際的光源方向。

為了方便使用,可以封裝為一個簡單的宏或者函數:

//anti projective alias
#define SHADOW_APA(worldNormal, shadow) min(saturate(saturate(dot(worldNormal.xyz,Uniform_SelfShadow_Direction[Uniform_LightIndex].xyz)) - SELFSHADOW_COS_MAX), shadow)

這里Uniform_SelfShadow_Direction就是方向光的方向或者spotlight的方向,是渲染shadowmap時使用的方向。

 


更新:

對於toon shading,由於非真實光照的緣故,大多不能使用NdotL,所以上邊方法不可行,有兩個方案:
1.使用HW shadowmap,HW PCF fliter能夠很大程度減少projective alias

2. 使用VSM。經過測試,VSM能過很大程度減少與projective alias,比HW shadowmap要好很多

 


更新筆記2:

前面提到的LiSPSM,效果不好的原因找到了,因為LiSPSM在計算光空間包圍盒的時候有點tricky,用了視錐的點往場景做raycast,得到更緊湊的包圍盒。簡單點用ascii圖示。。:

E                        F
+-----------------------+
|\                     /|
| \                   / |
|  \                 /  |
|   \               /   |
|    \             /    |
|     \           /     |
|      \         /      |
|       \       /       |
+--------+-----+--------|
A        B     C        D

上圖為光空間的,垂直於光方向的截面,BCEF是相機視錐投影,BC是近平面的點,EF是遠平面的點。LiSPSM需要用BCEF(實際上在3D空間有8個點)作為原點,光的(反)方向作為方向,做raycast,於場景包圍盒相交,得到的點用於計算土凸包和最小包圍盒,這么做是為了擴展包圍盒,將場景不可見的,但是影子可見的物體加入包圍盒

我之前用的是足夠大的光的正交視錐,和場景包圍盒相交,其實就是ADEF,這樣會將近平面BC放大到AD。很明顯用正交視錐來算的話,包圍盒和標准shadow map一樣大,主要是近平面被放大,會導致LiSPSM的透視投影變很大。。。

其實LiSPSM的raycast方法,在極端情況下也會有問題:

        R           F'                   Q 
        +-----------+-------------------+
        |            \                  |
     G  +H            \                 |
        |              \                |
        |C'             \               |
        |\               \              |
        | \               \             |
        |  \               \            |
        |   \               \           |
        +----+---------------+----------+
        O     C              F           P

 

比如上圖,OPQR是場景包圍盒,CF是視錐的近平面和遠平面上的點,FF'是光的反方先, raycast得到的點是C‘和F’。 raycast場景包圍盒,相交的凸包點為CFF‘C’,得到的光空間包圍盒為CFF’G,F‘G垂直於光方向並與場景包圍盒相交於H。 那么F’ R H, 這個區域內的物體是可能沒有投影的。 這種情況比較極端,在大場景下基本看不到,在查看模型的時候,模型的內部邊緣看(比如sponza場景的牆里面),就會出現。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM