OpenGL核心技术之模版测试

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN课程视频网址:http://edu.csdn.net/lecturer/144

上节给读者介绍了深度测试,本节介绍一下模版测试,模版测试跟深度测试是不同的,GPU都会执行片段着色器处理,当片段着色器处理完片段之后,模板测试(Stencil Test) 就开始执行了,和深度测试一样,它能丢弃一些片段。仍然保留下来的片段进入深度测试阶段,深度测试可能丢弃更多。模板测试基于另一个缓冲,这个缓冲叫做模板缓冲(Stencil Buffer),它是在深度测试之前执行的。

模板缓冲中的模板值(Stencil Value)通常是8位的,因此每个片段/像素共有256种不同的模板值(译注:8位就是1字节大小,因此和char的容量一样是256个不同值)。为了能让读者更直观的认识模版测试,下面通过图的方式说明:

OpenGL核心技术之模版测试_第1张图片

模板缓冲先清空模板缓冲设置所有片段的模板值为0,然后开启矩形片段用1填充。场景中的模板值为1的那些片段才会被渲染(其他的都被丢弃)。模版缓冲要遵守下面的规则:

  • 开启模板缓冲写入。
  • 渲染物体,更新模板缓冲。
  • 关闭模板缓冲写入。
  • 渲染(其他)物体,这次基于模板缓冲内容丢弃特定片段。

使用模板缓冲我们可以基于场景中已经绘制的片段,来决定是否丢弃特定的片段。

你可以开启GL_STENCIL_TEST来开启模板测试。接着所有渲染函数调用都会以这样或那样的方式影响到模板缓冲。

glEnable(GL_STENCIL_TEST);

要注意的是,像颜色和深度缓冲一样,在每次循环,你也得清空模板缓冲。

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

同时,和深度测试的glDepthMask函数一样,模板缓冲也有一个相似函数。glStencilMask允许我们给模板值设置一个位遮罩(Bitmask),它与模板值进行按位与(AND)运算决定缓冲是否可写。默认设置的位遮罩都是1,这样就不会影响输出,但是如果我们设置为0x00,所有写入深度缓冲最后都是0。这和深度缓冲的glDepthMask(GL_FALSE)很类似:

// 0xFF == 0b11111111
//此时,模板值与它进行按位与运算结果是模板值,模板缓冲可写
glStencilMask(0xFF); 

// 0x00 == 0b00000000 == 0
//此时,模板值与它进行按位与运算结果是0,模板缓冲不可写
glStencilMask(0x00); 

大多数情况你的模板遮罩(stencil mask)写为0x00或0xFF就行,但是最好知道有一个选项可以自定义位遮罩。

和深度测试一样,模版测试也有两个函数,决定何时模板测试通过或失败以及它怎样影响模板缓冲。模板测试的两个函数:glStencilFuncglStencilOpvoid glStencilFunc(GLenum func, GLint ref, GLuint mask)函数有三个参数:

  • func:设置模板测试操作。这个测试操作应用到已经储存的模板值和glStencilFuncref值上,可用的选项是:GL_NEVERGL_LEQUALGL_GREATERGL_GEQUALGL_EQUALGL_NOTEQUALGL_ALWAYS。它们的语义和深度缓冲的相似。
  • ref:指定模板测试的引用值。模板缓冲的内容会与这个值对比。
  • mask:指定一个遮罩,在模板测试对比引用值和储存的模板值前,对它们进行按位与(and)操作,初始设置为1。

在上面简单模板的例子里,方程应该设置为:

glStencilFunc(GL_EQUAL, 1, 0xFF)

它会告诉OpenGL,无论何时,一个片段模板值等于(GL_EQUAL)引用值1,片段就能通过测试被绘制了,否则就会被丢弃。

但是glStencilFunc只描述了OpenGL对模板缓冲做什么,而不是描述我们如何更新缓冲。这就需要glStencilOp登场了。

void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)函数包含三个选项,我们可以指定每个选项的动作:

  • sfail: 如果模板测试失败将采取的动作。
  • dpfail: 如果模板测试通过,但是深度测试失败时采取的动作。
  • dppass: 如果深度测试和模板测试都通过,将采取的动作。

每个选项都可以使用下列任何一个动作。

操作	描述
GL_KEEP	保持现有的模板值
GL_ZERO	将模板值置为0
GL_REPLACE	将模板值设置为用glStencilFunc函数设置的ref值
GL_INCR	如果模板值不是最大值就将模板值+1
GL_INCR_WRAP	与GL_INCR一样将模板值+1,如果模板值已经是最大值则设为0
GL_DECR	如果模板值不是最小值就将模板值-1
GL_DECR_WRAP	与GL_DECR一样将模板值-1,如果模板值已经是最小值则设为最大值

glStencilOp函数默认设置为 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何测试的任何结果,模板缓冲都会保留它的值。默认行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你必须像任意选项指定至少一个不同的动作。

使用glStencilFuncglStencilOp,我们就可以指定在什么时候以及我们打算怎么样去更新模板缓冲了,我们也可以指定何时让测试通过或不通过。什么时候片段会被抛弃。

学习了模版以后,如何在开发中使用,这个是我们最关心的,下面 展示一个用模板测试实现的一个特别的和有用的功能,叫做 物体轮廓(Object Outlining) 。效果如下图所示:

OpenGL核心技术之模版测试_第2张图片

物体轮廓就像它的名字所描述的那样,它能够给每个(或一个)物体创建一个有颜色的边。在策略游戏中当你打算选择一个单位的时候它特别有用。给物体加上轮廓的步骤如下:

  1. 在绘制物体前,把模板方程设置为GL_ALWAYS,用1更新物体将被渲染的片段。
  2. 渲染物体,写入模板缓冲。
  3. 关闭模板写入和深度测试。
  4. 每个物体放大一点点。
  5. 使用一个不同的片段着色器用来输出一个纯颜色。
  6. 再次绘制物体,但只是当它们的片段的模板值不为1时才进行。
  7. 开启模板写入和深度测试。

这个过程将每个物体的片段模板缓冲设置为1,当我们绘制边框的时候,我们基本上绘制的是放大版本的物体的通过测试的地方,放大的版本绘制后物体就会有一个边。我们基本会使用模板缓冲丢弃所有的不是原来物体的片段的放大的版本内容。

我们先来创建一个非常基本的片段着色器,它输出一个边框颜色。我们简单地设置一个固定的颜色值,把这个着色器命名为shaderSingleColor:

void main()
{
    outColor = vec4(0.04, 0.28, 0.26, 1.0);
}

我们只打算给两个箱子加上边框,所以我们不会对地面做什么。这样我们要先绘制地面,然后再绘制两个箱子(同时写入模板缓冲),接着我们绘制放大的箱子(同时丢弃前面已经绘制的箱子的那部分片段)。

我们先开启模板测试,设置模板、深度测试通过或失败时才采取动作:

glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

如果任何测试失败我们都什么也不做,我们简单地保持深度缓冲中当前所储存着的值。如果模板测试和深度测试都成功了,我们就将储存着的模板值替换为1,我们要用glStencilFunc来做这件事。

我们清空模板缓冲为0,为箱子的所有绘制的片段的模板缓冲更新为1,实现代码片段如下所示:

glStencilFunc(GL_ALWAYS, 1, 0xFF); //所有片段都要写入模板缓冲
glStencilMask(0xFF); // 设置模板缓冲为可写状态
normalShader.Use();
DrawTwoContainers();

使用GL_ALWAYS模板测试函数,我们确保箱子的每个片段用模板值1更新模板缓冲。因为片段总会通过模板测试,在我们绘制它们的地方,模板缓冲用引用值更新。

现在箱子绘制之处,模板缓冲更新为1了,我们将要绘制放大的箱子,但是这次关闭模板缓冲的写入:

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 禁止修改模板缓冲
glDisable(GL_DEPTH_TEST);
shaderSingleColor.Use();
DrawTwoScaledUpContainers();

我们把模板方程设置为GL_NOTEQUAL,它保证我们只箱子上不等于1的部分,这样只绘制前面绘制的箱子外围的那部分。注意,我们也要关闭深度测试,这样放大的的箱子也就是边框才不会被地面覆盖。

做完之后还要保证再次开启深度缓冲。

场景中的物体边框的绘制方法最后看起来像这样:

glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);  

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

glStencilMask(0x00); // 绘制地板时确保关闭模板缓冲的写入
normalShader.Use();
DrawFloor()  

glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
DrawTwoContainers();

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
shaderSingleColor.Use();
DrawTwoScaledUpContainers();
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);


实现的效果图如下所示:
OpenGL核心技术之模版测试_第3张图片


最后把Shader代码的文件stencil_single_color.frag给读者展示一下:

#version 330 core
out vec4 outColor;

void main()
{
    outColor = vec4(0.04, 0.28, 0.26, 1.0);
}
stencil_testing.vs代码如下所示:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texCoords;

out vec2 TexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    TexCoords = texCoords;
}
stencil_testing.frag代码如下所示:

#version 330 core
in vec2 TexCoords;

out vec4 color;

uniform sampler2D texture1;

void main()
{             
    color = texture(texture1, TexCoords);
}

接下来就是通过OpenGL的C++代码进行处理Shader了,下面把核心代码拿出来展示:

// 定义视口尺寸
    glViewport(0, 0, screenWidth, screenHeight);

    // 深度测试
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    glEnable(GL_STENCIL_TEST);
    glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

    // 加载Shader
    Shader shader("stencil_testing.vs", "stencil_testing.frag");
    Shader shaderSingleColor("stencil_testing.vs", "stencil_single_color.frag");

下面是对场景中的物体具体处理:

	// Clear the colorbuffer
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

        // Set uniforms
        shaderSingleColor.Use();		
        glm::mat4 model;
        glm::mat4 view = camera.GetViewMatrix();
        glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f);
        glUniformMatrix4fv(glGetUniformLocation(shaderSingleColor.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(shaderSingleColor.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
        shader.Use(); 
        glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));

        // Draw floor as normal, we only care about the containers. The floor should NOT fill the stencil buffer so we set its mask to 0x00
        glStencilMask(0x00);
        // Floor
        glBindVertexArray(planeVAO);
        glBindTexture(GL_TEXTURE_2D, floorTexture);
        model = glm::mat4();
        glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
        glDrawArrays(GL_TRIANGLES, 0, 6);		
        glBindVertexArray(0);	

        // == =============
        // 1st. Render pass, draw objects as normal, filling the stencil buffer
        glStencilFunc(GL_ALWAYS, 1, 0xFF);
        glStencilMask(0xFF);		
        // Cubes
        glBindVertexArray(cubeVAO);
        glBindTexture(GL_TEXTURE_2D, cubeTexture);  
        model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
        glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
        glDrawArrays(GL_TRIANGLES, 0, 36);
        model = glm::mat4();
        model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
        glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);	

        // == =============
        // 2nd. Render pass, now draw slightly scaled versions of the objects, this time disabling stencil writing.
        // Because stencil buffer is now filled with several 1s. The parts of the buffer that are 1 are now not drawn, thus only drawing 
        // the objects' size differences, making it look like borders.
        glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
        glStencilMask(0x00);
        glDisable(GL_DEPTH_TEST);
        shaderSingleColor.Use();
        GLfloat scale = 1.1;

这样模版测试的核心技术给读者就介绍完了,希望对大家有所帮助。。。。。。






你可能感兴趣的:(3D引擎,图形学编程,OpenGL核心技术)