这章全面讲述了用于实时阴影渲染中常见两种流派之一的阴影体(Shadow Volumes)技术,又称模板阴影(Stencil Shadows)技术,重点是得到正确的角度的情形,减少几何图形和填充率的消耗。
简单谈谈阴影生成技术,目前普遍采用的一般有三种:Planar Shadow、Shadow Mapping和Shadow Volume,前者类似投影,计算最简单,缺点只能绘制抛射在平面上的阴影;Shadow mapping利用站在光源处所沿光源法线看去所生成的深度图来检测场景中的体象素是否处于阴影中,缺点是光源与物体位置相对固定、且在极端情况下计算精度差,不太适合精确到象素的动态光阴场合;Shadow Volume是目前最适合精确表现动态光阴场景的技术,适用性最广,其典型的适用范例便是Doom 3,不足在于阴影体积引入了额外的顶点和面,加大了存储和处理强度,同时渲染出的阴影比较硬,如果要实现软阴影,仍需其他技术配合。
说实话,GEM中这篇文章写的挺难懂的,我还是以自己的角度,从网上等资料来理解下阴影体渲染。参考的一些文章链接如下:
https://blog.csdn.net/jxw167/article/details/65435329
https://blog.csdn.net/wang371372/article/details/41086435
阴影体渲染创建一个阴影体积,并仅在其外部的物体上处理照明,我们将使用模板缓冲区作为算法的关键组件, 因此名称 - Stencil Shadow Volume。阴影体积算法背后的想法是将光线减弱时创建的对象轮廓扩展到一个Volume中,然后使用一些简单的模版操作将该Volume映射到模板缓冲区中。 关键的想法是,当一个对象在Volume内(因此在阴影中)时,Volume前面的多边形会对对象的多边形进行深度测试,并且该Volume后部的多边形将失效相同的测试,或者说不参与测试。
我们将根据称为Depth Fail的方法设置模板操作,人们经常使用更直接的方法称为Depth Pass来实现阴影体积技术,但是其致命缺点是当视点在阴影中时,会导致模板计数错误,而Depth Fail的方法修复了该错误。
我们在左下角有一个灯泡,一个绿色的物体(称为遮挡物体),由于光而投下阴影, 在这个场景中也渲染了三个圆形的对象。 对象B被遮蔽,而A&C不是。 红色箭头限定阴影体积的区域(线的虚线部分不是它的一部分)。
我们首先将实际对象(A,B,C和绿色框)渲染到深度缓冲区中(从相机位置观察的)。 当我们完成后,我们可以获得最接近的像素的深度。 然后我们一个接一个地遍历场景中的对象,并为每个对象创建一个阴影体积。 这里的示例仅显示绿色框的阴影体积,但在完整的应用程序中,我们还将为圆形对象创建Volume,因为它们投射自己的阴影。 阴影体积是通过检测它的轮廓来创建的并将其扩展到无限远。 我们使用以下简单规则将该Volume渲染到模板缓冲区中:
1、如果在渲染阴影体积的背面多边形时深度测试失败,我们会增加模板缓冲区中的值。
2、如果在渲染阴影体积的前面多边形时深度测试失败,我们会减小模板缓冲区中的值。
3、在深度测试通过,模板测试失败情况下,我们什么都不做。
让我们看看使用上述方案的模板缓冲区会发生什么。
物体A:渲染阴影体的背面时深度测试失败(由于A的阻挡),所以模板缓冲值加1.渲染阴影体的前面时深度测试失败(由于A的阻挡),所以模板缓冲值减1,所以最后结果是对于物体A,起模板缓冲中的值为0.
物体B:渲染阴影体的背面时深度测试失败(由于B的阻挡),所以模板缓冲值加1.渲染阴影体的前面时深度测试成功所以最后结果是对于物体B,起模板缓冲中的值为1.
物体C:渲染阴影体的背面时深度测试成功,渲染阴影体的前面时深度测试也成功所以最后结果是对于物体C,起模板缓冲中的值为0.
请注意,到目前为止,我们还没有碰到色彩缓冲区。 当我们完成上述所有的操作后,我们再次使用标准的照明着色器渲染所有的对象,但是这次我们设置模板测试,使得只有模板值为零的像素才会被渲染。 这意味着只有对象A&C才能使其在屏幕上显示出来。
让我们看看如何把这个知识付诸实践。 正如我们前面所说,我们需要渲染当我们扩展遮挡物的轮廓时创建的体积。 我们所需要做的就是将轮廓边缘延伸到一个体积中, 这是通过为每个轮廓边缘从GS发射四(或实际上四角形拓扑中的四个顶点)来完成的。 前两个顶点来自剪影边缘,当我们沿着从光照位置到顶点的向量将边缘顶点延伸到无穷大时,生成其他两个顶点。 通过延伸到无限远,我们确保体积捕获位于阴影路径中的所有物体。 这个四边形如下图所示:
当我们重复这个从所有轮廓边缘发射四边形的过程时,会创建一个体积。 够了吗? 当然不。 问题是这个体积看起来像一个没有盖子的截锥体。 由于我们的算法依赖于检查体积的前后三角形的深度测试,所以我们可能会遇到一个情况,即从眼睛到像素的矢量可能没有通过正面或背面,如下图这个状况:
解决这个问题的方法是生成一个在两边封闭的体积。 这是通过创建一个正面和后面到体积(上图中的虚线)完成的。 创建前盖非常容易。 面向光的每个三角形都成为前盖的一部分。后盖需要将面向三角形的光的顶点延伸到无限远(沿着从矢量到每个顶点)并反转它们的顺序(否则所得到的三角形将指向体积内)。“无限”一词在这里已经提到过几次,我们现在需要确切地说明这是什么意思。 看看下面的图片:
我们看到的是从上面取出的截头锥体的图片, 灯泡发出一个穿过点p并继续无限远的光线。 换句话说,p扩展到无限远。 显然,在无穷远处,点p的位置是简单的(无穷大,无穷大,无穷大),但是我们不在乎。 我们需要找到一种光栅化阴影体积的三角形的方法,这意味着我们必须在投影平面上投影其顶点。 实际上这个投影平面是近平面。 虽然p沿着光矢量延伸到无穷远,但我们仍然可以在近平面上投射它。 这是通过从原点开始的虚线完成的,并在某处穿过光矢量。 我们要找到Xp,它是该矢量穿过近平面的点的X值。
我们将光矢量上的任何点描述为p + vt,其中v是从光源到点p的向量,t是从0到无穷大的标量。 从上图和三角相似之处可以看出:
其中n是近平面的Z值。 随着t到无穷大,化简公式如下所示:
所以这就是我们在近平面上如何找到“无限远”的投影,根据上述我们只需要乘以矢量(Vx,Vy,Vz,0)(其中V是从光源到矢量的点p的向量)来计算通过视图/投影矩阵并应用透视分割、
总结一下 Z-fail算法(John Carmack's Reverse)
1. 先关闭光源,将整个scence渲染一遍,获得深度值
2. 关闭深度写,渲染阴影体的背面,深度测试失败则模板值加1
3. 渲染阴影体的正面,深度测试失败则模板值减1
4. 最后模板值不为0的面便处于阴影体中,开启深度写
5. 用模板手法重新渲染一次加光的scence即可,阴影部分不渲染色度
6. 注意,该算法要求阴影体积是闭合的,即需要前后封口
7. 该方法不是没有缺陷的,有可能因为Z-far clip plane过近而导致模板计数错误
阴影体渲染要点总结
- 轮廓检测创建阴影体,封闭的面(但是在GEM这篇文章当时,轮廓检测一般只能运行在cpu中)
- Z-fail算法模板计数
- 渲染场景