体积阴影(Shadow Volumes)生成算法

我是3D初心者:) 不是技术百科全书,在blog上想说什么就说什么:) 下面以最快的速度简单谈谈阴影生成技术,目前普遍采用的一般有三种:Planar Shadow、Shadow Mapping和Shadow Volume,前者类似投影,计算最简单,缺点只能绘制抛射在平面上的阴影;Shadow mapping利用站在光源处所沿光源法线看去所生成的深度图来检测场景中的体象素是否处于阴影中,缺点是光源与物体位置相对固定、且在极端情况下计算精度差,不太适合精确到象素的动态光阴场合;Shadow Volume是目前最适合精确表现动态光阴场景的技术,适用性最广,其典型的适用范例便是Doom 3,不足在于阴影体积引入了额外的顶点和面,加大了存储和处理强度,同时渲染出的阴影比较硬,如果要实现软阴影,仍需其他技术配合。

这里我们快速往前跳,Perspective projection、Depth test、Stencil buffer等概念就不多谈了。Shadow Volume的一般步骤为:生成阴影体积(Mesh)和阴影渲染,阴影体积生成算法又分两种,一种是D3D SDK sample中所采用的方法,先分离/插补物体面,然后用Vertex Shader加速,对复制所得的未拉伸的阴影体积进行沿光线方向的Extrusion;另一种则是多数tutorial中常用的Software Renderer方式,首先拣选出所有向光面,将每一个向光面的所有边均加入一个list,每加入一个边时,如果list中已经存在相同的边了,则不加入,同时将list中相同的边删除,这样处理完所有的向光面后,list中便只会剩下物体向光面轮廓的边信息,将该轮廓沿光矢量方向进行延伸,然后完成侧面Quad插补和前后封口(如果使用的是Z-fail)即可。而对于阴影渲染,则一般用4个pass,具体有Z-Pass/Z-Fail两种做法:

Z-pass算法
1. 先关闭光源,将整个scence渲染一遍,此时一片漆黑,但获得了深度值
2. 关闭深度写,渲染阴影体的正面,深度测试通过则模板值加1
3. 然后渲染阴影体的背面,深度测试通过则模板值减1
4. 最后模板值不为0的面就在阴影体中,开启深度写
5. 用模板手法重新渲染一次加光的scence即可,让阴影部分为黑色
6. 其致命缺点是当视点在阴影中时,会导致模板计数错误
7. 同时,有可能因为Z-near clip plane过近而导致模板计数错误

Z-fail算法(John Carmack's Reverse)
1. 先关闭光源,将整个scence渲染一遍,获得深度值
2. 关闭深度写,渲染阴影体的背面,深度测试失败则模板值加1
3. 渲染阴影体的正面,深度测试失败则模板值减1
4. 最后模板值不为0的面便处于阴影体中,开启深度写
5. 用模板手法重新渲染一次加光的scence即可,阴影部分不渲染色度
6. 注意,该算法要求阴影体积是闭合的,即需要前后封口
7. 该方法不是没有缺陷的,有可能因为Z-far clip plane过近而导致模板计数错误

值得注意的方面
1. Z-pass由于不用封口,因此速度比Z-fail快,但存在处理不了的情况
2. Quake 3貌似使用的是z-pass shadow volume和planar shadow
3. 为保证足够robust,必须确保z-near/z-far中至少一个不出问题,Nvidia的论文推荐采用z-fail,用w=0来实现无穷远的z-far平面
4. 记住使用Z-fail一定要封口,而且阴影体积的每个面的法线必须正确地指向物体之外,包括front cap和back cap
5. 上面给出的是简单过程,阴影很硬,可以稍微变通一下:先以环境光渲染一遍,然后计算模板,再用模板渲染打光的scene,最后以alpha blend加深阴影

下面开始大面积地贴图,首先来看个Z-fail的具体例子(z-pass简单,就不举例了):从外面看一个面向光源的单面的情况。图中,虚线框起来的为阴影体积,圆形为点光源,其中,面ADFB和面DEF是back faces,而面ABC、ACED和CBFE则是三个front faces(注意这里顶点顺序用的是左手系),为说明问题(主要是为了说明在计算阴影体积时如何避免z-fighting),我故意把面ABC画在了黄色三角形下面一点点的位置,而且其中的灰色阴影是与平面共面的,而面DEF则处于平面下面一点点的地方。

a.png

首先进行2个back faces的depth test:粉红色的是stencil buffer中因Z- fail而加1的区域;然后进行3个front faces的depth test:亮绿色的是stencil buffer中因Z- fail而减1的区域

b.png

最后红绿区域正负相抵,stencil buffer中只剩下平面上的灰色三角区域中的值不为0,即原图阴影所在的位置。上面是个从单面外看的例子,值得注意的是,单面与两个背靠背双面的情况是不一样的,那么后者的计算结果是不是也是正常的呢?继续看图:

1.png

上图的红、绿细线分别表示两个背靠背单面,而粗线则构成了体积阴影,红粗线、绿粗线分别表示红面、绿面沿光线方向的的投影,一般而言,back cap处于无穷远处,而front cap则值得注意,为避免z-fighting,它的位置应该是在红面之后、绿面之前,也许你会说:“hey, 这里的front cap完全可以直接等于红面嘛”,是的,当在eye看来红面是背面时的确可以这么做,可当eye位置变化、红面不再是背面时,简单的以红面为front cap便会导致z-fighting。从图上可看出,当eye处于阴影内部时,模板计数也是正确的。下面再来看看仅有一个单面、且处于阴影内部的情况:

2.png

只要front cap注意了z-fighting的情况,其计算结果也是正确的,有意思的是,实际上当eye处于阴影内部时,在back culling的时候,三角形的背面便已经被去除了,因此在实际渲染时,在三角面的背面处并不会存在其深度信息,但即便如此,z-fail的模板计数仍是正确的。

3.png

最后,再来看一个计算方块的阴影的例子,可以看出,当front cap为背面时,阴影体积的计算很简单,无需考虑其z-fighting的情况,直接用方块的向光面作为front cap即可;但当front cap为正面时,还是面临着位置需要微调的问题。

总结一下,用z-fail方式来计算没有厚度的面的阴影和具有厚度的物体的阴影还是有点微妙区别的;在计算阴影体积时,要注意避免front cap处z-fighting的情况,稍微调整一下front cap的位置,即将front cap沿光线方向稍微向后挪那么一点点;或者是.... 等等,还有一种更简洁优雅的办法,即将Z-fail算法中的"Depth Test失败"理解成象素深度">="所在位置的深度而不仅仅只是">",将ZBufferFunction设为Campare.Less而不是Campare.LessEqual,这样的话便完全避开了z-fighting的问题,微调什么的都可以免了,无论是面还是物体,在任何情况下都可以直接用物体向光面作为front cap....faint...快写完了才考虑清楚...白废话了那么多....无比郁闷-_-b....收工.

posted @ 2007-01-04 19:13  neoragex2002  阅读(6894)  评论(4编辑  收藏  举报