LearnOpenGL学习笔记—高级OpenGL 02:模板测试

本文围绕OpenGL的模板测试展开,介绍了模板测试基于模板缓冲进行,可通过改写模板值决定片段的丢弃或保留。还阐述了模板函数glStencilFunc和glStencilOp的使用,以及利用模板测试实现物体轮廓的步骤,包括设置模板值、缩放物体、使用不同着色器等,该算法在游戏中较为常见。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LearnOpenGL学习笔记—高级OpenGL 02:模板测试

本节对应官网学习内容:模板测试
知识部分的文字阐述我主要是以我的个人理解改写了一下官网原文,让它读起来更通顺容易理解
然后在以前文章的程序基础(整个程序可见模型加载复习的笔记),进行了小修改

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结合应该可以把被遮挡物体渲染成填充式轮廓的效果。
类似于开了透视挂能看到墙背后的人影那样。
在以上的代码做如下小修改,加上深度测试的开启和关闭
在这里插入图片描述
得到透视的效果
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值