当前位置: 首页 > ops >正文

【OpenGL】LearnOpenGL学习笔记13 - 深度测试、模板测试

上接:https://blog.csdn.net/weixin_44506615/article/details/150562382?spm=1001.2014.3001.5501
完整代码:https://gitee.com/Duo1J/learn-open-gl

一、深度测试 (Depth Testing)

在最初绘制立方体的时候,我们通过启用 深度测试 来解决由于绘制顺序导致最后渲染结果前后混乱的问题
例如我们的背包,假如不开启深度测试,画面就会非常的诡异
未开启深度测试
现在,我们来详细阐述一下深度测试和深度缓冲 (Depth Buffer)
深度缓冲和颜色缓冲一样,在每个片段中存储了这个片段的深度信息
在深度测试启用之后,OpenGL会将每个片段的深度值和缓冲中的内容进行比较,具体比较的方式会根据我们之后会设置的比较方法来定,如果这个测试通过了的话,深度缓冲将会更新为新的深度值,测试失败则丢弃
深度测试是在片段着色器以及后续会提到的模板测试之后进行

我们可以通过以下代码来开启深度测试,并每帧清理深度缓冲

// 开启深度测试
glEnable(GL_DEPTH_TEST);
// 每一帧清理深度缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glDepthMask
同时我们可以通过以下方式来让所有的片段都只进行深度测试而不更新深度缓冲,这在特定的需求环境下会比较有用

glDepthMask(GL_FALSE);

glDepthFunc
上面提到,深度测试是会使用我们设置比较方法而定

glDepthFunc(GL_LESS);

这表示我们会在片段深度值小于缓冲深度值 (即片段离相机更近) 时通过测试
还有以下方法可以设置

方法描述
GL_ALWAYS永远通过深度测试
GL_NEVER永远不通过深度测试
GL_LESS在片段深度值小于缓冲深度值时通过
GL_EQUAL在片段深度值等于缓冲深度值时通过
GL_GREATER在片段深度值大于缓冲深度值时通过
GL_LEQUAL在片段深度值小于等于缓冲深度值时通过
GL_GEQUAL在片段深度值大于等于缓冲深度值时通过
GL_NOTEQUAL在片段深度值不等于缓冲深度值时通过

深度缓冲可视化
在片段着色器中,我们可以通过 gl_FragCoord.z 来获取到片段的深度值,gl_FragCoord的x、y分量则表示片段的屏幕空间坐标

FragColor = vec4(vec3(gl_FragCoord.z), 1.0);

运行后发现茫茫白一片
深度缓冲
靠近后可以观察到深度变化
深度缓冲
为何要靠这么近才能观察到深度值变化呢,接下来了解一下深度值的精度问题

深度值的精度
深度值是介于0~1之间,我们可以利用相机的近平面和远平面来作为范围计算深度值
线性深度值的计算
由此我们可以计算出片段的线性深度值,但是在实际情况下我们往往不会使用这样的线性深度缓冲,因为对于远距离我们不太需要那么高的精度,而对于近距离的物体,我们希望尽可能的有更高的精度
所以会通过以下方式来计算一个非线性的深度值
非线性深度值
因此,我们在深度缓冲的可视化中,需要离得比较近才可以观察到深度值的变化
不过,我们也可以通过一些计算将非线性的深度值转换为线性深度值
我们可以先将深度值从[0, 1]的范围映射到[-1, 1]的NDC坐标,再利用投影矩阵的逆变换反推出view空间下的值

FragmentShader.glsl

uniform float near;
uniform float far;float LinearizeDepth(float depth) 
{float z = depth * 2.0 - 1.0;return (2.0 * near * far) / (far + near - z * (far - near));    
}void main()
{// ...float depth = (LinearizeDepth(gl_FragCoord.z) - near) / (far - near);FragColor = vec4(vec3(depth), 1.0);
}

Camera.h

public:/*** 近平面*/float near = 0.01f;/*** 远平面*/float far = 100;

Main.cpp

int main()
{// 远平面改为5来观察我们的背包会更明显camera.far = 5;shader.SetFloat("near", camera.near);shader.SetFloat("far", camera.far);// 投影矩阵最后两位参数修改为Camera新增的near和farprojection = glm::perspective(glm::radians(camera.fov), screenWidth / screenHeight, camera.near, camera.far);// ...
}

编译运行,顺利的话可以看见以下图像
非线性转线性深度值
深度冲突 (Z - fighting)
如果两个平面非常紧密地平行排列的话,就会出现深度冲突问题,可以看见画面会一直闪烁
我们再绘制一个背包

model.Draw(shader);modelMatrix = glm::translate(modelMatrix, glm::vec3(0.01f, 0.0f, 0.0f));
shader.SetMat4("model", modelMatrix);
model.Draw(shader);

深度冲突
在移动时可以看见画面会一直闪烁不停
所以我们要注意不要把多个物体摆的太接近以至于其中的面会重叠
二是可以尽量将远平面设置的远一些 这样一来近处的物体就会有更高的精度
三是可以提高深度缓冲的精度,但会牺牲一定的性能

二、模板测试 (Stencil Testing)

在深度测试时说到,片段着色器后会先进行模板测试然后才是深度测试
模板测试是依赖 模板缓冲(Stencil Buffer) 来进行的
在模板缓冲中,通常每个模板值是8位,也就是每个片段一共有256中不同的模板值
我们可以在模板缓冲中写入特定值,当一个片段有这个特定的模板值我们就可以选择丢弃或是保留这个片段,如下图所示 (图片来自于LearnOpenGL)
模板测试
我们可以这样来启用模板测试并每帧清理模板缓冲

// 开启模板测试
glEnable(GL_STENCIL_TEST);// 每帧清理模板缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

glStencilMask
glDepthMask一样,我们可以通过glStencilMask来为模板测试设置一个掩码
将要写入缓冲的模板值会与掩码进行与运算(&)

glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)

glDepthFunc一样,我们可以通过glStencilFuncglStencilOp来设置模板测试方法

glStencilFunc

// 参数1: 同glDepthFunc
// 参数2: 比较的参考值
// 参数3: 掩码,与参考值和测试的模板值在比较前进行与运算(&)
// 当一个片段模板值为1的时候,片段会通过测试并被绘制,否则丢弃
glStencilFunc(GL_EQUAL, 1, 0xFF);

glStencilOp
我们可以通过这个函数来设置测试后的行为

// 参数1: 模板测试失败时采取的行为
// 参数2: 模板测试通过,深度测试失败时采取的行为
// 参数3:模板测试和深度测试都通过时采取的行为
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass);
行为描述
GL_KEEP保持当前存储的模板值
GL_ZERO将模板值设置为0
GL_REPLACE将模板值设置为glStencilFunc函数设置的ref
GL_INCR如果模板值小于最大值将模板值 + 1
GL_INCR_WRAPGL_INCR一样,但如果模板值超过了最大值则归零
GL_DECR如果模板值大于最小值则将模板值减1
GL_DECR_WRAPGL_DECR一样,但如果模板值小于0则将其设置为最大值
GL_INVERT按位翻转当前的模板缓冲值

默认情况下 glStencilOp 三个值均为 GL_KEEP
接下来我们来应用以下模板测试,为我们的背包画一个轮廓描边

物体轮廓
在游戏中我们选中一些单位或物体后,其上通常会出现一个描边来表示我们选中了他,通过模板测试创建物体轮廓的步骤如下

  1. 启用模板写入
  2. 在绘制需要创建轮廓的物体前,将模板函数设置为 GL_ALWAYS,每当物体的片段被渲染时,模板缓冲更新为1
  3. 渲染物体
  4. 禁用模板写入和深度测试
  5. 将物体缩放一点点
  6. 使用一个不同的片段着色器,输出一个单独的边框颜色
  7. 再次绘制物体,但只在它们的片段模板之不为1的时候绘制
  8. 再次启用模板写入和深度测试

这个步骤简单来说就是我们先画一遍原物体,写入模板值1
然后将物体扩大一点,再以纯色绘制且仅在模板值为0的时候 (没有绘制原物体) 保留
这样一来,就只有物体的边缘会被绘制上去

EdgeFragmentShader.glsl 新建
用于绘制描边的Shader

#version 330 coreout vec4 FragColor;void main()
{FragColor = vec4(1, 1, 1, 1);
}

Main.cpp

int main()
{// ...// 注意将 far 改回100camera.far = 100;glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LESS);// 启用模板测试glEnable(GL_STENCIL_TEST);// 模板和深度测试都通过后替换为参考值1glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);Shader shader("VertexShader.glsl", "FragmentShader.glsl");// 轮廓ShaderShader edgeShader("VertexShader.glsl", "EdgeFragmentShader.glsl");Model model("F:/Scripts/Cpp/LearnOpenGL/learn-open-gl/Resource/backpack/backpack.obj");while (!glfwWindowShouldClose(window)){float currentFrame = glfwGetTime();deltaTime = currentFrame - lastFrame;lastFrame = currentFrame;glClearColor(0.1f, 0.1f, 0.1f, 1.0f);// 每帧清理模板缓冲glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);ProcessKeyboardInput(window);glm::mat4 view = camera.GetViewMatrix();glm::mat4 projection = glm::mat4(1);projection = glm::perspective(glm::radians(camera.fov), screenWidth / screenHeight, camera.near, camera.far);// !注意先Use再设值!shader.Use();shader.SetMat4("view", view);shader.SetMat4("projection", projection);// ...光照参数// 设置描边Shader的VP矩阵edgeShader.Use();edgeShader.SetMat4("view", view);edgeShader.SetMat4("projection", projection);// 总是通过模板测试,并写入1glStencilFunc(GL_ALWAYS, 1, 0xFF);glStencilMask(0xFF);// 绘制原物体shader.Use();glm::mat4 modelMatrix = glm::mat4(1.0f);modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0f, 0.0f, 0.0f));shader.SetMat4("model", modelMatrix);model.Draw(shader);// 不等于1时通过glStencilFunc(GL_NOTEQUAL, 1, 0xFF);// 禁止写入模板缓冲glStencilMask(0x00);// 禁止深度测试glDisable(GL_DEPTH_TEST);edgeShader.Use();// 放大物体modelMatrix = glm::scale(modelMatrix, glm::vec3(1.01f, 1.01f, 1.01f));edgeShader.SetMat4("model", modelMatrix);model.Draw(edgeShader);// 开启模板写入glStencilMask(0xFF);glStencilFunc(GL_ALWAYS, 1, 0xFF);// 开启深度测试glEnable(GL_DEPTH_TEST);glfwSwapBuffers(window);glfwPollEvents();}shader.Delete();edgeShader.Delete();glfwTerminate();return 0;
}

编译运行,顺利的话可以看见以下图像
模板测试
完整代码可在顶部git仓库中找到

http://www.xdnf.cn/news/18325.html

相关文章:

  • Linux CentOS 安装 .net core 3.1
  • 1. 准备工作---数据分析编程 - 从入门到精通
  • 密码学——对称加密, 非对称加密和CA
  • 基于SpringBoot的流浪动物领养管理系统【2026最新】
  • 常见的端口扫描
  • 常德二院全栈国产化信创项目:开启医疗新质生产力的“头雁”之旅
  • Android 定位技术全解析:从基础实现到精准优化
  • 数据大屏全链路质量保障测试
  • 消息中间件(RocketMQ+RabbitMQ+Kafka)
  • C++手撕LRU
  • RocketMQ 消息消费 单个消费和批量消费配置实现对比(Springboot),完整实现示例对比
  • 链表-143.重排链表-力扣(LeetCode)
  • SQL视图、存储过程和触发器
  • npm全局安装后,cmd命令行可以访问,vscode访问报错
  • Django REST框架核心:GenericAPIView详解
  • GitHub Push 认证失败 fatal Authentication failed
  • OceanBase 分区裁剪(Partition Pruning)原理解读
  • Binlog Server守护MySQL数据0丢失
  • 基于Pytochvideo训练自己的的视频分类模型
  • python中view把矩阵维度降低的时候是什么一个排序顺序
  • 机器学习——数据清洗
  • 【论文阅读】Multi-metrics adaptively identifies backdoors in Federated Learning
  • Linux 文本处理与 Shell 编程笔记:正则表达式、sed、awk 与变量脚本
  • 本地文件上传到gitee仓库的详细步骤
  • Excel表格复制到word中格式错乱
  • Nginx 的完整配置文件结构、配置语法以及模块详解
  • 【学习笔记】大话设计模式——一些心得及各设计模式思想记录
  • Vue3全局配置Loading的完整指南:从基础到实战
  • PyTorch API 4
  • Mac 4步 安装 Jenv 管理多版本JDK