OpenGL GLFW 深度/模版测试 面剔除 混合与帧缓冲
深度测试
glDepthMask(GL_FALSE):禁止深度缓冲的写入,对某些透明对象的渲染可能会使用到。先启用深度写入不透明物体,然后静止写入来渲染透明物体;
MVP变换中的深度值:深度z的变换是非线性的,这样近处的物体深度精度很高,而远除的物体精度降低。
片段着色器内建gl_FragCoord向量的x和y分量代表了片段的屏幕空间坐标(其中(0, 0)位于左下角),z分量它包含了片段的深度值(0.0到1.0)。
z-buffer fighting:在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。
模版测试
GLfloat 与 float:
typedef float GLfloat;
通常GLfloat就是float的别名,但是使用GLfloat可以确保在对float有不同定义的硬件或平台上,float的数据类型大小保持一致。
模版测试实现描边
这种描边效果类似外边缘描边,常用于表示物体处与“被选中”的状态,一般不是我们经常在卡通渲染中看到的那种边缘描边。
注:每个glfw窗口都有独立的OpenGL上下文,在ImGui的窗口中操作渲染窗口的OpenGL上下文需要切换上下文环境。
这里的描边会随着模型原理相机而变细,为了更合理需要保证模型描边粗细基本是固定的。相关方法网上已经有很多了,具体内容就是在裁剪空间(clip space)下,将 模型的顶点的xy 沿 法线在xy平面下的单位向量与模型顶点w分量的乘积 偏移【偏移量提前乘以w,原因是在裁剪阶段之后会对顶点的 x,y,z 分量除以 w,将坐标转换为归一化设备坐标(NDC)】
唯一要注意一下的就是,法线在非均匀变换下不能直接用顶点的变换矩阵,否则无法得到正确的法线。
还有一件事,MVP变化都需要处理还是只处理MV变换即可?目前我是MVP都处理了,也是没有问题的。
面剔除实现描边
同样是先渲染模型,然后对顶点沿法线方向偏移一定距离后渲染,再对顶点偏移后的模型剔除朝向相机的面 glCullFace(GL_FRONT),就实现了一个最简单的模型描边。
当然,这种简单粗暴的方法效果可能并不好,比如在上图中模型裙边的四个面,每个面都有自己独立的顶点,结果在沿法线偏移后模型就完全变形了。
丢弃片段
GLSL中,通过discard可以终止当前着色。例如,可以通过透明度丢弃掉某些不需要的片段。
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
vec4 texColor = texture(texture1, TexCoords);
if(texColor.a < 0.1)
discard;
FragColor = texColor;
}
混合
透明物体渲染这一块是图形渲染中很经典的部分嘞。
混合无法正常显示问题
这块我创建了三个不同颜色的立方体,其中两个透明的立方体在前,一个不透明的立方体在后。先渲染不透明物体,再渲染透明物体,在设置混合模式后,发现不透明立方体不能正确显示。
检查深度缓冲区结果,需要注意的是 gl_FragCoord.z 是当前像素的深度,不是深度缓冲区的值。例如下图,禁止深度缓冲写入,用 gl_FragCoord.z 表示灰度依然能渲染出三个立方体,但不是想要的效果。
如果想拿到深度缓冲区的值,需要通过帧缓冲实现(具体见后文)。
glClear(GL_DEPTH_BUFFER_BIT)会受到glDepthMask的影响,上面不透明立方体不能正确显示的问题就是在第二阶段渲染到窗口时,禁止了深度缓冲区写入。导致在glClear(GL_D
EPTH_BUFFER_BIT)的时候,深度写入是禁止的。
简单的透明物体渲染
这种简单的实现方式会由于不透明物体的渲染顺序,而导致渲染的结果不太准确。如下图就是先渲染蓝色立方体后渲染红色立方体的结果,及先红后蓝的顺序渲染的结果。
此时可以增加先对所有透明的物体排序后再由远及近处理透明物体来避免大部分渲染顺序导致的问题,但在部分特殊情况下,例如物体在深度方向有重叠(像下面这种),依然会存在问题。
于是便出现了一系列的顺序无关的半透明混合(OIT)方法解决半透明物体渲染的问题。
Order Independent Transparency
Depth Peeling
Depth Peeling是NVIDIA 01年就提出来的一种渲染透明物体的方法,原理很简单,但用OpenGL实现有点麻烦。两个透明面重叠了貌似就会丢弃掉一个面。
Dual Depth Peeling:08年该方法被小幅改进了下,从单向剥离变成了双向剥离。
尝试在OpenGL中实现了一下Depth Peeling的半透明混合方法,我是用了N个深度和纹理贴图缓存每一次Peeling的结果,设置深度函数为GL_ALWAYS(就是这块出大问题),然后在片元着色器中判断当前的gl_FragCoord.z是否小于所有不透明物体的深度,并大于当前缓冲区的深度值(可能存在微小的浮点误差,看情况可以添加一个偏移量)。但效果很糟糕,出现很多很多的像素块闪烁。
啥也看不出来,搞的一点头绪都没有。就嗯看花了小半个上午才发现问题所在,问题就出现在GPU的并行处理上。异步也就是说有可能,一个像素如果被两个(或更多)片元A和B所包含,而该像素在B上时离相机更远,由于深度测试总是通过的,就可以导致A和B同时和深度缓冲区中的值做比较,但在写入深度缓冲时A又比B快一点,A先写入深度缓冲,而B随后又改写了A的深度缓冲,导致最终深度缓冲中存储的就变成了错误的值。
一句话总结,深度测试的内容在片元着色器中被并行判断了。
设置深度函数为GL_LESS就可以解决。即使A,B同时通过片元着色器的判断,由于深度测试和深度写入是顺序执行,所以最终写入深度缓冲区的依然是A。
Depth Peeling的效果比直接对物体排序好了很多 ^ ^。
这里depth peeling实现的混合颜色和默认的简单的透明物体渲染结果不一样,depth peeling实现的颜色更深一些,遂继续debug。
原因是由于我的实现是:先在自定义帧缓冲中绘制半透明物体的渲染结果 → 把结果渲染到默认帧缓冲中的一个与屏幕大小相同的四边形中。由于未特殊处理第一步渲染结果的alpha通道,在第二步中,渲染结果的alpha通道不为1.0,所以又和背景颜色(glClearColor设置的值)做了混合,导致颜色变淡。
对简单渲染和depth peeling最后一层的渲染在混合中应用背景alpha值(也就是1.0),即可都显示正确的颜色。
面剔除
面剔除通过在屏幕空间中顶点的环绕顺序是顺时针还是逆时针来判断一个三角形是正向三角形还是逆向三角形(前提是所有的顶点在组织三角形时统一按照顺时针或逆时针组织)。具体计算方法为,在光栅化阶段,可以用鞋带定理(Shoelace formula)计算三角形的面积,如果是逆时针顺序读取坐标,那么面积是正值,否则为负值。由面积值的正负可知顶点在屏幕空间中的环绕顺序。
关于鞋带定理:鞋带定理(Shoelace formula)求2D多边形面积 - 简书
ad-bc能计算出三角形面积这一块就不多啰嗦哩,主要看为什么正负和环绕顺序相关,这里简单证明一下每个向量在与原点构成的三角形中的环绕顺序与ad-bc结果正负的关系。
帧缓冲
详细内容LearnOpenGL都有,这里就说一下我的理解。glfw会创建一个默认的帧缓冲,当我们调用glfwSwapBuffers()来交换前后缓冲时,就是将默认的帧缓冲交换到了当前显示的窗口中。但有时候我们不想直接显示渲染的结果,这时候可以创建一个自定义的帧缓冲(可附加项:颜色缓冲区,深度缓冲区,模版缓冲区)。这样就可以先把渲染结果存储在用户定义的纹理中,再在默认的帧缓冲中采样之前保存的渲染结果,就可以输出后处理之后的结果。使用帧缓冲实现输出深度图。
glfwSwapBuffers()只会对默认的帧缓冲进行交换,是没法交换自定义的帧缓冲的,所以使用glfwSwapBuffers()时需要最后在默认帧缓冲中用一个和屏幕同样大小的四边形来输出结果。
我这块在尝试在窗口中显示深度缓冲区内容的时候,由于对纹理的理解不准确,遇到了些问题。
如果需要在一个shader中通过uniform使用多个纹理,需要将多个纹理单元和纹理绑定在一起,uniform sampler2D通过glUniform1i设置的值,就是纹理单元的索引值。
举个例子,在运行下列代码之后。
GLuint texture;
glGenTextures();
glBindTexture();
glTexImage2D();我们就得到了一个 w * h 分辨率的纹理,texture就是标识这个纹理的名字。然后我们可以把纹理作为某个自定义的帧缓冲的颜色缓冲区。当我们想要采样这个纹理的时候,只需要用glActiveTexture(GL_TEXTURE0 + 0)将第0个纹理单元给激活,并用glBindTexture把texture绑定到GL_TEXTURE_2D中,这时候texture的纹理就在第0个纹理单元内了,此时只需要再将第0个纹理单元和shader中对应的uniform sampler2D绑定,在shader中,就可以使用texture中存储的纹理了。如果还有其他的纹理,同样用一个空闲的纹理单元绑定后,再设置对应uniform sampler2D的值为该纹理单元的索引就可以了。
再啰嗦一下,这块写了这么多,是因为我一开始以为texture这个GLuint类型的变量可以直接做glUniform1i的第二个参数,导致明明我的纹理有内容,但在shader中却采样不到,如下图,Debug了很长时间。