还记得这张图吗,执行完片段着色器以后就会执行“测试与混合”,模板测试属于其中之一。这个阶段主要有三个环节:
- 模板测试,根据模板缓冲来判断是否应该丢弃片段,开发可以将模板缓冲值设定成想要的值。一般而言,模板缓冲占8位
- 深度测试,防止被阻挡的面渲染到其它面的前面,如箱子绘制中,箱子背面显示不能显示出来,主要用于形成立体感
- 混合,主要是根据alpha值进行颜色混合,这个后面再讲
注意,这三个环节,先进行模板测试,再做深度测试,再做混合。
今天讲模板测试。
1、模板缓冲原理
模板缓冲的一个简单的例子如下:
模板缓冲首先会被清除为0,之后在模板缓冲中使用1填充了一个空心矩形。场景中的片段将会只在片段的模板值为1的时候会被渲染(其它的都被丢弃了)。
模板缓冲操作允许我们在渲染片段时将模板缓冲设定为一个特定的值。通过在渲染时修改模板缓冲的内容,我们写入了模板缓冲。在同一个(或者接下来的)渲染迭代中,我们可以读取这些值,来决定丢弃还是保留某个片段
模板缓冲大致步骤为:
- 启用模板缓冲的写入。
- 渲染物体,更新模板缓冲的内容。
- 禁用模板缓冲的写入。
- 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。
那我们怎么写入模板缓冲呢?这与模板函数有关
2、模板函数
模板函数说明如下:
glEnable(GL_STENCIL_TEST); //启动模板缓冲
glClear(GL_STENCIL_BUFFER_BIT); //清理模板缓存,绘制之前必须调用
glStencilMask(0xFF) //设置模板缓冲掩码,设置的值和0xFF与操作,意思是允许写入模板缓冲
glStencilMask(0x00) //禁止写入模板缓冲
模板函数中最重要的两个函数单独拎出来说一下:
glStencilFunc(GLenum func, GLint ref, GLuint mask) 一共包含三个参数:
func:设置模板测试函数(Stencil Test Function)。这个测试函数将会应用到已储存的模板值上和glStencilFunc函数的ref值上。可用的选项有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。它们的语义和深度缓冲的函数类似。ref:设置了模板测试的参考值(Reference Value)。模板缓冲的内容将会与这个值进行比较。mask:设置一个掩码,它将会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算。初始情况下所有位都为1。
举个粟子:
glStencilFunc(GL_EQUAL, 1, 0xFF)
这会告诉OpenGL,只要一个片段的模板值等于(GL_EQUAL)参考值1,片段将会通过测试并被绘制,否则会被丢弃。
但是glStencilFunc仅仅描述了OpenGL应该对模板缓冲内容做什么,而不是我们应该如何更新缓冲。这就需要glStencilOp这个函数了。
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一样,但如果模板值超过了最大值则归零 |
| GL_DECR | 如果模板值大于最小值则将模板值减1 |
| GL_DECR_WRAP | 与GL_DECR一样,但如果模板值小于0则将其设置为最大值 |
| GL_INVERT | 按位翻转当前的模板缓冲值 |
默认情况下glStencilOp是设置为(GL_KEEP, GL_KEEP, GL_KEEP)的,所以不论任何测试的结果是如何,模板缓冲都会保留它的值。默认的行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你需要至少对其中一个选项设置不同的值。
3、物体轮廓
我们通过模板测试来绘制轮廓效果。这里有两个问题:
- 怎么判断是边缘呢?通过模板测试,如果我们先绘制一个立方体,绘制的片段模板缓冲都为1,那这个立方体周边一圈的模板缓冲就是0了,这样是不是能够判断出边缘呢
- 怎么绘制这一小圈红边呢?我们绘制一个稍大一点的立方体,这个立方体通体是红色的,并且和原立方体在同一位置,红色立方体的片段中有两类,一类是和原立方体重合的,它们的模板缓冲值为1,另一类是稍大出原立方体,它们的模板缓冲是0,那我们可以设定只有模板缓冲为等于1的才能绘制,否则丢弃,即实现这个红色边框
现在我们就要来写入模板缓冲了,怎么写呢?分如下几步:
- 在绘制(需要添加轮廓的)物体之前,将模板函数设置为
GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。 - 渲染物体。
- 禁用模板写入以及深度测试。
- 将每个物体缩放一点点。
- 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
- 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
- 再次启用模板写入和深度测试。
最后贴出代码:
void StencilSample::draw() {
LOGI("draw");
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
objectShader.use();
glBindVertexArray(mVao);
auto rat = MyGlRenderContext::getInstance()->getWidth() * 1.0f /
MyGlRenderContext::getInstance()->getHeight();
glm::mat4 projection = glm::perspective(glm::radians(45.0f), rat, 0.1f, 100.0f);
glm::mat4 view = camera.getViewMatrix();
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(60.0f), glm::vec3(0.5f, 1.0f, 0.0f));
objectShader.setMat4("projection", projection);
objectShader.setMat4("view", view);
objectShader.setMat4("model", model);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mFirstId);
objectShader.setInt("firId", 0);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
glBindVertexArray(mVao);
singleShader.use();
float scale = 1.05f;
singleShader.setMat4("projection", projection);
singleShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(60.0f), glm::vec3(0.5f, 1.0f, 0.0f));
model = glm::scale(model, glm::vec3(scale, scale, scale));
singleShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);
}
最后,提醒一点,android上如果要用模板测试,需要在GlSurfaceView的初始化中配置下,否则模板测试将不生效
setEGLConfigChooser(8,8,8,8,16,8);