首先贴一个链接,该链接内有大量基于OpenGL的渲染技术教程和Code Sample,本文基本上在其Tutorial 40的基础上进行了翻译,并加入了部分自己的理解。原文在此:
http://ogldev.atspace.co.uk/index.html
Shadow Volume,即阴影体技术。是CG中非常常见的阴影渲染技术。在自己动手实现之前,也看了好多原理上的东西。但是纸上得来终觉浅,绝知此事要躬行啊! 以此文记录下Shadow Volume实现过程中的种种。
简单地讲,光线照射空间几何物体,被物体遮挡住的空间没有不能受到光源光线的直射,这个空间同样可以用一个几何体表示,这个几何体就是所谓的Shadow Volume(以下简称SV),即阴影体。位于SV里的物体的即为被阴影包围的物体。
实现时的一些关键点:
1、SV的生成。
2、Z-Fail Stencil Test.
3、阴影的渲染。
1、SV的生成:
如上图所示,左图的绿色部分和右图的灰色部分即为 阴影体。阴影体是实实在在的Mesh,跟左图的”人“和右图的”摩托车"一样,都是由三角形面片组成的。我们需要根据光源的位置和阴影的生产者(Shadow Caster)的形状去生成这个Mesh(某物体A遮住了光源,产生了阴影,我们称物体A为Shadow Caster,简称SC。某物体B被物体A遮挡,在B上造成了阴影,我们称物体B为Shadow Receiver,简称SR)。仔细观察上图,我们会发现阴影体起始于SC面向光源的面,而终止于SR。阴影体在SC上的面像个盖子(cap),在SR上的面像底儿(bottom),盖子和底儿之间的三角形面片围一圈形成边儿(surrounding),这个密闭空间就是阴影体的空间。用一个简单的三角形作为SC,如下图所示:
绿色的Cap(即三角形ABC),深灰的bottom(即三角形A‘C‘B‘)以及surroundings(四边形CBB‘C‘, CC‘A‘A, AA‘B‘B)组成了SV Mesh。!注意上述三角形的顺序,遵循右手定则。
但是实际上,阴影体的Cap并不是位于SC上,而是沿着光源的方向有一个小的偏移量。阴影体的Bottom也不在SR上,而在无穷远处,上图中的A‘,B‘,C‘分别为A,B,C沿着入射光方向被投射到无穷远处的对应点。如下图所示:
紫色的三角形A‘‘B‘‘C‘‘是真正的Cap。这是为了避免Z-fighting。
那么,生成阴影体Mesh的问题可以转化为寻找盖子,底儿和边儿的过程。有了这些面儿,将它们围起来就是一个闭合的阴影体Mesh。
下面开始生成SV Mesh。对于SC的一个三角形面片T,它有三个边a,b,c,三个邻面Ta,Tb,Tc,分别与T共享边a,b,c。那么伪代码如下:
1 for every triangle facet T in the SC: 2 3 if T faces the light 4 5 generate the cap triangle by applying a displacement to the original T. //生成Cap 6 7 generate the bottom triangle by projecting the original T to infinity. //生成Bottom 8 9 for every adjacent triangle Tx ( x = a,b,c ) 10 11 if Tx faces the light 12 13 edge x is not part of the silhouette, continue. 14 15 else 16 17 edge x is part of the silhouette, generate the surrouding triangles. 18 19 else 20 21 continue
上面的伪代码在Geometry Shader中实现。
上面的伪代码中提到了 Silhouette (轮廓)。是的,SV Mesh 的 Surrounding Triangles 只在属于 Silhouette 的三角形边处生成。关于silhouette如何检测,上述伪代码已经说得比较清楚了,其原理也比较简单,但是Silhouette detection在大多数情况下仍然作为一个单独的问题来讨论。
下面就简单提一下:
silhouette Detection(边界检测)。边界检测相关资料很多了,原理也很简单。在计算机图形学中,物体由三角形面片(triangle facet)组成。以光照边界检测为例,物体外部有某光源(point,spot or direction light ),这个物体上的所有三角面片要么直接受该光源照射(该面法向量与入射光线方向夹角大于90小于180°),要么不直接受光源照射(该面法向量与入射光线方向夹角大于等于0°小于等于90°),那么必然有一个“边的集合”位于这两种三角形面片相交的边界处。边缘检测即找出这个“边的集合”。如下图二维示意图所示。面向和背向光源的三角面片共享的边即我们需要寻找的边。(该图摘自http://ogldev.atspace.co.uk/www/tutorial39/tutorial39.html ,在原图基础上加入中文图示)
如何去寻找呢?很简单。如下图所示,对某三角形A,有邻面B,C,D。如果A面对光源,那么遍历其三个邻面,如果邻面背对光源,那么A与该邻面共享的那条边即是边界。如果其三个邻面也全都面对光源,那么面A的三条边都不属于边界。
该边缘检测的计算过程可以在Geometry shader中进行。由于Geometry shader 可以访问Primitive的adjacency信息,所以可以方便地对每个三角形的邻面进行信息读取和计算。当然,在OpenGL Application端需要向Pipeline提供具有adjacency信息的数据并按照一定地规则进行排列。例如上图中的三角形A,在提供此Primitive信息的时候应该按顶点索引(0,1,2,3,4,5)的顺序在内存中排列。具体如何从任意3d模型中计算adjacency信息,在此不详细说明。
如何将SC上的顶点project到无穷远处作为Bottom的顶点呢?如下图所示:
光源将顶点P投射到无穷远处,然后投影到near clip(近裁剪面)的上,其X轴坐标为Xndc.
(图引自 http://ogldev.atspace.co.uk/www/tutorial40/tutorial40.html )
n为近裁剪面的距离,v为光源到顶点P的向量,t为标量,从0到正无穷。当t趋向正无穷时,就得到我们想求的Xndc。
(图引自 http://ogldev.atspace.co.uk/www/tutorial40/tutorial40.html )
同理,Yndc也可由此得到。
至此,SV的生成到此结束。
下图是实验结果:五环上方有旋转的点光源,下方是个球体。粉色的部分是阴影体。