Unity移動端動態陰影

Kaitiren發表於2017-08-02

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

基於Cubemap的動態軟陰影

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

請輸入圖片描述

其中反射、折射部分參考:Reflections Based on Local Cubemaps in UnityARM Guide for Unity Developers,下面主要介紹下軟陰影部分原理。

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

請輸入圖片描述

生成Cubemap細節可以參考AssetStore中的原始碼

利用生成的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。

請輸入圖片描述

這是最近幾年手遊應用比較廣泛的方法,網上有很多相關文章,比如:結合Projector和Rendertexture實現實時陰影ProjectorShadow(手游上的實時陰影方案)

另外AssetStore也有不少類似外掛:Fast Shadow Projector

請輸入圖片描述


角色腳下陰影面片

對於遊戲中的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,這樣無論是點光、聚光、平行光就都轉為平行光。
請輸入圖片描述

左圖是Uniform(近處精度不足),中間是LISPSM(近處、遠處都不錯),右面是PSM(遠處精度不足)。LISPSM具體細節參考:
https://www.cg.tuwien.ac.at/research/vr/lispsm/shadows_egsr2004_revised.pdf

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

左圖是Standard Shadow Map,右圖是Variance Shadow Map,具體細節參考:Variance Shadow MappingVSM的demosMatt's Variance Shadow Maps

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,而近處包含的場景範圍比遠處小,所以近處陰影圖的精度會更高。

d) 渲染場景陰影
請輸入圖片描述
關於CSM和PSSM具體細節參考:Cascaded Shadow MapsParallel-split shadow maps for large-scale virtual environmentsPSSM from GPU Gems 3


相關文章