Skip to content

CSMBlend

zilch edited this page Jul 22, 2021 · 1 revision

级联阴影过渡算法 (Cascade Shadow Mapping Blend)

在前面的阴影相关系列中,已经实现了:

如果仔细观察场景,会发现当两级的Cascade Shadow Mapping(后面简称CSM)分辨率相差较大,以及当我们使用较大的PCF滤波核时,CSM阴影在两级边界处会出现非常生硬的突变。如下图所示:

本文将探讨以下问题:

  • 如何在相邻级别的CSM之间进行阴影过渡
  • 如何动态计算合适的过渡距离
  • 如何使最远处的阴影通过渐变消失

本文将基于Unity SRP来实现上述功能,因此还会顺带介绍一下Unity中的CSM。

1. Unity中的CSM

CSM的思想是对摄像机视锥进行分割,并对每个区域使用单独的ShadowMap贴图,这样就能使用不同的精度来描述不同的区域。但是在具体到技术实现细节时,会有多种不同的方案。例如摄像机视锥区域如何分割?如何计算光视角的近平面和远平面?之前的一篇文章关于这个话题已经有了比较详尽的讨论,详情请戳 -> CSM进阶之路

本文既然是在Unity的环境下去实现CSM Blend,那么就要说下Unity中CSM的方案。

Unity会为每一级摄像机视锥生成相应球形包围盒,相应级别的ShadowMap将会覆盖此球形包围盒内的像素。我们可以在编辑器中使用Gizoms将Shadow Cascade球形包围盒绘制出来,如下图所示:

使用球形包围和来计算灯光视角的投影矩阵有如下的好处:

  • 当摄像机运动和转向时(光线方向和摄像机朝向的相对角度会发生变化),ShadowMap能够保持稳定的分辨率,有效避免阴影边缘的抖动(shimmering edge effect)。

Unity中的球形包围盒实现是比较奇特的,它并未完全包裹住分割后的子视锥,因为没有Unity的源码,我并不清除其内部具体实现算法,只能通过测试来发现一下Unity中CSM的特性:

  • 它不能保证按距离分割后的N级子视锥中的像素能在N级的CSM中采样到。
  • 它可以保证在N级包围球内的像素可以在N级CSM中采样到

因此在进行CSM混合时,不能以像素到摄像机的距离为标准进行混合。只能以像素到包围球边界的距离为标准进行混合。

2. 计算CSM过渡区域

要在CSM之间进行Blend,很容易想到的一种算法如下:

  • 判定哪些像素位于Blend区域。
  • 针对Blend区域的像素计算混合因子
  • 采样计算相邻两级的ShadowMap并计算阴影强度,使用混合因子混合

那么如何判定哪些像素位于Blend区域呢?最容易想到的一种方式是计算世界坐标到球形包围盒表面的距离。 当这个距离小于一定阈值时,就认为进入本级CSM的边缘了,可以开始向下一级CSM过渡了。伪代码如下:

#define CASCADE_SHADOW_BLEND_DIST 0.5
#define CASCADE_COUNT 4

for(int i = 0; i < CASCADE_COUNT; i ++ ){
    float4 cullingSphere = _cascadeCullingSphere[i];
    float distToCenter = length(positionWS - cullingSphere.xyz); //坐标到球心距离
    float distToSurface = cullingSphere.w - distToCenter; //坐标到球面距离
    if(distToSurface < 0){
        //小于0说明像素不在本级的CSM内
        continue;
    }
    blend = saturate(1 - distToSurface / CASCADE_SHADOW_BLEND_DIST);
    cascadeIndex = i;
    return;
}

伪代码里给予了一个固定距离0.5米作为混合过渡区域。我们可以实现一个ShadowDebugPass,将每级的CSM区域用不同的颜色标示出来,同时将过渡区域也用混合色标示出来,效果如下:

红圈标示的为过渡区域,依次为1->2, 2->3, 3->4

很容易就发现一个问题,越远的CSM,其过渡区域投影到屏幕空间后占据的范围越小。这会导致远处的CSM过渡效果不理想。

我们可以将2、3区域的阴影Blend放大之后做对比,效果如下:

区域2由于在屏幕空间有足够的过渡区域,因此呈现出比较好的混合效果。区域3因为没有足够的过渡区域,于是突变感依旧很强。

2.1 动态的Blend Distance

为了解决以上的问题,我们需要思考,混合区域的大小究竟与哪些因素相关。

2.1.1 距离因素

距离是一个很显而易见的因素。

根据摄像机投影的几何原理易知,物体在屏幕空间的成像大小,与其在camera forward向量上的投影距离成正比。我们期望的是,CSM之间无论远近,应当在屏幕空间有相近大小的过渡区域。反推一下即:越远的CSM之间,在世界空间内应当有越大的过渡区域,与其到摄像机的距离呈正比

我们可以使用如下的代码来动态计算混合区域大小:

float blendDistScale = 0.1;

float distanceProjOnForward = dot(positionWS - _WorldSpaceCameraPos,_WorldSpaceCameraForward);

float blendDist = CASCADE_SHADOW_BLEND_DIST * distanceProjOnForward * blendDistScale;

考虑了距离因素后,远处的CSM级别之间过渡区域所有变大,相应的混合效果也有所提升。细心的你可能会发现,不同级别之间的过渡区域仍然并不完全一致。那是因为我们使用点到球面的距离来定义过渡区域,投影在平面上,即是平面与"球环"的相交区域。因此过渡区域不一致是可以理解的,我们也不需要完全精准的数学一致,只需要最终出来的效果ok就好了。

但是很显然,目前虽然有所改善,但还是不够。

2.2 考虑CSM之间的分辨率差

这是由于不同级别的CSM之间,其ShadowMap的分辨率差异是有所不同的。分辨率差异越大,大PCF核形成的边缘模糊效果差异也越大,以及使用的ShadowBias差异也越大。越大的差异,就需要越大的过渡区域来进行混合才行。

在先前的自适应Shadow Bias文章中,我们已经计算好了每个级别的_CascadeShadowBiasScale,这个BiasScale已经同时考虑了ShadowMap分辨率和PCF核大小。因此我们可以直接拿这个参数来作为衡量前后两级CSM的差异的标准。使用的代码如下:

float deltaBiasScale = 1 + abs(_CascadeShadowBiasScale[i + 1] - _CascadeShadowBiasScale[i]);
blendDist *= deltaBiasScale;

于是远处CSM的过渡区域又放大了不少,阴影过渡混合效果也更好了。

2.3 优化过渡区域

To be Writen ...