MIT 6.837:Real-time Shadow 实时渲染阴影

MIT 6.837:Real-time Shadow 实时渲染阴影

在实时渲染中,全局光照有光照贴图来减少计算量,而阴影也是一个需要考虑的问题。除此之外,由于光源不总是理想点光源,有一定面积的光源会产生软阴影现象,如何实现软阴影也是一个需要研究的问题。

软阴影

点光源会在物体之后形成一块光照无法照到的区域,这是物体的阴影。由于阴影中的任何一点都没有光线射入,其阴影的亮度是一致的。

58 - MIT 6.837:Real-time Shadow 实时渲染阴影

而对于有面积的光源来说,它照射的物体下,会分成三个区域:本影(完全不能被照亮)、半影(只被部分光源的面积照亮)和完全被照亮的区域。其中,半影部分中也有从最暗到最亮的过渡。

59 - MIT 6.837:Real-time Shadow 实时渲染阴影

当使用面光源照亮一个物体时,其后方形成的阴影边缘不再锐利,而是存在亮度缓慢过渡的半影区域,这就是所谓的“软阴影”现象。

60 - MIT 6.837:Real-time Shadow 实时渲染阴影

要实现这种软阴影,需要在每一个要渲染的像素上向光源面积上随机追踪多条光线,根据被遮挡的比例来确定其亮度。这将成倍地增加需要追踪光线的数量,因此可以考虑引入一些优化,例如可以不判断 tMin 条件,遇到一个遮挡物就停止追踪(但会引入一些错误被遮挡的情况);由于光线的分部相对集中,可以先测试之前光线的遮挡物体。

阴影的预处理方法

平面阴影

最简单的情况,对要生成阴影的平面做一次该物体的投影。无法处理自阴影,且很多时候阴影并不投射到平面上,效果不佳。

61 - MIT 6.837:Real-time Shadow 实时渲染阴影

投影贴图阴影

一个观察是,计算阴影与计算屏幕投影是一个类似的过程。屏幕投影时将可见的物体深度记录下来(Z 缓冲区),将其颜色作为像素值;而计算阴影时,则是从光源的角度取最近的物体,对应光线上更远的物体则被遮挡而产生阴影。

因此,我们可以从光源的视角看过去,得到某一几何体遮挡的区域,保存为贴图纹理,并将其按投影关系应用到该几何体背后的平面上。

62 1024x362 - MIT 6.837:Real-time Shadow 实时渲染阴影

这是一个比平面投影更泛用的方法,但需要确定遮挡物体和阴影投射物体,也无法处理自遮挡问题。除此之外,由于纹理本身是图片,也存在分辨率的问题,导致投影结果中出现锯齿。

阴影贴图

这是一种和投影贴图不同的做法,注意它们之间的区别。

我们曾提到,可以利用阴影和投影的相似性转化为光源视角的观察,而投影贴图中在这个观察中得到了遮挡物在平面上的透视投影。另一种思路是借鉴 Z 缓冲区的做法,在贴图中保存场景中每个交点到光源的距离,而阴影在对应光线中距离更远的点处产生。

63 - MIT 6.837:Real-time Shadow 实时渲染阴影

利用这种思路,判断一个点是否在阴影里的计算变为,将物体与视线的交点转换到光源视角的坐标系中,再用透视投影变换找到阴影贴图中的像素,并确认交点的距离是否比贴图中的距离更远。

这种方法可以正确地处理自遮挡问题,但仍然无法解决分辨率导致的锯齿问题,且还会存在超出贴图区域和精度问题。

如果使用一张平面贴图来实现阴影贴图,其覆盖的区域是有限的,无法正确处理区域外的阴影计算。一种方法是使用球形贴图来代替平面贴图。

精度问题一向是图形学中可能遇到的棘手问题,在阴影贴图中主要出现在判断距离处,由于计算精度损失等,判断距离时最好加入一个偏移量 epsilon,但它的取值选取需要以具体情况而定。

63 1024x404 - MIT 6.837:Real-time Shadow 实时渲染阴影

另一问题是由于贴图分辨率有限,投影时对贴图进行了上采样,导致了锯齿问题,锯齿在投影角度小时会更加明显。一种方法是对上采样后的阴影进行滤波,滤波的对象是是否在阴影面积中,利用一个低通滤波器使阴影的边缘变得平滑,卷积核的大小越大,突变边缘的过渡段越宽。

可以结合阴影贴图与投影贴图实现类似投影灯的效果,如图所示。

64 1024x422 - MIT 6.837:Real-time Shadow 实时渲染阴影

阴影的实时方法

一种实现实时阴影的方法依赖阴影体积与模板缓冲区(stencil buffer)。

模板缓冲区是用来对像素做标记的缓冲区,其本质是一个二维整数数组。

一种模板缓冲区的用途是实现是实时镜面反射。在正常绘制结束后,将屏幕上处于镜面中的像素在模板中标记为 1,并在这些像素中绘制反射的几何体。

65 - MIT 6.837:Real-time Shadow 实时渲染阴影

而阴影体积是一种表示阴影的方法。在点光源下,被一个面遮挡的部分体积类似一个台体,例如上图中被四边形遮挡的体积为一个没有底的四棱台,被这 5 个平面包围的部分可以认为被遮挡。

测试一个点是否在阴影里的方法有多种。最简单的办法是测试是否在 5 个面包含的面里,这种方法需要表示出所有阴影体积,且需要对所有光源和阴影体积做计算,开销很大。

另一种方法利用 Z 缓冲区。在追踪光线时,直接在屏幕空间绘制阴影体的各个面,绘制朝相机的面时测试 Z 缓冲区,通过则使模板缓冲区加 1;绘制背对相机的面时则减 1。由于测试 Z 缓冲区时阴影中的面会遮挡阴影体的背面,这些面对应像素的模板缓冲区会保留加上的 1,而未被遮挡处取值会被减回 0,这样可以快速把阴影中的面找出,并只在那些未被遮挡的像素处计算光照。这一方法的伪代码如下

Initialize stencil buffer to 0
Draw scene with ambient light only
Turn off frame buffer & z-buffer updates
Draw front-facing shadow polygons
  If z-pass → increment counter
Draw back-facing shadow polygons
  If z-pass → decrement counter
Turn on frame buffer updates
Turn on lighting and redraw pixels with counter = 0
66 - MIT 6.837:Real-time Shadow 实时渲染阴影

这种方法无法处理相机在阴影体中的情况,如上图所示,未被遮挡的点模板值反而为 -1。可以通过先判断相机是否在阴影体中来修正模板值,或裁剪阴影体,但这些方法开销较大。

一种比较好的做法被称作 Z-Fail,它将深度测试通过改为失败,先渲染背面,失败则加 1;再渲染正面,失败则减 1。这样,阴影中的部分将保留背面的加 1,而阴影外的部分则依然被抵消。此外,这种方法能够正确处理相机在阴影内部及外部两种情况。

67 - MIT 6.837:Real-time Shadow 实时渲染阴影

这一方法相当于从相机的对向无穷远处往相机投射测试光线,并执行和 Z-Pass 方法类似的过程。在实际的计算中,无穷远是无法实现的,通常选择在远裁剪平面裁剪阴影体积,以避免阴影体积超出裁剪区域造成的问题。

阴影体积可以通过仅保留轮廓边缘(正投影与背投影结果中都包含的边缘)来减少阴影体的面数来优化。

使用阴影体积实现实施阴影也有一些缺点和限制,例如阴影体积本身是几何体,会增加场景的复杂度;阴影体面有时狭长,光栅化开销大;模板缓冲区的值范围有限制,复杂度的场景可能导致计数溢出;如需要使用轮廓边缘优化,则物体中间不可镂空等。

总结

68 1024x465 - MIT 6.837:Real-time Shadow 实时渲染阴影

参考资料



发表回复

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据