之前写过ShadowVolume,但是那是老版本(DX8)的DXSDK中的sample。而老版本并不是一个很好的解决方案。而DX9中的ShadowVolume还算是比较可取的方案。在这里主要对DX9中的ShadowVolume中的重点难点进行梳理。详细的细节,必须参考sample中的代码。
DX9中的shadowVolume最大的特点是建立了阴影体网格,而不是之前DX8中的动态生成轮廓边集合。建立阴影体网格最大的好处就是通过GPU来分担原来由CPU负责的轮廓边识别。而且由于新的阴影体积总体算法的更新,使得轮廓边识别更容易。缺点也很显然多了很多内存需求。
我们还是先大体了解一下基本思路。
- 我们的原始模型建立阴影体网格。主要的方法是为每一个面都建立单独顶点,这样面与面之间不会形成牵扯,以便进行随后的背面移动。
- 其次在只是用环境光的条件下渲染场景到color buffer和z buffer里面。
- 再关闭z buffer和 color buffer,并开通stencil buffer,使用depth-fail方法将阴影体渲染至stencil buffer中。
- 最后根据stencil buffer中标记识别出不在阴影的区域,并对这些区域添加正常光照。
其中在sample的代码里面最难理解的区域是GenerateShadowMesh函数。而GenerateShadowMesh中最难理解的有好几处。
首先是ConvertAdjacencyToPtRep(),这个函数是获得用点表示的相邻信息。
比如下面的四边形
其中四边形由6个顶点组成,而不是我们常见的4个顶点,在这种情况下通过ConvertAdjacencyToPtRep()我们能得到一个和顶点个数相等的一个DWORD数组,数组的下标表示顶点索引,而数组内的值就是相应位置顶点的点索引。 比如加入上图有6个顶点。
则通过此函数会得到这么一个数组
DWORD PtRep[ 6 ] = { 0, 1, 2, 2, 1, 5 };
我们可以看到如果有顶点和以前出现的顶点相邻很近,则后出现的顶点用前者的索引。通过这个数组我们可以知道下标是3的元素和下标是2的元素的值一样,即 PtRep[ 2 ] == PtRep[ 3 ];
通过得到邻近信息,我们就可以借助它,识别出使用不同索引表示的相邻点,进而识别相邻边。比如在上图中我们怎么通过计算知道 边(V1-V2) 和边(V2-V3)相邻呢?
我们可以利用邻近数组PtRep,看看边(V1-V2)和边(v2-V3)是不是同一条边(暂且不管边的方向)。那么我们可以看到边 (v1-v2)是( PtRep[ 1 ], PtRep[ 2 ] ),边(V2-V3)是( PtRep[ 2 ], PtRep[ 3 ] ),也就是边( 1, 2 ) 和边( 2, 1 ). 从而我们可以看到在用邻近数组表示边的,相邻的边即使原本的顶点索引不同,但在邻近数组中都是一样。所以MS的高手们早就知道这么秒用了。
另外就是边映射结构体。仔细看看应该能看明白里面的逻辑。
主要是记录每条的三个信息。
- 在邻近数组中的边表示
- 相邻两条边中第一条边在普通顶点索引列表中的边表示
- 相邻两条边中第二条边在普通顶点索引列表中的边表示
后面的两个信息主要是用于信息保存,以便后面使用的。
然后稍微有点难度的就是补洞操作。这里我们十分注意在Depth-Fail技术中,阴影网格必须是全封闭。 所以补洞操作时必须的。
全封闭也就是3d中没有任何漏洞。不像在Depth-pass技术中那样只需要延伸阴影边,在这里我们需要cap。也即是阴影的前冒和后帽。实际上就是阴影网格中和物体同形状的部分。如下图所示的Cap,也就是原本物体网格的相对于灯光的前面和后面。
如果没有冒的话,在某些情况是会出现槽糕后果。例如
在No caps中利用Depth-Fail是无法在墙上绘出阴影的,因为眼睛方向对应的地方没有阴影网格。而加了Cap之后就可以在墙上绘出阴影了。
另外在阴影网格处理的VertexShader中,对于当物体和灯光转换到相机坐标中后,如果物体比灯在z方向上更远离视点,我们采用将物体移至远裁截面稍微靠前的位置。这样一定能让整个阴影网格不被裁减掉。以保证不会发现像No Caps图中的情况。采用的公式在sample中都写着,稍微画个图,或者想想就可以理解。然而当物体比灯在z方向上更靠近视点的,我们将物体的坐标改成物体和灯光构成的向量。这是为什么?这里我们要提到齐次坐标,在齐次空间中我们认为(1, 3, 2, 1 )和 ( 2, 6, 4, 2 )是同一点。他们的代表是(
1, 3, 2, 1 ),也就是我们将w不等于1的都通过所以分量除以w来使得w等于1.但当w=0时,我们认为( 1, 3, 2, 0 )是一个点,它离原点无限远,并且它和原点的连线与向量( 1, 3, 2 )方向一致。一开始可能会认为为什么不在原来的点的基础上沿灯光到像素方向进行平移呢?其实当我们采用无穷远的方案时,就不必再原来点的基础上了,我们可以忽略,而直接在原点的基础上平移,仔细想想,无穷远的距离与有限距离比起来,有限距离总是可以忽略的,因为它占的比率可以无限放小,以至于忽略。
最后一点小领会就是在想实现颜色叠加时,如一个白灯照亮一个场景,如果再添加一个绿灯,场景需要再添加上绿灯引起的相应的颜色。在白灯已经对场景处理一次之后,绿灯的处理仍然要用到白灯建立起来的zbuffer,为了让绿灯处理的场景能够通过zbuffer,我们把ZFunc改为小于并且等于。关键是等于,可以是的绿灯继续使用zbuffer,而不用清除。
基本上这是索引ShadowVolume中比较难理解的地方了。我通过完全手动重新这个sample比较全面的理解里面的细节。如果有问题可以联系我。