遊戲中的陰影實現

遊資網發表於2020-03-03
遊戲中的陰影實現


陰影對於提高遊戲真實感非常總要,簡單總結下游戲中的陰影實現.

先來看下陰影的組成部分

遊戲中的陰影實現

陰影的組成部分


1.平面對映

最簡單的情況就是物體在一個平面上投射陰影,這種情況下只是需要通過矩陣把產生陰影點面投射到平面上.
遊戲中的陰影實現


從v點對映到p點:   遊戲中的陰影實現

令   遊戲中的陰影實現

推導後寫成矩陣的形式:

遊戲中的陰影實現


如果receiver不是一個無窮大的平面,需要通過stencil buffer標記出需要接受陰影的部分.

同樣需要注意避免這種情況,這時候不應該繪製出陰影.

遊戲中的陰影實現

右邊的情形下不應該繪製出陰影


如果想要Soft Shadow的效果,可以在光源周圍使用多個點進行投射陰影,然後取出一個平均值,但是這樣產生的陰影效能消耗很大,一般需要幾百次取樣平均才能得到較好的效果.

另外一種方法是先得到陰影貼圖,然後做一個blur處理.這樣比較快,缺點就是不符合實際陰影近處清晰遠處模糊的特點.

2.Projector實現

和上面的方法非常類似,區別在於先把陰影從光源的視角先渲染occluder得到陰影,再投射到receiver上,這樣就可以把陰影作用到不平坦的面上.

這兩種方法的缺點就是需要明確地知道occluder和receiver,只能適用於簡單的場景.但是效能消耗相對較小,所以在手游上仍然能看到這些方法的應用.

甚至在手游上可以直接將陰影做成一張固定的貼圖,以decal的形式貼到地面上,雖然是很簡單的形式,也能極大地增強真實感.

3.Shadow Volumes <參考GPU Gems 3 Chapter11>

一種非常過時的渲染陰影的方法,但是其思想很值得學習借鑑.

1.shadow volume:

就是從光源沿著模型邊緣拉伸至無限遠處加上前蓋後蓋形成的形狀.

遊戲中的陰影實現

shadow volume


2. z-pass演算法:

shadow volume陰影的思想就是取一條從視點到目標點的線,每次進入shdow volume,計數加一,每次離開計數減一,這樣計數為0的部分就是無陰影的地方.

通常使用stencil buffer來實現,從視點渲染shadow volume集合體,開啟z-test,正面部分+1,背面部分-1,stencil buffer為0的部分就是無陰影部分.

遊戲中的陰影實現

z-pass


3. z-fail演算法:

z-pass演算法有個缺陷,當視點在shadow volume中的時候,會產生錯誤的結果.

所以就有了z-fail的演算法,z-fail演算法其實就是從物體背面計數,在z-test fail的幾何體部分,在進入shdow volume時計數-1,離開時計數+1,這樣就可以規避這個缺陷.

遊戲中的陰影實現

z-fail


不過z-fail演算法普遍要比z-pass要慢,因為從背面渲染shadow volume,通常會覆蓋更多的畫素點,其次從上圖可以看出,使用z-fail時必須渲染shadow volume的capping部分(前蓋後蓋).因此在渲染前可以做一個攝影機是否在shdow volume中的簡單判斷,來決定使用z-pass或者z-fail演算法.

4. 生成陰影體的步驟:

一種最常見的一種生成shadow volume的方法,不過這種方法要求目標模型是封閉的多邊形網格(沒有空洞,裂隙,自相交).

分為三部分: front capping 前蓋-> back capping 後蓋-> silhouette 輪廓拉伸成的側面

front capping就是取模型中面向光源的三角面,方向判斷可以通過判斷面法線和光源方向的乘積的正負值來判斷.

back capping 取模型中背向光源的面,沿光源方向拉伸到無窮遠處.

silhouette 判斷兩個臨接面與光源方向不同的邊,認為是輪廓邊,將每條邊擴充套件拉伸到無窮遠處形成一個四邊形面.

5. 在無窮遠出的渲染:

如何表示無窮遠處的點?使用齊次座標將w分量置為0,xyz表示方向即可.

如何避免圖元在攝影機far clip plane外被裁剪掉?

一種方法是使用 GL_DEPTH_CLAMP_NV 擴充套件, 將far plane外的點clamp到裁剪空間中.不過這個方法好像是隻適用於OpenGL 和 NVIDIA顯示卡.

另外一種方法是稍微修改下攝影機的裁剪矩陣,將far plane設定為無窮遠

遊戲中的陰影實現

普通攝影機矩陣


變成下面這樣:

遊戲中的陰影實現

遠裁面在無窮遠處的攝影機矩陣


當然精度或有微乎其微的減少.

6. 適用於非封閉模型的方法:

把模型分成兩部分,一部分是面向光源的面,一部分是背向光源的面,分別進行拉伸生成shadow volume,就可以支援非封閉模型.缺點是原來的輪廓邊相當於生成了兩次,造成效能浪費.

遊戲中的陰影實現

左邊是面向光源面,右邊是背向光源面,兩個加在一起形成正確的結果


7. 使用Geometry shader生成Shadow volume

使用GS可以將生成shadow volume的工作移交給GPU,不過必須用TRIANGLE_STRIP的方式來輸入模型.

使用GL_TRINGLES_ADJACENCY_EXT模式來向GS中輸入三角形圖元,就可以獲取三角形的鄰接面,以此在GS中進行輪廓邊判斷,輸出Shdow volume等操作.

遊戲中的陰影實現

Geometry Shader中輸入的頂點


4. Shadow Map

當下應用最廣泛最常見的方法,從光源處出發向光照的方向渲染需要產生陰影的物體,得到儲存最近處物體的深度值的shdow map.

對於directional light使用一個足夠大的orthographic projection包住所有需要渲染的物體, spot light使用一個和自己光照範圍相當的frustrum, omini light沿六個方向生成類似於 cubic environment mapping的 omnidirectional shadow maps.

渲染物體光照時,將畫素點代入到光照的矩陣中,和shadow map中該點的深度值比較,如果深度值大於shadow map中深度值,說明該點在陰影中.

遊戲中的陰影實現


因為Shadow map的解析度限制,可能會出現 self-shadowing,因此需要加上一個小的bias偏移量.

遊戲中的陰影實現

self-shadowing


5. Shadow Map 增強

1.Cascaded Shadow Maps(CSM)
參考文獻
通常在渲染視角附近的物體時需要更高的shadow map精度,而直接生成的shadow map往往不符合這個條件,所以將frustum分割成數個部分,提高視角附近shadow map的精度.

遊戲中的陰影實現

CSM



遊戲中的陰影實現

Unity中的CSM,不同的顏色代表不同的CSM區域


2. Percentage-Closer Filtering (PCF)

參考文獻
在取樣點附近周圍選取一些點,分別進行depth-test,將測試結果進行平均.

現在的硬體大多提供周圍四點取樣的加權PCF深度測試(OpenGL中的sampler2DShadow, DirectX中的 SampleCmp)

再向外的PCF就需要手動偏移取樣點,簡單的方法是使用N*N的Grid方式取樣,高階的方法是在一個disk中用預計算好的Possion分佈(或者其他帶抖動的分佈方式)的點取樣,然後每個畫素取樣時對disk進行旋轉,產生soft shadow的效果.

遊戲中的陰影實現

從左到右是 4*4 Grid取樣, 12點Possion取樣, 12點Possion取樣 + 旋轉, Possion分佈圖


3. Percentage-Closer Soft Shadows (PCSS)

參考文獻
根據光源到目標點距離和Occluder到光源距離,計算PCF的軟陰影程度值,再進行PCF處理,得到近處銳利遠處模糊的陰影.

遊戲中的陰影實現


遊戲中的陰影實現

PCSS的陰影效果


4. Linearized Depth
參考文獻
普通的陰影可能會出現在遠處精度不足的情況,因為一般的陰影深度z值不是線性的,在近處精度大,遠處精度小.所以有線性陰影的做法.

改變普通的FS程式碼,大致寫成這樣:

  1. float4 vPos = mul(Input.Pos,worldViewProj); vPos.z = vPos.z * vPos.w / Far; Output.Pos = vPos;
複製程式碼

先將深度值除以Far遠平面的值,得到0-1的線性陰影深度值,再乘以w值,這樣在光柵化時得到深度值vPos.z / vPos.w,自然是我們得到的0-1的線性陰影深度值.

5. Variance Shadow Map(VSM)

參考文獻

用PCF產生軟陰影時,每計算一次深度值,需要取樣很多個點的深度進行比較然後求和.

VSM是一種Filtered Shadow Map,可以對Shadow map進行blur或者mipmap,每次計算陰影時,不需要取樣目標點周圍的很多個點,節省效能.

生成兩張Shadow Map,一張是普通的深度值,另外一張儲存深度值的平方.

對兩張Shadow map進行blur,每個新的畫素點的值是原來點周圍點值的加權平均.

遊戲中的陰影實現

兩張ShadowMap 中值的含義,可以得到方差值


如果求得的目標點的深度值小於ShadowMap中的 E(x)值,認為該點被完全點亮,不渲染陰影,這裡和普通的陰影渲染一樣.

當目標點深度值大於E(x)值時,根據Chebyshev不等式推匯出該點周圍點深度值大於目標點深度值的概率:

遊戲中的陰影實現


根據這個概率值,就可以來計算軟陰影的程度.

6. Exponential Shadow Map ESM

參考文獻

ESM也是一種類似VSM的Filtered Shadow Map

假設d代表shadow map中的深度值,z代表目標點的深度值,得到陰影函式f(d, z) = 0 (當d > z) / 1 ( 當d<=z),f(d,z)叫做Heaviside step function.

ESM就是用一個指數函式來模擬f(d,z):

圖中可以看出指數函式和Heaviside step function很相近,而且c值越大,近似效果越好.

遊戲中的陰影實現


Shadow Map中儲存exp(c*d)的值,可以進行blur,來產生軟陰影.

普通的ESM會有精度限制的問題,會限制c的值不能太大,所以有改進版的ESM,具體比較可以參考這個切換到ESM - KlayGE遊戲引擎以及上面的參考連結.

7. Pack To RGBA

某些移動平臺不支援浮點數紋理,需要將shadow map的深度值pack到RGBA貼圖中
  1. //Pack:
  2. vec4 comp = fract(depth * vec4(255.0 * 255.0 * 255.0, 255.0 * 255.0, 255.0, 1.0));
  3. comp -= comp.xxyz * vec4(0.0, 1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0);
  4. //UnPack:
  5. float depth = dot(texture((m_tex), (m_uv)), vec4(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0))
複製程式碼

也有使用256替換255的版本,但是用255比256是要好的.在某些硬體上用256會有表現不一致情況,而且精度略低些.

作者:TC130專欄地址:https://zhuanlan.zhihu.com/p/104687855



相關文章