基础的shadow map分为两个步骤:

  1. 在光源处放置一个光源相机,且朝向和位置与光源相同,以光源相机为视角采样场景深度:Z_l
  2. 回到角色相机,以角色相机看向场景,再将当前视角下的物体的world position转换到光源相机的视角下,计算深度Z_{main}。与Z_l比较,若Z_{main} > Z_l,说明该点处于阴影,否则不在

Shadow Cast

其中Shadow Cast即为上述第一个步骤

inline float4 EncodeFloatRGBA(float v)
{
    float4 enc = float4(1.0, 255.0, 65025.0, 16581375.0) * v;
    enc = frac(enc);
    enc -= enc.yzww * float4(1.0/255.0,1.0/255.0,1.0/255.0,0.0);

    return enc;
}

PSOutput ShadowCast(PSInput i)
{
    PSOutput o = (PSOutput)0;

    float depth = i.positionCS.z / i.positionCS.w;
    #if defined (SHADER_TARGET_GLSL)
        depth = depth * 0.5f + 0.5f;
    #elif defined(UNITY_REVERSED_Z)
        depth = 1.f - depth;
    #endif

    o.color = EncodeFloatRGBA(depth);

    return o;
}

Cascade Shadow

基本的shadow mapping存在一定的问题:引擎中对纹理是自动提供Mipmap,而阴影是没有的,这就导致相机离不管远近,采样的shadow map的分辨率是相同的。若shadow map的分辨率过高,采样的性能消耗是不能接受的;但shadow map的分辨率过低,阴影又会有锯齿

级联阴影贴图(Cascaded Shadow Mapping)可以解决这个问题,它为不同距离的物体绘制不同分辨率的阴影贴图。对于透视相机,近处的物体占据屏幕的大部分区域所以理应应有分辨率更高的阴影贴图;而远处的物体使用分辨率更低的阴影贴图

Cascaded Shadow Mapping的核心在于把光源相机的视锥体(AABB)进行多层分级,从近到远,每层视锥体逐渐变大。其中的难点在于如何构造AABB

  1. 初始化主相机和光源相机的视锥体近远平面的八个顶点
    /// <summary>
    /// 近远平面的四个顶点坐标
    /// </summary>
    struct FrustumCorners
    {
       public Vector3[] nearCorners;
       public Vector3[] farCorners;
    }
    
    void InitFrustumCorners()
    {
       _mainCameraFrustumCorners = new FrustumCorners[4];
       _dirLightCameraFrustumCorners = new FrustumCorners[4];
    
       for (int i = 0; i < 4; ++i)
       {
           _mainCameraFrustumCorners[i].nearCorners = new Vector3[4];
           _mainCameraFrustumCorners[i].farCorners = new Vector3[4];
           _dirLightCameraFrustumCorners[i].nearCorners = new Vector3[4];
           _dirLightCameraFrustumCorners[i].farCorners = new Vector3[4];
       }
    }
    
  2. 计算主相机的视锥体近远平面的八个顶点在world space下的坐标值
    private static Vector4 _CSMSplitRatio = new Vector4(0.067f, 0.133f, 0.267f, 0.533f);
    
    void CalcMainCameraFrustumCorners()
    {
       float near = _mainCamera.nearClipPlane;
       float far = _mainCamera.farClipPlane;
    
       float[] nears = 
       { 
           near, 
           near + far * _CSMSplitRatio[0],
           near + far * (_CSMSplitRatio[0] + _CSMSplitRatio[1]), 
           near + far * (_CSMSplitRatio[0] + _CSMSplitRatio[1] + _CSMSplitRatio[2])
    
       };
       float[] fars =
       {
           near + far * _CSMSplitRatio[0],
           near + far * (_CSMSplitRatio[0] + _CSMSplitRatio[1]),
           near + far * (_CSMSplitRatio[0] + _CSMSplitRatio[1] + _CSMSplitRatio[2]),
           far
       };
    
       _CSMSplitNear = nears;
       _CSMSplitFar = fars;
       Shader.SetGlobalVector("_G_CSM_Split_Nears", new Vector4(_CSMSplitNear[0], _CSMSplitNear[1], _CSMSplitNear[2], _CSMSplitNear[3]));
       Shader.SetGlobalVector("_G_CSM_Split_Fars", new Vector4(_CSMSplitFar[0], _CSMSplitFar[1], _CSMSplitFar[2], _CSMSplitFar[3]));
    
       for (int j = 0; j < 4; ++j)
       {
           // 计算local space下相机近平面的四个顶点
           _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _CSMSplitNear[j], Camera.MonoOrStereoscopicEye.Mono, _mainCameraFrustumCorners[j].nearCorners);
           _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _CSMSplitFar[j], Camera.MonoOrStereoscopicEye.Mono, _mainCameraFrustumCorners[j].farCorners);
    
           for (int i = 0; i < 4; ++i)
           {
               // 将local space下相机近平面的四个顶点转换到world space
               _mainCameraFrustumCorners[j].nearCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraFrustumCorners[j].nearCorners[i]);
               _mainCameraFrustumCorners[j].farCorners[i]  = _mainCamera.transform.TransformPoint(_mainCameraFrustumCorners[j].farCorners[i]);
           }
       }
    }
    
  3. 将主相机的视锥体变换到光源空间,并计算光源相机的AABB
    void CalcLightCameraFrustumCorners()
    {
       if (_dirLightCamera == null) return;
    
       for (int j = 0; j < 4; ++j)
       {
           for (int i = 0; i < 4; ++i)
           {
               _dirLightCameraFrustumCorners[j].nearCorners[i] = _dirLightCamerasData[j].transform.InverseTransformPoint(_mainCameraFrustumCorners[j].nearCorners[i]);
               _dirLightCameraFrustumCorners[j].farCorners[i]  = _dirLightCamerasData[j].transform.InverseTransformPoint(_mainCameraFrustumCorners[j].farCorners[i]);
           }
    
           float[] corners_x =
           {
               _dirLightCameraFrustumCorners[j].nearCorners[0].x, _dirLightCameraFrustumCorners[j].nearCorners[1].x,
               _dirLightCameraFrustumCorners[j].nearCorners[2].x, _dirLightCameraFrustumCorners[j].nearCorners[3].x,
               _dirLightCameraFrustumCorners[j].farCorners[0].x, _dirLightCameraFrustumCorners[j].farCorners[1].x,
               _dirLightCameraFrustumCorners[j].farCorners[2].x, _dirLightCameraFrustumCorners[j].farCorners[3].x
           };
           float[] corners_y =
           {
               _dirLightCameraFrustumCorners[j].nearCorners[0].y, _dirLightCameraFrustumCorners[j].nearCorners[1].y,
               _dirLightCameraFrustumCorners[j].nearCorners[2].y, _dirLightCameraFrustumCorners[j].nearCorners[3].y,
               _dirLightCameraFrustumCorners[j].farCorners[0].y, _dirLightCameraFrustumCorners[j].farCorners[1].y,
               _dirLightCameraFrustumCorners[j].farCorners[2].y, _dirLightCameraFrustumCorners[j].farCorners[3].y
           };
           float[] corners_z =
           {
               _dirLightCameraFrustumCorners[j].nearCorners[0].z, _dirLightCameraFrustumCorners[j].nearCorners[1].z,
               _dirLightCameraFrustumCorners[j].nearCorners[2].z, _dirLightCameraFrustumCorners[j].nearCorners[3].z,
               _dirLightCameraFrustumCorners[j].farCorners[0].z, _dirLightCameraFrustumCorners[j].farCorners[1].z,
               _dirLightCameraFrustumCorners[j].farCorners[2].z, _dirLightCameraFrustumCorners[j].farCorners[3].z
           };
    
           float min_x = Mathf.Min(corners_x);
           float max_x = Mathf.Max(corners_x);
           float min_y = Mathf.Min(corners_y);
           float max_y = Mathf.Max(corners_y);
           float min_z = Mathf.Min(corners_z);
           float max_z = Mathf.Max(corners_z);
    
           _dirLightCameraFrustumCorners[j].nearCorners[0] = new Vector3(min_x, min_y, min_z);
           _dirLightCameraFrustumCorners[j].nearCorners[1] = new Vector3(max_x, min_y, min_z);
           _dirLightCameraFrustumCorners[j].nearCorners[2] = new Vector3(max_x, max_y, min_z);
           _dirLightCameraFrustumCorners[j].nearCorners[3] = new Vector3(min_x, max_y, min_z);
    
           _dirLightCameraFrustumCorners[j].farCorners[0] = new Vector3(min_x, min_y, max_z);
           _dirLightCameraFrustumCorners[j].farCorners[1] = new Vector3(max_x, min_y, max_z);
           _dirLightCameraFrustumCorners[j].farCorners[2] = new Vector3(max_x, max_y, max_z);
           _dirLightCameraFrustumCorners[j].farCorners[3] = new Vector3(min_x, max_y, max_z);
    
           /// 避免移动相机视角造成阴影抖动
           float width = Vector3.Magnitude(_dirLightCameraFrustumCorners[j].nearCorners[1] - _dirLightCameraFrustumCorners[j].nearCorners[0]);
           float height = Vector3.Magnitude(_dirLightCameraFrustumCorners[j].nearCorners[2] - _dirLightCameraFrustumCorners[j].nearCorners[1]);
           float cross = Vector3.Magnitude(_dirLightCameraFrustumCorners[j].farCorners[2] - _dirLightCameraFrustumCorners[j].nearCorners[0]);
           float maxDis = Mathf.Max(width, height);
           maxDis = cross;
           if (!_crossDisCached.TryGetValue(j, out maxDis))
           {
               if (cross != 0)
               {
                   _crossDisCached.Add(j, cross);
                   maxDis = cross;
               }
           }
           else
           {
               if (cross > maxDis)
               {
                   _crossDisCached[j] = cross;
               }
           }
    
           float unitPerPixel = maxDis / shadowMapRTs[j].width;
           Vector3 lightCameraPosOS = _dirLightCameraFrustumCorners[j].nearCorners[0] + (_dirLightCameraFrustumCorners[j].nearCorners[2] - _dirLightCameraFrustumCorners[j].nearCorners[0]) * 0.5f;
           lightCameraPosOS.x = Mathf.Floor(lightCameraPosOS.x / unitPerPixel) * unitPerPixel;
           lightCameraPosOS.y = Mathf.Floor(lightCameraPosOS.y / unitPerPixel) * unitPerPixel;
    
           _dirLightCamerasData[j].transform.position = _dirLightCamerasData[j].transform.TransformPoint(lightCameraPosOS);
           _dirLightCamerasData[j].transform.rotation = RenderSettings.sun.transform.rotation;
       }
    }
    

    将AABB可视化如下:

  4. 构建光源相机的position、rotation、近远平面、aspect、orthographicSize

    void CalcDirLightCameraSplits(int index)
    {
       _dirLightCamera.transform.position = _dirLightCamerasData[index].transform.position;
       _dirLightCamera.transform.rotation = _dirLightCamerasData[index].transform.rotation;
    
       _dirLightCamera.nearClipPlane = _dirLightCameraFrustumCorners[index].nearCorners[0].z;
       _dirLightCamera.farClipPlane  = _dirLightCameraFrustumCorners[index].farCorners[0].z;
    
       _dirLightCamera.aspect = 1;
    
       float cross = Vector3.Magnitude(_dirLightCameraFrustumCorners[index].farCorners[2] - _dirLightCameraFrustumCorners[index].nearCorners[0]);
       float maxDis = -1;
       if (!_crossDisCached.TryGetValue(index, out maxDis))
       {
           if (cross != 0)
           {
               _crossDisCached.Add(index, cross);
               maxDis = cross;
           }
       }
       else
       {
           if (cross > maxDis)
           {
               _crossDisCached[index] = cross;
           }
       }
    
       _dirLightCamera.orthographicSize = maxDis  * 0.5f;
    }
    
  5. 最后再进行shadow map
    // 获得cpu端设置的级联等级
    float4 CSMWeights = GetCSMWeights(view_z);
    
    float3 normalBias = normalWS * _G_Shadow_NormalBias;
    float3 shadowPos0 = TransformWorldToShadow(posWS + normalBias, _G_Matrix_WorldToShadow[0]);
    float3 shadowPos1 = TransformWorldToShadow(posWS + normalBias, _G_Matrix_WorldToShadow[1]);
    float3 shadowPos2 = TransformWorldToShadow(posWS + normalBias, _G_Matrix_WorldToShadow[2]);
    float3 shadowPos3 = TransformWorldToShadow(posWS + normalBias, _G_Matrix_WorldToShadow[3]);
    
    float bias = GetSlopeBias(normalWS, lightDirWS);
    float depth0 = shadowPos0.z + bias;
    float depth1 = shadowPos1.z + bias;
    float depth2 = shadowPos2.z + bias;
    float depth3 = shadowPos3.z + bias;
    
    // 进行shadow map
    float shadow0 = PCF_High(shadowPos0.xy, depth0, _G_ShadowMap_Tex0);
    float shadow1 = PCF_High(shadowPos1.xy, depth1, _G_ShadowMap_Tex1);
    float shadow2 = PCF_High(shadowPos2.xy, depth2, _G_ShadowMap_Tex2);
    float shadow3 = PCF_High(shadowPos3.xy, depth3, _G_ShadowMap_Tex3);
    sum = shadow0 * CSMWeights[0] + shadow1 * CSMWeights[1] + shadow2 * CSMWeights[2] + shadow3 * CSMWeights[3];
    

    可以得到如下结果:

  6. Debug具体的级联分布

    sum *= CSMWeights;
    


他们曾如此骄傲的活过,贯彻始终