Skip to content

CascadeShadowMappingAdvanced

zilch edited this page Jun 8, 2021 · 2 revisions

CSM进阶之路

近日看Cascade Shadow Mapping的一些文章(以下简称CSM),总结一下于此。

CSM的思想是对摄像机视锥(Camera Frustram)进行分割,并对分割后的每个子视锥区域( Camera Subfrustum)进行单独ShadowCaster操作生成ShadowMap贴图,这样就能使用不同精度的阴影贴图来描述不同的区域。但是在具体到技术实现细节时,会有多种不同的方案。例如摄像机视锥区域如何分割?如何计算光视角的正交投影矩阵?

一个完成的CSM流程会包含如下几个步骤:

  1. 摄像机视锥分割
  2. 子视锥包围盒计算
  3. 光源投影矩阵计算
  4. ShadowMap贴图渲染
  5. ShadowReceive计算

本文着重讨论以下几个点:

  • 摄像机视锥的分割算法
  • 若干种包围盒算法
  • 若干种Cascade Selection算法

1. 视锥分割

CSM的第一步是将视锥由近到远分割成若干个子视锥。

那么具体分几块,每一块的比例又是多少呢?分几块暂且不提,先说说分割比例。

我们期望的目标是让每一级的CSM阴影分辨率在投影到摄像机屏幕空间时,有相似的分辨率。这也是CSM的终极目标,即解决Shadow Perspective Alias的问题。阴影在摄像机屏幕空间的分辨率可以由以下的数学模型定义:

其中dp摄像机近平面的一小段距离,ds是shadowMap平面的小段距离。利用简单的几何知识,不难得出以下公式:

$$ \frac{dp}{ds}=n*\frac{dz}{z*ds}\frac{\cos(\varphi)}{\cos(\vartheta)} $$

  • n为摄像机近平面距离
  • $\varphi$和$\vartheta$为平面法线的角度参数

我们的目标是让dp/ds为常数,与距离z无关。同时我们可以不考虑平面法线因素(因为此处只是为了解决Perspective Alias,与平面法线无关),于是可以将 $\frac{\cos(\varphi)}{\cos(\vartheta)}$也视作常数项。便有:

$$ \frac{dz}{z*ds} = k $$

k为常数。

对以上公式积分,可求得 $k = \ln(f/n)$,详细的积分过程可以参考文献[5] 。

因此有:

$$ \frac{ds}{dz} = \frac{1}{k*z} $$

对上式积分,即有:

$$ s = \int_0^sds=\int_n^z \frac{dz}{k*z}=\frac{\ln(z/n)}{\ln(f/n)} $$

最终推出z与s的关系式:

$$ z = n * (\frac{f}{n})^s $$

通常来说,当我们执行CSM分割的时候,s是等比分割的(即N张ShadowMap的贴图大小一致),基于此,我们可以利用以上的公式来计算z向的分割比例如下:

假如我们将视锥分成N份(通常为1-4,也有推崇大力出奇迹理念给到8的,比如原神),那么理想的分割距离$z_i=n(f/n)^{i/N},i=1,..,N$*

以上是一种理论上的理想分割,在场景物体从近到远分布均匀的情况下才有比较好的表现。但在实际应用中,是很少出现以上理想情况的。

因为使用上述公式计算出来的一级CSM通常距离很小,在多数情况下都是比较空旷的,这就造成了大量的浪费. (例如far=1000,near=1,N=4的情况下,一级CSM距离为5.6m)

因此在实际应用中,通常会与线性分割进行一个结合,公式如下:

$z_i=lerp(n*(f/n)^{i/N} , n + \frac{i * (f-n)}{N},\lambda),i=1,..,N$

$\lambda$是一个[0,1]区间的控制参数,用以在对数和线性之间进行插值。

2. 包围盒计算

在完成了视锥分割之后,接下来要对每个子视锥进行包围盒计算。包围盒通常来用确认光锥的上下左边4个面。接下来要介绍几种不同的包围盒算法,并分析其优劣。

2.1 最紧致的包围盒算法

这是最容易想当然的一种包围盒。具体实现如下:

红色区域为待计算的子视锥区域。

  • 对平行光,以任意一个点作为原点,构建LightViewSpace。
  • 将子视锥的8个顶点投影到LightViewSpace
  • 通过xMax,xMin,yMax,yMin,zMin,zMax确定包围盒的上下左右前后6个面。

这种包围盒的优点就是对shadow map texture的利用率高。但缺点也非常明显。

考察入射光方向与摄像机朝向变化时,包围盒的变化情况:

当光与摄像机朝向垂直时,包围盒近平面尺寸较小。当光与摄像机平行时,包围盒近平面尺寸达到最大。这意味着当我们转动摄像机时,包围盒的近平面尺寸会不停的变化,这会引起场景ShadowMap分辨率的变化,从而产生一种称为阴影边缘闪烁的瑕疵(shimmering edge effect or shadow flickering)。详情可以参考[6]的moving-the-light-in-texel-sized-increments章节。这其实不是CSM带来的锅,而是ShadowMap技术本身就存在的锅。

Youtube上有效果视频->戳: GDC'09 Frostbite Shadow Stabilization Off

为了解决这个问题。人们自然考虑让光源近平面保持一个固定的大小。这就引入了Fixed Size Box Bounding.

2.2 最大方形包围盒

该算法将计算出一个最大尺寸的方形包围盒,以覆盖所有入射光方向情形。 易知,包围盒的最大尺寸将在NM与NF两者中产生(请自行扩展到3D)。 算得最大尺寸后,我们只需要在2.1的基础上,将上下左右4个面同时往外偏移达到最大尺寸即可。

这种方式可以解决shimmering edge effect。但衍生出了如下两个问题:

  • 带来了一定的ShadowMapTexture浪费。
  • 第一节中给出的分割比例算法失效

因为第一节中的所有计算,都是基于"子视锥占满ShadowMapTexture"这个假设而进行的。如果我们采用了更宽松的包围盒,那么子视锥投影到ShadowMapTexture时,将不再占满。因而对s的积分,将不能在0-1上进行。此种情况下,对s的积分将稍微复杂一些,本文将不做推导了,有兴趣的同学可以自己推推看。

2.3 最大球形包围盒

另一种方法是构造子视锥的外切球作为包围盒,如下图所示:

然后以球的上下左右4个切面作为光源正交视角的上下左右4面。数学上容易证明,通过最大球形包围盒来构造的光源正交矩阵,其Viewport要比2.2中的更大。

那么这种浪费带来的好处是什么呢? 这就体现在Cascade Selection阶段了。详细的放到后面再说。

2.4 紧凑球形包围盒

这是我在测试Unity CSM包围盒的时候发现的。 但我还没有找到相关资料,也暂不清楚详细的算法,此处我给一下简单的示意

如图所示,通过这种算法生成的球形包围盒,并不会覆盖住整个子视锥,靠近远平面两个角落的区域将位于球形包围盒之外,并落入到下一级的包围盒中。

[1]中博客作者也采用了他自己发明的一种紧凑型球形包围盒,原文如下:

Another method I am using is a variation of method 2 to calculate tighter bounds. I using a sphere with the diameter of size equal to the splitting distance for each cascade and calculate a light space bounding box around that. So if splitting distance is d, I am using d/sqrtf(2.0f) as sphere radius. sphere center is preCascadeOffset + d/2.0f. With this scheme, I have to use map selection mode at rendering time since shadow map for Cascade 1 doesn’t necessarily covers all the pixels for cascade1 but those pixels will be present in cascade 2 for sure. Result is a much tighter bound and hence much more resolution for nearby objects where it’s needed. (There’s a small bug in my current implementation where some area near border b/w cascade 1 and 2 appears black when eye camera is in the same direction as light direction. I am still debugging it and will post the results later.)

但在我看来,他的这种方式是有问题的,里面加粗的那段我认为他太想当然了,按照他的算法cascade2是无法在所有情况下都cover住cascade1漏下的那些像素的。因此后面出现a small bug也就不奇怪了。

3. Cascade Selection

Cascade Selection是在ShadowReceive阶段,针对像素判定其应该属于哪一级的CSM的过程。通常有以下三种:

  • View Distance Selection
  • Bounding Sphere Selection
  • Map Selection

其各有适用场景,下面依次说明。

3.1 View Distance Selection

该算法是计算像素世界坐标在Camera forward向量上的投影距离和Cascade分割距离做对比,来判定落入哪一个子视锥。

很显然,想要使用View Distance Selection,包围盒必须完全覆盖子视锥。像2.4那种紧凑的球形包围盒就不行。

3.2 Bounding Sphere Selection

该算法只适用于球形包围盒。通过计算像素世界坐标到包围球球心的距离,与半径做比较来判定是否位于包围球内,这也是2.3相较于2.2的优势之处。因为通过这种方式,可以让每一级的Cascade Shadow Texture利用率更高,如下图所示:

3.3 Map Selection

该算法的思想是直接像素投影到Cascade Shadow Texture Space。如果uv在0-1内,则命中。

该算法拥有最高的Shadow Texture利用率,但是也最耗计算。因为像素需要与每个Cascade投影矩阵相乘。

4. 更多

  • 以上没有讨论光源正交投影的近平面和远平面计算,这应该是可以直接沿用传统ShadowMap相关算法的,与Cascade关系不大。
  • 在[2]中还提到了Single Pass生成多张Cascade Shadow Map的技术,貌似使用了DX10的什么API,没细看,感觉不错
  • Google了一下,发现NVIDIA的Maxwell architecture也提供了类似的技术[6]

5. 参考

[1] chetanjags, Real-Time shadows : Cascaded Shadow Maps, 2015

[2] Johan Andersson, Single-pass Stable Cascaded Bounding Box Shadow Maps and Geometry-shader based Decal Generation, GDC 2009

[3] Microsoft, cascaded-shadow-maps

[4] NVIDIA Corporation, Cascaded Shadow Maps

[5] Fan Zhang Hanqiu Sun Leilei Xu Lee Kit Lun, Parallel-Split Shadow Maps for Large-scale Virtual Environments, 2006

[6]https://docs.nvidia.com/gameworks/content/gameworkslibrary/graphicssamples/opengl_samples/cascadedshadowmapping.htm