实时阴影文献综述
本文为「计算机科学思想史」文献综述,资料来自网络,仅供参考,最好别撞题(
绪论
研究背景
随着计算机图形学理论和图形硬件的飞速发展,真实感图形渲染技术已经在电影、游戏等领域得到了广泛的应用。其中,阴影技术是真实感图形渲染的重要组成部分,它能够为场景增加真实感,提高场景的逼真度。14在以电子游戏为代表的实时渲染中,探索高效、高质量的实时阴影算法则显得尤为重要。
阴影不仅能增加场景的真实感,还能提供一些关于场景的视觉线索。5如上图所示,左图中的两个球虽然位置完全相同,但它们投射的阴影有所不同,右边的球看起来更加靠近纸面,而且更高一些。而右图表明,即使是比较粗糙的阴影也是很有帮助的,人眼并不是那么挑剔。不过,研究表明,更加自然的软阴影能提供更多的视觉线索。此外,阴影还可以表现某些艺术效果。4
阴影的基本理论
阴影是由于光线被物体遮挡而产生的较暗的区域。这个定义不是很严谨,但对于基本的实时阴影已经足够了。它只考虑了来自光源的光照(直接光照),不考虑间接光照,而且假设物体是不透明的。9
阴影由光源(light source)、遮挡物(occluder/blocker/shadow caster)和接收物(receiver)产生,包括本影(umbra)和半影(penumbra)两部分。其中,本影是完全被遮挡的区域,半影是部分被遮挡的区域。4如果只渲染本影,那么阴影就是硬阴影(hard shadow);如果同时渲染了半影,那么阴影就是软阴影(soft shadow)。9软阴影在具有更高的真实性的同时,也会带来更大的计算开销。
图形硬件简介
在 20 世纪 90 年代中期,随着 3D 渲染的需求增加和 DRAM 价格的下降,以 3dfx Voodoo 为代表的早期 3D 加速卡,将 CPU 从重复的光栅化和纹理映射中解放出来,大幅提高了实时 3D 渲染性能。2000 年前后,以 NVIDIA GeForce 为代表的 GPU 开始支持硬件 T&L(Transform and Lighting),为顶点处理提供了硬件加速。一年后 GPU 开始支持可编程着色器(programmable shader),允许开发者自定义顶点和像素的处理流程,使各种实时渲染技术得以实现。此后几何着色器、曲面细分等高级技术引入了 GPU。1
近年来,GPU 开始支持实时光线追踪,在进一步提升真实感的同时,也为实时阴影、全局光照等传统问题提供了新的思路。
3D 图形的渲染通过硬件加速的渲染管线来完成。如上图所示,渲染管线包括顶点处理、三角形处理、光栅化、片段(像素)处理、帧缓冲处理等步骤。13其中,很多阴影算法需要用到像素着色器(pixel shader),像素着色器不仅可以从顶点着色器、纹理中获得数据,还能将结果写入渲染目标(render target),作为下次像素处理的输入。14有的阴影算法也会用到帧缓冲处理功能,例如模板缓冲(stencil buffer)。
基本硬阴影算法
平面阴影(planar shadow)
平面阴影的思想非常简单,但不那么准确,因此在现在的实时渲染中是比较过时的。平面阴影由 Blinn 在 1988 年提出。2平面阴影就是通过一个变换矩阵将三维物体投影到一个平面上,并用深色渲染这个投影。考虑一种特殊情况,当阴影平面是 $y=0$ 时,容易推导出对应的变换矩阵为
$$M=\begin{pmatrix}
l_y & -l_x & 0 & 0\\
0 & 0 & 0 & 0\\
0 & -l_z & l_y & 0\\
0 & -1 & 0 & l_y
\end{pmatrix}$$
其中,$l$ 为光源。令待投影的点为 $v$,投影后的点为 $p$,则有 $Mv=p$。以上结论也可以推广到一般平面的情况。10
如果直接渲染平面阴影,由于阴影和平面的深度相同,运算精度问题会导致明显的 Z 冲突(z-fighting),导致画面闪烁走样。一种解决方法是增加一个偏移值(bias),使得阴影总是在平面上方。10另一种解决方法是先渲染平面,再关闭深度测试并渲染阴影,接下来打开深度测试来渲染剩下的场景。4
如果平面是有边界的,投影的阴影可能会超出边界而显得不真实。此时使用模板缓冲标记平面区域就能解决。10模板缓冲还能解决阴影重叠导致的多次渲染问题,如果不处理可能会导致阴影颜色不一致。4
显然,平面阴影要求接收物必须是一个平面,否则就会出现穿插现象。除此以外,平面阴影的缺点还包括可能产生反影(anti-shadow)和假影(false shadow)。如上图所示,左图为正确的阴影,右图则是反影。反影是由于光源位于遮挡物和接收物之间导致的。而假影指的是阴影出现在接收物的反面(即位于光源和遮挡物的另一面)。9因此,使用平面阴影需要对位置进行判断,并通过 w 坐标等方式裁剪掉错误的阴影。
阴影纹理(shadow texture)
为了将平面阴影能在曲面上正常工作,一种方法是将平面阴影图像作为纹理投影到(非平面的)接收物上。这种方法也被称为投影阴影映射(projective shadow mapping),但这个名字很容易和更常用的阴影映射(shadow mapping)混淆。4
阴影纹理算法自然可以分为两步,首先将遮挡物通过平面阴影算法渲染到一张阴影纹理中,然后由应用程序显式计算出接收物对应的纹理坐标,并在渲染接收物时应用阴影纹理。
阴影纹理的一个缺点是必须区分遮挡物和接收物,每个遮挡物需要创建一张阴影纹理,而且无法支持自遮挡(self-shadowing)。另一个问题是,和平面阴影一样,应用程序需要判断光源和遮挡物的位置关系,以避免出现反影。接下来介绍的算法试图解决这些限制。10
不过,阴影纹理也有一些有趣的优点,例如容易通过滤波来伪造软阴影,在离线渲染中方便艺术家调整阴影纹理等。4
阴影体(shadow volume)
阴影体算法由 Crow 在 1977 年提出。3阴影体算法不需要可编程着色器,只需要模板缓冲,因此在早期的 GPU 上也能很好地工作。为了展示阴影体的思想,如上图所示,在只有一个点光源和一个三角形的场景中,从点光源向三角形连线产生的棱锥,在三角形「下面」的部分就是阴影体,其与接收物相交的部分就会产生阴影。
一种朴素的实现是,首先渲染不带阴影的整个场景,然后对于每个三角形,对整个屏幕的所有像素运行像素着色器,判断像素是否在阴影体内,如果是则渲染阴影。以现代硬件眼光看,这种方法的计算开销太大了,尤其考虑到现代屏幕的分辨率很高。但早期的硬件,例如像素平面机(Pixel-Planes machine)能高效实现这种方法。另一种策略是对静态场景进行预计算。4
假设摄像机在阴影之外。考虑上图中的场景,其中红色部分为遮挡物,蓝色的圆柱和正方体为接收物。从摄像机发出的每条光线,维护一个计数器,如果遇到阴影体的正面(朝向摄像机的一面)则计数器加一,如果遇到阴影体的背面则减一。到达接收物时,如果计数器为正,则渲染阴影,因为光线进入阴影体的次数大于离开阴影体的次数。接下来考虑使用模板缓冲来加速这个过程:
- 清空模板缓冲。
- 使用环境光渲染整个场景,得到场景的深度缓冲。
- 将深度缓冲设为只读,保留深度测试。
- 对于每个遮挡物,渲染阴影体的正面,模板操作是将模板缓冲的值加一。
- 对于每个遮挡物,渲染阴影体的背面,模板操作是将模板缓冲的值减一。
- 渲染接收物,如果模板缓冲的值等于 0,则计算来自光源的光照,否则渲染阴影。
值得注意的是,在 4、5 步中,只有通过场景深度测试的像素才会更新模板缓冲。10上图中蓝色部分就是更新过模板缓冲的区域。圆柱接收物挡住了后面的阴影体反面,因此其模板值大于 0;正方体则没有物体挡住。
上述的算法被称为 z-pass,无法处理摄像机在阴影内的情况。尽管可以在 CPU 上计算摄像机在阴影体内的数量,作为模板缓冲的偏移值,但会增加 CPU 负担。而且如果视锥体(viewing frustum)的近平面与阴影体屏幕相交,会产生多个偏移值,导致更复杂的解决方案。因此有人提出了一种健壮的 z-fail 算法,通过接收物到无穷远处经过的阴影体平面数量来判断。z-fail 算法由 Carmack 普及,并在 DOOM 3 中使用。但 z-fail 比 z-pass 要慢一些。4
阴影体算法有不少缺点。首先,其开销变化很大,与摄像机、光源和物体的位置密切相关,这会导致算法性能很不稳定。10其次,阴影体需要进行很多几何处理,在广泛使用曲面细分(tessellation)的现代实时渲染中开销大。另外,算法需要更新很多像素的模板缓冲,对填充率有较高的要求。4
阴影映射(shadow mapping)
阴影映射是现在引用最广的实时阴影算法,由 Williams 在 1978 年提出。11阴影映射的思想是,从光源的视角渲染场景,但仅保留深度缓冲。然后正常从摄像机渲染场景,对于每个像素,如果其深度大于阴影贴图中的值,则渲染阴影。9阴影映射简单、高效,因为是图像空间的算法,与场景复杂度无关,没有上面算法的缺点。
如上图所示,左上图表示从光源渲染到阴影贴图(shadow map)的过程,左下图是示例场景的阴影贴图。物体越靠近光源,其 z 值越小,因此颜色越深。右上图展示了判断阴影的过程,其中 a 点光源深度和摄像机深度相同,不在阴影中;而 b 点深度不同,因此是阴影。右下图是渲染结果。
但阴影映射也有一些问题。由于深度精度问题以及特定的几何关系,在没有遮挡物的情况下,光源深度会略低于屏幕深度,从而产生自遮挡走样(self-shadow aliasing/shadow acne)。解决方法也很直接,在阴影贴图上增加一个偏移量。但如果偏移量太小则无法解决自遮挡,如果太大又可能导致阴影「悬浮」问题(Peter Panning),如上图所示。
另一个问题是在一些情况下,例如光源与接收物夹角较小的时候,阴影贴图的采样率无法匹配屏幕渲染的采样率,导致锯齿现象。上左图展示了阴影锯齿,右图使用 LiSPSM 算法来减少锯齿。PSM、TSM、LiSPSM 等算法都试图解决锯齿问题,但都有各自的优缺点,因此没有广泛应用。
目前应用最广的方案是 CSM(cascaded shadow maps),如上图所示,通过将视锥体平行分割成几部分,并分别记录各自的阴影贴图,在靠近摄像机的贴图中使用较高的采样率。10
软阴影算法
PCF
PCF(percentage closer filtering)在阴影映射的基础上模拟软阴影。普通的阴影映射在判断当前像素是否在阴影中时,只比较了阴影贴图上的一个像素。而 PCF 则通过对阴影贴图对应像素周围进行多次采样,统计在阴影中的像素比例,从而模拟软阴影。下面简单介绍 PCF 的原理。
如上图所示,左图展示了面光源形成软阴影的情况,棕色线标记了本影和半影区域。于是,P 点的阴影强度可以通过从 P 点向面光源采样来获得,被遮挡的比例就是阴影强度。而右图是 PCF 对点光源产生软阴影的过程,通过从光源视角,也就是在阴影贴图上采样,如红色圈所示,来近似左图的软阴影。
上图则展示了不同的采样方式的渲染结果。图一使用 $4\times 4$ 网格最近邻采样,可以看到明显的走样。图二使用了图四所示的 12 点圆盘泊松采样,质量有所提高,不过仍然有一些走样。而图三则将不同像素的采样模式进行随机旋转,将走样转变为不那么明显的噪声。
PCF 的自遮挡和悬浮问题更加突出。另外,PCF 的采样面积是一个常量,导致所有阴影都一样软,在遮挡物与接收物接触的时候,显得尤其不真实。10
PCSS
如上图所示,图一是完全的硬阴影,图二是 PCF 的软阴影,图三是 PCSS(percentage closer soft shadows)的软阴影。可以发现图三明显比图二更真实,其关键的差异在于 PCSS 根据遮挡物到接收物的距离,来确定 PCF 的采样面积:当遮挡物靠近接收物时,采样面积小,更加接近硬阴影;反之则采样面积大,变成软阴影。即
$$w_\text{sample}=w_\text{light}\frac{d_r-d_o}{d_r}$$
其中,$w_\text{sample}$ 为采样区域宽度,$w_\text{light}$ 为光源宽度,$d_r$ 为接收物到光源的距离,$d_o$ 为遮挡物到光源的平均距离。10
因此,使用 PCSS 来生成软阴影,需要以下步骤:首先统计附近区域内的遮挡物的平均深度,然后根据上式估算半影区域的宽度,并作为 PCF 的采样区域,最后利用 PCF 生成软阴影。13
利用 Shader Model 5.0 的特性,AMD 实现了一个更快的 PCSS,并将其称为 CHS(contact hardening shadows)。CHS 通过生成阴影贴图的 mipmap,来解决半影大小受阴影贴图分辨率影响的问题。
除了具有较高的性能开销,PCSS 的另一个缺点是,其假设使用平均遮挡物深度能有效估计半影大小。假设两个遮挡物分别是一个路灯和远处的山,将它们的深度取平均显然是不合适的。为了解决这个问题,一些研究者提出了使用 GPU 来进行反向投影(backprojection)。10
VSM
与 PCF 需要多次采样不同,VSM(variance shadow maps)试图通过一次采样来生成软阴影。为了避免 PCF 的多次采样,VSM 假设阴影贴图中的深度是一个单峰分布,并使用切比雪夫不等式(Chebyshev’s inequality)来估计累计分布函数(CDF):
$$P(x>t)\le \frac{\sigma^2}{\sigma^2+(t-\mu)^2}\qquad (t>\mu)$$
CDF 的值就直接与阴影强度相关,直接近似取等于。其中 $t$ 为当前像素深度,$\mu$ 为采样区域深度均值,$\sigma$ 为深度方差。为了获得深度分布的期望和方差,需要支持对阴影贴图上的区域计算平均值,并记录深度和深度的平方,通过 $\text{Var}(X)=\text{E}(X^2)-\text{E}^2(X)$ 计算方差。而计算区域的平均值,可以通过 mipmap 估算,但更好的方法是使用二维前缀和。
可以用类似的方法计算 PCSS 中的平均遮挡物深度。阴影贴图中的像素可以分为遮挡物和非遮挡物,而所有像素的平均深度是已知的,遮挡物和非遮挡物的比例也容易用切比雪夫不等式估算。假设非遮挡物的深度等于当前深度,就能估算出遮挡物的平均深度,从而完成对整个 PCSS 过程的加速。13
VSM 相比 PCF 等算法,优雅的解决了阴影偏移量的问题。另外,由于只需要一次采样,VSM 的性能也很好,而且不受阴影软硬程度的影响。10上图中左图使用了 PCSS,右图使用 VSM。7
VSM 的缺点是依赖切比雪夫不等式,当遮挡物深度分布不符合其要求时,会出现漏光(light bleeding)现象,导致完全被遮挡的区域仍然是亮的。例如上图中车底区域就出现了漏光。可以设置一个偏移量来缓解这个问题,但会导致半影区域减小。对于很少有多个遮挡物的场景,例如地形阴影,使用 VSM 比较合适。
为了更好的解决漏光问题,就需要更好的表示更复杂的深度分布。EVSM(exponential VSM)以及 moment shadow mapping 等方法通过记录更高阶矩,即深度的更高次幂,来实现这个目标。10
非传统算法
光线追踪(raytracing)
光线追踪是从摄像机发出光线,然后递归追踪光线与场景中物体的交点,完成反射、折射等,最终计算出当前像素的颜色。上图中的 Whitted 光线追踪使用 shadow ray 和传统着色方式进行着色,而现代的路径追踪采用蒙特卡洛积分,实现基于物理的渲染(PBR)。
由于现代 3D 模型都非常复杂,光线与物体求交是最耗时的计算,于是 BVH(bounding volume hierarchy)等加速结构试图解决这个问题。即便如此,光线追踪的计算开销仍然很大,因此传统上只用于离线渲染。12但随着 GPU 的发展,近年来 GPU 开始加入硬件加速的光线追踪,例如 NVIDIA 的 RTX 系列,使用 RT Core 来加速 BVH 的遍历。DXR(DirectX Raytracing)和 Vulkan 等 API 也都提供了光线追踪的支持。
光线追踪可以解决传统算法的很多问题,例如自遮挡、阴影锯齿等,还能实现自然的软阴影和支持半透明物体。但即使有硬件加速,采样的光线数量依然有限,需要通过自适应采样等方法来提高阴影质量。如上图所示,图一使用了每像素 4 样本(spp),有很明显的走样;图二使用了自适应采样,使用 0~5 spp,可以看到阴影质量有明显提高;图三使用了 256 spp,作为参考。在 GeForce RTX 2080 Ti 上,三图的帧生成时间分别是 3.6 ms、2.7 ms、200 ms。6
混合光线追踪(hybrid raytracing)
在不支持光线追踪的硬件上,使用 BVH 进行光线追踪的开销比较大,有人提出使用 primitive map 来实现光线追踪阴影。如上图所示,primitive map 也是从光源计算的,不过每个像素保存的是覆盖这个像素的图元(primitive)列表,由计数器和索引列表组成。N 和 d 参数则需要根据具体模型进行调整。在生成阴影时,从当前像素发出 shadow ray 与 primitive map 中的三角形求交。
但为了统计与每个像素相交的图元,需要保守光栅化(conservative rasterization)的支持。如图所示,只有当开启保守光栅化之后,才能保证所有相交的像素都被枚举到。硬件保守光栅化需要 DirectX 12/11.3 以及硬件支持,如果不支持也可以使用软件实现。
如图所示,混合光线追踪在不同的硬件上表现良好,还能和 PCSS、FXAA 等算法结合使用。另外软件实现保守光栅化的开销也不大。8
距离场软阴影(distance field soft shadows)
距离场由距离函数定义,其输入是空间中的任何点,输出是该点到任何物体的最短距离。一般使用的是 SDF(signed distance field),即距离可以是负数,表示点在物体内部。SDF 可以很容易实现物体融合等效果。
通过光线行进(ray marching)或称为球追踪(sphere tracing),可以实现光线与物体求交。这可以视为一种特殊的光线追踪。其思想是,SDF 返回的距离是向任何方向移动都安全的距离,称为「安全距离」。向光线方向移动安全距离,然后不断迭代,直到发散或者收敛。
同理,在光线行进的过程中,可以计算当前位置的「安全角度」,用它来近似阴影强度即可。在实际实现中,可以使用
$$\text{min}(\frac{k\cdot\text{SDF}(p)}{p-o},1.0)$$
来计算,其中 $k$ 用来决定阴影的软硬程度,$k$ 越大则阴影越硬。
SDF 软阴影的计算效率高,质量也较好(类似光线追踪)。但需要预计算 SDF,而且需要大量的存储开销,目前技术仍然不太成熟。13
总结与展望
本文介绍了一些常见的实时阴影算法的原理和优缺点。目前最流行的算法是阴影映射,其开销比较固定,硬件实现效率高,而且可以通过 PCSS 等方法实现软阴影。但阴影映射也存在由于屏幕像素与光源采样不匹配造成的走样问题,以及阴影偏移量的问题。
阴影体算法没有走样和偏移问题,但开销不稳定是严重的不足。尽管近年来的研究给出了一些改善,但不足以在实时渲染中广泛使用。
随着实时光线追踪硬件的逐渐普及,基于光线追踪的阴影是一个大趋势。目前的实时光线追踪仍然需要结合传统算法,通过自适应采样来平衡质量和性能。而 SDF 软阴影则是一个很有潜力的方向,但目前仍然不够成熟。
- 1.关于图形硬件的历史,欢迎阅读我的博客上的相关系列文章 ↩
- 2.J. Blinn. “Me and My (Fake) Shadow”. In: IEEE Comput. Graph. Appl. 8.1 (Jan. 1988), pp. 82–86. issn: 0272-1716. ↩
- 3.Franklin C. Crow. “Shadow Algorithms for Computer Graphics”. In: SIGGRAPH Comput. Graph. 11.2 (July 1977), pp. 242–248. issn: 0097-8930. doi: 10.1145/965141.563901. url: https://doi.org/10.1145/965141.563901. ↩
- 4.Ulf Assarsson Elmar Eisemann Michael Schwarz and Michael Wimmer. Real-Time Shadows. CRC Press, 2012. ↩
- 5.Ronen Gvili. Real-Time Hard Shadows. 2004. url: https://www.cs.tau.ac.il/~dcor/Graphics/cg-slides/hard-shadow.pdf. ↩
- 6.Michael Wimmer Jakub Boksansky and Jiri Bittner. “Ray Traced Shadows: Maintaining Real- Time Frame Rates”. In: Ray Tracing Gems: High-Quality and Real-Time Rendering with DXR and Other APIs. Ed. by Eric Haines and Tomas Akenine-Möller. Apress, 2019. Chap. 13. ↩
- 7.Hubert Nguyen. “GPU Gems 3”. In: Addison-Wesley Professional, 2007. Chap. 8. ↩
- 8.Jon Story. Hybrid Ray-Traced Shadows. 2015. url: https://developer.download.nvidia.cn/assets/events/GDC15/hybrid_ray_traced_GDC_2015.pdf. ↩
- 9.tkstar. 实时阴影技术——概述. 2022. url: https://zhuanlan.zhihu.com/p/358766145. ↩
- 10.Naty Hoffman Tomas Akenine-Moller Eric Haines. “Real-Time Rendering”. In: 4th ed. CRC Press, 2018. Chap. 7. ↩
- 11.Lance Williams. “Casting Curved Shadows on Curved Surfaces”. In: SIGGRAPH Comput. Graph. 12.3 (Aug. 1978), pp. 270–274. issn: 0097-8930. doi: 10.1145/965139.807402. url: https://doi.org/10.1145/965139.807402. ↩
- 12.Lingqi Yan. Ray Tracing 1. 2020. url: https://sites.cs.ucsb.edu/~lingqi/teaching/resources/GAMES101_Lecture_13.pdf. ↩
- 13.Lingqi Yan. Recap of CG Basics. 2021. url: https://sites.cs.ucsb.edu/~lingqi/teaching/resources/GAMES202_Lecture_02.pdf. ↩
- 14.郑军将. “实时阴影生成技术研究”. MA thesis. 杭州电子科技大学, 2011. ↩