1. 程式人生 > >Unity移動端動態陰影

Unity移動端動態陰影

譯文: https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html

基於Cubemap的動態軟陰影

ARM公司曾利用Unity開發過兩款技術Demo(Ice Cave 和 Chess Room),裡面充分發揮了Cubemap的強大威力—既用來做地面反射、冰塊折射,還用來做動態軟陰影,利用簡單的技術做出了高品質的畫面。下面是Ice Cave的效果:

請輸入圖片描述

以此國際象棋屋為例,屋子中間放置一個Reflect probe來拍攝周圍環境,只用了Cubemap的RGB通道,而周圍環境的Alpha其實也代表了光是穿透了窗戶還是被牆壁遮擋,那就可以利用Cubemap剩餘的Alpha通道就可以來儲存光和周圍環境的遮擋情況,Alpha通道圖如下:

請輸入圖片描述

利用生成的Cubemap渲染陰影主要分為兩步,一是向量L(vertex-to-light)轉換為Lp(校準過的vertex-to-light,用來取樣Cubemap用),二是軟陰影處理。

1. L到Lp向量校準:

輸入引數:
_EnviCubeMapPos >> Cubemap 中心座標
_BBoxMax >> 包圍盒最大座標,生成Cubemap時自動生成
_BBoxMin >> 包圍盒最小座標,生成Cubemap時自動生成
V: >> 頂點座標
L: >> vertex-to-light向量,已normalized

輸出引數:
Lp >> 校準後的vertex-to-light向量,作為UV去取樣Cubemap

校準過程:

// Working in World Coordinate System.
vec3 intersectMaxPointPlanes = (_BBoxMax - V) / L;
vec3 intersectMinPointPlanes = (_BBoxMin - V) / L;
// Looking only for intersections in the forward direction of the ray.    
vec3 largestRayParams = max(intersectMaxPointPlanes, intersectMinPointPlanes);
// Smallest value of the ray parameters gives us the intersection.
float dist = min(min(largestRayParams.x, largestRayParams.y), largestRayParams.z);
// Find the position of the intersection point.
vec3 intersectPositionWS = V + L * dist;
// Get the local corrected vector.
Lp = intersectPositionWS - _EnviCubeMapPos;

先利用線和包圍盒求交點,從包圍盒位置到交點的向量就是Lp,然後利用Lp去取樣Cubemap用於著色。
請輸入圖片描述

float shadow = texCUBE(cubemap, Lp).a;

另外背面要特殊處理下,防止陰影穿透問題。

if (dot(L,N) < 0)
  shadow = 0.0;
shadow *= max(dot(L, N), 0.0);

2. 軟陰影:
陰影平滑的過程比較有趣,首先Cubemap過濾方式選擇tri-linear filtering,然後計算vertex-to-intersection-point(頂點到交點)向量的長度,然後乘以外部傳入係數:

float texLod = length(IntersectPositionWS - V);
texLod *= distanceCoefficient;

為了平滑陰影,我們用texCUBElod 去取樣Cubemap,其中UV的XYZ來自Lp,W來自vertex-to-intersection-point(頂點到交點)的距離。

Lp.w = texLod;
shadow = texCUBElod(cubemap, Lp).a;

下圖也可以看到離窗戶越遠處的陰影越模糊。
請輸入圖片描述
這種陰影比較適合室內環境、點光源位置不變、內部有移動物體的情況。

地面雲陰影

對於地面上雲陰影,用實時燈光照射出陰影顯然是不划算,可以直接在地面Shader中混合一個運動的雲圖就能達到類似效果。
請輸入圖片描述

我用Shaderforge拖出了一個簡單的版本:
請輸入圖片描述
另外這種方法也可以用來做地面風雪效果。

植物搖曳陰影

對於樹、草、旗子這類位置不變但有搖曳動畫的物體,可以預先把陰影烘焙到貼圖中,然後把陰影圖作為單獨貼圖、或地面貼圖Alpha通道傳送到地面shader中,然後只需要新增陰影晃動的特性就可以隨植物晃動而晃動,伴隨有一種真實陰影的感覺。另外注意陰影的方向、和植物晃動的方向同步等細節。
請輸入圖片描述請輸入圖片描述
具體細節可以參考:手機遊戲中大量植物影象的偽陰影渲染

肆 · 結合Projector和Rendertexture的實時陰影

建立一個跟隨主相機的陰影相機,改為正交投影,設定單獨的shadow Layer,將需要投射陰影物體設定到shadow layer,為此陰影相機設定渲染目標到一個渲染紋理RTT_Shadow。另外建立一個Projector,為它設定一個材質Mat_Proj,並將RTT_Shadow傳到Mat_Proj的shader中進行著色,另外為防止投影相機邊緣的刺刺的長線,要設定一個陰影衰減紋理,如果需要軟陰影則需要另外Blur。

請輸入圖片描述

請輸入圖片描述

角色腳下陰影面片

對於遊戲中的NPC、雜兵、野怪這些非關鍵性角色可以直接設定一個陰影面片來模擬陰影,當然如果地面起伏比較大可能會有穿插問題。

請輸入圖片描述

Light Probe

具體細節參考Unity手冊不贅述了:Light Probes

請輸入圖片描述

Shadow Maps

1.Standard Shadow Mapping
基本思想是在光源位置放置一個相機(Light space Camera),畫一遍深度得到深度圖,在渲染場景時將pixel座標轉換Light Space計算深度,然後比較它深度和深度圖中的深度,如果比深度圖中深度大就意味著在陰影中,否則在被照亮。

陰影的鋸齒有兩類:透視導致的鋸齒(Perspective alias)和投影導致的鋸齒(Project alias)。

2.PCF
投影導致的鋸齒是因為燈光投射方向和物體表面夾角過小時多pixel對應陰影圖的一個texel,這可以通過提高陰影圖的大小來解決,也可以通過Percentage Closer Filtering來柔化邊緣。PCF就是在繪製時,除了繪製當前點還會對周圍畫素進行多次取樣、混合來柔化鋸齒,常用PCF有:使用隨機取樣實現soft shadow泊松取樣等。
請輸入圖片描述

3.PSM
透視導致的鋸齒是因為透視的近大遠小所導致的,於是就有了Perspective Shadow Map,它將整個Shadow Map的計算過程轉到歸一化裝置空間(NDC)來計算,這就消除了近大遠小的問題。下圖是Standard Shadow Map和經過Perspective Shadow Map優化過的陰影,陰影明顯更細緻。

請輸入圖片描述請輸入圖片描述

可是PSM本身有很大侷限性,比如影子質量比較依賴視角方向、近處陰影與遠處陰影Z分佈過大。

4.LISPSM
在PSM的基礎上又有了新的陰影技術Light Space Perspective Shadow Maps,它是在和燈光方向垂直的方向構建View Frustrum,然後將燈光、場景都轉到這個View Frustrum的Perspective space,然後再計算Shadow Map,這樣無論是點光、聚光、平行光就都轉為平行光。
請輸入圖片描述

5.VSM(方差陰影)
在使用PCF時一般不能提前對Shadow Map進行模糊處理,因為這會導致PCF計算不準,而Variance Shadow Maps則沒有這樣的限制。VSM儲存的Shadow Map不僅包括深度,還有深度的平方,這時可以對Shadow Map做過濾,然後利用切比雪夫不等式計算出大於當前深度的概率上限,也就是陰影區的概率。切比雪夫不等式:
請輸入圖片描述
請輸入圖片描述請輸入圖片描述

6.CSM / PSSM
這是兩種分別研究發表但是原理幾乎一樣的陰影技術,Unity用的就是CSM,而其中PSSM是幾個中國人(Zhang F, Sun H Q, Xu L L, et al,觀摩大佬風采)提出的。它們的原理如下:

a) 對攝像機視錐體內沿著Z由近到遠切陰影圖分為多張,而切分是兩種切分規則的混合,一種是均勻切分,一種是指數切分,兩者按照一定比率混合起來。
請輸入圖片描述

b) 對每一塊分別計算一個光源投影空間內平移、縮放的矩陣cropMatrix,它可以將切分的多塊移動、縮放到光源的視椎中,這個矩陣和正交投影矩陣非常像。

// Build a matrix for cropping light's projection
   // Given vectors are in light's clip space
Matrix Light::CalculateCropMatrix(Frustum splitFrustum)
{
  Matrix lightViewProjMatrix = viewMatrix * projMatrix;
  // Find boundaries in light's clip space
  BoundingBox cropBB = CreateAABB(splitFrustum.AABB,
                                  lightViewProjMatrix);
  // Use default near-plane value
  cropBB.min.z = 0.0f;
  // Create the crop matrix
  float scaleX, scaleY, scaleZ;
  float offsetX, offsetY, offsetZ;
  scaleX = 2.0f / (cropBB.max.x - cropBB.min.x);
  scaleY = 2.0f / (cropBB.max.y - cropBB.min.y);
  offsetX = -0.5f * (cropBB.max.x + cropBB.min.x) * scaleX;
  offsetY = -0.5f * (cropBB.max.y + cropBB.min.y) * scaleY;
  scaleZ = 1.0f / (cropBB.max.z - cropBB.min.z);
  offsetZ = -cropBB.min.z * scaleZ;
  return Matrix( scaleX,     0.0f,     0.0f,  0.0f,
                   0.0f,   scaleY,     0.0f,  0.0f,
                   0.0f,     0.0f,   scaleZ,  0.0f,
                offsetX,  offsetY,  offsetZ,  1.0f);
}

c) 針對切分的每一塊渲染陰影圖,一般陰影圖大小一樣的,比如都是1024*1024,而近處包含的場景範圍比遠處小,所以近處陰影圖的精度會更高。