Shadow
下面是一张wiki上的图片,左边的场景描述了光照环境中的阴影表现,墙上和地板上有逆光的球体阴影。右边的图表现的是这个阴影的实现方式-shadow

上面的图说明的是,我们在应用中,首先必须根据将造成阴影(即遮掩体)的物体生成一个阴影体。有了这个阴影体后,我们利用3D渲染引擎的stencil buffer和z buffer,就可以渲染出我们需要的阴影。我在这里着重指出的是,我们会用到stencil buffer和z buffer,两者缺一不可。有相关的文章会指出shadow volume的名字是stencil shadow volume,这个名字并不精确。事实上,这种生成阴影的方法,必须用到z buffer。读完本文读者自然会明白。
我们下面先列出shadow volume渲染阴影的步骤:
1、根据物体和光照位置计算出我们上面提到的阴影体;
2、在不使用stencil buffer的状况下,渲染接受阴影的物体;
3、启用stencil buffer,渲染两次第一步生成的阴影体。两次渲染中,面的顶点顺序相反,分别用DirectX的INCR(对stencil entry实行加1)和DECR(对stencil entry实行减1)的方式;
4、渲染产生阴影的物体,这次依然启用stencil buffer,不过给定的值是stencil buffer的初始值;
5、关闭stencil buffer,渲染其他物体。
在上面的5个步骤中,我们都启用z buffer,除了第3步,关闭z buffer的write功能外,其他都可以write。网络上和书本上可以看到的其他shadow volume例子中,第4步中的方式大多数跟我的不一样,不一样的地方在于它们没有启用stencil buffer。这样其实会产生bug,一般只要你把例子的渲染影子的色彩改成亮色再给场景加入环境光就可以看到,该现象表现为发出阴影的物体身上,会有影子的颜色。说明在stencil buffer中,在第3步后,除了我们要的阴影部分的位置之外,还有其他位置也有一样的值,这个是我们不需要的。具体原因在下面文字中可以找到。
有了上面的5个步骤,我们逐步来看每一步在计算机内部发生的状况,了解这些情况后,shadow volume的核心思想也就很清晰了。
1、此步是每一帧都需要做的,因为随着物体的移动、转动,投出的阴影会一直变化,因此我们的阴影体本身会一直变化。
2、这一步使用z buffer,但是不使用stencil buffer,因此,渲染完毕后,z buffer中有阴影接受物体的相关z值,但是stencil buffer中依旧是开始的初始化值,在示例程序中,为0;
3、此步使用z buffer,但关闭z buffer的写(write)功能,也就是说,z test依旧进行,意味着绘制阴影体的时候,如果在刚才的阴影接受体后面的阴影体,将不会绘制(也就不会写相关值到stencil entry)。stencil buffer是必须开启的,因为要的就是在两次绘制阴影体的过程中,让stencil buffer记录下阴影的位置。这里的z buffer和stencil buffer配合可以说非常的经典:在阴影接收体后面的阴影体,因为z test的存在,不会影响stencil entry;在阴影接收体前面的阴影体,我们用DX中的INCR和DECR功能,正好一加一减,在stencil buffer中留下除跟阴影接收体相交的部分(这个说法不是很准确,因为在跟物体重合的部分,也有值留下,不过我们有方式解决这个问题);
4、渲染产生阴影的物体,这一步必须记住也开启stencil buffer,并且把参考值设置成stencil buffer初始化的值,这样的话,属于物体部分的stencil entries,会在这一步重新被重置。经过这一步,stencil buffer中的除初始值外的值,在形态上表现出来就是在阴影接受体上的阴影。
5、该步就是正常的渲染就可以了。
在示例程序中,我们如果改变阴影(那个跟屏幕一般大小的QUAD)的颜色,再把镜头拉远的话,实际上是可以看到在平面的下方有物体的相似体,这个是我们生成的阴影体的边缘行为,无法通过DECR和INCR来消除,上面我们需要通过绘制物体的时候重置相关的stencil entries也是这个原因。如果这个部分我们需要用一定的方式来解决的话,剪裁面是一种方式。不过在我们的这个程序中,利用跟背景一样的色彩也是个方式。还有,镜头位置控制也是个方式,特别是在游戏中,在那个平面下的部分可能本身就已经是地下了,玩家是不可能看到这个地下的。
附录:
1、示例代码工程, Visual Studio 2008编译

浙公网安备 33010602011771号