在OpenGL中,深度测试(Depth Testing)是一种用于处理场景中多个物体的绘制顺序的技术。它用于解决物体的可见性问题,确保只有最前面的物体像素被绘制,从而实现真实的3D场景效果。
深度测试的基本思想是根据物体在场景中的距离,维护一个深度缓冲区(Depth Buffer),它是一个和颜色缓冲区大小相同的缓冲区。在深度缓冲区中,每个像素都存储着一个深度值(通常是浮点数或整数),表示物体到观察点(摄像机)的距离。这个距离通常是从摄像机视点到物体的距离。
深度测试默认是禁用的,所以如果要启用深度测试的话,我们需要用GL_DEPTH_TEST选项来启用它:
glEnable(GL_DEPTH_TEST); |
深度测试
屏幕空间坐标与通过OpenGL的glViewport
所定义的视口密切相关,并且可以直接使用GLSL内建变量gl_FragCoord
从片段着色器中直接访问。gl_FragCoord
的x和y分量代表了片段的屏幕空间坐标(其中(0, 0)
位于左下角)。gl_FragCoord
中也包含了一个z
分量,它包含了片段真正的深度值。z
值就是需要与深度缓冲内容所对比的那个值。
深度冲突
深度冲突(Depth Conflict)在OpenGL中指的是由于有多个物体在相同的屏幕位置上,并且它们的深度值相同或非常接近,导致深度测试无法准确地决定哪个物体应该在前面绘制,从而产生了视觉上的错误。
深度冲突通常在以下情况下出现:
- 在一个或多个物体的表面上有很多小的细节或凹凸,导致深度值在这些区域上非常接近,难以区分哪个物体在前面。
- 在一个场景中有多个物体重叠在一起,并且它们的表面在同一深度上,导致深度测试无法准确判断哪个物体应该在前面绘制。
为了解决深度冲突,有以下几种方法:
- 使用更高精度的深度缓冲:可以使用更高精度的深度缓冲,比如使用32位浮点数来存储深度值,这样能够减少深度冲突的可能性。
- 调整近平面和远平面:如果你的场景非常大或非常小,可能会导致深度缓冲的精度问题。通过调整近平面和远平面的值,可以增加深度缓冲的有效范围,从而减少深度冲突的可能性。
- 使用透明度排序:如果你的场景中有透明物体,可以考虑对它们进行按照透明度排序,并按照远到近的顺序绘制它们。这样可以确保透明物体正确地显示在其他物体的后面。
- 使用多边形偏移(Polygon Offset):多边形偏移是一种通过微调深度值的方法来解决深度冲突的技术。它可以在绘制深度冲突的物体时,稍微改变它们的深度值,使它们稍微偏移一些,从而防止深度冲突。
- 使用深度测试函数:OpenGL提供了几种深度测试函数,比如
GL_LESS
、GL_LEQUAL
、GL_GREATER
等等。你可以根据场景的需求选择合适的深度测试函数,以调整深度测试的行为。
深度冲突是一个相对常见的问题,特别是在复杂的场景中。解决深度冲突通常需要结合多种方法,具体取决于场景的复杂程度和需求。选择合适的解决方法有助于提高渲染效果和场景的真实感。
模板测试
本质上就是:1.首先清空模板缓冲;2.写入模板缓冲;3.根据写入的模板缓冲在模板测试时丢弃和显示一些片段。
通过使用模板缓冲,可以根据场景中已绘制的其它物体的片段,来决定是否丢弃特定的片段。
glStencilFunc(GL_ALWAYS, 1, 0xFF); // 所有的片段都应该更新模板缓冲,并且将每个像素的模板值设置为1 |
混合
OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。一个有色玻璃窗是一个透明的物体,玻璃有它自己的颜色,但它最终的颜色还包含了玻璃之后所有物体的颜色。这也是混合这一名字的出处,我们混合(Blend)(不同物体的)多种颜色为一种颜色。所以透明度能让我们看穿物体。
其他问题
stb_image库踩坑
加载stb_image库时,宏声明应在头文件之前,否则会找不到函数:
摄像机位置
添加空格上升镜头位置,ctrl降低镜头位置:
void ProcessKeyboard(Camera_Movement direction, float deltaTime) |
模型加载库
- 和材质和网格(Mesh)一样,所有的场景/模型数据都包含在Scene对象中。Scene对象也包含了场景根节点的引用。
- 场景的Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引。
- 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
- 一个网格包含了多个面。Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的(见[你好,三角形](https://learnopengl-cn.github.io/01 Getting started/04 Hello Triangle/))。
- 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。
所谓网格,就是渲染物体的最小单位
通过使用Assimp,我们可以加载不同的模型到程序中,但是载入后它们都被储存为Assimp的数据结构。我们最终仍要将这些数据转换为OpenGL能够理解的格式,这样才能渲染这个物体。
GLFW处理多个键同时输入
设置摄像机更快速的移动shift+w/s/a/d
:
// Faster |
着色器类中的异常捕捉
// ensure ifstream objects can throw exceptions: |
深入理解顶点着色器和片段着色器
在OpenGL核心模式的渲染中,这两个着色器是不能跳过的着色器,必须手动编写。
抗锯齿
超采样抗锯齿(Super Sample Anti-aliasing, SSAA):它会使用比正常分辨率更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样(Downsample)至正常的分辨率。这些额外的分辨率会被用来防止锯齿边缘的产生。虽然它确实能够解决走样的问题,但是由于这样比平时要绘制更多的片段,它也会带来很大的性能开销。所以这项技术只拥有了短暂的辉煌。
多重采样抗锯齿(Multisample Anti-aliasing, MSAA):它借鉴了SSAA背后的理念,但却以更加高效的方式实现了抗锯齿。渲染图元时,不只使用像素中心点作为采样点,而是将一个像素分成多个子采样点(这也是为什么叫做多重采样的原因)。每个像素片段着色器只运行一次,其所使用的顶点数据会插值到每个像素中心,结果储存在每个被覆盖的子采样点中,每个像素内部会将采样点的颜色平均化。
在OpenGL中使用MSAA,可以通过Hint的方式,在创建窗口前调用:
glfwWindowHint(GLFW_SAMPLES, 4); |
现在再调用glfwCreateWindow创建渲染窗口时,每个屏幕坐标就会使用一个包含4个子采样点的颜色缓冲了。GLFW会自动创建一个每像素4个子采样点的深度和样本缓冲。这也意味着所有缓冲的大小都增长了4倍。
ImGui
为了方便的查看场景,将鼠标禁用,使用光标回调函数来控制摄像机镜头的方向,所以使用imgui时不能使用鼠标拖动控件,如下:
因此要设置一个快捷键来唤出光标,捕捉并禁用光标的代码是在创建窗口后:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); |
而实现快捷时有一个问题,实现摄像机在场景中移动是在渲染循环中使用processInput(window)
函数,获取键盘输入来控制,这样可以实现一次按下连续控制,这也正是问题所在,如果直接在此函数中设置快捷键,按下一次会在每次渲染循环中都调用一次,表现为光标一直闪烁(因为一直在重复 获取光标、隐藏光标、获取光标…)。
解决方法是定义键盘回调函数,将切换光标的代码放到回调函数中,这样按一次键盘释放之前不会重复调用。
bool show_mouse = false; // global variable |
初始化
在渲染循环之外:
// Init Dear Imgui |
使用
在渲染循环内:
ImGui_ImplOpenGL3_NewFrame(); |
同一行放置多个控件
ImGui::SameLine(); |
可实现此语句前后的两个控件不换行。
控件例子
ImGui::Begin("Cube and Lighting"); // Create a window called "Cube and Lighting" and append into it. |
小行星光照方向变化
实例化渲染时,模型矩阵aInstanceMatrix
会从直接传入顶点着色器,而不是uniform赋值,因此直接在GPU中计算法向量Normal = mat3(transpose(inverse(aInstanceMatrix))) * aNormal;
,这样光照的计算就不会因为模型变换而改变方向。
|
Cubemap
加载天空盒
需要加载六个纹理。
unsigned int loadCubemap(vector<std::string> faces) |
即所谓天空盒子,可以在场景四周贴上纹理,看起来就像是在一些场景中。
必须要注意的是,纹理图片必须是jpg格式的,大小必须为1024的倍数,比例必须是正方形。
// Sky box vertex data |
顶点着色器:
|
片段着色器:
|
然后渲染:
// draw skybox as last |
贴图下载网站
有许多网站提供免费和付费的Cubemap贴图下载。你可以在这些网站上找到各种类型和风格的Cubemap贴图,以满足你的需求。以下是一些常见的资源网站:
- HDRI Haven (https://hdrihaven.com/): 这是一个提供高动态范围图像(HDRI)和Cubemap贴图的免费资源网站。你可以在这里找到多种环境和天空盒子贴图。
- Textures.com (https://www.textures.com/): 这个网站提供各种类型的纹理资源,包括Cubemap贴图。它有免费和付费选项。
- Unity Asset Store (https://assetstore.unity.com/): 如果你使用Unity引擎,Unity Asset Store提供了丰富的Cubemap贴图资源,其中一些是免费的,而另一些需要购买。
- Unreal Engine Marketplace (https://www.unrealengine.com/marketplace/): 如果你使用Unreal Engine,这个市场提供了许多Cubemap贴图资源,其中一些也是免费的。
- Google搜索:你可以在Google上搜索”Cubemap textures”、”Skybox textures”或者其他相关关键词,找到更多的免费和付费资源。
请注意,在使用免费或付费资源时,务必遵循相关许可协议和版权规定。有些资源可能需要你在商业项目中使用时付费或注明来源。另外,选择适合你项目需求的高质量贴图是非常重要的,这将有助于提升你项目的视觉效果和真实感。
stb_image库踩坑
加载stb_image库时,宏声明应在头文件之前,否则会找不到函数:
摄像机位置
添加空格上升镜头位置,ctrl降低镜头位置:
void ProcessKeyboard(Camera_Movement direction, float deltaTime) |
模型加载库
- 和材质和网格(Mesh)一样,所有的场景/模型数据都包含在Scene对象中。Scene对象也包含了场景根节点的引用。
- 场景的Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引。
- 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
- 一个网格包含了多个面。Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的(见[你好,三角形](https://learnopengl-cn.github.io/01 Getting started/04 Hello Triangle/))。
- 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。
所谓网格,就是渲染物体的最小单位
通过使用Assimp,我们可以加载不同的模型到程序中,但是载入后它们都被储存为Assimp的数据结构。我们最终仍要将这些数据转换为OpenGL能够理解的格式,这样才能渲染这个物体。
GLFW处理多个键同时输入
设置摄像机更快速的移动shift+w/s/a/d
:
// Faster |
着色器类中的异常捕捉
// ensure ifstream objects can throw exceptions: |
深入理解顶点着色器和片段着色器
在OpenGL核心模式的渲染中,这两个着色器是不能跳过的着色器,必须手动编写。
抗锯齿
超采样抗锯齿(Super Sample Anti-aliasing, SSAA):它会使用比正常分辨率更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样(Downsample)至正常的分辨率。这些额外的分辨率会被用来防止锯齿边缘的产生。虽然它确实能够解决走样的问题,但是由于这样比平时要绘制更多的片段,它也会带来很大的性能开销。所以这项技术只拥有了短暂的辉煌。
多重采样抗锯齿(Multisample Anti-aliasing, MSAA):它借鉴了SSAA背后的理念,但却以更加高效的方式实现了抗锯齿。渲染图元时,不只使用像素中心点作为采样点,而是将一个像素分成多个子采样点(这也是为什么叫做多重采样的原因)。每个像素片段着色器只运行一次,其所使用的顶点数据会插值到每个像素中心,结果储存在每个被覆盖的子采样点中,每个像素内部会将采样点的颜色平均化。
在OpenGL中使用MSAA,可以通过Hint的方式,在创建窗口前调用:
glfwWindowHint(GLFW_SAMPLES, 4); |
现在再调用glfwCreateWindow创建渲染窗口时,每个屏幕坐标就会使用一个包含4个子采样点的颜色缓冲了。GLFW会自动创建一个每像素4个子采样点的深度和样本缓冲。这也意味着所有缓冲的大小都增长了4倍。
ImGui
为了方便的查看场景,将鼠标禁用,使用光标回调函数来控制摄像机镜头的方向,所以使用imgui时不能使用鼠标拖动控件,如下:
因此要设置一个快捷键来唤出光标,捕捉并禁用光标的代码是在创建窗口后:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); |
而实现快捷时有一个问题,实现摄像机在场景中移动是在渲染循环中使用processInput(window)
函数,获取键盘输入来控制,这样可以实现一次按下连续控制,这也正是问题所在,如果直接在此函数中设置快捷键,按下一次会在每次渲染循环中都调用一次,表现为光标一直闪烁(因为一直在重复 获取光标、隐藏光标、获取光标…)。
解决方法是定义键盘回调函数,将切换光标的代码放到回调函数中,这样按一次键盘释放之前不会重复调用。
bool show_mouse = false; // global variable |
初始化
在渲染循环之外:
// Init Dear Imgui |
使用
在渲染循环内:
ImGui_ImplOpenGL3_NewFrame(); |
同一行放置多个控件
ImGui::SameLine(); |
可实现此语句前后的两个控件不换行。
控件例子
ImGui::Begin("Cube and Lighting"); // Create a window called "Cube and Lighting" and append into it. |
- 本文标题:OpenGL-3 进阶
- 创建时间:2023-07-24 14:52:01
- 本文链接:2023/07/24/note/Framework/OpenGL/opengl_advance/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!