本笔记的核心记录实现方式为基于专题进行分区和记录,可能会有不定期更新,请跟随更新。
硬阴影:shadow mapping
核心思路
从光源的视角先渲染一张“深度图”(shadow map);【Pass1:记录被光线照亮的最末z值】
然后从相机视角正常渲染,每个片元把自己投影到光源空间去查这张深度图;
如果该片元的深度值 大于 shadow map 中记录的值,就说明它被别的物体挡住了 → 标记为阴影(硬阴影)。
实现逻辑
务必注意这里每一步所采取的空间是哪一个空间,这对于理解尤为关键。
-
光源空间深度图
• 建立一个以光源为原点的投影矩阵(正交或透视,取决于光源类型)。
• 把场景以“光源就是相机”的方式光栅化一遍,只记录每个像素的 z(或光源空间深度)到一张浮点纹理。
• 这一步没有颜色计算,纯粹是“从光源看过去,最近的物体有多深”。 -
正常渲染(相机视角)
• 每个片元拿到自己在世界空间的位置 P。
• 把 P 变换到光源的投影空间得到坐标 (u_light, v_light, z_light)。
• 用 (u_light, v_light) 去采样 shadow map,得到光源空间最近深度 d_map。
• 比较:如果 z_light > d_map + ε(ε 是避免 z-fighting 的小偏移),说明光源看不到它 → 该片元在阴影里;否则在光照下。
• 最终光照计算时直接乘以 0(或一个可调节的“阴影强度”)即可得到硬阴影。 -
特点与局限
• 只考虑 直接遮挡,不处理二次反射、间接光、软阴影(PCF/PCSS 只是做了滤波,本质仍是硬阴影采样)。
• 分辨率有限 → 出现阴影锯齿(shadow acne、perspective aliasing)。
• 需要处理“阴影粉刺”(shadow acne)和“彼得潘”现象(peter-panning)。
• 无法处理半透明投射的彩色阴影,也无法处理焦散(caustics)等复杂光效。
Strength
- 在图像空间解决
- 在确定且固定光源参数的情况下,无需对物体进行实时的光照计算,你只需第一遍Pass运行后保存对应的ShadowMap纹理
Weakness
- 由于在光栅化下输出纹理采样,会存在一定程度的走样现象。(放大的越大影响越大)
- 可能产生自遮蔽效应(一种错误的环境遮蔽效果)//数值方向的自阴影问题 ——可能会产生Shadow Bias(错误的阴影曲线)
自遮挡问题
:假设每个纹理点上都有一个深度(本质上的连续的位置产生了离散效果)也就是记录的深度不连续,因而致使光线求交后同一个像素表示的阴影区域大于理想上的实际区域,从此导致了阴影的严重走样,如例图:(本人灵魂手绘)
本质上来讲,是一种采样导致的失真。

- 对于使用了Alpha Blend的内容有很大影响
- 深度距离是基于光源方向的,不一定是点光源,此为示例。
- 最好情况:垂直照射(影响较小)、接近平行照射(光照接近无线远阴影)
- 本质上是相机视角下采样的像素和光照视角下采样的像素无法一一对应
一个修正的trick:bias
我们利用ShadowBias,由于我们知道不同角度下的偏移量大小,因此我们在实际采样计算的时候将偏离的对应部分基于角度余弦值做一个映射,对所有角度下施加不同的偏移值,以保证效果的相对完好。
Bias的问题:Peter Pan阴影悬浮
当你进行bias时会丢失一些原本可能会有阴影消失问题,就会有一些相应的边界阴影由于偏移值离开了和遮挡物的接触。
(其实目前的方法就是更多的优化bias,找一些参数用以特化优化这个问题,以Trick代Solve)
另一种修正的问题:双深度纹理(正面剔除)
采样最近和第二近距离的两个深度(计算两个深度的深度图)——利用两张深度图的中间坐标
- 可以通过深度图来计算阴影强度,变相提高同一纹理记录的考虑量。这样的话我们就抬高了厚度,这样就能保证边缘的地方阴影可以完成贴合
Extra Problem
相比传统方法多进行了一次深度纹理计算,提高了计算复杂时间,且需要记录最小和次小的两个depth buffer有很大的影响 实时渲染 从不相信 复杂度! 只相信冷冰冰的实际速度2333
数学原理
在RTR中,我们更希望将不等式近似为约等式子进行解决 当然,我们需要进行一个近似估计时候要考虑什么情形下才可以形成这一等式
一个重要的积分思想(奇技淫巧)
(在许多工业使用中起到不可替代的重要作用)
- 显然这是一个离谱的不合理公式(右边式子的分母但凡学过数分都觉得离谱),这个分母作为一个归一化常数,用于平均化能量,可以移动项(一个用来归一的常数用于平衡左右能量)
- 适用的场景:g(x)的积分范围较小、g(x)较为光滑(变化相对较小,相对低频)
在Shadow Mapping上的应用
回顾一下经典渲染方程(积分阴影):
我们将其简单记录为L f V三项:L为辐照强度、f为BRDF函数(描述表面反射入射光到出射角度的情况)、以及V函数(可见度函数) 我们得到的颜色值应该由视角逐光源根据可视情况以方向为被积值进行正球面积分,但是这样耦合过高,只能使用逐个计算后再积分的方法进行计算; 在RLR中,我们考虑渲染方法: 通过这个方式,我们把着色值域变为(可视程度·辐照函数/平衡因数)
可行解释
- 我们做积分的时候,点光源/方向光源只有在对应的光照角度、射线方向上才会进行这一积分,而这一积分我们恰恰满足了上述的积分函数,即积分域相对较小
- 后面的子项(G(x)积分的类似项)——在光源是正常面光源,且着色点考虑的是diffuse(漫反射表面)时,BRDF项就是一个相对的光滑函数。 (因此,对于漫反射的环境光,我们不推荐使用Shadow Casting的相应方法)
软阴影:PCSS(Percentage closer soft shadows)
Why Soft Shading:Difference
存在有阴影到无阴影区域的过渡处理——由于绝大多数光源实际上是面光源而非点光源,所以不会投射出棱角分明的阴影;区别图见下:

引入—— PCF方法:Percentage Closer Filtering
- 这种方法一开始是为了解决在Shadow Mapping中影阴影的走样表现的,通过滤波等方式来优化相应的表现。
- 不是后处理方案,即对于走样的ShadowMapping通过滤波方式解决问题。
- 没有锐化阴影边界的原因——走样不是因为像素的深度不平均导致的,因为走样的核心原因是像素值不连续以及像素的不一一对应,相当于滤波和没有滤波的表现力近似。
实现
- 当对Shadowmap取值采样时,我们不仅考虑映射过来的对应像素值,我们还考虑这一像素周围的8个像素点对应的深度和实际点的深度(Shadow里面)做一个比较,把做比较后的相应值进行比较再取平均得到这一点的Visibility的系数。(可以考虑进行加权)
- 这种实现并不是对Shadowmap进行的模糊操作,而是ShadingPoint实际参与计算的时候去判断期是不是真正做到了遮挡(把遮挡(1)、未遮挡(0)的二值化结果利用Visualbility项的系数变化为一个加权后判定是否完全遮挡的软阴影值减少锋利度)——这种方法既不是纹理滤波也不是后处理。(考虑光源和物体遮挡的一种逻辑),笔者认为这是一种吧点光源换成较小面光源计算的方法。
- 加权的参与范围会决定阴影的光滑程度以及涉及的程度——因此人们想到人们认为软阴影就是PCF进行夸张的的范围均值来实现一个类似软阴影的效果。根据光源的亮度/位置等更加智能的进行软阴影着色。
完整算法流程
- 遮挡关系:在Shadow Map附近取一定区域,获取附近的遮挡物,并加权估算出平均深度;
- 判定软阴影范围:根据平均的遮挡深度,估算出后续的滤波范围
- 在此进行缩小范围后的卷积相关功能。
Weakness
速度很慢(卷积开销),影响性能;在附近finding blocker以及获取像素上记录的深度值加权的时候会有较大的影响。
古尔丹,代价是什么?:范围越大,效果越好,速度越慢,反之会有场景噪声。
Pcss:Percentage Closer Soft Shadow
我们发现,阴影的软硬程度与遮挡物与阴影的距离有关。
阴影的FilterSize应该取决于Blocker Distance(相对平均的投射遮挡物深度)。
从这张图而言,我们根据相似三角形原理,我们移动Blocker的位置,w位置是软阴影的作用区域,明显的Blocker的移动会显著影响软阴影的范围。(光源为线、面均匀光源,图中为二维示例下影子投射的边界)其中w越大阴影越小,相关的计算公式是:
一个很显而易见的相似三角形公式。
- 对于一个Shading Point,我们的Blocker Depth并非什么时候都是浅显的,显而易见的。因而我们更需要进行一个平均值(判断一下大概的位置)。(ShadowMap上做平均)
- 其实对于面光源我们不可能生成ShadowMap,因此我们在实现软阴影的部分的时候,我们将光源移动到面光重心位置再控制这一范围进行等效。
- 卡bug:我们的Block选取多少参与考虑?(选多大区域?)——但这个范围实在Shadow Map上决定的。
- 方案1:约定一个固定的范围。
- 方案2:根据光线 Shadowmap Scene的距离比例以及Light的范围,我们根据这个比例就可以近似估计到会有多少BlockPoint参与Blocker Search,也就是边界连线的时候生成一个在ShadowMao上的交点实现的Shadow Map。
计算遮挡的时候忽略掉不可能遮挡的对应值,否则无法解决这个遮挡问题
问题:Pass的开销性能及其恐怖,走了好几遍开销,所以需要一些Trick优化其对应的复杂度。
Going-Deeper:VSM(Variance Shadow Mapping)
引入:观察PCF、PCSS原理
在PCF进行的卷积中,我们采用的公式是:
我们在pcss中与 使用二值判断来界定:
其中 表示一个阶梯函数,为 非0即1的一个判断值,利用记录深度和场景深度比较来判定阴影比较的结果,用于判断点是否可视,之后可以选择进行平均或者加权平均。 而PCF不是对阴影滤波后进行比较,也不是将输出图像进行滤波;公式上表示:
(并非对贴图进行滤波)
(并非对结果滤波) 下面的这两个办法的滤波都是基于图像的降噪处理,并非对采样进行处理。 PCSS是一个Cost-High的算法,因此不是一个很理想的工业实现方案,基于这一点,在工业界的解决方案是对depth等部分进行一个系数采样,随后在图像空间利用数字图像处理技术进行去噪,这里在光线追踪部分再加以阐释。(随机采样filker:每一帧的噪声随机都不一样,可能会出现场景抖动的问题。)
原理(VSSM)
- 用于优化PCSS的渲染效率问题——加速PCF这一步(step3)
- 在PCF步骤的加速:判断在区域内究竟有多少对应pixel的深度浅于原有深度。(percentage closer):有多少texels在给定区域的深度浅于给定值——抽象成分位数问题,也就是一个排序算法问题——如果我们有一个近似的频率直方图,我们就可以知道这个问题。
- 在论证中我们发现这一概率大概接近于正态分布——>因此我们只需要知道某个区域内的均值、方差就可以快速得到这一结论。这样我们就转化成了这个区域的均值+方差;
均值的解决方案
在均值方向我们就可以利用——MIPMAP进行近似估计实现(各向同性警告)。 (MipMap在未确定的平面会有影响:且非正方形区域会有一定影响,利用各向异性过滤等可能会有一定的优化效果。) 更精准的2d表上进行实现的话,我们需要利用SAT(Summed Area Tables) 这种数据结构的相应paper
附:Sat的对应实现和思想
一个基础思想:前缀和算法(可以很轻松的拥有计算其相应的总和部分) 对于输入的数组,我们预处理一遍对应的和,在处理的时候是静态前缀和,那么表中的每个元素都是先前的元素的累计和。
- 优势:某n项的和可以用其对应的数组和之前的预处理数组之间计算,就可以获得给定区间和等一系列算法。 对于二维,一维前缀和属于计算机专业最基础的部分,感兴趣可以去代码随想录进行一下学习。 ——这个Sat的逐帧动态生成比较慢,所以说加速的逻辑可能使用CUDA实现会有一定帮助,但是这是一种串行组织,并行性不强。
方差的解决方案
方差是一个计算复杂度更大的问题,但是我们可以从概率论的公式:方差等于平方的期望与期望的平方作差来解决均值问题。 一开始的时候我们Pass1记录的一张ShadowMap,在进行计算的时候,再存储一张“Square Shadow Map用于也记录这张深度图的平方”,由此获取到对应的正态分布以加速第一步的卷积程度估计。 这样的话,我们可以通过期望和方差获得对应的概率分布(PDF)。
一种估计Trick:切比雪夫不等式
切比雪夫不等式的 表示如下:
- 这是一种适用于任何分布的估计办法:其中k为待定输入参数,可以通过分布的参数,均值和方差来控制对应的系数,从而可以在给定任意一个分布的一个概率上界。
- 因为这是一个不等式,而在方程中我们更想要的是一个用于估计、近似的办法用于实现。
- 显然这种方法与打两张map记录开销较小。但是这个公式要求等式的符号开口等稳定不变,所以对于大范围均值还可以有一定的保证,但是对小于50%分位可能会有一定的影响。
更深入的优化:Blocker Search的优化
对于传统的blocker search,还是不可避免的进行卷积计算,我们需要计算遮挡物的平均深度。(假设物体位置位于place7,我们需要计算的是遮挡物深度(更靠近的6depth等位置)而不是所有屏幕上的物体深度) 也就是根据Z与t的大小关系一般地将场景中深度分为更深部分和未遮挡部分,对遮挡部分进行平均深度计算。 虽然我们不知道遮挡物平均深度和非遮挡物平均深度。但是我们显然可以得到加权平均的公式:
其中N为采样总数、N1N2分别代表遮挡和非遮挡部分。 我们在这里再次套用切比雪夫不等式,套用估计N1/N。那么另外的一部分就是N2(遮挡物深度) 但是即使如此假设,我们把t设为对应的物体深度值,我们仍然缺乏某个项来真正精确的获取我们想要的平均深度。
因此,为了近似计算,我们是用来一个更加邪门的假设:把非遮挡物深度设定为t(也就是深度重合的一定平面)
这部分算法是实时渲染领域的一些玄学加速办法,但是这里面利用了大量
不科学假设,其中运用了大量的近似和假设,但是效果在实时渲染领域是可以接受的。 此外,这种效果在与光源不平行的时候就有一定问题;其次现在我们更倾向于在图像域进行DL方向的去噪处理以获取一个较为理想的效果,这里面的VSSM是一种快速的Trick,一种实时渲染的好的思想。 在VSSM中实际利用的是切比雪夫不等式,速度很快
性能(不要盲信O(1))
- 深度算法:O(1)
- 平方图:O(1)——SAT较慢
- 利用切比雪夫不等式计算遮挡权重:O(1)
- 所有的步骤都根据数学原理每帧生成,其实本质上还是有一定cost的。
Moment Shadow Mapping(Tears of Age)
在VSSM解决PCSS的问题的时候,我们也可以发现由于使用了过多的Trick假设,遮挡的加权时候利用了太多的估计,因此在切比雪夫不等式的效果减弱的时候,我们会发现一些明显有问题的Shading Problem:
- 切比雪夫隐含了单峰值的条件,更确切地说,是隐含了一种认为遮挡物的分布接近于正态分布的假设,而对遮挡物的某些摆放缺乏预测性:
在这样的情况下,会有很多部分出现(过亮部分、过暗部分)和理想情况有明显差异的曝光点(导致出现很明显的Shading Error)——尤其是过曝的亮点会有很直观的走样。
解决思想:利用更高阶矩描述分布
矩的阶数=原式中的x的次幂(类似于概率论中我们考虑的k阶中心距和中心极限定理) 那么在在进行解决函数的时候,k阶矩能表示k/2个台阶;因此我们利用越多的矩,就能更加精确的拟合原有函数的分布情况;——其实其类似于某种展开的思想:恢复的项数越多其越接近其核心的分布。——一般来说四阶矩的近似效果已经足够优秀。
- 优势:效果针对与其开销来说很客观
- 劣势:存储上虽然很好,但是这种2step,模拟对应的算法相对来说极为复杂,对于工业界有一定效果。 附录:对应paper:Moment Shadow Mapping