Shadow Mapping
ShadowMap的原理
假设有一个模拟太阳位置的摄像机,在可观察的视线范围内,物体表面各个点到摄像机的距离可以组合成一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置。这张深度图就叫做ShadowMap。
深度图是通过模拟太阳位置的摄像机获取到的,并非真正渲染场景的视角,真正渲染场景的视角,是B点所在位置的相机。
假设B观察到的三个点p1、p2、p3,在渲染的过程中,首先会把这三个点的顶点位置变换到光源空间下,这样就能得到它们在光源空间中的三维位置信息:sun(p1)、sun(p2)、sun(p3)。
使用xy分量对ShadowMap进行纹理采样,就能获得它们的深度信息sun-depth(p1)、sun-depth(p2)、sun-depth(p3)
B视角下这三个点的深度信息可以通过z分量获得,它们的深度信息是B-depth(p1)、B-depth(p2)、 B-depth(p3)
通过同一位置在不同视角下世界空间的深度值对比,就能判断出该点是否处于阴影中。如果阴影深度值小于顶点深度值,那么这个点就处在阴影中。否则这个点就被光源照亮。
Shadow的优劣
ShadowMapping的核心在于两次Pass,第一次Pass会得到这么一张名为ShadowMap的Texture,第二次Pass则是利用这张Texture来进行渲染。
其好处在于一旦拿到ShadowMap之后,就可以利用这张Texture来渲染纹理,而不需要实际的场景。
其坏处有两个,一个是自遮挡现象,另一个问题是走样问题。
自遮挡问题
自遮挡问题产生的原因

自遮挡问题最后的效果就是如左图所示,地面上的纹路并非摩尔纹,而是由于ShadowMap这个Texture产生的自遮挡问题。
如右图所示,当从灯光角度生成ShadowMap纹理图时,可以注意到这张图本身依旧是拥有自己的像素精度的,假设地板上的红色部分为每块像素,会发现其朝向这个点光源,越往后,像素之间的实际距离会越大(这里图上没有表现出来),可见这个ShadowMap图是不连续的。
当眼睛朝向某个点去看时,比如最右侧的红色部分,可以注意到如果此时要渲染此处的阴影,会发现这里实际上对应的ShadowMap图是橙色部分,前面的像素会成为这个阴影实际渲染的方式,也就是被遮挡住了,这就是所谓自遮挡。
继续推到,会发现当这个光源最垂直与地面时是最好的,越偏、越平行于地面,这个自遮挡问题就会越严重。
自遮挡问题的解决方案

一种简单的思路是给一个长度,比如途中黄色部分,假设如果你的ShadowMap上面标识的像素到摄像机观察的点之间的距离小于这个黄色,就视作为没有被遮挡。从而直接渲染,但是后果就是如左图所示,虽然环境不再会出现奇怪的纹路,但是角色身上会出现一些地方的阴影被渲染失败。
目前工业界没有较好的解决方案,基本都是会选择找一个比较好的参数来控制这个效果。
学术界有一个效果还不错的解决方案,名为Second-depth shadow mapping,意思是会渲染出一个深度和一个次深度,而计算的时候取这两个深度的中间值。
比如我们认准某个角色的脚,frontfaces就是鞋面,seconddepth就是鞋底,在我们渲染鞋底某个地方时,因为取的是中间位置,所以不会因为鞋面和鞋底导致这里不能被正常渲染。
该方法并不流行,也不会有人这么做。
在实时渲染领域,绝对的速度才是第一优先级,无人在意复杂度。
走样问题
走样问题产生的原因

ShadowMap本身依旧是一张Texture,有自己的精度,所以会出现这样的情况。
渲染背后的数学原理
用于近似的公式
微积分中有着大量的不等式,但是在实际渲染中一般都会在乎其相等的这一部分,常用的一个等式如下:
$$\int_{\Omega} f(x)g(x) , \mathrm{d}x \approx \frac{\int_{\Omega} f(x) , \mathrm{d}x}{\int_{\Omega} \mathrm{d}x} \cdot \int_{\Omega} g(x) , \mathrm{d}x$$
把两个乘积的积分拆成了积分的乘积,实际上这是一个错的,不能这么做,但是我们只是近似将其看作可以这么去做这件事,有以下两种情况可以当作是准确的:
1.当$g$的support非常小的时候,也就是当你实际的积分范围挺小的时候,这个积分就是一个近似是准确的。(函数的支撑集(support) 指的是「函数值非零的定义域范围」)
2.当$g$这个函数足够光滑的时候是可以的,也就是$g$在他自己的积分域的变化不要太大。
这是实时渲染的渲染方程:
$$L_o(\mathrm{p}, \omega_o) = \int_{\Omega^+} L_i(\mathrm{p}, \omega_i) f_r(\mathrm{p}, \omega_i, \omega_o) \cos \theta_i V(\mathrm{p}, \omega_i) , \mathrm{d}\omega_i$$
使用近似公式后:
$$L_o(\mathrm{p}, \omega_o) \approx \frac{\int_{\Omega^+} V(\mathrm{p}, \omega_i) , \mathrm{d}\omega_i}{\int_{\Omega^+} \mathrm{d}\omega_i} \cdot \int_{\Omega^+} L_i(\mathrm{p}, \omega_i) f_r(\mathrm{p}, \omega_i, \omega_o) \cos \theta_i , \mathrm{d}\omega_i$$
此时,前部分为Visibility,后部分为Shading。这一套公式其实是正好符合我们的ShadowMap的,前部分Visibility为阴影的渲染,后半部分则为正常的着色。
结合我们所说的两种可以将其看作为准确的情况,会发现只有当是点光源和方向光源的时候是可以这样做的,这就是ShadowMap可以作为阴影的数学基础。
除了这种情况,在面光源某些特殊情景也是可以如此进行拆分。
PCF-PCSS软阴影
早期PCF是用来做抗锯齿的,后来才发现可以被用来做软阴影,就有了PCSS。
PCF
PCF先对阴影贴图邻域内的每一个采样点,单独做深度比较,得到 0/1 的二值可见性;再对这些可见性结果做加权平均,得到最终的平均可见性V(x)。
我们可以定义一个 3x3 的核,核的中心对准实际要计算阴影的片元,核的其它区域对准其周围的片元。然后为核上的 9 个区域标记上权重,并且这些权重的和为 1,比如这里可以使用最简单的权重值 1/9。接着对核上的 9 个 区域进行单独的阴影计算——在阴影中计为 0,不在则记为 1。最后,让每个结果乘上其对应的权重,即可得到一个 [0, 1] 范围内的小数,我们可以把这个数当作这一点的可见性。
$$\begin{bmatrix} 1 & 0 & 0 \ 1 & 1 & 0 \ 1 & 1 & 1 \end{bmatrix}$$
$$visibility = (1 + 0 + 0 + 1 + 1 + 0 + 1 + 1 + 1) \times \frac{1}{9} = \frac{2}{3}$$
在为片元着色的时候,就可以用这个数值来进行插值,而不是原本的非 0 即 1。
可以看出,这其实就是一个卷积核形式的模糊,不过这里模糊的是目标片元与其周片元的阴影比较结果,这样,处于阴影中心的片元,得到的结果就会更趋近于 1,处于阴影边缘的片元,得到的结果就会更趋近于 0,这样就实现了阴影边缘的渐变模糊效果。
PCF 进行的是 ShadowMap 的多次采样后比较结果的 Filter,而不是对 ShadowMap 本身进行 Filter。因为使用 ShadowMap 的时候,最终是比较出了一个 Bool 值(大于或小于),所以即使对 ShadowMap 进行了 Filter ,计算阴影边缘时,还是边界分明的一边大于一边小于,无法达成渐变过渡的模糊效果。
PCSS

对于图中的钢笔,我们会注意到,这个钢笔距离纸面越近,阴影越硬,距离纸面越远,阴影越软。PCF 计算出来的阴影边缘是非常平均的,做不了这件事情。
这种效果只需要在 PCF 上稍加修改即可实现。已知在 PCF 中,采样核(范围)越大,产生的阴影边缘就越模糊,那么我们在使用 PCF 计算的时候,让遮挡物距离阴影接收物近的部分,采样核减小一点,距离远的部分则扩大一点,这样就近似实现了这种效果。这也就是PCSS的思路。
我们定义一个blocker distance,也就是遮挡物和投射阴影的物品的距离。

从相似三角形中提炼的公式:
$$w_{Penumbra} = (d_{Receiver} - d_{Blocker}) \cdot w_{Light} / d_{Blocker}$$
PCSS的算法步骤如下:
区块搜寻(Blocker search):以着色点片元为中心,范围搜索,得到这个范围内遮挡物的平均深度$d_{Blocker}$。。
半影估计(Penumbra estimation):根据平均深度,根据上述公式估计计算$w_{Penumbra}$。
滤波(Filtering):根据步骤2计算出来的$w_{Penumbra}$,确定 PCF 的卷积核大小,然后使用传统的PCF计算阴影。
PASS慢的原因:第一部和第三部几乎都要对所有的Texture进行一个检索,所以速度慢
VSSM(Variance Soft Shadow Mapping)
原本的第三步相当于找出哪些点是需要被计算为阴影的,本质其实就是在一个数值下寻找那些是符合要求的,而这样的分布是可以看作为正态分布的,因此VSSM就是做了这么一件事情。
对于正态分布,我们所要知道的其实就是均值和方差。
均值可以使用MIPMAPing和SAT方法。
方差则是使用一个经典公式:$\mathrm{Var}(X) = E(X^2) - E^2(X)$,对于后者$E^2(X)$,我们使用一开始的ShadowMap即可,而前者$E(X^2)$我们实际上,只需要一个新的ShadowMap,记录的是深度的平方即可。
而最后我们所需要的其实就是这个曲线下的面积(这个面积可以直接查表拿到):
切比雪夫不等式:
$$P(x > t) \leq \frac{\sigma^2}{\sigma^2 + (t - \mu)^2}$$
该不等式可以在不知道具体分布的情况,利用均值和方差计算落在某一部分内的概率(这里指的红色,上界)。

VSSM解决了第三个问题,那么我们回过头去看第一个问题。

$$\frac{N_1}{N} z_{unocc} + \frac{N_2}{N} z_{occ} = z_{Avg}$$
根据分布的近似,可以知道遮挡和没遮挡的比例,现在我们知道整个的平均深度,知道两部分的比例,这里大胆的假设没遮挡的深度都是和该点的深度一样,比如上图的深度是7,那些8899全都看作7。这样就能计算得到$z_{ooc}$,也就是遮挡物的平均深度。
MIPMAP and Summed-Area Variance Shadow Maps
没有什么特别能讲的,MIPMAP和SAT都很简单,搜一下就可以了喵。
VSSM的一些问题
由于在使用VSSM的时候用了大量的假设,其中一个假设就是将其分布看作为正态分布。但是如果阴影之间没那么连续,比如一个镂空比较多的物体作为参考物,此时就根本不能看作正态分布了。


