今天我们来聊一些更深入的东西:
Depth Testing
在之前的学习过程中,我们有接触过深度缓冲,彼时的深度缓冲的意义就是可以让我们体现出深度的差异从而达到立体的效果,而现在我们将在深度缓冲的基础进一步扩展,进行深度测试来让所有片段的深度值与缓冲的内容进行比较。
这里有一些我们常用的深度测试函数,定义深度测试的规则:
因为我没有合适的纹理,我就直接用之前用过的笑脸和箱子纹理来演示吧:
呃可能有一点惊悚了,但是效果还是达到了:当我们的深度测试函数为:
我们的深度测试虽然开启但是形同虚设,根本没有远近关系和立体感,我们将GL_ALWAYS修改为GL_LESS之后:
显然这才是我们想要的。
在这里我们会牵扯到一个数学上的问题:精度问题。我们都知道深度值是一个[0,1]的值,这意味着无论在裁剪空间中具体的坐标是多少我们都需要进行z值(深度)的转化。
但是在真正的实践中,我们会使用这样的线性方程吗?
简单地说,大多数值给到了近平面,或者说离近平面越近我们的精度越高,反之越远我们的精度则越低,这样可以大大减少我们近平面处的深度冲突问题。
什么是深度冲突?
深度冲突本质上其实就是我们的物体的深度值过于接近时导致视觉上错乱的一种情形。
Stencil Testing
什么是模板测试?想象包括我在内的一部分小伙伴都已经忘了这个流程了,让我们回忆一下渲染管线的流程:
可以看到在我们的片段着色器之后名为测试与混合,这里的测试其实就是我们的模板测试和深度测试。
其实简单地说就是经过片元着色器之后我们地模板测试要选择性地丢弃一些片段,这样可以舍弃一些不必要地片段或者实现一些更酷炫的效果。
模板缓冲和颜色缓冲还有深度缓冲一样,都是一开始要被清零(否则我们比较的值是之前比较过的值),然后我们和深度测试类似地需要一个模板函数来定义模板测试比较的过程。
我们用一个物体轮廓的实例来展示模板测试的效果:
代码如下:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // don't forget to clear the stencil buffer!
首先,最开始时一定要记得清空模板缓冲。
glStencilMask(0x00);
// floor
glBindVertexArray(planeVAO);
glBindTexture(GL_TEXTURE_2D, floorTexture);
shader.setMat4("model", glm::mat4(1.0f));
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
第二步,我们要去设置模板掩码(设置为0x00),这个掩码代表我们不会去对该部分进行模板测试,在实际的代码中我们针对的是地板部分。
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
// cubes
glBindVertexArray(cubeVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, cubeTexture);
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
接着是我们的方块部分,我们把所有方块部分都写入我们的模板缓冲中(模板函数是always通过)
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
float scale = 1.1f;
// cubes
glBindVertexArray(cubeVAO);
glBindTexture(GL_TEXTURE_2D, cubeTexture);
model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
model = glm::scale(model, glm::vec3(scale, scale, scale));
shaderSingleColor.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f));
model = glm::scale(model, glm::vec3(scale, scale, scale));
shaderSingleColor.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glStencilMask(0xFF);
glStencilFunc(GL_ALWAYS, 0, 0xFF);
glEnable(GL_DEPTH_TEST);
然后我们改变我们的模板函数改为NOTEQUAL,也就是只绘制模板值不为1的部分,联系我们之前将所有的方块的像素都写入模板且值为1,这样我们就实现了绘制边框的逻辑。
完成这一切之后我们先短暂地关闭深度测试,绘制完成之后我们再重新开启深度测试。(为了防止我们绘制的轮廓被遮挡)
效果如下:
Blending
OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。一个有色玻璃窗是一个透明的物体,玻璃有它自己的颜色,但它最终的颜色还包含了玻璃之后所有物体的颜色。这也是混合这一名字的出处,我们混合(Blend)(不同物体的)多种颜色为一种颜色。所以透明度能让我们看穿物体。
不难看出,所谓的混合其实就是实现透明的方法,多种物体带着不同的透明度重叠在一起,就能出现多种多样的视觉效果。
我们首先来考虑一种极端的情况:全透明和完全不透明。
比如这个草的纹理,显然我们只需要这个草的部分而草后续的白色的部分我们希望完全丢弃。
在之前的学习中我们已经学会了如何设置图片纹理的alpha值,也就是用来表示透明度的参数,但是光这样我们的程序可不知道具体该舍弃哪些片段保留哪些片段,我们还需要一些更精准的操作。完成这个丢弃操作的核心是一个名叫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;
}
在我们的片元着色器中,我们将纹理颜色透明度中小于0.1的部分直接丢弃,这样我们就可以筛选透明度实现片段丢弃了。
额,可以看到我们不小心把地板的透明部分也筛掉了,或许我们该创建两个着色器来分别处理地板和草,但我就不多浪费这点时间了。
这是针对完全丢弃的片段的处理方式,可是对于并没有那么透明的部分,我们不能直接丢掉,而是应该采用混合的方式。
可以看到,我们的混合方程的内容其实就是将纹理的颜色向量与存储在颜色缓冲中的颜色向量分别乘以一个因子之后相加即可。
关于这两个因子的设置,我们有一系列函数来使用:
关于多个半透明纹理的渲染,我们的混合方程固然很重要,但其实还有一点很重要:
具体来说怎么给透明物体排序呢?
简单地说就是利用map来自动对物体到摄像头的距离进行排序,我们再逆序取元素即可,
代码如下:
// configure global opengl state
// -----------------------------
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
开启混合之后我们就去获取所有的窗户:
vector<glm::vec3> windows
{
glm::vec3(-1.5f, 0.0f, -0.48f),
glm::vec3(1.5f, 0.0f, 0.51f),
glm::vec3(0.0f, 0.0f, 0.7f),
glm::vec3(-0.3f, 0.0f, -2.3f),
glm::vec3(0.5f, 0.0f, -0.6f)
};
这是我们的片元着色器的定义:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
FragColor = texture(texture1, TexCoords);
}
于是我们就用这个着色器来渲染即可。
glBindVertexArray(transparentVAO);
glBindTexture(GL_TEXTURE_2D, transparentTexture);
for (std::map<float, glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
model = glm::mat4(1.0f);
model = glm::translate(model, it->second);
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
效果如图: