OpenGL入门学习——第十二课,OpenGL片断测试

大家好。现在因为参加工作的关系,又是长时间没有更新。趁着国庆的空闲,总算是又写出了一课。我感觉入门的知识已经快要介绍完毕,这课之后再有一课,就可以告一段落了。以后我可能会写一些自己在这方面的体会,做一份进阶课程。

现在即将放出的是第十二课的内容。

首先还是以前课程的连接:

第一课,编写第一个OpenGL程序
第二课,绘制几何图形
第三课,绘制几何图形的一些细节问题
第四课,颜色的选择
第五课,三维的空间变换
第六课,动画的制作
第七课,使用光照来表现立体感
第八课,使用显示列表
第九课,使用混合来实现半透明效果
第十课,BMP文件与像素操作
第十一课,纹理的使用入门
第十二课,OpenGL片断测试 –→ 本次课程的内容

片断测试其实就是测试每一个像素,只有通过测试的像素才会被绘制,没有通过测试的像素则不进行绘制。OpenGL提供了多种测试操作,利用这些操作可以实现一些特殊的效果。
我们在前面的课程中,曾经提到了”深度测试”的概念,它在绘制三维场景的时候特别有用。在不使用深度测试的时候,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉,这样的效果并不是我们所希望的。
如果使用了深度测试,则情况就会有所不同:每当一个像素被绘制,OpenGL就记录这个像素的”深度”(深度可以理解为:该像素距离观察者的距离。深度值越大,表示距离越远),如果有新的像素即将覆盖原来的像素时,深度测试会检查新的深度是否会比原来的深度值小。如果是,则覆盖像素,绘制成功;如果不是,则不会覆盖原来的像素,绘制被取消。这样一来,即使我们先绘制比较近的物体,再绘制比较远的物体,则远的物体也不会覆盖近的物体了。
实际上,只要存在深度缓冲区,无论是否启用深度测试,OpenGL在像素被绘制时都会尝试将深度数据写入到缓冲区内,除非调用了glDepthMask(GL_FALSE)来禁止写入。这些深度数据除了用于常规的测试外,还可以有一些有趣的用途,比如绘制阴影等等。

除了深度测试,OpenGL还提供了剪裁测试、Alpha测试和模板测试。

因为论坛开始支持附件,现在把程序源代码和所使用的图片一起打包上传

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();
}



其中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,以致于在绘制后其颜色不会有任何变化,但是这个像素的深度值有可能会被修改,而如果是使用模板测试,没有通过测试的像素其深度值不会发生任何变化。而且,模板测试和混合功能中,像素模板值的修改方式是不一样的。
4、深度测试
在本课的开头,已经简单的叙述了深度测试。这里是完整的内容。

深度测试需要深度缓冲区,跟模板测试需要模板缓冲区是类似的。如果使用GLUT工具包,可以在调用glutInitDisplayMode函数时在参数中加上GLUT_DEPTH,这样来明确指定要求使用深度缓冲区。
深度测试和模板测试的实现原理很类似,都是在一个缓冲区保存像素的某个值,当需要进行测试时,将保存的值与另一个值进行比较,以确定是否通过测试。两者的区别在于:模板测试是设定一个值,在测试时用这个设定值与像素的”模板值”进行比较,而深度测试是根据顶点的空间坐标计算出深度,用这个深度与像素的”深度值”进行比较。也就是说,模板测试需要指定一个值作为比较参考,而深度测试中,这个比较用的参考值是OpenGL根据空间坐标自动计算的。

通过glEnable/glDisable函数可以启用或禁用深度测试。
glEnable(GL_DEPTH_TEST); // 启用深度测试
glDisable(GL_DEPTH_TEST); // 禁用深度测试

至于通过测试的条件,同样有八种,与Alpha测试中的条件设置相同。条件设置是通过glDepthFunc函数完成的,默认值是GL_LESS。
glDepthFunc(GL_LESS);

与模板测试相比,深度测试的应用要频繁得多。几乎所有的三维场景绘制都使用了深度测试。正因为这样,几乎所有的OpenGL实现都对深度测试提供了硬件支持,所以虽然两者的实现原理类似,但深度测试很可能会比模板测试快得多。当然了,两种测试在应用上很少有交集,一般不会出现使用一种测试去代替另一种测试的情况。

小结:
本次课程介绍了OpenGL所提供的四种测试,分别是剪裁测试、Alpha测试、模板测试、深度测试。OpenGL会对每个即将绘制的像素进行以上四种测试,每个像素只有通过一项测试后才会进入下一项测试,而只有通过所有测试的像素才会被绘制,没有通过测试的像素会被丢弃掉,不进行绘制。每种测试都可以单独的开启或者关闭,如果某项测试被关闭,则认为所有像素都可以顺利通过该项测试。
剪裁测试是指:只有位于指定矩形内部的像素才能通过测试。
Alpha测试是指:只有Alpha值与设定值相比较,满足特定关系条件的像素才能通过测试。
模板测试是指:只有像素模板值与设定值相比较,满足特定关系条件的像素才能通过测试。
深度测试是指:只有像素深度值与新的深度值比较,满足特定关系条件的像素才能通过测试。
上面所说的特定关系条件可以是大于、小于、等于、大于等于、小于等于、不等于、始终通过、始终不通过这八种。
模板测试需要模板缓冲区,深度测试需要深度缓冲区。这些缓冲区都是在初始化OpenGL时指定的。如果使用GLUT工具包,则可以在 glutInitDisplayMode函数中指定。无论是否开启深度测试,OpenGL在像素被绘制时都会尝试修改像素的深度值;而只有开启模板测试时,OpenGL才会尝试修改像素的模板值,模板测试被关闭时,OpenGL在像素被绘制时也不会修改像素的模板值。
利用这些测试操作可以控制像素被绘制或不被绘制,从而实现一些特殊效果。利用混合功能可以实现半透明,通过设置也可以实现完全透明,因而可以模拟像素颜色的绘制或不绘制。但注意,这里仅仅是颜色的模拟。OpenGL可以为像素保存颜色、深度值和模板值,利用混合实现透明时,像素颜色不发生变化,但深度值则会可能变化,模板值受 glStencilFunc函数中第三个参数影响;利用测试操作实现透明时,像素颜色不发生变化,深度值也不发生变化,模板值受 glStencilFunc函数中前两个参数影响。
此外,修正了第十课、第十一课中的一个函数中的bug。在grab函数中,应该在最开头加上一句glReadBuffer(GL_FRONT);以保证读取到的内容正好就是显示的内容。

因为论坛支持附件了,我会把程序源代码和所使用的图片上传到附件里,方便大家下载。

===================== 第十二课 完 =====================

顶点缓冲和索引缓冲是OpenGL 1.5版本所提供的功能,因此首先检查OpenGL版本是否达到1.5。因为Windows仅直接支持 OpenGL 1.1的函数,更高版本的函数应该使用wglGetProcAddress函数来获得这些函数的指针,然后利用函数指针进行间接调用。而且这些函数所需要使用的常量在一个叫做glext.h的头文件中定义,可在google上搜索下载该文件的最新版本。
Vertex Buffer Object(顶点缓冲对象)所涉及的函数:
glGenBuffers:分配缓冲对象编号
glBindBuffer:绑定缓冲。可以绑定顶点缓冲和索引缓冲。
glBufferData,glBufferSubData:修改缓冲中的全部数据或部分数据。第一次修改时应该使用glBufferData,以后可以选择使用glBufferData或glBufferSubData。
glMapBuffer,glUnmapBuffer:glMapBuffer锁定缓冲数据并把缓冲数据映射到内存,然后可以用一个指针进行灵活的访问和修改,修改完成后利用glUnmapBuffer确认修改并解除锁定。

顶点缓冲/索引缓冲使用示例。
注意:该程序使用C语言编写(不是C++)。使用了两个工具包,GLUT和GLEE。其中:GLUT的安装方法在本课程的第一课里面有描述。GLEE实际上就是两个文件glee.h和glee.c,从网上下载这两个文件的最新版本并放到工程中,和下面的代码一起编译。

代码过长,分开发送。

#define WindowWidth 512
#define WindowHeight 512
#define WindowTitle “OpenGL — Vertex Buffer Objects 测试”

#include “GLee.h”
#include <GL/glut.h>
#include <stdio.h>

static GLfloat gf_RotateAngle = 0.0f;
static int gi_Rotating = 1;

void display( void)
{
#define length_half (1.0f)
// 混合数组,用六个值表示一个顶点(前三项为颜色,后三项为顶点坐标),共8个顶点
static GLfloat vertex_list[6*8] =
{
0.0f, 0.0f, 0.0f, -length_half, -length_half, -length_half,
0.0f, 0.0f, 1.0f, -length_half, -length_half, length_half,
0.0f, 1.0f, 0.0f, -length_half, length_half, -length_half,
0.0f, 1.0f, 1.0f, -length_half, length_half, length_half,
1.0f, 0.0f, 0.0f, length_half, -length_half, -length_half,
1.0f, 0.0f, 1.0f, length_half, -length_half, length_half,
1.0f, 1.0f, 0.0f, length_half, length_half, -length_half,
1.0f, 1.0f, 1.0f, length_half, length_half, length_half
};
// 索引数组,每四个顶点表示一个平面,共六个平面
static GLuint index_list[4*6] =
{
0, 1, 3, 2,
4, 5, 7, 6,
0, 2, 6, 4,
1, 3, 7, 5,
0, 1, 5, 4,
2, 3, 7, 6
};
// 标记本函数是否为第一次调用,如果是,可进行初始化
static int isFirstCall = 1;
#undef length_half

接楼上。

// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);

// 设置视角
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60, 1, 1, 10);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(3, 3, 3, 0, 0, 0, 0, 1, 0);

// 旋转
glRotatef(gf_RotateAngle, 0.0f, 1.0f, 1.0f);

// 初始化GLEE,GLEE会自动读取动态连接库中的1.1以上版本OpenGL函数(如果有的话)
GLeeInit();

// 根据OpenGL所支持VBO的情况,有三种方式执行渲染
if( _GLEE_VERSION_1_5 ) // 支持OpenGL 1.5,使用标准的VBO函数
{
if( isFirstCall )
{
GLuint iVertexBuffer = 0;
GLuint iIndexBuffer = 0;

// 设置顶点缓冲
glGenBuffers(1, &iVertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, iVertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_list),
vertex_list, GL_STATIC_DRAW);

// 设置索引缓冲
glGenBuffers(1, &iIndexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iIndexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index_list),
index_list, GL_STATIC_DRAW);

// 其它设置与顶点数组的设置方式类似,只有在设置指针时不同
// 不要直接使用指针,而使用偏移地址
// vertex_list中的数据已经被保存到iVertexBuffer所对应的缓冲对象中,因此使用零作为偏移地址
// 如果缓冲对象中有很多杂乱的数据,则通过指定不同的偏移地址即可选择不同数据,不需要重新绑定
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glInterleavedArrays(GL_C3F_V3F, 0, 0);
}
// 不要直接使用指针,而使用偏移地址
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, 0);
}
else if( _GLEE_ARB_vertex_buffer_object ) // 不支持OpenGL 1.5,但以ARB扩展的形式支持VBO
{
// 与标准形式的VBO几乎相同
// 只是函数名有无ARB后缀,常量名有无_ARB后缀这一点区别
if( isFirstCall )
{
GLuint iVertexBuffer = 0;
GLuint iIndexBuffer = 0;

glGenBuffersARB(1, &iVertexBuffer);
glBindBufferARB(GL_ARRAY_BUFFER_ARB, iVertexBuffer);
glBufferDataARB(GL_ARRAY_BUFFER_ARB, sizeof(vertex_list),
vertex_list, GL_STATIC_DRAW_ARB);

glGenBuffersARB(1, &iIndexBuffer);
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, iIndexBuffer);
glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, sizeof(index_list),
index_list, GL_STATIC_DRAW_ARB);

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glInterleavedArrays(GL_C3F_V3F, 0, 0);
}
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, 0);
}
else // 不支持VBO,使用Vertex Array代替
{
if( isFirstCall )
{
printf( “your system does not support the VBO.\n”);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glInterleavedArrays(GL_C3F_V3F, 0, vertex_list);
}
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);
}

// 交换缓冲
glutSwapBuffers();

isFirstCall = 0;
}

void idle( void)
{
if( gi_Rotating )
{
gf_RotateAngle += 0.1f;
if( gf_RotateAngle >= 360.0f )
gf_RotateAngle = 0.0f;
}
display();
}

void keyboard( unsigned char c, int x, int y)
{
gi_Rotating = !gi_Rotating;
}

int main( int argc, char* argv[])
{
// GLUT初始化
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
glutInitWindowPosition(100, 100);
glutInitWindowSize(WindowWidth, WindowHeight);
glutCreateWindow(WindowTitle);
glutDisplayFunc(&display);
glutIdleFunc(&idle);
glutKeyboardFunc(&keyboard);

// 开始显示
glutMainLoop();

return 0;
}

顶点缓冲对象(Vertex Buffer Object, VBO)的使用步骤如下:
1、用glGenBuffers分配缓冲对象编号,该函数的使用方法与分配纹理对象编号类似。
2、用glBindBuffer绑定缓冲对象,可以用GL_ARRAY_BUFFER来绑定到顶点缓冲,也可以用GL_ELEMENT_ARRAY_BUFFER来绑定到索引缓冲。
3、用glBufferData指定缓冲中的数据,需要分别指定顶点缓冲的数据和索引缓冲的数据。注意该函数的最后一个参数,指明了该数据的用途,这个用途将帮助OpenGL决定如何进行优化。其中GL_STATIC_DRAW表示数据一旦确定,几乎不会更改,而且数据是用于绘制的。

成功使用以上三个函数后,很多本来接受指针的OpenGL函数在用法上就会发生变化。例如glColor3fv函数,本来接受一个指针,该指针指向三个连续的 GLfloat类型的值,用于确定颜色。但因为现在绑定了GL_ARRAY_BUFFER,所以该函数接收一个偏移值offset,表示从已经绑定的缓冲中第offset个字节开始,取三个GLfloat类型的值,用于确定颜色。(注意:虽然偏移值是一个整数,但是你还是需要将它强制转换为 glColor3fv所接受的指针类型,以确保编译能够顺利完成。)

在OpenGL 1.1版本中,提供了”顶点数组”的功能,可以把很多顶点数据放到数组中,然后通过调用很少的函数就完成绘制,而不必像OpenGL 1.0那样使用glBegin, glEnd以及大量的 glColor*, glNormal*, glTexCoord*, glVertex*等函数。这些功能都是通过指针完成的。因此在绑定了顶点缓冲后,也可以用缓冲中的数据和指定偏移值的方式来代替原来的数组。数组是保存在内存中的,而缓冲数据则有可能是直接保存在显卡上,因此有望得到性能的优化。

本程序绘制一个旋转的立方体,按任意键可以开启/关闭旋转。
利用一个数组保存立方体中八个顶点的颜色和坐标,利用一个数组表示立方体六个面中每一面所包含的顶点的索引,然后利用顶点缓冲和顶点数组联合进行绘制。可以看到,除了第一次初始化外,以后只需要调用glDrawElements就可以完成绘制立方体所需要的所有动作。
程序有三种实现。当OpenGL版本为1.5或以上时,直接使用顶点缓冲对象;当OpenGL版本不足1.5,但支持ARB扩展形式的顶点缓冲对象时(此时也需要OpenGL 1.4版本),使用该形式的顶点缓冲对象;如果以上两者都不满足,则只好放弃使用顶点缓冲对象,而直接使用顶点数组。最后一种方式在性能上将会略低于前两种。
我测试用的是Intel的集成显卡,支持OpenGL 1.4以及ARB扩展形式的顶点缓冲对象。因此后两种方式我都测试并正确运行。至于第一种方式,就拜托有独立显卡(可能至少需要Geforce4 MX 440级别的显卡并安装最新驱动)的朋友去测试了。

标签: ,
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值