OpenGL模板测试

模板测试
 
模板测试是所有OpenGL测试中比较复杂的一种。

首先,模板测试需要一个模板缓冲区,这个缓冲区是在初始化OpenGL时指定的。如果使用GLUT工具包,可以在调用glutInitDisplayMode函数时在参数中加上GLUT_STENCIL,例如:
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_STENCIL);


在Windows操作系统中,即使没有明确要求使用模板缓冲区,有时候也会分配模板缓冲区。但为了保证程序的通用性,最好还是明确指定使用模板缓冲区。如果确实没有分配模板缓冲区,则所有进行模板测试的像素全部都会通过测试。

通过glEnable/glDisable可以启用或禁用模板测试。



OpenGL在模板缓冲区中为每个像素保存了一个“模板值”,当像素需要进行模板测试时,将设定的模板参考值与该像素的“模板值”进行比较,符合条件的通过测试,不符合条件的则被丢弃,不进行绘制。
条件的设置与Alpha测试中的条件设置相似。但注意Alpha测试中是用浮点数来进行比较,而模板测试则是用整数来进行比较。比较也有八种情况:始终通过、始终不通过、大于则通过、小于则通过、大于等于则通过、小于等于则通过、等于则通过、不等于则通过。

glStencilFunc(GL_LESS, 3, mask);


这段代码设置模板测试的条件为:“小于3则通过”。glStencilFunc的前两个参数意义与glAlphaFunc的两个参数类似,第三个参数的意义为:如果进行比较,则只比较mask中二进制为1的位。例如,某个像素模板值为5(二进制101),而mask的二进制值为00000011,因为只比较最后两位,5的最后两位为01,其实是小于3的,因此会通过测试。

如何设置像素的“模板值”呢?glClear函数可以将所有像素的模板值复位。代码如下:

glClear(GL_STENCIL_BUFFER_BIT);


可以同时复位颜色值和模板值:

glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);


正如可以使用glClearColor函数来指定清空屏幕后的颜色那样,也可以使用glClearStencil函数来指定复位后的“模板值”。

每个像素的“模板值”会根据模板测试的结果和深度测试的结果而进行改变。

glStencilOp(fail, zfail, zpass);


该函数指定了三种情况下“模板值”该如何变化。第一个参数表示模板测试未通过时该如何变化;第二个参数表示模板测试通过,但深度测试未通过时该如何变化;第三个参数表示模板测试和深度测试均通过时该如何变化。如果没有起用模板测试,则认为模板测试总是通过;如果没有启用深度测试,则认为深度测试总是通过)
变化可以是:
GL_KEEP(不改变,这也是默认值),
GL_ZERO(回零),
GL_REPLACE(使用测试条件中的设定值来代替当前模板值),
GL_INCR(增加1,但如果已经是最大值,则保持不变),
GL_INCR_WRAP(增加1,但如果已经是最大值,则从零重新开始),
GL_DECR(减少1,但如果已经是零,则保持不变),
GL_DECR_WRAP(减少1,但如果已经是零,则重新设置为最大值),
GL_INVERT(按位取反)。

在新版本的OpenGL中,允许为多边形的正面和背面使用不同的模板测试条件和模板值改变方式,于是就有了glStencilFuncSeparate函数和glStencilOpSeparate函数。这两个函数分别与glStencilFunc和glStencilOp类似,只在最前面多了一个参数face,用于指定当前设置的是哪个面。可以选择GL_FRONT, GL_BACK, GL_FRONT_AND_BACK。

注意:模板缓冲区与深度缓冲区有一点不同。无论是否启用深度测试,当有像素被绘制时,总会重新设置该像素的深度值(除非设置glDepthMask(GL_FALSE);)。而模板测试如果不启用,则像素的模板值会保持不变,只有启用模板测试时才有可能修改像素的模板值。(这一结论是我自己的实验得出的,暂时没发现什么资料上是这样写。如果有不正确的地方,欢迎指正)
另外,模板测试虽然是从OpenGL 1.0就开始提供的功能,但是对于个人计算机而言,硬件实现模板测试的似乎并不多,很多计算机系统直接使用CPU运算来完成模板测试。因此在一些老的显卡,或者是多数集成显卡上,大量而频繁的使用模板测试可能造成程序运行效率低下。即使是当前配置比较高端的个人计算机,也尽量不要使用glStencilFuncSeparate和glStencilOpSeparate函数。

从前面所讲可以知道,使用剪裁测试可以把绘制区域限制在一个矩形的区域内。但如果需要把绘制区域限制在一个不规则的区域内,则需要使用模板测试。
例如:绘制一个湖泊,以及周围的树木,然后绘制树木在湖泊中的倒影。为了保证倒影被正确的限制在湖泊表面,可以使用模板测试。具体的步骤如下:
(1) 关闭模板测试,绘制地面和树木。
(2) 开启模板测试,使用glClear设置所有像素的模板值为0。
(3) 设置glStencilFunc(GL_ALWAYS, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);绘制湖泊水面。这样一来,湖泊水面的像素的“模板值”为1,而其它地方像素的“模板值”为0。
(4) 设置glStencilFunc(GL_EQUAL, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);绘制倒影。这样一来,只有“模板值”为1的像素才会被绘制,因此只有“水面”的像素才有可能被倒影的像素替换,而其它像素则保持不变。

   我们仍然来看一个实际的例子。这是一个比较简单的场景:空间中有一个球体,一个平面镜。我们站在某个特殊的观察点,可以看到球体在平面镜中的镜像,并且镜像处于平面镜的边缘,有一部分因为平面镜大小的限制,而无法显示出来。整个场景的效果如下图:


绘制这个场景的思路跟前面提到的湖面倒影是接近的。
假设平面镜所在的平面正好是X轴和Y轴所确定的平面,则球体和它在平面镜中的镜像是关于这个平面对称的。我们用一个draw_sphere函数来绘制球体,先调用该函数以绘制球体本身,然后调用glScalef(1.0f, 1.0f, -1.0f); 再调用draw_sphere函数,就可以绘制球体的镜像。
另外需要注意的地方就是:因为是绘制三维的场景,我们开启了深度测试。但是站在观察者的位置,球体的镜像其实是在平面镜的“背后”,也就是说,如果按照常规的方式绘制,平面镜会把镜像覆盖掉,这不是我们想要的效果。解决办法就是:设置深度缓冲区为只读,绘制平面镜,然后设置深度缓冲区为可写的状态,绘制平面镜“背后”的镜像。
有的朋友可能会问:如果在绘制镜像的时候关闭深度测试,那镜像不就不会被平面镜遮挡了吗?为什么还要开启深度测试,又需要把深度缓冲区设置为只读呢?实际情况是:虽然关闭深度测试确实可以让镜像不被平面镜遮挡,但是镜像本身会出现若干问题。我们看到的镜像是一个球体,但实际上这个球体是由很多的多边形所组成的,这些多边形有的代表了我们所能看到的“正面”,有的则代表了我们不能看到的“背面”。如果关闭深度测试,而有的“背面”多边形又比“正面”多边形先绘制,就会造成球体的背面反而把正面挡住了,这不是我们想要的效果。为了确保正面可以挡住背面,应该开启深度测试。
绘制部分的代码如下:

void draw_sphere()
{
     // 设置光源
     glEnable(GL_LIGHTING);
     glEnable(GL_LIGHT0);
     {
         GLfloat
             pos[]      = {5.0f, 5.0f, 0.0f, 1.0f},
             ambient[] = {0.0f, 0.0f, 1.0f, 1.0f};
         glLightfv(GL_LIGHT0, GL_POSITION, pos);
         glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
     }

     // 绘制一个球体
     glColor3f(1, 0, 0);
     glPushMatrix();
     glTranslatef(0, 0, 2);
     glutSolidSphere(0.5, 20, 20);
     glPopMatrix();
}

void display(void)
{
     // 清除屏幕
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 设置观察点
     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(60, 1, 5, 25);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(5, 0, 6.5, 0, 0, 0, 0, 1, 0);

     glEnable(GL_DEPTH_TEST);

     // 绘制球体
     glDisable(GL_STENCIL_TEST);
     draw_sphere();

     // 绘制一个平面镜。在绘制的同时注意设置模板缓冲。
     // 另外,为了保证平面镜之后的镜像能够正确绘制,在绘制平面镜时需要将深度缓冲区设置为只读的。
     // 在绘制时暂时关闭光照效果
     glClearStencil(0);
     glClear(GL_STENCIL_BUFFER_BIT);
     glStencilFunc(GL_ALWAYS, 1, 0xFF);
     glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
     glEnable(GL_STENCIL_TEST);

     glDisable(GL_LIGHTING);
     glColor3f(0.5f, 0.5f, 0.5f);
     glDepthMask(GL_FALSE);
     glRectf(-1.5f, -1.5f, 1.5f, 1.5f);
     glDepthMask(GL_TRUE);

     // 绘制一个与先前球体关于平面镜对称的球体,注意光源的位置也要发生对称改变
     // 因为平面镜是在X轴和Y轴所确定的平面,所以只要Z坐标取反即可实现对称
     // 为了保证球体的绘制范围被限制在平面镜内部,使用模板测试
     glStencilFunc(GL_EQUAL, 1, 0xFF);
     glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
     glScalef(1.0f, 1.0f, -1.0f);
     draw_sphere();

     // 交换缓冲
     glutSwapBuffers();

     // 截图
     grab();
}



其中display函数的末尾调用了一个grab函数,它保存当前的图象到一个BMP文件。这个函数本来是在第十课和第十一课中都有所使用的。但是我发现它有一个bug,现在进行了修改:在函数最开头的部分加上一句:glReadBuffer(GL_FRONT);即可。注意这个函数最好是在绘制完毕后(如果是使用双缓冲,则应该在交换缓冲后)立即调用。

大家可能会有这样的感觉:模板测试的设置是如此复杂,它可以实现的功能应该很多,肯定不止这样一个“限制像素的绘制范围”。事实上也是如此,不过现在我们暂时只讲这些。

其实,如果不需要绘制半透明效果,有时候可以用混合功能来代替模板测试。就绘制镜像这个例子来说,可以采用下面的步骤:
(1) 清除屏幕,在glClearColor中设置合适的值确保清除屏幕后像素的Alpha值为0.0
(2) 关闭混合功能,绘制球体本身,设置合适的颜色(或者光照与材质)以确保所有被绘制的像素的Alpha值为0.0
(3) 绘制平面镜,设置合适的颜色(或者光照与材质)以确保所有被绘制的像素的Alpha值为1.0
(4) 启用混合功能,用GL_DST_ALPHA作为源因子,GL_ONE_MINUS_DST_ALPHA作为目标因子,这样就实现了只有原来Alpha为1.0的像素才能被修改,而原来Alpha为0.0的像素则保持不变。这时再绘制镜像物体,注意确保所有被绘制的像素的Alpha值为1.0。
在有的OpenGL实现中,模板测试是软件实现的,而混合功能是硬件实现的,这时候可以考虑这样的代替方法以提高运行效率。但是并非所有的模板测试都可以用混合功能来代替,并且这样的代替显得不自然,复杂而且容易出错。
另外始终注意:使用混合来模拟时,即使某个像素原来的Alpha值为0.0,以致于在绘制后其颜色不会有任何变化,但是这个像素的深度值有可能会被修改,而如果是使用模板测试,没有通过测试的像素其深度值不会发生任何变化。而且,模板测试和混合功能中,像素模板值的修改方式是不一样的。

 

你可能感兴趣的:(OpenGL)