一步步学OpenGL(39) -《模型轮廓识别检测[阴影锥1]》
教程 39
模型轮廓识别检测[阴影锥1]

原文: http:// ogldev.atspace.co.uk/ww w/tutorial39/tutorial39.html
CSDN完整版专栏: https:// blog.csdn.net/cordova/a rticle/category/9266966
理论介绍
这篇我们讨论一种检测物体轮廓的方法,确切的说,这里的轮廓指的是当光源从任意方向照向3D物体时创建的3D轮廓,当移动光源时,轮廓很可能会跟着改变。这个和在二维图像空间识别轮廓的原理是完全不同的,图像的轮廓检测是识别出2D图像中物体的边界,跟光源位置是没关系的。
轮廓检测本身是个重要的话题,但这里不展开讨论,我们的目标第一步就是实现一下 模板阴影锥(Stencil Shadow Volume) 。模板阴影锥是渲染阴影的一种技术,尤其是对点光源的阴影渲染很有用,这个技术下一篇文章会详细介绍。
下面的图片展示了我们想要识别的轮廓:

上图中的轮廓指的是被光线照亮的椭圆区域。
从3D模型的角度来说,一个模型是由三角形组成的,所以模型轮廓也应该是由三角形的边拼起来的。那么怎么判断三角形的一条边是否是轮廓的一部分呢?这里面的技巧来源于 漫反射光照模型 。
根据漫反射光照模型,光照强度是由三角形法线和入射光方向的的点积决定的,如果三角形面的朝向远离甚至背向光照方向,结果可能接近0甚至小于0,以至于光源影响不到三角面的光照。 为了能确定一个三角形的边是否是模型轮廓的一部分,我们要找到相邻的三角形的公共边,然后计算光照方向与两个三角形的法线各自的点积。如果两个点积结果正负不一样,说明两个三角形一个面向光源,一个背向光源,则他们的公共边是属于轮廓边的。
下面的图片展示了一个简化的模型,简化到二维来展示上面计算轮廓的原理:

红色箭头表示光线,光线投射到三条边上(在三维空间这三条边则表示三角面),三条边的发现分别为1、2、3,可以看出这三条法线与光线反方向向量的点积是明显大于0的。
两个蓝圈标记的就是物体的轮廓,因为法线1是面向光源的,而法线6是背向光源的,另外的3和4也同样。而完全朝向光源的边(这里例子中是点)不算是轮廓,例如1和2之间的,2和3之间的。
可以看出计算模型轮廓的算法很简单,但我们还需要考虑每个三角形的三个邻边的问题: 邻接三角形 。也就是对每个三角形我们要和邻近的三个三角形做三次点积看三条边是否是轮廓,但Assimp这个库并不支持邻接三角形的这种计算,这里我们只好自己在代码里通过一个简单的算法来实现。
轮廓计算应该在管线的那个阶段最好呢?首先考虑我们要对光线方向和三角形法线及其三个邻接三角形的法线求点积,也就是说我们 需要访问整个图元的数据,所以在VS顶点着色阶段是不行的。这样在GS几何着色阶段似乎是可行的,因为可以访问图元的所有顶点数据 ,但是邻接三角形的数据如何访问呢?好在OpenGL在设计的时候考虑到了这方面的事情, 创建了一种叫做'triangle with adjacencies'的拓扑类型。这样只要你在顶点buffer中提供了邻接三角形的信息,在GS阶段每个三角形就可以访问到6个顶点数据,而不是3个 。多出来的三个就是三个邻接三角形各自独有的第三个顶点了。具体可以看下图:

图中三个红色的顶点属于我们要处理的原始三角形,另外三个蓝色的顶点则是三个邻接三角形各自非共用的另外的顶点(这里先不用看e1-e6几条边,它们是在实现的源码中用到的)。当我们以上面的数据格式填充数据到顶点buffer后,每个顶点都会执行VS顶点着色器,包括邻接点和非邻接点。如果GS几何着色阶段也存在,GS会在这样有6个顶点(包括三角形顶点和邻接三角形顶点)的一组数据上执行。
当存在GS阶段时,输出的拓扑结构是由开发者决定的,而如果没有GS阶段光栅器会自行识别处理这种数据模式,并且只会光栅化实际的三角形,不包括邻接三角形。有人反馈这种模式的设置,在intel HD 3000的Macbook上会出错,如果有类似的问题可以简单实现一个GS进行兼容处理,或者改变一下数据拓扑类型。
注意在顶点buffer中,邻接顶点和常规顶点数据有相同的数据格式和属性,本质没有区别,是邻接顶点还是常规顶点只取决于6个顶点它们的相对位置。在一个模型中,三角形之间是连续相互邻接的,同一个顶点有时候是邻接顶点,有时候是常规顶点,这个取决于当前的三角形是哪个,再次看出这种索引绘制对于节省顶点buffer空间非常有效。
源代码详解
见源码和原文。