原理篇:
常见的渲染方程如下:
��(�,��)= ��(�,��) +∫Ω��(�,��)�(�,��,��)(�,��)���
在不考虑自发光项与考虑阴影对于着色结果的影响之后可以将方程变化为如下形式:
在这其中添加的 �(�,��) 可以简单理解为:当计算来自 �� 方向光源对着色的影响的时候,需要反向射出一条可以射线,如果射线在到达光源前击中了其他物体时,就认为这条来自光源的光线对着色点没有贡献。
��(�,��)= ∫Ω��(�,��)�(�,��,��)(�,��)�(�,��)���
利用上述渲染方程进行正确的着色和阴影计算是非常耗时的因为它需要进行积分,因此在光栅化的实时渲染中需要对该方程进行简化:
(微积分的近似公式)
Image
��(�,��)= (∫Ω�(�,��)���/∫Ω���)∫Ω��(�,��)�(�,��,��)(�,��)���
如此就可以将 �(�,��) 从渲染方程中抽离出来。此处关于推导的解释可以看Games202第三节课。
而通过该方程可以将着色和阴影分开,也就是说,可以在先计算着色,在此基础上再乘上阴影计算的结果,即可近似正确的表示阴影。
基础篇:
1. Shadow Mapping
渲染阴影场景涉及两个主要的绘图步骤。第一个生成阴影贴图本身,第二个将其应用于场景。根据实施(和灯的数量),这可能需要两次或更多次绘图过程(多次绘图指的是场景中存在多个光源)。
shadow mapping 的基础原理:
总结:Shadow Mapping的原理是首先将场景从光源的视角进行深度图的渲染,然后再从观察者的视角进行渲染,并使用深度比较来检测可见性并生成阴影。
Shadow Mapping的基本步骤如下:
- 从光源视角渲染深度图:先将相机移动到光源位置,以光源为视点向场景中渲染深度图,即从灯光的角度生成一个视锥体,然后对视锥体内的场景使用透视投影矩阵进行投影。
- 生成阴影贴图:将深度信息保存,并将结果存入一个纹理,作为阴影贴图
Image
- 从相机视角渲染场景:现将相机移回到视点位置,在进行场景渲染前需要将阴影贴图传递给着色器
Image
- 计算阴影:在片元着色器中,使用深度测试函数来检查当前像素是否被光源照射到。首先通过使用当前片元的深度值与阴影贴图中对应位置处的深度值进行比较,如果当前像素被遮挡则该像素被认为是在阴影中,否则它处在光照下。
示例:
下图是第一个渲染 pass 中渲染出的光源
Image
下图示从相机视角,第二个 pass 中渲染场景深度图
Image
由此可以进行下一步深度比较,计算阴影操作。
传统shadow mapping 的问题
1.精度问题
在shadow mapping 由光源点生成的一张深度图中,由于深度图的本质是一张纹理,而纹理是一种离散化的数字信号,从深度图还原源场景深度信息时难免对发生精度不同的问题。
Image
而由于精度原因容易产生自遮挡的问题。
自遮挡问题产生的原因是:
Image
解决方案:
Image
上面左图显示了这种自遮挡的现象大多出现在原本没有阴影的地方。这些地方本该直接被光源照的,但却出现了条纹状的阴影。其原因是光源是倾斜照射的,并且Shadow Map产生的深度图的分辨率有限。
解决方案是容忍一段区间的遮挡物,即将阴影图中的深度值偏移一段。这一段距离就称为bias,如下。
但这种情况的 bias过大变会造成上面右图的人物的模型和阴影没有连上的情况。
此外还有一种解决方案是Games202中讲解的学术界(工业界中不用)中用的一个解决方法:
记录最小深度和次小深度的平均,作为后续的深度进行比较。
Image
工业界不用的原因
- 要求物体双面渲染(这在游戏中完全不可能),有正面就得有反面,面片也得做成box
- 开销太大,可能并不值得,虽然O(n),但是GPU里面并行处理下会爆炸(不过实时渲染不注重复杂度只注重速度)
问题: 必须要是两面的
2.走样问题
Image
由于单一的shadowmap会存在精度不足,我们在性能与效果中平衡提出Cascade Shadowmap技术。
Cascade Shadow Mapping是一种用于实时渲染的实时阴影技术。通常在实时渲染中,光源(如太阳)的影响区域比较大,如果只使用单个深度图(Shadow Mapping)来计算阴影,很容易出现阴影质量不佳或者阴影断层的问题。而阴影CSM技术可以将一个视锥体分割成多个级别,每个级别使用不同的投影矩阵和深度贴图来计算阴影,从而提高阴影的质量和稳定性。
阴影CSM的基本原理如下:
将摄像机的视锥体划分为多个子视锥体:将摄像机的视锥体沿着近裁剪面到远裁剪面进行划分,将每个子视锥体映射到一个具有固定大小的2D矩形区域内。
对于每个子视锥体,渲染深度贴图:对于每个子视锥体,使用投射矩阵将场景渲染到一个深度贴图中,并保存深度值信息。
计算阴影:对于每个像素,使用与Shadow Mapping类似的算法来比较深度贴图中对应点的深度值和当前像素与光源的距离,来判断该像素是否被阴影所遮盖。
合并结果:将所有子视锥体的阴影贴图进行合并,并将阴影应用于场景中的对象。
中等篇
软阴影的形成
Image
umbra:
本影指的是阴影最里面的区域,它看起来是最暗的。换句话说,本影代表光完全被阻挡体阻挡的阴影部分。
Penumbra:
半影是一个 物体阴影的一部分,其中只有一部分光束被遮挡体阻挡。半影区就是我们在图形学中的软阴影部分
antumbra:
是指遮挡物体似乎完全位于光源的中心区域内。 (这在咱们的文章中不做讲解)
理想中的点光源会造成硬阴影,但是现实中的光源由于本身存在体积,会形成拥有半影区的软阴影。
硬阴影与软阴影之间的关系不是简单地将阴影的边缘模糊化处理,而是由于现实生活中的光源都存在一定的面积和体积,而这种面积和体积会产生软阴影。
更多自然世界中的阴影的知识可以从下面链接中了解。
什么是影子:13 个有趣的事实 -zh-cn.lambdageeks.com/how-is-shadow-formed/#um编辑
PCF的原理
Image
如图,上图为硬阴影,下图为软阴影。
PCF相对于传统的shadow mapping 做了那些改变::
之前的ShadowMapping过程中,假设现在得到了一张阴影图,接下来需要对一个着色点的深度 �(�) 和阴影图的采样结果 �(�) 作比较,得到一个二元的结果即为在阴影中为1,不在阴影中为0。正因为这种二元性,才产生了硬阴影的没有过渡(或者说走样现象)。
PCF的做法是得到阴影图后,如下图所示,选择一个以该着色点映射在阴影图中的位置为中心的 n x n 的filtering(核),用这个着色点的深度值 �(�) 分别和filtering中每个深度值的采样结果 �(��) 进行比较,最后得到一个 n x n 的二元比较结果,再对这个 n x n 的二元的比较结果进行filter(平均),最终得到这个着色点的阴影可见性的值,而这个结果不再是非0即1的值,而是一个在(0,1)之间的浮点数。
总结:
PCF技术通过对阴影贴图进行采样的方式,对像素周围的多个采样点进行插值和混合,从而更加准确地确定每个像素点的阴影强度。具体而言,PCF方法将每个像素的阴影采样点划分为一个网格区域,并在每个区域内进行多个采样。通过对这些采样点的深度值进行比较和插值,可以得到一个更加平滑和准确的阴影效果。
这项技术的重点在如你去如何理解Filtering(本质上是平均)的意义。在这个过程中,Filtering的尺寸决定了阴影的软硬程度。Filtering的尺寸越大,得到的阴影越软,尺寸越小,得到的阴影越硬,如下图可见。
Image
经过PCF之后就可以得到一个相对真实的阴影结果。基本上不会存在错误!
Image
PCF的缺点与改进措施概述
在实时的渲染中PCF的致命缺点是慢,原本的在shadow map贴图中的一次查找变成了 7乘7 甚至是 9乘9 的像素。
所以,如果想在实时渲染中使用这种方式的软阴影技术需要对渲染过程进行优化:
在抗锯齿的是时候的第一个算法一定是SSAA,但是这样抗锯齿处理算法需要4倍的显存...,但是MSAA其举出思想依旧是一个像素点采样多次,但从原来的全部整个屏幕全部的像素,变成了屏幕画面的边缘部分。
PCF的优化也是同理-如下图,软阴影与硬阴影的之间的变化随着阴影的投射物(笔杆)与阴影的接受物(书本)的距离有关
Image
(由此我们就可以引出PCSS,即在阴影生成过程中 blocker distance <-> Filter size呈现一定的关系)
PCSS的原理
PCSS 是非常经典的一个制作软阴影的算法
PCSS的核心原理是根据光源大小和着色点与遮挡物的距离自适应调节PCF的滤波核大小。阴影边缘的滤波核大小由半影距离决定,如下图所示。 通过相似三角形原理可知, 半影距离由光源的尺寸 、光源与遮挡物的距离 、以及着色平面与遮挡物的距离决定。
下图用来详细说明对应的数学公式来源,通过相似可以得到 ��������� 而我们通过 ��������� 的大小就可以对应出filtering 的大小。 ��������� 越大 filtering 就越大。
Image
PCSS首先假定光源是一个区域光,传统的点光源,聚光灯和平行光,都对应着某种模拟但从广义上来讲他们依旧是属于面光源。
����������=(��������−��������)∗����ℎ�/��������
那么根据上面的操作我们可以总结PCSS生成软阴影的步骤:
● 步骤1:Blocker Search 某个着色点连向光源,找到shadow map上该像素周围一块区域的纹素所记录的深度值,把区域所有texel都找一遍,判断是不是遮挡物,如果是遮挡物,则累加,最后除以遮挡物的个数,以这个平均值作为遮挡物的深度即上面的 �������� 。
● 步骤2:Penumbra estimation 用$d_{Blocker}$计算得到 ���������� ,从而计算得到filtering尺寸
● 步骤3:PCF 利用filtering尺寸,进行可见性值的计算。
注意在可见性计算部分可以,最简单的可以从周围的取若干像素信息然后平均混合;也可以根据一定比例插值;比如按照泊松分布来进行采样。
PCSS算法的优化
在PCSS算法中步骤1与步骤3比较慢。
step1 寻找 blocker的效率低下 step3 在 filtering 进行采样依旧是很耗费性能。
优化方法1:稀疏采样 缺点:由于稀疏草药会出现噪点,需要在最后在图像空间上做一次滤波。稀疏采样计时滤波之后会产生抖动。
优化方法2:对于PCSS计算的过程中做一系列的近似操作,这就引出了VSSM (Variance Soft Shadow Mapping)
VSSM的原理
VSSM的核心思想是快速寻找 blocker and filtering ,他的快速寻找是通过一系类近似操作降低时间复杂度。
可见性值的优化处理: 在刚刚步骤3的核心是找到着色点的可见性值,而可见性值是通过,p的深度值 �(�) 在阴影图中的filtring �� 中排第几。换句话说,需要得到 �(�) 占 �� 的百分比。
近似处1
我们将深度值,近似的设想为正态分布,正态分布图像由期望和方差决定(也就是说我们只要知道Filtering中深度值的期望与方差就可以大致知道深度分布的一个情况)。
剩下的全部问题在于期望(均值)和方差的获取。
- 期望通过 SAT和mipmap。
- 方差,数学公式计算 �(�)=�(�2)−�2(�) :
一块区域的平均值(方差)的计算:
使用MipMap
优点:
- 快速、近似、正方形
缺点:
- 插值结果只是近似。当查询区域在某层上不太对齐像素格的时候,需要双线性插值。
- 当查询的范围不为2的n次方时,还要再进行一次层间插值,即三线性插值
- 如果查询区域是长方形区域查询,还得加入各向异性过滤
使用SAT (类似算法中的前缀和) Summed Area Table可以用来高效地计算图像上任意矩形区域内所有像素值的和,而不需要遍历该区域内的每个像素。这对于一些需要频繁计算图像区域总和的算法非常有用,如图像滤波、特征检测等。
具体来说,Summed Area Table是通过对原始图像进行一次积分得到的。对于给定的像素坐标(x, y),Summed Area Table中该位置的值表示了原始图像中从(0, 0)到(x, y)的矩形区域内所有像素值的累积和。换句话说,Summed Area Table中的每个元素表示了其左上角矩形区域内所有像素值的累积和。
特点:
- 百分百准确范围查询结果,但是计算花销较大
得到 E(X) 和 D(X) 之后我们就有了正态分布图像,接下来就需要计算可见性。
可见性值的计算
我们需要通过正态分布,确认百分比数值(可见性值)。
在概率论中,PDF(概率密度函数)为连续型随机变量的概率密度函数,CDF(累积分布函数)为概率密度函数的积分。
也就是说,对于一个值x,只需要求出CDF(x),就可以得到百分之多少的值是小于x的,即1 - 可见性的值。
Image
CDF一般比较难计算,VSSSM又找到一个不等式对它进行近似,即切比雪夫不等式:
�(�>�)≤σ2/(σ2+(�−μ)2)
所以有:
���=1−�(�>�)≈1−�2�2+(�−�)2
同时切比雪夫不等式有一个苛刻的假设:t必须在均值的右边。
(当笔者做到这里的时候有一个疑问:把阴影区间的分布的CDF(x)为什么不将正态分布图像转变为标准正态分布后查表得出呢QAQ)
VSSM加速PCF步骤的总结:
- 生成shadow map的同时,生成一张存放深度的平方的平方深度图(Square depth map)。两个通道分别存放即可,不需要额外一张texture,
- 求深度图上某区域的均值,MipMap或者SAT,O(1)
- 求平方深度图上某区域的均值,依旧MipMap或者SAT,O(1)
- 知道均值,根据公式得到方差
根据切比雪夫不等式直接求出该点可见性Visibility
VSSM 的问题
VSSM 做了很多的假设,所以会存在种种的问题。
比如漏光(Light leaking)
当物体的深度不是呈现正态分布的时候会出现的问题。这是一个很明显的错误,(为了解决该问题可以使用Moment shadow mapping)
Image
虽然VSSM很快,但是准确度有些堪忧。
高级篇
距离场软阴影
距离场,它反映了任意一个点到某个物体的最小距离。将它可视化后如下:
Image
优点:
- 快速(查询快)
- 质量高
缺点:
- 需要预处理(慢)
- 需要大量存储空间
ray-SDF intersection
在ray marching的过程中,是解求光线打到物体上的点。
SDF(p) 是点 p 到达最近表面的距离,在ray marching过程很重要的概念是步长,每一次射线前进的距离就是步长也就是SDF(p)。正因如此,就可以认为SDF(p)为点p的“安全距离":从点p出发,按任意方向走SDF(p)距离,都不会碰到物体。 有了“安全距离”的概念后,选定一个起点 �1 后和方向后,按方向走 ���(�1) 距离到 �2 ,再按方向走 ���(�2) 距离,递归直到打到物体。(我们认为当直线与最近平面的距离小于某一个值的时候就是大中了物体)
Image
Distance Field Soft Shadow
SDF还可以应用在软阴影中。在软阴影中,我们用SDF来近似得到一个着色点被遮挡物遮挡的程度。 我们引入"安全角度“的概念:将光源抽象成面光源,从着色点 P沿某方向 ���� 看向光源,将光线从 ���� 朝物体方向旋转最大角度 � ,光线不会打到物体,则 � 为”安全角度“。
那么 � 的计算如下:
对于光线上任意一点Q,它的”安全距离“为SDF(Q),则”安全角度“为光线方向和着色点到圆切线的角度。 于是,安全角度的大小可以反映阴影的软硬程度(Visibility)。安全角度越小,遮挡越多,越趋近于硬阴影。安全角度越大,遮挡越少,越趋近于软阴影。
Image
如上图所示,安全角度的求解方法
问题又变成了如何求 � ?一种思路是求反三角函数,
�=���������(�)|�→−�→|
用简单的乘法运算去近似该公式
�=min�⋅���(�)|�→−�→|,1.0 。 这个近似式的k项,它决定阴影的软硬程度。当k小时, ���(�)|�→−�→| 需要更大的值(更少区间)才能>=1,最终映射到1。当k大时, ���(�)|�→−�→| 只需要较小的值(更多区间)就能>=1,最终映射到1。也就是说,k越大反映阴影更硬。
Image
参考:
- Real-Time Rendering 4th Edition-2018-英文版
- http://www.ownself.org/blog/2010/percentage-closer-filtering.html
- games202
- https://www.yuque.com/gaoshanliushui-mbfny/sst4c5/sl2q1b#49a7623c