平面无光照阴影

  • 移动端高性能
  • 只适用于在平面表现的投影

实现步骤

  1. 压扁:角色阴影压扁
  2. 投影偏移:沿投影方向在地平面($XZ$ 平面)平移
  3. 投影拉长:直接平移看不出来阴影拉长的效果,所以还乘以顶点压扁前的 $Y$ 轴高度作为系数进行平移

平面投影阴影

平面投影映射不是一个基于图像的解决方案

内容:根据光的方向,把物体的每个顶点投影到平面地面上。
Pasted image 20221209130631
数学原理:相似三角形
光 Light 所在的点 L 已知,V 已知,P 很容易就能求得

缺点

  • 只能投影到平面(阴影的接收物只能是平面)
  • 投影物体必须在光线和平面之间

投影阴影

平面投影阴影的阴影接收物只能是平面,为了在曲面上得到阴影,做了以下改进:

投影阴影

  1. 把光源当做一个相机/投影器
  2. 然后将阴影投影渲染到一张纹理
  3. 最后渲染阴影接收者时,将上一步得到的阴影合并进去
  • Pasted image 20221209144815

投影阴影在 Unity 中的实现
Pasted image 20221209144939

  • 第一步,设置 Project 组件,通过它的参数使用给它的材质生成一个视锥体
  • 第二步,使用 Render Texture 生成一张纹理,将阴影绘制到纹理中
  • 第三步,将设置 Project 组件的物体和阴影纹理进行混合

ShadowMapping

shadow mapping 是一个完全在图像空间 (images space)中的算法

理解阴影

  • 如何确定“是不是阴影”

    • 我们能看见+光能看见 = 正常渲染
    • 我们能看见 + 光看不见 = 阴影
  • GAMES101 中,讲过的关于阴影的理解:

    • 是一个经典的双 Pass 做法

    • Pass1:从光源看向场景,记录看到点的深度

      • 需要从光源的位置渲染整个场景的深度图(这张图就是shadowmap
    • Pass2:从相机看向场景

      • 再从相机位置渲染这个场景
    • 在投影回光源所在的图像上,比较在相机位置渲染和光位置的深度,结果可以这样理解:

      • 深度一致:说明相机和光都能看到
      • 深度不一致:我们能看到,但是光看不到 =>阴影
        Pasted image 20221209145349

阴影图/深度图(Shadow Map)

Pasted image 20221209145547

  • 光源视角下(shadowmap)
    • 我们从光源看向场景的角度渲出一张深度图,这张深度图:
    • 离光源越近就越黑(0);越远就越白(1)
  • 这张图就反映出了场景中物体的远近关系

[!NOTE] 在后文图示中,为什么我们将 shadowmap 画在光源下面的一个平面上
因为渲染得到的二维图像就是在视锥体的近裁剪平面上的图形
Pasted image 20230104154537|500

Pasted image 20221209145655
阴影映射的流程

    1. 从光源的位置生成一张深度图,这一步称之为阴影映射(ShadowMap)。
    1. 从摄像机的视角,渲染整个渲染整个场景的物体。每次渲染时,需要和 shadowmap 的深度做比较(深度测试)。

      比如左下角红色点时相机可以看到的位置,先转换成阴影映射的坐标系(光源空间),然后以光源视角比较深度值,如果一个片元的深度>它在 shadowmap 中的深度,那么就认为它在阴影中。

总结/注意事项:

  • ShadowMapping 本质上是一种图像空间做法,也就是说生成 shadow 这一步不需要这个场景的几何信息
  • ShadowMapping 只能做硬阴影
  • 投影后的 Z 值
    • 关于深度, 我们在 101 中讨论过一个问题, 在做透视投影时, 我们是将透视投影挤压成正交投影, 然后再拍平, 在这个过程中中间的点是会向近平面移动还是向远平面移动?答案是中间的所有点会被推向远平面. [[02 空间变换#思考:挤压后 z 值为(n+f)/2的点会挤向 n 还是 f]]
    • 所以, 在经过投影之后得到的 Z 其实不是实际上几何上的点到 Light 的距离, 因此在真正生成阴影时比较两个 pass 中的 depth 时需要一致。也就是要么都用投影后的 Z 值比较, 要么通过两点的位置得一向量算实际距离.

Unity 中的屏幕空间阴影映射

  1. 在屏幕空间渲染一张DepthTexture
  2. 从光源空间渲染出shadowmap
  3. 将屏幕空间深度图转换到光源空间,进行深度比较,得出屏幕空间 shadowmap。
  4. 在绘制物体时,用物体的屏幕坐标 uv采样第三步中生成的屏幕空间 shadowmap

从 FrameDebugger 查看过程
Pasted image 20221209151539
Pasted image 20221209151553

自阴影/阴影痤疮

原理

自阴影/自遮挡 (self-shadowing)也被称为**阴影痤疮/粉刺(Shadow Acne)

6ead296a05b80777473845429dcbbef8_MD5 6368ed87749ac4a26550234e84ff7c15_MD5

  • 地板上的东西会感觉像摩尔纹, 但并不是它是由数值精度造成的一种现象。

产生阴影痤疮的主因是 ShadowMap 分辨率的问题,如果分辨率比较小,导致在场景中多个片元在计算阴影时对应上了同一个 ShadowMap 的纹素,因而导致判断该片元到底在不在光线可到达的片元之前或者之后出现了问题。

Pasted image 20230621105312 1
假设 ShadowMap 分辨率很小(如图 ShadowMap 一个框对应一个纹素,纹素的颜色就是储存的深度)

  • 红色框出的纹素存储的是光源到 $P$ 点的距离(即深度值)为 $L$
  • $A、B、C、D$ 是在光源空间处于不同位置坐标的片元

从摄像机视角来看, 4 个片元都没有被其它物体遮住,所以 $La、Lb、Lc、Ld$ 长度无论是多少,都应该能被光源照亮。将 4 个片元的位置转换到光源空间,得出 $L_a、L_b、L_c、L_d$ 分别对应于光源到片元 $A、B、C、D$ 的距离。

将该距离和 ShadowMap 存储的深度值比较时,因为 ShadowMap 分辨率太小,4 个片元都只能使用 $L$ 作为比较值,最终 $La<L,Lc<L$,片元 $A$ 和 $C$ 被照亮,$Lb>L$,$Ld>L$,片元 $B$ 和 $D$ 被遮挡,于是导致了阴影痤疮。

侧面视角理解:

  • 如下图所示,在映射 shadowmap 的过程中,用表面上点的 uv采样阴影映射的深度值都是一个值(shadowmap 的一格代表的是一个纹素,在这一格里面采样阴影映射得到的值都是一样的)
  • 例如:红点和蓝点采样后得到的深度值都是一样的,这一段所有点采样得到的深度值都和红线(代表深度值)相等。
    Pasted image 20221209153218

理解纹素和分辨率的关系

长宽相等的一张纹理,分辨率越大,纹素越小
Diagram

图中的一个小方格代表一个纹素

解决办法

增大分辨率(无效)

增大 ShadowMap 分辨率可以减小世界空间纹素大小,痤疮会变小,但不会消失,同时痤疮数量也会变多,所以无法通过修改 ShadowMap 分辨率来解决问题

阴影偏移 Shadow Bias

解决阴影痤疮最直接的办法就是计算出 La、Lb、Lc 和 Ld 的长度,沿着这些线的反方向往回拉一拉,即减去一个微小的偏移值,使得最终 La、Lb、Lc 和 Ld 的长度都小于 L,这样原本应该能被照亮的地方确实被照明了,这种方法叫做调整阴影偏移(Shadow Bias)。

阴影偏移的方法:

  1. 深度偏移(Depth Bias),增加深度偏移会使该像素向光源靠近
  2. Unity 中采用的阴影偏移值的计算方法是基于物体斜度(Slope)的,称为 “基于斜度比例的深度偏移”(Slope Scale Based Depth Bias)算法。大部分改善对阴影深度贴图采样误差的算法,其核心思想是分析待绘制场景中各部分内容对采样误差的影响程度

**上述偏移会造成的问题

  • 偏差设过小,仍然会有部分阴影痤疮
  • 偏差设置过大时,发生阴影分离,即阴影与遮挡物之间发生脱节:也叫做Peter panning 问题(学术界叫作 detached shadow)

    Peter Panning 名称的由来:Peter 是西方漫画中的人物,他和影子是可以分开的
    Pasted image 20221209152205

  1. 法线偏移(Normal Bias),沿着表面法线方向向外偏移,移动距离等于一个世界空间中纹素大小的偏移。虽然会让阴影的位置发生稍微的改变,可能导致边缘不对齐或添加假阴影,但可以较好的消除阴影痤疮并且避免 Peterpanning
    Pasted image 20230621105522

    假阴影,因为阴影从 Cube 的面上伸展出来,从而影响到了地面。可以通过设置斜度比例偏差来解决这个问题,但它没有一个固定且完美的值。

Unity 中的阴影偏移:
Pasted image 20221209153656

阴影平坠 Shadow Pancaking

Unity 中应用了阴影平坠(Shadow Pancaking)技术,可以剔除那些不希望看到的阴影
该技术的想法是渲染定向光的阴影时,通过剪裁光照空间,给该空间设定阴影视椎体近裁剪平面,只有位于该平面内的物体才能投射阴影,且阴影视椎体近裁剪平面会尽可能的向前移动,意在减少沿光照方向渲染 ShadowMap 时使用的光照空间范围,这可以提高 ShadowMap 的精度,减少阴影痤疮。

如下图所示,主要就是取灯光照射方向和视锥体之间的交集
淡蓝色圆圈表示产生阴影投射的物体
深蓝色矩形框表示原来的灯光空间
绿色线表示经过优化后的近平面,把所有不在视锥体也就是看不见的阴影投射物体都排除了

Pasted image 20230621105611

这样子,等于我们将阴影投射的物体截断在优化后的灯光近裁剪平面之内(在顶点 Shader 中),需要注意的是虽然一般情况下表现都没问题,但是遇到了一些特别大的横跨这个近裁剪平面的三角面时就会出问题。
如图所示,蓝色的三角面中只有一个顶点是在近裁剪平面之外的,并且被截断了,这样就出问题了,会改变三角面的形状,产生错误的阴影。
Pasted image 20230621105613

Unity 可以通过调整 QualitySettings 中的 ShadowNearPlaneOffset 属性避免发生这种问题,该属性用于调整阴影视椎体近裁剪平面的偏移。
Pasted image 20230625161036|400
如果将此值设置得过高,则最终还是会引入阴影痤疮,因为它会提高 ShadowMap 需要在光照方向上覆盖的范围;
如果该值调的太低,又会产生漏光。如上图,绿色的平面会被往上推很多,则原本该产生阴影的圆形将被部分裁剪而不再产生投影,造成了阴影镂空了一部分。
Pasted image 20230621105618|500

解决阴影平坠造成的漏光:[[04 定向阴影#^sqd3eh]]

双深度阴影映射

GAMES202 提到了一个解决方案:Second-depth shadow mapping,实际中并没有人去使用这个技术, 因为场景内的物体必须都是 watertight(非面片),还有就是开销太大。

简单介绍下原理:
舍弃 biasd 的概念, 在渲染时不仅存最小的深度, 我们还要存第二小的深度, 然后我们用最小深度和第二小深度中间的深度来作比较:

530dda48b31bab142f1bec8a0ad4f2d6_MD5

假设一根光线照过来, 我们不用最小深度来比较, 而是用由最小和第二小深度所得到的红色线来做后续的阴影, 此处就没有 Bias 的事情了.

阴影走样

走样,最明显的表现就是锯齿,我们可以看下边的例图

Pasted image 20221209153803

  • ? 在什么阶段会走样?
    Pass1:光照空间生成 shadowmap 时—> CSM
    Pass2:从相机位置对 shadowmap 进行重采样时 —> 滤波

Pass1 透视走样

最严重的问题:透视走样,透视走样指的是阴影越靠近相机,其边缘的锯齿化越严重

6a7sdASd|450

可以看到,距离越近,走样越严重

  • ? 透视走样形成原因:

    • ShadowMap 的分辨率是固定的(由程序指定值),同样大小的一个阴影所对应的 ShadowMap 中纹素大小也是固定的(由于 ShadowMap 使用正交投影采集,因此 ShadowMap 中的每个纹理像素都有固定的世界空间大小
    • 如果使用透视相机,经过透视投影后,根据近大远小的原理,在渲染时,阴影越靠近摄像机,越容易出现多个片元从 ShadowMap 中的同一纹素进行采样的情况,这几个片元得到的是相同的阴影值,从而产生锯齿边。使用高分辨率的 ShadowMap 可以降低锯齿边,但渲染时会占用更多内存和带宽。
  • ? 如何解决

    • 方法一:因为我们在使用 shadowmap 时,相机是经过透视投影的,但生成 shadowmap 时并没有经过透视投影,所以我们可以在生成 shadowmap 时就进行一次透视投影
    • 方法二:[[09 阴影#CSM 级联阴影映射]](透视走样最有效的解决方案)

Pass2 硬阴影锯齿

Pasted image 20230625164105|400

  • ? 阴影锯齿形成原因? 这里要整理一下,描述的不清楚
    判断一个片元是否在阴影内而进行深度测试时,要把该片元从当前摄像机观察空间转换到光源空间,因为转换矩阵不一样,且 ShadowMap 分辨率不大,导致观察空间中多个片元对应 ShadowMap 同一个纹素。
    例如两个黑色锯齿中间的空白部分,本来这部分也应该处于黑色阴影中的,但因为采样到的 ShadowMap 中的纹素刚好不是黑色的,即那个纹素刚好不在黑色阴影下,就会导致阴影锯齿。

  • ? 如何解决

  1. 最直接的办法就是提高 ShadowMap 分辨率,但内存占用会变大,这种方法也只是减轻问题。实际开发中通常采用适中的分辨率的 ShadowMap 加上区域采样方法改善锯齿现象。
  2. 滤波(Filter) PCF/PCSS 等
    Pasted image 20221209163546
  • 滤波:在图像处理中,通过滤波强调一些特征或者去除图像中一些不需要的部分 (高斯模糊就是一种滤波方式)
  • 滤波是一个领域操作算子,利用给定像素周围的像素的值决定该像素的最终输出值

补充:

  • 滤波就是抹掉特殊频率的东西
  • 高通滤波 = 边界
  • 低通滤波 = 模糊
  • 滤波(Filtering)=卷积(Convolution)=平均(Averaging)

CSM 级联阴影映射

Cascaded Shadow Map

透视走样最有效的解决方案
Pasted image 20230621105133
原理:将摄像机的视锥体按一定比例分成若干层(Cascade),每个层级对应一个子视锥体,每一层都单独计算相关的 ShadowMap,在渲染大场景时就可以避免使用单张 ShadowMap 的各种缺点,更好的兼顾精度和性能问题。

核心在于:尽量减少近平面和远平面之间的像素差距

Unity 就是使用的这种技术,我们可以在工程的 Project Setting->Quality->Shadows 看到:

  • Unity 的阴影源码中可以为每个定向光支持最多 4 个级联
  • 单级联只有一层,相当于正常的 Shadow Map
    Pasted image 20221209163324

PCF 百分比渐进过滤

软阴影和硬阴影

软硬阴影的区别:硬阴影没有一个明显的从有阴影到没有的过度/界限(因为绝大多数的生活中的光源是面光源)
Pasted image 20221209164343
Pasted image 20230622213757
Umbra:本影区(影子中光源完全照射不到的部分)
Penumbra :半影区(黑暗与光明之间的),半影区的影子比较软。

ShadowMapping 只能实现硬阴影
软阴影方案:PCF/pCsS、

后文中提到的 Filter Size 即滤波核大小,在这里也可以称为半影区大小

原理

Percentage Closer filtering 百分比渐进过滤

PCF 的可以实现抗锯齿+软阴影。

Pasted image 20221209163837

PCF 中,不是取 shadowmap 上一个点的深度去比较,而是取 shadowmap 上任意一个点的周围一个 Filter 大小的区域(滤波核),将这个区域的所有点都去做一次深度比较(0 或 1 的结果),最后再对这些结果做一个平均(不是非 0 即 1 的结果,而是一个权重值/像素的颜色值)

Filter Size 滤波核大小

  • 可以是规则滤波,3x3 或者 5x5 等
  • 也可以采用泊松滤波(Poisson DIsk) 的形式来分布一定数量的采样点 Pasted image 20221209164052

Filter Size重要吗?

  • 如果小(比如 1×1 = 没做 Filter),结果是锐利(sharpener)对应硬阴影
  • 如果大,结果是 softer,对应软阴影
  • 既然 Filter 的大小可以决定阴影的软硬,我们不妨这样理解:软阴影 = 硬阴影做一个非常大的 Filter

Pasted image 20230625200816|150 Pasted image 20230625200826|150Pasted image 20230625200839|150Pasted image 20230625200843|150

PCF 2x2, 3x3, 5x5, and 7x7.

注意事项

  1. PCF 不是直接在最后生成的阴影上进行 filtering。
    如下图,可以发现没有解决走样的问题。就跟我们在反走样时一样, 我们不能先得到一个走样的结果再去做在这个走样的结果上进行模糊。
    2586f23c8f8f35667133cf9c8c941265_MD5|500

  2. 也不是对 shadow map 进行 filtering
    如果直接在 shadow map 上 filtering 就会造成阴影和物体交界直接糊起来,而且在第二个 pass 上做深度测试还是非 0 即 1 的结果, 最后得到的仍然是硬阴影。

我们之前在做点是否在阴影中时, 把 shading point 连向 light 然后跟 Shadow map 对应的这一点深度比较判断是否在阴影内, 之前我们是做一次比较, 这里的区别是, 对于这个 shading point 我们仍要判断是否在阴影内, 但是我们把其投影到 light 之后不再只找其对应的单个像素, 而是找其周围一圈的像素, 把周围像素深度比较的结果加起来平均一下,就得到一个 0-1 之间的数,就得到了一个模糊的结果。
Pasted image 20230622214840

如图, 蓝点是本来应该找的单个像素, 现在我们对其周围 3 * 3 个像素的范围进行比较, 由于是在 Shadow map 上, 因此每个像素都代表一个深度, 我们让在 shadow map 上范围内的每个像素都与 shading point 的实际深度进行一下比较, 如果 shadow map 上范围内的像素深度小于 shading point 的实际深度, 则输出 1, 否则输出 0.

从而得到 9 个非 0 即 1 的值:

最终我们用得到的加权平均值 0.667 作为 shading point 的可见性。在计算阴影的时候我们就拿这个作系数来绘制阴影。

现实世界的软硬阴影

Pasted image 20221209164914
我们发现阴影在笔尖的地方是硬的,远的地方就很虚(软),得出两个结论:

  1. 遮挡物 Blocker 与阴影接受物 Reciever 的距离越小, 阴影越锐利.
  2. 要想做一个软阴影的效果,应该给硬阴影各个位置不同大小的 filter,这样就能做到符合现实世界的阴影效果。

那么这个不同位置不同的 Filter Size怎么计算呢?
PCSS 可以解决这个问题。

PCSS 百分比渐进软阴影

Percentage Closer Soft Shadows

[!NOTE] PCF 和 PCSS 的区别

  • PCF 采用的 Filter 是固定的
  • PCSS 在不同位置使用了不同的 Filter

PCSS 本质上就是求出了阴影中需要做 PCF 的半影部分后,再进行 PCF 的计算。这样动态调节了半影范围,也就是动态设置了 PCF 的搜索范围,这样我们的硬阴影部分清晰,软阴影部分模糊,动态的实现了不错的软阴影效果。

[!summary] PCSS 的步骤

  1. 计算指定范围(即通过计算得到的范围 [[09 阴影#^2480r1]]) 的 blocker 平均深度。
  2. 通过 blocker 平均深度计算 filter size。
  3. 有了 filter size 就可以按照 PCF 方式绘制软阴影。 ^rybveh
  • ? 如何计算一个软阴影的不同位置不同的 Filter Size?
    blocker distance 即图中的 $d_{Blocker}$:遮挡物和光源的距离。这不是单个点的距离,更详细的说,是相对的平均的遮挡物深度

Pasted image 20221209165125

  • 上方黄色线段表示 Light(光),中间绿色线段表示 Blocker(遮挡物),下边蓝色线段就是 Receiver(阴影的接收物)
  • 我们可以看到左下和右上两个黄色虚线形成的三角是两个相似三角形

如果我们将 Blocker 的位置移动一下, 比如越靠近 receiver, 我们会发现 $W_{Penumbra}$ 也就会越小。

用数学来表示半影区($W_{Penumbra}$), 由相似三角形 :

$$
\frac{W_{Light}}{W_{Penumbra}}=\frac{d_{Blocker}}{d_{Receiver}}
$$

$$
W_{Pemumbra}= W_{Light}\cdot \frac{d_{Receiver}}{d_{Blocker}}
$$

这里的 $d_{Recevier}$ 和 $W_{Light}$ 的大小我们是知道的,所以我们只需要拿到 blocker 的深度即可。

  • ? 如何确定一个 blocker 距离面光源的位置 $d_{Blocker}$?

不能直接使用 shadow map 中对应单个点的深度来代表 blcoker 距离, 因为如果该点的深度与周围点的深度差距较大(遮挡物的表面陡峭或者对应点正好有一个孔洞),将会产生一个错误的效果, 我们选择使用平均遮挡距离来代替,所以平常我们指的 blocker depth 其实是 Average blocker depth.

blocker 上的每个点距离光源的距离是不同的,深度也是不一样的。这里我们采用取平均深度的方式来表示 blocker 的深度。

  • @ 求 blocker 距离的过程如下:
  1. 把观察空间下的目标 shading point 转换到 light space,目的是获取 shading point 的深度
  2. 找到 shading point 在 shadow map 上对应的像素。
  3. 如果 shading point 的深度大于这个 shadow map 上点对应的深度, 则说明 shadow map 上的点就是一个 Blocker, 然后我们取 shadow map 上这个点 (像素) 周围的一些像素
  4. 在周围像素张找出能够挡住 shading point 的点的像素, 并求出他们的深度平均值作为 blocker 的深度,得到了一个在 0 到 1 之间的软阴影效果。

这个方法关键点在于取 shadow map 上这个点 (像素) 周围的一些像素,我们需要确定取多少像素。 ^2480r1

一般我们有两种方法可以解决这个问题。
第一种,就是自己规定一个, 比如 4 * 4, 16 * 16, 比较简单但不实用.
第二种,shading point 连向 light,覆盖 shadowmap 的区域(红色区域),我们只在这个区域范围内找 blocker
Pasted image 20230622222325|500
17a5f4b8a24467710c615889bd174d97_MD5|500

我们计算 shadow map 的时候在光源处设置过相机,如图所示,我们把 shadow map 放在由相机看向场景形成的视锥中的近截面上, 然后 shading point 连向 light,在 shadow map 上截出来的面就是要查询计算平均遮挡距离的部分。这部分的深度求一个均值,就是 Blocker 到光源的平均遮挡距离

VSSM 方差软阴影映射

Variance Soft Shadow mapping,针对性解决 PCSS 步骤 1 和步骤 2 较慢的问题。

回顾一下 PCSS 的步骤:
![[09 阴影#^rybveh]]

PCSS 中步骤 1 和步骤 3 需要对整个区域的各个 texels 的深度进行比较,所以会导致很慢。

如果觉得区域过大不想对每一个 texels 都进行比较, 就可以通过随机采样其中的 texels,而不是全部采样,会得到一个近似的结果, 近似的结果就可能会导致出现噪声。

工业的处理的方式就是先稀疏采样进行PCSS得到的结果是有噪声的, 接着再在图像空间进行降噪。至于如何降噪在 real-time ray tracing 讲.

由于需要在一个范围内进行比较,那么步骤 1 和 3 的时间开销会决定整个算法的时间开销,此外为了得到越 “软” 的阴影意味着需要使用更大的 filtering size,会导致速度越慢。为了解决这两步慢的问题, 就有人提出了 Variance Soft Shadow Mapping。
Variance soft shadow mapping 主要解决了 PCSS 中第一步和第三步慢的问题.

解决第三步

PCSS 第三步我们使用 PCF 的方法绘制软阴影,我们要在 shadow map 上对其周围 Filter Size 内的像素的各个最小深度与 Shading point 比较,从而判断是否遮挡, 也就是要求出 Filter Size 内有百分之多少的像素比它浅。

这个过程很像在考试成绩出来后, 你知道了自己的成绩, 你想知道自己在班级中的排名, 因此你需要知道班级中所有人的成绩从而进行比较来判断自己是百分之几,这就是 PCF 的做法.

但现在我们就是为了避免这种时间消耗大的做法.

那么一个不错的办法就是, 对班级所有人的成绩做成一个直方图,根据直方图我们可判断出自己的成绩排名。

Pasted image 20230623114042

如果我们不需要那么准的话,就可以当做一个正态分布,正态分布就只需要知道均值 $\mu$ 和标准差 $\sigma$, 更加的方便快速。

而 VSSM 使用了切比雪夫不等式,更快速的计算。

VSSM 的 key idea 是快速计算出某一区域内的均值和方差.

均值:

对于快速的求一个范围内的求均值, 我们可以想到在 games101 中学到的 mipmap 方法. 但是 mipmap 毕竟是不准的, 而且只能在正方形区域内查询. 因此引入 Summed Area Tables (SAT)

方差:

VSSM 用结合了期望方差之间的关系的一个公式来得到方差:
[[概率论与数理统计#4 方差 D(X)和标准差#计算方法]]
$D (X) = E (X^2) - [E (X)]^2$

这个公式的含义是用 方差 = 平方值的期望 - 期望值的平方 .

用这个公式的原因是:

在 shadow map 中我们存储的是 $depth$, 因此 $depth$ 也就是公式中的 $X$, 在指定区域范围后, 可以快速的求出区域范围的平均值 (期望), 因此也可以很快求出区域范围内平均值的平方, 也就是求出了 $E^2(X)$ .

那么求 $E(X^2)$ , 我们就需要额外生成一张 shadow map, 但是这张图上存的不是 $depth$, 而是 $depth^2$ , 然后再在指定范围区域内快速求出平均值, 也就是求出了平方值的期望, 求出了 $E(X^2)$ , 这张存储了 $depth^2$ 的 shadow map 叫做 square-depth map.

也就是为了求方差, 需要在生成 shadow map 时再存储一张 square-depth map。

到此为止我们就快速获得了均值和方差。那么回到问题本身:

  • 有多少百分比的像素是比 Shading point 也就是 不会挡住 Shading point 的只需要计算出下图 PDF 中白色面积 的值就行了。
  • 有多少百分比的像素是比 Shading point 也就是 会挡住 Shading point 的只需要计算出下图 PDF 中灰色面积 的值就行了。

Pasted image 20230624144757

[!quote] CDF 和 PDF
CDF [[概率论与数理统计#3 随机变量的分布函数]]: 累积分布函数 (cumulative distribution function),又叫概率分布函数,分布函数,是概率密度函数的积分,能完整描述一个实随机变量 X 的概率分布。
PDF[[概率论与数理统计#4 连续型随机变量及其分布]]:概率密度函数(probability density function), 在数学中,连续型随机变量的概率密度函数是一个描述这个随机变量的输出值,在某个确定的取值点附近的可能性的函数。

其实想知道 CDF,也就是求出 PDF 曲线下对应的面积.

对于一个通用的高斯的 PDF,对于这类 PDF,可以直接把 CDF 结果,输出为一个表,叫误差函数 Error Fuction,误差函数有数值解,但是没有解析解,在 C++ 中的函数 ERF(lower_limit,[upper_limit]) 函数可以计算 CDF。

Pasted image 20230624144956
$$
P(x>t) \le \frac{\sigma^2}{\sigma^2 + (t-\mu)^2}
$$
切比雪夫不等式不需要知道随机变量满足什么分布(即图中我们不知道函数曲线是什么样子),只需要知道期望($\mu$: mean)和方差($\sigma$: variance)就可以算一个随机变量取的值($x$)超过某个值($t$)的概率不会超过 $\displaystyle\frac{\sigma^2}{\sigma^2 + (t-\mu)^2}$

  • 切比雪夫不等式是一个粗略的概率估计
  • 必须满足 $t>\mu$

这里使用了一个 Trick 的方法,将这个不等式近似为相等.

那么就可以通过均值和方差获得图中红色面积的值。因此就可以不用计算 CDF 了,而是通过 1 - 求出的 x>t 的面积值得到 CDF.

加速第三步总结:

  1. 通过生成 shadow map 和 square-depth map 得到期望值的平方和平方值的期望再根据 $D(X) = E(X^2) - [E(X)]^2$ 得到方差
  2. 通过 mipmap 或者 SAT 得到期望
  3. 得到期望和方差之后, 根据切比雪夫不等式近似得到一个 depth 大于 shading point 点深度的面积., 也就是求出了未遮挡 Shading point 的概率, 从而可以求出一个在 1-0 之间的 visilibity.

也就是省去了在这个范围内进行采样或者循环的操作, 大大加速了第三步.

如果场景 / 光源出现移动就需要更新 MIPMAP,本身还是有一定的开销,但是生成 MIPMAP 硬件 GPU 支持的非常到位,生成非常快(几乎不花时间),而是 SAT 会慢一点,这个后面进行分析。

解决第一步

到目前为止 VSSM 也只解决了第三步 PCF Filter 的问题,PCSS 在第一步要需要求在范围内将所有像素的深度走一遍从而求平均遮挡深度 Average Blocker Depth 的问题并未解决。

我们以图中的 $5*5$ 范围为例, 假设我们的 Shading point 的深度是 7.

我们将其分为两个区域, 蓝色是深度小于 shading point 的遮挡区域, 其平均深度为 $Z_{occ}$ 红色是深度大于 shading point 的非遮挡区域. 其平均深度为 $Z_{unocc}$. 并且我们认为区域内的像素总数为 $N$, 非遮挡的像素为 $N_1$ 个, 遮挡的像素为 $N_2$ 个。

Pasted image 20230624150706

核心思路:

我们需要去计算求出 $Z_{unocc}$ 和 $Z_{occ}$ , 通过他们之间的关系我们可以得出一个加权平均公式:

$\displaystyle\frac{N_1}{N} * Z_{unocc} + \frac{N_2}{N} * Z_{occ} =Z_{Avg}$

非遮挡像素占的比例 * 非遮挡物的平均深度 + 遮挡像素占的比例 * 遮挡物的平均深度 = 总区域内的平均深度.

计算步骤:

  1. 通过生成 shadow map 和 square-depth map 得到期望值的平方和平方值的期望再根据 $D(X) = E(X^2) - [E(X)]^2$ 得到方差

  2. 通过 mipmap 或者 SAT 得到期望,即总区域的平均深度 $Z_{Avg}$

  3. 有了期望和方差,就可以根据切比雪夫不等式近似求出 $\displaystyle\frac{N1}{N}$ (非遮挡物的比例)和 $\displaystyle\frac{N2}{N}$ (遮挡物的比例)。

$\displaystyle \frac{N1}{N} = P(x > t)$

$\displaystyle \frac{N2}{N} = 1 - P(x > t)$

  1. 此时公式中的 $Z_{unocc}$ 和 $Z_{occ}$ 仍然是不知道的, 这里有一个 Trick:
    我们假设非遮挡物的平均深度 $Z_{unocc}$ = shading point 的深度(该假设的合理性:绝大多数阴影的接收者为平面。但也以为如此,在曲面和与光源不平行时会出现问题)

  2. 至此我们只剩下 $Z_{occ}$ 的深度, 将所有值代入可求出遮挡物的平均深度 $Z_{occ}$

VSSM 的做法实在是十分聪明, 采用了非常多的大胆假设,同时非常的快,没有任何噪声,本质上其实也没有用正态分布,是直接用切比雪夫不等式来进行近似。但是现在最主流的方法仍然是 PCSS, 因为人们对噪声的容忍度变高加上降噪的技术越来越高明, 因此大多数人采用 PCSS.

MIPMAP 和 SAT

VSSM 中如何加速第一步和第三步的我们知道了, 那么如何在区域范围内快速的求出均值?

有两个方法: MIPMAP 和 SAT.

最简单的方法自然是 MIPMAP, 我们在 GAMES101 里学过, 他是一个快速的, 近似的, 正方形的范围查询, 由于他要做插值, 因此即便是方形有时也会不准确. 同时当插值的范围不是 2 的次方时,也就是在两个 MIPMAP 之间时,还要再进行一次插值,也就是 “三线性插值”,这样会让结果更加不准确, 因此局限性太大且准确度也不算高.

SAT 是百分百准确的方式. SAT 的出现是为了解决范围查询 (在区域内快速得到平均值), 并且, 范围内求平均值是等价于范围内求和的, 毕竟总和除以个数 = 平均值.

在 1 维情况下其实就是一维数组, SAT 这种数据结构就是做了预处理,也就是在得到一维数组时, 先花费 O (n) 的时间从左到右走一遍, 并且在走的同时把对应的累加和存入数组 SAT 中,那么 SAT 上任意的一个元素就等于原来数组从最左边的元素加到这个元素的和, 如图 SAT 数组第 2 个元素表示原数字前 2 个元素之和。

那么查询下图中 SUM 区域总和时候,就是 SAT 数组中第六个元素减去第三个元素。相当于使用了 $O(n)$ 的时间,把预计算做了一遍。

Pasted image 20230624153444

那么在 2 维(二维数组)情况下有一个任意矩形(横平竖直),蓝色矩形内的总和为两个绿色矩形内的总和减去两个橙色矩形内的总和。同样可以采用 SAT 的方法,做一个表,做出从左上角到这个元素的和。因此只需要查表 4 次就可以得出精准的区域求和。

在 2 维情况下,可以通过建立 m 行中每行的 SAT 和在每行的 sat 基础上再建立 n 列中每列的 SAT,最终可以获得一个 2 维的 SAT 因此最终的 SAT, 但是由于 gpu 的并行度很高, 行与行或列与列之间的 sat 可并行, 因此具有 $O(m×n)$ 的时间复杂度。

Pasted image 20230624153637

图中坐标系有误,应该是左上角为原点

其实这个算法就是 leetcode 1314 matrix block sum 的算法.

MSM 矩阴影映射

Moment shadow mapping

VSSM 是为了解决 PCSS 的问题, 但 vssm 由于做了很多假设,当假设不对的时候会有问题。

cecf3e01215ce550560213f43777dadc_MD5

比如右图,只有三个片的遮挡的情况下,那么深度的分布就在这三个遮挡度深度周围,形成了三个峰值,自然就会出现假设描述的不准。

1aa2457474073a47deeb8b5a4dd3246f_MD5

切比雪夫不等式虽然不需要知道分布,但他大概的分布形状和正态分布类似,当实际分布的形状和正太分布差别过大时,如图。
蓝色曲线为实际的分布,涂蓝色的区域为未遮挡的比例,但通过切比雪夫推算的为遮挡的比例时涂红色区域,可以看到比例大于涂蓝色的区域。使得计算结果未遮挡的比例偏大,造成暗部过亮(漏光 light leaking ), 如下图。相反也可能出现过暗的情况。
Pasted image 20230624154937|400

在阴影的承接面不是平面的情况下也会出现阴影断掉的现象。
Pasted image 20230624155016|450

因此人们为了避免 VSSM 中不符合正态分布情况下的问题, 就引入了更高阶的矩 (moments) 来得到更加准确的深度分布情况。矩的定义有很多,最简单的矩就是记录一个数的次方,VSSM 就等于用了前两阶的矩。这样多记录几阶矩就能得到更准确的结果。

如果保留前 M 阶的矩,就能描述一个阶跃函数,阶数等 2/M, 就等于某种展开。越多的阶数就和原本的分布越拟合。一般来说 4 阶就够用。

Pasted image 20230624155240

Pasted image 20230624155523

Pasted image 20230624155545

距离场软阴影 Distance Field

距离场是其他领域的研究,我们只要能拿到信息就可以。

特性:

  1. 比 shadowmap 快,但效果不如 PCSS(因为取近似误差较大)
  2. 需要预计算,内存占用大
  3. 动态物体需要实时计算
  4. SDF 生成的物体不容易参数化(uv 不容易得到),不方便贴纹理

有向距离场 Signed Distance Function

Pasted image 20230714114227

SDF 可视化

Distance Function:对于任一点,给出到最近的对象上的最小距离 (可以是带符号的距离,即有向距离场,物体内部为负,物体外部为正)

Pasted image 20230714104506

Blend 移动的边界:
Pasted image 20230714111349

Blend 任意任意两个 SDF,得到一个新的 SDF
Pasted image 20230714111554
SDF 大多用在与几何之类的方向上, 比如我有两个物体的 SDF, 我们将两个 SDF 进行 Blend 操作, 从而得到一个新的 SDF, 也就得到了一个新的几何物体, 因为当距离 0 为实,即可认为是物体的边界,SDF 可以很准确地反应物体的边界。几何转换 SDF 的时候,可以很好地把几何进行过渡。

SDF 的应用:

Ray Marching

假设我们已经知道场景的 SDF, 现在有一根光线, 我们试图让光线和 SDF 所表示的隐含表面进行求交, 也就是我们要用 Ray Marching : 进行求交.

Pasted image 20230714111756
任意一点的 SDF 是预计算的, 因此在 $P_{0}$ 点时, 我们以它的 $SDF(P_0)$ 为半径做一个圆 (此处假设在 2D 内, 如果在 3D 内则是一个球), 在这个圆内无论是哪个方向前进, 只要不超过半径距离, 都不会碰见物体, 是安全的。

因此我们可以利用这个特性不断的朝一个方向前进, 以 $P_{0}$ 开始朝一方向走 $SDF(P_{0}$) 到达 $P_{1}$ 点, 若仍与物体表面相距甚远, 则以 $P_{1}$ 点为新起点继续走 $SDF(P_{1}$) 到达 $P_{2}$ 点, 假设此处 $P_{2}$ 点的 $SDF$ 足够小, 也就是代表离物体表面足够接近了, 则进行求交操作.

如果在超一方向 trace 非常远的距离但仍然什么都没 trace 到, 此时就可以舍弃这条光线, 也就是停止了.
SDF

SDF 软阴影

将安全距离的概念进行延伸,在任意一点通过 sdf 可以获得一个安全角度 safe angle(图中灰色部分)
Pasted image 20230714112418|500
我们取点 P 为 shading point 往一方向打出一根光线, 光线上的一点 a, 有一个 SDF 值 SDF (a), 也就是在 a 点以 SDF (a) 为半径所做的球或圆内是安全的, 不会碰到物体.

把 shading point 和面光源相连,所得到的安全角度越小,被遮蔽的可能越高,就可以认为

  • safe angle 越小,阴影越黑,越趋近于硬阴影;

  • safe angle 够大就视为不被遮挡没有阴影, 也就越趋近于软阴影。

  • ? 如何从 ray marching 中求出 safe angle?
    Pasted image 20230714113316|450
    我们以 $o$ 为起点, 沿一个方向推进, 仍然是 ray marhcing 的步骤, 在 $p1$ 点以 $SDF(p1)$ 进行推进, 其余点也是一样, 此处主要是为了求 safe angle,。

  1. 我们在起点 $o$ 沿每个点的 $SDF$ 为半径所形成圆做切线, 从而求出各个点的 safe angle,
  2. 取其中最小的角度作为 safe angle.

那么我们该怎么去计算这个角度?

从图我们可以知道, 以 $p1$ 点为例, 从 $o$ 点到 $p1$ 的距离为斜边, $sdf(p1)$ 函数值是圆的半径,当然也是 p1 到切点的长度,p1 到到切点是直角边, 因此我们用 $arcsin$ 就可以求出 safe angle 了.
$$
\arcsin\frac{\mathrm{SDF}(p)}{p-o}
$$
但是 arcsin 的计算量其实是十分大的, 因此在 shader 中我们不用反三角函数。

只要 sdf 长度除以光线走过的距离乘一个 k 值,再限定到 1 以内,就能得到遮挡值或者说是 visibility,而 k 的大小是控制阴影的软硬程度.
$$
\min\left{\frac{k\cdot\mathbf{SDF}(p)}{p-0},1.0\right}
$$
Pasted image 20230714114113|300

我们从图中分可以看出来, 当 k 值越大时候, 就越接近硬阴影的效果, 也就是它限制了可能半影的区域:

  • k 越小, 半影区域越大, 越接近软阴影效果.
  • K 越大, 半影区域越小, 越接近硬阴影效果.