Unity基礎6 Shadow Map 陰影實現

這篇實現來的有點墨跡,前先後後折騰零碎的時間折騰了半個月才才實現一個基本的shadow map流程,只能說是對原理理解更深入一些,但離實際應用估計還須要作不少優化。這篇文章大體分析下shadow map的基本原理、Unity中實現ShadowMap陰影方式以及一些有用的參考。php

1 . Shadow Map 基本原理

基本的shadow Map 原理, 參考 "Unity基礎(5) Shadow Map 概述". 其基本步驟以下:html

  • 從光源的視角渲染整個場景,得到Shadow Map
  • 實際相機渲染物體,將物體從世界座標轉換到光源視角下,與深度紋理對比數據得到陰影信息
  • 根據陰影信息渲染場景以及陰影

2. 採集 Shadow Map 紋理

Unity 獲取深度紋理的方式能夠參考以前的日記:Unity Shader 基礎(3) 獲取深度紋理 , 筆記中給出了三種獲取Unity深度紋理的方式。 若是採用自定義的方式來獲取深度,能夠考慮使用EncodeFloatRGBA對深度進行編碼。另外,能夠經過增長多個subshader實現對不一樣RenderType 陰影的支持。git

SubShader
    {
        Tags { "RenderType"="Opaque" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 depth: TEXCOORD0;
            };
        
            v2f vert (appdata_base v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.depth = o.vertex.zw ;
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {
                float depth = i.depth.x/i.depth.y ;
                return EncodeFloatRGBA(depth) ;
            }
            ENDCG
        }
    }

3 建立ShadowMap相機

1. 類型
shadow Map的相機會根據光源的不一樣有所差別,直線光使用平行投影比較合適,點光源和聚光燈帶有位置信息,適合使用透視投影, 這篇文章以平行光和平行投影爲例來實現。對於平行投影相機而言,主要關於方向、近平面、遠平面、視場大小。github

1. 建立算法

以光源爲父節點建立相機,設置投影方式以及 RenderTexture對象。其方向與父節點保持一致。windows

2. 視場匹配app

陰影實現中shadow map佔用的空間是最大的,合適的相機視場設置能夠在一樣資源下得到更好的效果、更高的精度。在Common Techniques to Improve Shadow Depth Maps一文中給出相機參數適應場景的兩種方式:FIt to scene和 FIt to view. 對於Fit to Scene,其實現流程:性能

  • 利用場景中全部物體mesh的bounds計算整個場景的包圍盒AABB,須要注意的是mesh.bounds是相對於模型空間,需轉換到世界空間再計算整個場景AABB
  • 將包圍盒轉換到光源空間,這裏能夠利用transparent.worldToLocalMatrixhguod得到轉換矩陣
  • 相機參數設置:
    • 取包圍盒x、y方向最大、最小值,其差值的一半做爲相機size;
    • 包圍盒中點做爲相機位置
    • 相機方向與光源方向相同
    • 近平面和遠平面使用包圍盒Z方向最大值、最小值

Fit to Scene方式計算整個場景的AABB來攝像 Shadow Map採集相機參數,但若是場景相機視場比較小的狀況下,好比FPS遊戲中角色,這種方式就不是很合適。對於這種狀況,Fit to VIEW 更合適。優化

4 世界座標轉換到Shadow Map 相機NDC空間

判斷是否爲陰影須要比較場景中物體深度與Shadow Map中深度值,這個過程須要確保兩者在一個空間中。深度採集保存在shadow map貼圖中的數值是NDC空間數值,因此渲染物體時會將物體從世界座標轉換到Shadow Map相機空間下,而後經過投影計算轉換到NDC座標,也就是原理圖中的\(z_b\) 。投影矩陣參數能夠傳遞到shader'中進行,以下:ui

//perspective matrix
    void  GetLightProjectMatrix(Camera camera)
    {
        Matrix4x4 worldToView = camera.worldToCameraMatrix;
        Matrix4x4 projection  = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false);
        Matrix4x4 lightProjecionMatrix =  projection * worldToView;
        Shader.SetGlobalMatrix ("_LightProjection", lightProjecionMatrix);
    }

pixel shadow 中 計算NDC座標:

fixed4 object_frag (v2f i) : SV_Target
{
    //計算NDC座標
    fixed4 ndcpos = mul(_LightProjection , i.worldPos);
    ndcpos.xyz = ndcpos.xyz / ndcpos.w ;
    //從[-1,1]轉換到[0,1]
    float3 uvpos = ndcpos * 0.5 + 0.5 ;
    ...
    ...
}

5. 陰影計算

經過比較場景物體轉換到shadow map相機NDC空間深度\(z_b\)與shadow map貼圖中深度值\(z_a\)便可判斷頂點是否在陰影區域。以原理圖爲例,若是 \(z_b\)大於\(z_a\), 頂點是在遮擋物體以後,處於陰影區域。須要注意的是對shadow map 紋理採樣座標須要將場景物體頂點在shadow map相機NDC空間下的座標轉換到[0,1]的範圍。下面的代碼沒有結合光照:

fixed4 object_frag (v2f i) : SV_Target
{
    //計算NDC座標
    fixed4 ndcpos = mul(_LightProjection , i.worldPos);
    ndcpos.xyz = ndcpos.xyz / ndcpos.w ;
    //從[-1,1]轉換到[0,1]
    float3 uvpos = ndcpos * 0.5 + 0.5 ;
    float depth = DecodeFloatRGBA(tex2D(_LightDepthTex, uvpos.xy));
    if(ndcpos.z < depth  ){return 1;}
    else{return 0;}
}

6. Shadow acne 與 Peter Panning

|

深度紋理分辨率的關係,會存在場景中多個頂點對深度紋理同一個點進行採樣來判斷是否爲處於陰影的狀況,再加上不一樣計算方式的精度問題就會產生圖上Shadow acne的狀況,具體能夠參考:https://www.zhihu.com/question/49090321 ,描述的比較詳細。

5.1 shadow bias

最簡單的作法是對場景深度或者貼圖深度作稍微的調整,也就是 shadow bias,

shadow bias的作法簡單粗暴,若是偏移過大就會出現 Peter Panning的狀況,形成陰影和物體分割開的狀況。
mark

5.2 Slope-Scale Depth Bias

更好的糾正作法是基於物體與光照方向的夾角,也就是Slope-Scale Depth Bias,這種方式的提出主要是基於物體表面和光照的夾角越大, Perspective Aliasing的狀況越嚴重,也就越容易出現Shadow Acne,以下圖因此。若是採用統一的shadow bais就會出現物體表面一部分區域存再Peter Panning 一部分區域還存在shadow acne。
mark
更好的辦法是根據這個slope進行計算bias,其計算公式以下,\(miniBais + maxBais * SlopeScale\) , 其中\(SlopeScale\)能夠理解爲光線方向與表面法線方向夾角的tan值(也便是水平方向爲1的狀況下,不一樣角度對應的矯正量)。

float GetShadowBias(float3 lightDir , float3 normal , float maxBias , float baseBias)
{
     float cos_val = saturate(dot(lightDir, normal));
             float sin_val = sqrt(1 - cos_val*cos_val); // sin(acos(L·N))
             float tan_val = sin_val / cos_val;    // tan(acos(L·N))

             float bias = baseBias + clamp(tan_val,0 , maxBias) ;

             return bias ;
}

不過Bias數值是個有點感性的數據,也能夠採用其餘方式,只要考慮到這個slopescale就行,好比:

// dot product returns cosine between N and L in [-1, 1] range
// then map the value to [0, 1], invert and use as offset
float offsetMod = 1.0 - clamp(dot(N, L), 0, 1)
float offset = minOffset + maxSlopeOffset * offsetMod;

// another method to calculate offset
// gives very large offset for surfaces parallel to light rays
float offsetMod2 = tan(acos(dot(N, L)))
float offset2 = minOffset + clamp(offsetMod2, 0, maxSlopeOffset);

7. Shadow Map Aliasing

mark
解決完shadow acne後,放大陰影邊緣就會看到這種鋸齒現象,其主要緣由還在於shadow map的分辨率。物體多個點會採集深度紋理同一個點進行陰影計算。這個問題通常能夠經過濾波緊進行處理,好比多重採樣。

Pencentage close Filtering(PCF),最簡單的一種處理方式,當前點是否爲陰影區域須要考慮周圍頂點的狀況,處理中須要對當前點周圍幾個像素進行採集,並且這個採集單位越大PCF的效果會越好,固然性能也越差。如今的GPU通常支持2*2的PCF濾波, 也就是Unity設置中的Hard Shadow 。

//PCF濾波
float PercentCloaerFilter(float2 xy , float sceneDepth , float bias)
{
    float shadow = 0.0;
    float2 texelSize = float2(_TexturePixelWidth,_TexturePixelHeight);
    texelSize = 1 / texelSize;

    for(int x = -_FilterSize; x <= _FilterSize; ++x)
    {
        for(int y = -_FilterSize; y <= _FilterSize; ++y)
        {
            
            float2 uv_offset = float2(x ,  y) * texelSize;
            float depth = DecodeFloatRGBA(tex2D(_LightDepthTex, xy + uv_offset));
            shadow += (sceneDepth - bias > depth ? 1.0 : 0.0);   
                 
        }    
    }
    float total = (_FilterSize * 2 + 1) * (_FilterSize * 2 + 1);
    shadow /= total;

    return shadow;
}

mark

改進算法
Shadow Map Antialiasing 對PCF作了一些改進,能夠更快的執行。Improvements for shadow mapping in OpenGL and GLSL 結合PCF和泊松濾波處理,使用PCF相對少的採樣數,就能夠得到很好的效果OpenGl Tutorial 16 : Shadow mapping也採用了相似的方式。相似的算法還有不少,不一一列舉。

7 其餘

7.1 Perspective Aliasing

pixels close to the near plane are closer together and require a higher shadow map resolution. Perspective shadow maps (PSMs) and light space perspective shadow maps (LSPSMs) attempt to address perspective aliasing by skewing the light's projection matrix in order to place more texels near the eye where they are needed. Cascaded shadow maps (CSMs) are the most popular technique for dealing with perspective aliasing.
mark
參考:Cascaded Shadow Maps , 具體實現能夠參考:http://blog.csdn.net/ronintao/article/details/51649664

7.2 Screem space shadow map

Unity 5.4版本以後陰影的基本原理相似,可是處理方式有點差別,具體能夠查看: Screem space shadow map

8 總結

陰影的處理有不少方式,有本專著《實時陰影技術》對陰影處理作了不少介紹,翻了下果斷放棄了,老是得到一個效果好、性能好的陰影效果仍是須要費點時間。

工程下載:https://github.com/carlosCn/Unity-ShadowMap-Test.git

挺讚的一篇文章:
Unity移動端動態陰影總結
Unity Shadow Map實現

參考

Unity基礎(5) Shadow Map 概述
OpenGL Shadow Mapping
OpenGl Tutorial 16 : Shadow mapping
Shadow Map Wiki
Shadow Acne知乎
Common Techniques to Improve Shadow Depth Maps
Cascaded Shadow Maps
Percentage Closer Filtering
Variance Shadow Map Papper
Shadow Mapping Summary
Improvements for shadow mapping in OpenGL and GLSL
Screem space shadow map
Unity移動端動態陰影總結

相關文章
相關標籤/搜索