1、剪裁测试 剪裁测试用于限制绘制区域。我们可以指定一个矩形的剪裁窗口,当启用剪裁测试后,只有在这个窗口之内的像素才能被绘制,其它像素则会被丢弃。换句话说,无论怎么绘制,剪裁窗口以外的像素将不会被修改。 有的朋友可能玩过《魔兽争霸3》这款游戏。游戏时如果选中一个士兵,则画面下方的一个方框内就会出现该士兵的头像。为了保证该头像无论如何绘制都不会越界而覆盖到外面的像素,就可以使用剪裁测试。 可以通过下面的代码来启用或禁用剪裁测试:
glEnable(GL_SCISSOR_TEST); // 启用剪裁测试
glDisable(GL_SCISSOR_TEST); // 禁用剪裁测试 可以通过下面的代码来指定一个位置在(x, y),宽度为width,高度为height的剪裁窗口。
glScissor(x, y, width, height);
注意,OpenGL窗口坐标是以左下角为(0, 0),右上角为(width, height)的,这与Windows系统窗口有所不同。 还有一种方法可以保证像素只绘制到某一个特定的矩形区域内,这就是视口变换(在第五课第3节中有介绍)。但视口变换和剪裁测试是不同的。视口变换是将所有内容缩放到合适的大小后,放到一个矩形的区域内;而剪裁测试不会进行缩放,超出矩形范围的像素直接忽略掉。 2、Alpha测试 在前面的课程中,我们知道像素的Alpha值可以用于混合操作。其实Alpha值还有一个用途,这就是Alpha测试。当每个像素即将绘制时,如果启动了Alpha测试,OpenGL会检查像素的Alpha值,只有Alpha值满足条件的像素才会进行绘制(严格的说,满足条件的像素会通过本项测试,进行下一种测试,只有所有测试都通过,才能进行绘制),不满足条件的则不进行绘制。这个“条件”可以是:始终通过(默认情况)、始终不通过、大于设定值则通过、小于设定值则通过、等于设定值则通过、大于等于设定值则通过、小于等于设定值则通过、不等于设定值则通过。 如果我们需要绘制一幅图片,而这幅图片的某些部分又是透明的(想象一下,你先绘制一幅相片,然后绘制一个相框,则相框这幅图片有很多地方都是透明的,这样就可以透过相框看到下面的照片),这时可以使用Alpha测试。将图片中所有需要透明的地方的Alpha值设置为0.0,不需要透明的地方Alpha值设置为1.0,然后设置Alpha测试的通过条件为:“大于0.5则通过”,这样便能达到目的。当然也可以设置需要透明的地方Alpha值为1.0,不需要透明的地方Alpha值设置为0.0,然后设置条件为“小于0.5则通过”。Alpha测试的设置方式往往不只一种,可以根据个人喜好和实际情况需要进行选择。 可以通过下面的代码来启用或禁用Alpha测试:
glEnable(GL_ALPHA_TEST); // 启用Alpha测试
glDisable(GL_ALPHA_TEST); // 禁用Alpha测试 可以通过下面的代码来设置Alpha测试条件为“大于0.5则通过”:
glAlphaFunc(GL_GREATER, 0.5f);
该函数的第二个参数表示设定值,用于进行比较。第一个参数是比较方式,除了GL_LESS(小于则通过)外,还可以选择: GL_ALWAYS(始终通过), GL_NEVER(始终不通过), GL_LESS(小于则通过), GL_LEQUAL(小于等于则通过), GL_EQUAL(等于则通过), GL_GEQUAL(大于等于则通过), GL_NOTEQUAL(不等于则通过)。 现在我们来看一个实际例子。一幅照片图片,一幅相框图片,如何将它们组合在一起呢?为了简单起见,我们使用前面两课一直使用的24位BMP文件来作为图片格式。(因为发布到网络上,为了节约容量,我所发布的是JPG格式。大家下载后可以用Windows XP自带的画图工具打开,并另存为24位BMP格式) 注:第一幅图片是著名网络游戏《魔兽世界》的一幅桌面背景,用在这里希望没有涉及版权问题。如果有什么不妥,请及时指出,我会立即更换。 在24位的BMP文件格式中,BGR三种颜色各占8位,没有保存Alpha值,因此无法直接使用Alpha测试。注意到相框那幅图片中,所有需要透明的位置都是白色,所以我们在程序中设置所有白色(或很接近白色)的像素Alpha值为0.0,设置其它像素Alpha值为1.0,然后设置Alpha测试的条件为“大于0.5则通过”即可。这种使用某种特殊颜色来代表透明颜色的技术,有时又被成为Color Key技术。 利用前面第11课的一段代码,将图片读取为纹理,然后利用下面这个函数来设置“当前纹理”中每一个像素的Alpha值。
/* 将当前纹理BGR格式转换为BGRA格式
有了纹理后,我们开启纹理,指定合适的纹理坐标并绘制一个矩形,这样就可以在屏幕上将图片绘制出来。我们先绘制相片的纹理,再绘制相框的纹理。程序代码如下:* 纹理中像素的RGB值如果与指定rgb相差不超过absolute,则将Alpha设置为0.0,否则设置为1.0 */ void texture_colorkey(GLubyte r, GLubyte g, GLubyte b, GLubyte absolute) { GLint width, height; GLubyte* pixels = 0; // 获得纹理的大小信息 glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); // 分配空间并获得纹理像素 pixels = (GLubyte*)malloc(width*height*4); if( pixels == 0 ) return; glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels); // 修改像素中的Alpha值 // 其中pixels[i*4], pixels[i*4+1], pixels[i*4+2], pixels[i*4+3] // 分别表示第i个像素的蓝、绿、红、Alpha四种分量,0表示最小,255表示最大 { GLint i; GLint count = width * height; for(i=0; i<count; ++i) { if( abs(pixels[i*4] - b) <= absolute && abs(pixels[i*4+1] - g) <= absolute && abs(pixels[i*4+2] - r) <= absolute ) pixels[i*4+3] = 0; else pixels[i*4+3] = 255; } } // 将修改后的像素重新设置到纹理中,释放内存 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels); free(pixels); }
void display(void)
{ static int initialized = 0; static GLuint texWindow = 0; static GLuint texPicture = 0; // 执行初始化操作,包括:读取相片,读取相框,将相框由BGR颜色转换为BGRA,启用二维纹理 if( !initialized ) { texPicture = load_texture("pic.bmp"); texWindow = load_texture("window.bmp"); glBindTexture(GL_TEXTURE_2D, texWindow); texture_colorkey(255, 255, 255, 10); glEnable(GL_TEXTURE_2D); initialized = 1; } // 清除屏幕 glClear(GL_COLOR_BUFFER_BIT); // 绘制相片,此时不需要进行Alpha测试,所有的像素都进行绘制 glBindTexture(GL_TEXTURE_2D, texPicture); glDisable(GL_ALPHA_TEST); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex2f(-1.0f, -1.0f); glTexCoord2f(0, 1); glVertex2f(-1.0f, 1.0f); glTexCoord2f(1, 1); glVertex2f( 1.0f, 1.0f); glTexCoord2f(1, 0); glVertex2f( 1.0f, -1.0f); glEnd(); // 绘制相框,此时进行Alpha测试,只绘制不透明部分的像素 glBindTexture(GL_TEXTURE_2D, texWindow); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 0.5f); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex2f(-1.0f, -1.0f); glTexCoord2f(0, 1); glVertex2f(-1.0f, 1.0f); glTexCoord2f(1, 1); glVertex2f( 1.0f, 1.0f); glTexCoord2f(1, 0); glVertex2f( 1.0f, -1.0f); glEnd(); // 交换缓冲 glutSwapBuffers(); } 其中:load_texture函数是从第11课中照搬过来的(该函数还使用了一个power_of_two函数,一个BMP_Header_Length常数,同样照搬),无需进行修改。main函数跟其它课程的基本相同,不再重复。 程序运行后,会发现相框与相片的衔接有些不自然,这是因为相框某些边缘部分虽然肉眼看上去是白色,但其实RGB值与纯白色相差并不少,因此程序计算其Alpha值时认为其不需要透明。解决办法是仔细处理相框中的每个像素,在需要透明的地方涂上纯白色,这也许是一件很需要耐心的工作。 大家可能会想:前面我们学习过混合操作,混合可以实现半透明,自然也可以通过设定实现全透明。也就是说,Alpha测试可以实现的效果几乎都可以通过OpenGL混合功能来实现。那么为什么还需要一个Alpha测试呢?答案就是,这与性能相关。Alpha测试只要简单的比较大小就可以得到最终结果,而混合操作一般需要进行乘法运算,性能有所下降。另外,OpenGL测试的顺序是:剪裁测试、Alpha测试、模板测试、深度测试。如果某项测试不通过,则不会进行下一步,而只有所有测试都通过的情况下才会执行混合操作。因此,在使用Alpha测试的情况下,透明的像素就不需要经过模板测试和深度测试了;而如果使用混合操作,即使透明的像素也需要进行模板测试和深度测试,性能会有所下降。还有一点:对于那些“透明”的像素来说,如果使用Alpha测试,则“透明”的像素不会通过测试,因此像素的深度值不会被修改;而使用混合操作时,虽然像素的颜色没有被修改,但它的深度值则有可能被修改掉了。 因此,如果所有的像素都是“透明”或“不透明”,没有“半透明”时,应该尽量采用Alpha测试而不是采用混合操作。当需要绘制半透明像素时,才采用混合操作。 3、模板测试 模板测试是所有OpenGL测试中比较复杂的一种。 首先,模板测试需要一个模板缓冲区,这个缓冲区是在初始化OpenGL时指定的。如果使用GLUT工具包,可以在调用glutInitDisplayMode函数时在参数中加上GLUT_STENCIL,例如:
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_STENCIL);
在Windows操作系统中,即使没有明确要求使用模板缓冲区,有时候也会分配模板缓冲区。但为了保证程序的通用性,最好还是明确指定使用模板缓冲区。如果确实没有分配模板缓冲区,则所有进行模板测试的像素全部都会通过测试。 通过glEnable/glDisable可以启用或禁用模板测试。
glEnable(GL_STENCIL_TEST); // 启用模板测试
glDisable(GL_STENCIL_TEST); // 禁用模板测试 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(); } |
【OpenGL入门】OpenGL片断测试
最新推荐文章于 2024-09-14 23:01:09 发布