Chapter12 片段测试
片断测试其实就是测试每一个像素,只有通过测试的像素才会被绘制,没有通过测试的像素则不进行绘制。
之前用的深度测试就是一种片段测试,还有裁剪,也是一种片段测试。
OpenGL提供了深度测试、剪裁测试、Alpha测试和模板测试这4中片段测试。
12.1 剪裁测试
剪裁测试用于限制绘制区域。我们可以指定一个矩形的剪裁窗口,当启用剪裁测试后,只有在这个窗口之内的像素才能被绘制,其它像素则会被丢弃。
启用和禁用剪裁测试如下:
glEnable(GL_SCISSOR_TEST);
// 启用剪裁测试
glDisable(GL_SCISSOR_TEST);
// 禁用剪裁测试
可以通过下面的代码来指定一个位置在(x,y),宽度为width,高度为height的剪裁窗口:
glScissor(x, y, width, height);
注意,OpenGL窗口坐标是以左下角为(0, 0),右上角为(width, height)的,这与Windows系统窗口有所不同(windows系统窗口是左上角为(0,0))
12.2 Alpha测试
Alpha测试就是检查像素的Alpha值,满足条件才绘制,一般有下面这几种:
- 始终通过
GL_ALWAYS
- 始终不通过
GL_NEVER
- 小于多少通过
GL_LESS
- 小于等于等于多少通过
GL_LEQUAL
- 等于多少通过
GL_EQUAL
- 大于多少通过
GL_GREATER
- 大于等于多少通过
GL_GEQUAL
- 不等于多少通过
GL_NOTEQUAL
启用和禁用Alpha测试如下:
glEnable(GL_ALPHA_TEST);
// 启用Alpha测试
glDisable(GL_ALPHA_TEST);
// 禁用Alpha测试
设置Alpha测试条件为大于0.5通过的代码如下:
glAlphaFunc(GL_GREATER, 0.5f);
第一个参数是比较方式(即上面写的这些,始终通过、不通过、大于等等),第二个显然是设定的值。
函数原型: void glAlphaFunc (GLenum func, GLclampf ref);
12.2.1 例子
假设现在有一个照片和一个相框,试着将他们组合在一起
由于我们用的是24位BMP,只保存了3个8位的RGB信息,没有alpha值,所以无法直接使用alpha测试。
观察到相框的部分都是白色的,所以需要透明的位置都是白色的,可以设置白色的alpha值为0.0,而其他都为1.0,设置通过的条件为大于0.5通过。
这种使用某种特殊颜色来代表透明颜色的技术,有时又被成为Color Key技术。
示例代码(其余代码和纹理的例子一样,不再赘述):
/* 将当前纹理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();
}
结果如下:
从上面的结果可以看到,相框和图片的衔接有些不自然,这是因为我们把白色的alpha值都设置为0.0,而相框的某些地方虽然人眼看不是白色,但实际上还是白色的,所以会出现这种情况。
补充一点:
OpenGL测试的顺序是:剪裁测试、Alpha测试、模板测试、深度测试。如果某项测试不通过,则不会进行下一步。
12.3 模板测试
首先,模板测试需要一个模板缓冲区,可以在调用glut中的glutInitDisplayMode
函数时在参数中加上GLUT_STENCIL
,如下:
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_STENCIL);
启用或禁用模板测试。
glEnable(GL_STENCIL_TEST);
// 启用模板测试
glDisable(GL_STENCIL_TEST);
// 禁用模板测试
OpenGL在模板缓冲区中为每个像素保存了一个“模板值”,像素进行模板测试时,将设定的模板值和该像素的模板值进行比较,满足条件绘制,不满足则不绘制。
比较的情况和alpha测试相同,不再赘述,给出下面一个例子:
glStencilFunc(GL_LESS, 3, mask);
函数原型:void glStencilFunc (GLenum func, GLint ref, GLuint mask);
表示的是小于3通过,前两个参数和alpha测试相同,第三个表示的意思是如果进行比较,则只比较mask中二进制为1的位(掩码,等价于先按位与,然后再比较)。
例如,某个像素模板值为5(二进制101),而mask的二进制值为00000011,因为只比较最后两位,5的最后两位为01,其实是小于3的,因此会通过测试。
可以使用glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
来清除所有像素的模板值,之后再进行设置。
每个像素的“模板值”会根据模板测试的结果和深度测试的结果而进行改变:
glStencilOp(fail, zfail, zpass);
glStencilOp
函数制定了在模板测试不通过、模板测试通过但是深度测试不通过以及两者均通过这三种情况时如何变化。
变化可以是:
- GL_KEEP(不改变,这也是默认值)
- GL_ZERO(回零)
- GL_REPLACE(使用测试条件中的设定值来代替当前模板值)
- GL_INCR(增加1,但如果已经是最大值,则保持不变)
- GL_INCR_WRAP(增加1,但如果已经是最大值,则从零重新开始)
- GL_DECR(减少1,但如果已经是零,则保持不变)
- GL_DECR_WRAP(减少1,但如果已经是零,则重新设置为最大值)
- GL_INVERT(按位取反)。
使用剪裁测试可以把绘制区域限制在矩形区域内,但是如果要不规则区域,则需要模板测试了。我们直接看一个例子吧,上面都太抽象了,表示我也看不懂-_-。
12.3.1 例子
空间中有一个球体,一个平面镜。我们站在某个特殊的观察点,可以看到球体在平面镜中的镜像,并且镜像处于平面镜的边缘,有一部分因为平面镜大小的限制,而无法显示出来。整个场景的效果如下图:
主要代码:
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();
}
12.4 深度测试
深度测试需要深度缓冲区,跟模板测试需要模板缓冲区是类似的。如果使用GLUT工具包,可以在调用glutInitDisplayMode
函数时在参数中加上GLUT_DEPTH
,这样来明确指定要求使用深度缓冲区。
通过glEnable/glDisable函数可以启用或禁用深度测试。
glEnable(GL_DEPTH_TEST);
// 启用深度测试
glDisable(GL_DEPTH_TEST);
// 禁用深度测试
至于通过测试的条件,同样有八种,与Alpha测试中的条件设置相同。条件设置是通过glDepthFunc函数完成的,默认值是GL_LESS。
glDepthFunc(GL_LESS);
前面已经用过了,这里不再赘述了,用法也比较简单,下面是动画一节的代码。
glEnable(GL_DEPTH_TEST); //启动深度测试(这样后绘制的图形如果在已经存在的图形的前面,它会被遮住,而不是遮住别人
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清空颜色和深度缓冲