作者:i_dovelemon
来源:CSDN
日期:2014 / 10 / 20
主题: Shadow Volume
引言
游戏中,往往有很多的光影效果。想要营造出很好的光影效果,物体在光源照射下的阴影就必不可少。本节内容就向大家讲述如何构建阴影。
Shadow Volume
构建阴影的方法有很多,常用的两种方法是Shadow Volume和Shadow mapping。本片博文将向大家讲述使用Shadow Volume来构建阴影的方法。
Shadow Volume的算法是由Frank Crow在1977年提出的。一个Shadow Volume将一个3D场景划分成两个部分,分别是阴影区域和非阴影区域。这种算法,自从Doom 3yi依赖,就变的非常的出名。John Carmack在前人的基础上,提出了一种改进的方法,使得Shadow Volume方法在游戏业十分的出名。总的来说,就是通过Stencil buffer来标记Shadow volume所划分的阴影和非阴影区域。从而在当前的Color
Buffer中进行绘制,在Stencil Buffer中标记了某块区域是阴影,那么我们就在Color Buffer的对应区域绘制成黑色阴影。
说了这么多的Shadow Volume,那么Shadow Volume到底是什么了?它的具象表现又是怎么样的?为了理解Shadow Volume的概念,我们先来了解下,为什么物体会有阴影。(额。。。,这个很明显吧!!!)
我们知道,光线是直线传播的。如果光线遇到了不可穿越的阻挡物,那么光线无法将被物体遮挡的部分进行光照。在没有光照的情况下,自然这块区域就变成了黑色,从而形成了阴影。
我们来看一张图:
从上图中我们就可以看出,Shadow Volume是值由光源发出的线与被照射物体的边缘顶点连线,然后截取一段,这样的一段几何体,我们就称之为Shadow Volume。物体本身正对着光源的那一个面,我们称之为Front Cap,而背面截取的那一段,被称为Back Cap。这样,就唯一的定义了一个Shadow Volume了。
Z-PASS算法
我们假设,我们已经根据物体构建出来了这样的一个Shadow Volume。有了这个Shadow Volume之后该怎么做了?
想想我们之所有提出这个Shadow Volume的原因,是想要将我们的场景划分称为两个部分:阴影区域和非阴影区域。也就是说,由这个Shadow Volume所构建出来的空间,应该就是不能够被照亮的,也就是说,它就是阴影部分。所以,我们能不能把这个阴影部分通过某种方法标记出来了?读者可能想到,好吧,既然Shadow Volume就是阴影部分,那么我们直接将这个Shadow Volume当做一个模型,并且给它赋予黑色,然后直接画出来,如何?的确,通过这样的方法,阴影的确画出来了。但是,同样的,空间中没有任何物体存在的部分,由于你将Shadow
Volume当做一个物体模型,使得这块部分也被画出来了。读者啊,请仔细的观察下现实世界中的阴影,阴影只会在物体上显示出来,空间本身并不会显示阴影。所以这样的方法,肯定是不行的。但是它提出了一个想法,通过绘制Shadow Volume,来对图像进行标记,标记出那些存在物体,并且在Shadow Volume中的阴影像素部分。好了,这个就是Shadow Volume方法能够实现的原理所在。
那么,通过什么样的方式来进行标记阴影像素部分了?这里就需要用到3D图形库所带有的特性了。
DirectX支持使用Stencil buffer。这里的算法就要借助于Stencil Buffer来标记阴影部分。
下面是使用Z-Pass算法绘制阴影的步骤:
1.正常的使用环境光和发射光来绘制当前场景,从而获取到场景的Depth信息。
2.开启Stencil Buffer,关闭Depth buffer和Color buffer绘制所有的Shadow Volume(我们不是在场景中显示Shadow Volume,只是用来标记,所以关闭掉Color Buffer和Depth Buffer)。绘制Shadow Volume的时候,先绘制正对视点的那些面,并且设置,当Depth Test通过的时候,对应的Stencil Buffer值加1.然后绘制Shadow
Volume的背面,并且设置当Depth Test通过的时候,对应的Stencil Buffer的值减1。
3.使用屏幕渲染(2D渲染,就是对屏幕直接画一张黑色的图)的方法,对屏幕使用黑色渲染,并且只对那些Stencil buffer中的值大于等于1的像素进行渲染。
通过上面的方式,那些被标记了的像素位置的Stencil Buffer的值都是大于等于1的,然后在屏幕渲染之后,就会变成了黑色部分,从而形成了阴影部分。
读者,看到这里可能会和我一样的去想,为什么这样做了以后就标记成功了?原理是如何的了?
我们来讨论下这种算法的依据是什么。
首先,存在这样的一个事实。当我们从视点,也就是眼睛的部位发射一条射线到一个物体上去。如果这条射线,穿过了Shadow Volume的前面(Front face),并且也穿越了背面(Back Face)。那么这个物体自然就是在Shadow Volume外面的。如果值仅仅的穿越过Front Face,而没有穿越过Back face,那么也就是说这个物体就在Shadow Volume中,即这个物体是需要接受阴影的,我们就来标记它一下。这里讨论的是一个Shadow
Volume的情况,当多个Shadow Volume同时出现的时候,这个事实依然是正确的。如果读者对文字不清楚,那么来看下下图:
(本图来自于网络)
从上图中,我们可以看到,只要穿越了Shadow Volume的前面我们就+1,穿越了后面就-1。而在Shadow Volume里面的值都是大于等于1的。在外面的值刚好是0。很容易理解了吧!!!
基于这个事实,我们就能够想到我们首先将正常的场景绘制出来,那么整个场景的Depth map就有了。每一个像素对应的Depth Value我们都知道了。为了模拟出来上面讨论的那种射线穿越过前面就+1,穿越后面就-1的特性,我们并不是真的发出射线来进行判断(虽然理论上的确可以做到,但是那要消耗太大了,套用一个网友的话来说就是,仅仅是一个效果而已,开销太大的算法完全没有使用的必要),而是通过一种很有技巧性的方法来。
我们现在已经有了Depth map的值,那么我们先绘制所有Shadow Volume的front face。在进行绘制的时候,自然就会进行Depth Test(Z-Test,如果读者对这些概念都不懂,那么本篇文章不适合读者。),如果Depth Test通过了,那么就表示当视点的当前位置,Shadow volume的front face中通过Depth Test的位置是在物体的前方的,也就是说,你发射射线到这个物体的话,一定会穿越我,所以,我就+1,表示射线穿越过我了。这种行为就是上面的”绘制front
face的时候,一旦Depth Test通过了,Stencil Buffer的值就+1“。同样的,对于Shadow Volume的背面,如果它也通过Depth Test,那么就表示这个点也是在物体前面的,射线也会穿越它,所以就-1。这种行为就是上面的”绘制shadow volume的back face的时候,一旦Depth Test通过了,stencil buffer的值-1“。好了,通过这样的方法,我们就能够将最终显示的图像的Stencil buffer全部计数完毕。那么,当stencil buffer的值大于等于1的时候,也就是说该像素点是阴影。如果stencil
buffer的值为0,就表示该点不是阴影或者不存在物体,我们不做任何处理。
哈!多么高技巧的手法啊!真佩服想出这种算法的人,通过这样的技巧,大大的减少了发射射线来的计算开销。果然,算法无止境,只是想到与想不到的区别。
Oops!
上面的方法,已经能够实现大部分的阴影了(wow,大部分???还有没有办法实现的情况???)。但是,读者发现没有,上图中,都是假设视点在Shadow Volume之外,如果视点在Shadow Volume中了(这种情况常常出现,躲在Shadow里的人们啊!!)?读者自己试一下,会发现在绘制Shadow Volume的时候,由于裁剪会把Shadow Volume的前面给裁剪掉,这就造成了,没有绘制这些面,那么stencil
buffer中的值自然就缺少了计数。所以会出现不正常的阴影。
(来源与网络)
从上图,读者就可以在Shadow volume里面的物体,它对应的stencil buffer的值,并不是大于等于1而是0。所以这就导致错误了。
Z-FAIL算法
前面提到,John Carmack(3D游戏鼻祖,读者可自行搜索)在Doom3中,使用了一种新的方法来改进了这样的缺陷。我们称之为Z-FAIL算法。这个算法的整体思路还是和上面的算法一致。只是在对那种行为的模拟上进行了改进。上面讨论说,如果视点在shadow volume里面的话,从视点发射射线遇到物体的话,会出现少计数的情况。那么,也就是说视点如果总是在shadow volume外面的话,那就能保证,这个算法是正确的,对吗?
而视点不可能总是在shadow volume外面,所以我们来虚拟一个视点,这个视点总是存在所有的shadow volume的深度最深处。如果不理解的话,来看看下面的图片:
这样,我们在通过这个virtual eye来进行上面的算法,是不是就总是成功的了,对吧!而这样进行的话,就必然要与原来的运算相反。也就是说,对于virtual eye的front face实际上就是shadow volume的back face,而对于virtual eye的back face就是shadow view相对于真实eye的front face。同样的对于virtual eye来说的depth
test通过,对真真的eye来说实际上是depth test失败。好了,这样就能得到下面关于Z-FAIL的算法了:
1.使用环境光和发射光绘制正常的场景,获取depth test的值。
2.开启stencil buffer,关闭depth write, color buffer,先绘制shadow volume的背面,并且设置当depth test失败的时候,stencil buffer的值+1。然后再绘制shadow volume的前面,并且设置depth test成功的时候,stencil buffer的值-1。
3.使用屏幕渲染,渲染屏幕,绘制阴影。
好了,聪明吧!!!使用了一个等价代换的思想就能够将原本不能成功情况,变的成功了。这种算法,无论视点在何处,都能够成功的绘制出来。
读者啊,请注意,这里说明的virtual eye的思想,只是我对它这个算法的理解,John Carmack是如何想出Z-FAIL算法的,我并不知道,这里仅仅提供给大家参考,能够通过这种方法理解,最好不过了。
Ghost Shadow
在作者自己试验编写这个Demo的时候,总是会出现一个莫名其妙的Shadow,如下图所示:
上面的阴影是正常的,但是下面不知道怎么回事又出现了一个阴影。作者苦苦调试,改代码,弄了一整天,最后才在ShaderX系列文章中,关于Shadow Volume的一章中找到的原因所在。
原来构建Shadow volume的时候,你的Shadow volume需要无限延伸,也就是说,在绘制shadow volume的时候,它的背面肯定有一些被远裁剪面给裁剪掉了,这就导致了一部分的stencil buffer没有进行计数,从而出现这种情况。在ShaderX的文章中,提出了一种改进的策略,读者可以自行去了解。这里作者就偷下懒,没有将shadow volume构建的无限延伸,而是在深度范围以内,这也就没有问题了。
读者啊,这些方法都是通过计算机模拟出来的,并不是真实世界阴影形成的方式。他们只能够在一些条件情况下可以使用,可能在另外的环境,比如ShaderX中提到的Terrian地形上的阴影,就又会出现问题。所以,读者慢慢的挣扎吧!遇到这些情况,慢慢想办法来解决满足它。总有一天,我们完全可以利用计算进行真实情况下的阴影模拟。(作者傻帽了,现在好像就可以了,使用Ray tracing的方法,就能够模拟出来真实的情况了,可惜的是,这种算法计算量太大,现在的计算机硬件技术难以事实的计算出来,如果哪一天出现了能够实时的支持Ray
tracing, Photo ,Radiace这些图形学算法的时候,游戏的画面将会发生质的改变啊!!!期待ing。。。。)
好了,下面是最终的截图:
这篇文章只是介绍算法的思想,至于实现,就需要作者自己去慢慢的探索了。文章也没有讲述如何构建Shadow volume,感兴趣的读者可以看ShaderX中的文章来了解。
相关链接:
http://blog.163.com/wmk_2000_ren/blog/static/138846192201019114117466/