本节对应官网学习内容:模板测试
知识部分的文字阐述我主要是以我的个人理解改写了一下官网原文,让它读起来更通顺容易理解
然后在以前文章的程序基础(整个程序可见模型加载复习的笔记),进行了小修改
1 模板测试
当片段着色器处理完一个片段之后,接着会执行模板测试(Stencil Test)。
这个测试和深度测试一样,它也可能会把一些片段丢掉,经过模板测试后被保留的片段会接着进入深度测试。
如同深度测试根据深度缓冲进行,模板测试是根据模板缓冲(Stencil Buffer)来进行的,我们可以在渲染的时候利用它来获得一些有意思的效果。
一个模板缓冲中,(通常)每个模板值(Stencil Value)是8位的(即每个像素/片段一共能有256种不同的模板值)。
我们可以改写这些模板值来设置为我们想要的值,根据设置片段与模板值的关系,我们就可以选择丢弃或是保留这个片段了。
- GLFW自动为我们配置一个模板缓冲,但其它的窗口库可能不会默认创建,所以记得要查看库的文档。
模板缓冲的一个简单的例子如下:
- 在上面这个例子中,模板缓冲首先被全部清除为0,之后使用1填充了一个空心矩形。于是场景中的片段就只会在片段的模板值为1的时候会被渲染(其它的都被丢弃了)。
模板缓冲操作可以让我们在渲染片段时,将模板缓冲设定为一个特定的值。
通过在渲染时修改了这些值,这一步也就是我们写入了模板缓冲。
在同一个(或者接下来的)渲染迭代中,我们可以通过读取这些值来决定丢弃还是保留某个片段。
- 使用模板缓冲大体的步骤如下:
1、启用模板缓冲的写入。
2、渲染物体,更新模板缓冲的内容。
3、禁用模板缓冲的写入。
4、渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。
所以,通过使用模板缓冲,我们可以根据场景中已绘制的其它物体的片段,来决定是否丢弃特定的片段。
我们可以通过启用GL_STENCIL_TEST
来启用模板测试。在这一行代码之后,所有的渲染调用都会以某种方式影响着模板缓冲。
和颜色和深度缓冲一样,我们也需要在每次迭代之前清除模板缓冲。
和深度测试的glDepthMask函数一样(回顾:OpenGL允许我们禁用深度缓冲的写入,只需要设置它的深度掩码(Depth Mask)设置为GL_FALSE就可以了)
模板缓冲也有一个类似的函数。glStencilMask允许我们设置一个位掩码(Bitmask),它会与将要写入缓冲的模板值进行与(AND)运算。
默认情况下设置的位掩码所有位都为1,不影响输出,但如果我们将它设置为0x00,写入缓冲的所有模板值最后都会变成0.这与深度测试中的glDepthMask(GL_FALSE)是等价的。
glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)
根据文章所述说,大部分情况下我们都只会使用0x00或者0xFF作为模板掩码(Stencil Mask),但是其实是可以设置自定义的位掩码的,这个我们清楚就好。
2 模板函数
我们在深度测试中设置过深度测试函数(glDepthFunc)
同样的,我们对模板测试应该通过还是失败,以及它之后应该如何影响模板缓冲,也是有一定控制的。
一共有两个函数能够用来配置模板测试:glStencilFunc和glStencilOp。
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。
最开始的那个模板测试的例子,我们是只采用模板值为1的情况
也就是用了这个代码glStencilFunc(GL_EQUAL, 1, 0xFF)
它告诉OpenGL,只要一个片段的模板值等于(GL_EQUAL)参考值1,片段将会通过测试并被绘制,否则会被丢弃。
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)一共包含三个选项
- sfail:模板测试失败时采取的行为。
- dpfail:模板测试通过,但深度测试失败时采取的行为。
- dppass:模板测试和深度测试都通过时采取的行为。
每个选项都可以选用以下的其中一种行为:
- 默认情况下glStencilOp是设置为(GL_KEEP, GL_KEEP, GL_KEEP)的,所以不论任何测试的结果是如何,模板缓冲都会保留它的值。
默认的行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你需要至少对其中一个选项设置不同的值。
所以,通过使用glStencilFunc和glStencilOp,我们可以精确地指定更新模板缓冲的时机与行为了,我们也可以指定什么时候该让模板缓冲通过,即什么时候片段需要被丢弃。
3 物体轮廓
仅仅是知识的部分可能还是不能够完全理解模板测试的工作原理,所以接下来将会展示一个使用模板测试就可以完成的有用特性,它叫做物体轮廓(Object Outlining)。
物体轮廓所能做的事情正如它名字所描述的那样。我们将会为物体在它的周围创建一个很小的有色边框。
当想要在策略游戏中选中一个单位进行操作的,想要告诉玩家选中的是哪个单位的时候,这个效果就非常有用了。
为物体创建轮廓的步骤如下:
- 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
- 渲染物体。
- 禁用模板写入以及深度测试。
- 将每个物体缩放一点点。
- 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
- 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
- 再次启用模板写入和深度测试。
这个过程将每个物体的片段的模板缓冲设置为1,当我们想要绘制边框的时候,我们主要绘制放大版本的物体中模板测试通过的部分,也就是物体的边框的位置。我们主要使用模板缓冲丢弃了放大版本中属于原物体片段的部分。
此处模板可以类比PS中的蒙版,每一次绘制会检测之前的Stencil Buffer里的内容,和当前设置的Stencil Func的方式进行对比,满足条件才会在某一点绘制;同时,此次绘制如果打开了Stencil Buffer写入的话,就会再次将此次绘制的范围,用此次设置的StencilMask,写入到Stencil Buffer中去。
所以我们首先来创建一个很简单的片段着色器,它会输出一个边框颜色。
我们新建一个Border.frag
非常简单,只是输出一个片段颜色
#version 330 core
in vec4 vertexColor;
out vec4 FragColor;
void main(){
FragColor = vec4(0.04, 0.28, 0.26, 1.0);
}
根据这个片段着色器,加上之前用的顶点着色器
组合成一个新的shader实例使用
我们开启模板测试
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
在更新缓存时,更新模板值
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
我们回顾一下之前绘制的顺序,我们绘制了10个立方体,然后绘制了1个机器人。
我们的机器人在方块中间,我们为了让边框能够在机器人的前后分别显示,需要首先绘制机器人。而后再绘制原先的十个方块,再往绘制边框(为了之后绘制时深度测试能起作用)。
我们修改之前的渲染循环代码,改一下顺序,加上模板测试的值
//方块 0代表模型
for (unsigned int i = 0; i < 11; i++)
{
if (i != 0) {
int k = i - 1;
//Set Model matrix
modelMat = glm::translate(glm::mat4(1.0f), cubePositions[k]);
float angle = 20.0f * (k);
//float angle = 20.0f * i + 50*glfwGetTime();
modelMat = glm::rotate(modelMat, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
}
else {
modelMat = glm::translate(glm::mat4(1.0f), { 0,-10,-5 });
}
//Set view matrix
viewMat = camera.GetViewMatrix();
//Set projection matrix
projMat = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
//Set Material -> Shader Program
myShader->use();
//Set Material -> Uniforms
#pragma region Uniform
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "modelMat"), 1, GL_FALSE, glm::value_ptr(modelMat));
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "viewMat"), 1, GL_FALSE, glm::value_ptr(viewMat));
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "projMat"), 1, GL_FALSE, glm::value_ptr(projMat));
glUniform3f(glGetUniformLocation(myShader->ID, "cameraPos"), camera.Position.x, camera.Position.y, camera.Position.z);
glUniform1f(glGetUniformLocation(myShader->ID, "time"), glfwGetTime());
glUniform3f(glGetUniformLocation(myShader->ID, "objColor"), 1.0f, 1.0f, 1.0f);
glUniform3f(glGetUniformLocation(myShader->ID, "ambientColor"), 0.1f, 0.1f, 0.1f);
glUniform3f(glGetUniformLocation(myShader->ID, "lightD.color"), lightD.color.x, lightD.color.y, lightD.color.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightD.dirToLight"), lightD.direction.x, lightD.direction.y, lightD.direction.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightP0.pos"), lightP0.position.x, lightP0.position.y, lightP0.position.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightP0.color"), lightP0.color.x, lightP0.color.y, lightP0.color.z);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP0.constant"), lightP0.constant);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP0.linear"), lightP0.linear);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP0.quadratic"), lightP0.quadratic);
glUniform3f(glGetUniformLocation(myShader->ID, "lightP1.pos"), lightP1.position.x, lightP1.position.y, lightP1.position.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightP1.color"), lightP1.color.x, lightP1.color.y, lightP1.color.z);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP1.constant"), lightP1.constant);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP1.linear"), lightP1.linear);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP1.quadratic"), lightP1.quadratic);
glUniform3f(glGetUniformLocation(myShader->ID, "lightP2.pos"), lightP2.position.x, lightP2.position.y, lightP2.position.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightP2.color"), lightP2.color.x, lightP2.color.y, lightP2.color.z);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP2.constant"), lightP2.constant);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP2.linear"), lightP2.linear);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP2.quadratic"), lightP2.quadratic);
glUniform3f(glGetUniformLocation(myShader->ID, "lightP3.pos"), lightP3.position.x, lightP3.position.y, lightP3.position.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightP3.color"), lightP3.color.x, lightP3.color.y, lightP3.color.z);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP3.constant"), lightP3.constant);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP3.linear"), lightP3.linear);
glUniform1f(glGetUniformLocation(myShader->ID, "lightP3.quadratic"), lightP3.quadratic);
glUniform3f(glGetUniformLocation(myShader->ID, "lightS.pos"), lightS.position.x, lightS.position.y, lightS.position.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightS.color"), lightS.color.x, lightS.color.y, lightS.color.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightS.dirToLight"), lightS.direction.x, lightS.direction.y, lightS.direction.z);
glUniform1f(glGetUniformLocation(myShader->ID, "lightS.constant"), lightS.constant);
glUniform1f(glGetUniformLocation(myShader->ID, "lightS.linear"), lightS.linear);
glUniform1f(glGetUniformLocation(myShader->ID, "lightS.quadratic"), lightS.quadratic);
glUniform1f(glGetUniformLocation(myShader->ID, "lightS.cosPhyInner"), lightS.cosPhyInner);
glUniform1f(glGetUniformLocation(myShader->ID, "lightS.cosPhyOuter"), lightS.cosPhyOuter);
#pragma endregion
myMaterial->shader->SetUniform3f("material.ambient", myMaterial->ambient);
myMaterial->shader->SetUniform1f("material.shininess", myMaterial->shininess);
if (i == 0) {
glStencilMask(0x00); // 记得保证我们在绘制机器人的时候不会更新模板缓冲
model.Draw(myShader);
}
else {
glStencilFunc(GL_ALWAYS, 1, 0xFF); // 更新模板缓冲函数,所有的片段都要写入模板
glStencilMask(0xFF); // 启用模板缓冲写入
//正常绘制十个正方体,而后记录模板值
cube.DrawArray(myMaterial->shader, myMaterial->diffuse, myMaterial->specular, myMaterial->emission);
//现在模板缓冲在箱子被绘制的地方都更新为1了,我们将要绘制放大的箱子,也就是绘制边框
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 禁止模板缓冲的写入
border->use();
//Set Model matrix
modelMat = glm::translate(glm::mat4(1.0f), cubePositions[i-1]);
float angle = 20.0f * (i-1);
modelMat = glm::rotate(modelMat, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
modelMat = glm::scale(modelMat, glm::vec3(1.2, 1.2, 1.2));
glUniformMatrix4fv(glGetUniformLocation(border->ID, "modelMat"), 1, GL_FALSE, glm::value_ptr(modelMat));
glUniformMatrix4fv(glGetUniformLocation(border->ID, "viewMat"), 1, GL_FALSE, glm::value_ptr(viewMat));
glUniformMatrix4fv(glGetUniformLocation(border->ID, "projMat"), 1, GL_FALSE, glm::value_ptr(projMat));
//因为之前设置了GL_NOTEQUAL,它会保证我们只绘制箱子上模板值不为1的部分
cube.DrawArray(border,1,1,1);
glStencilMask(0xFF);
}
}
最后我们得到效果图(我们为了机器人的遮挡效果开启了深度测试,所以也导致的物体边框会遮挡其他物体)
物体轮廓算法在需要显示选中物体的游戏中非常常见。这样的算法能够在一个模型类中轻松实现。
可以在模型类中设置一个boolean标记,来设置需不需要绘制边框。
- 代码上其实就是画了两个图形。
一般情况下,两个图形都会画出来(当然深度测试会挡住一部分)。
现在加入了模板测试,那么第一个画的图形的模板值全替换为1(关键操作GL_REPLACE 和 开启缓冲输入)。
第二个图形,放大一点点,换个单色的着色器,那么一般情况下,会有有一个大一圈的单色模型挡住了第一个图形(深度测试的锅)。
哪怕关了深度测试,图形依然是一片绿的(后绘制的覆盖前绘制的)。
放大并进行2th render的方法描边并不适用于所有的模型。描边这个章节可以深入到法线扩展/卡通渲染(Toon Shading)。
Stencil Testing和Depth Testing结合应该可以把被遮挡物体渲染成填充式轮廓的效果。
类似于开了透视挂能看到墙背后的人影那样。
在以上的代码做如下小修改,加上深度测试的开启和关闭
得到透视的效果