混合&面剔除


混合

混合主要用来做透明度,一个物体透明的意思其实就是它显示出来的颜色一部分由它自己的颜色提供,另一部分由它后面的颜色提供,我们通过对纹理的alpha值进行设置,就可以做出透明效果。

丢弃片段

有些图片并不需要半透明,只需要根据纹理颜色值,显示一部分,或者不显示一部分,没有中间情况。比如说草,如果想不太费劲地创建草这种东西,你需要将一个草的纹理贴在一个2D四边形上,然后将这个四边形放到场景中。然而,草的形状和2D四边形的形状并不完全相同,所以你只想显示草纹理的某些部分,而忽略剩下的部分。

我们在载入图片的时候需要告诉OpenGL去使用alpha通道,然后去获取它的四个通道,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//绑定相应的VAO,存储数据
unsigned int transparentVAO, transparentVBO;
glGenBuffers(1, &transparentVBO);
glGenVertexArrays(1, &transparentVAO);
glBindBuffer(GL_ARRAY_BUFFER, transparentVBO);
glBindVertexArray(transparentVAO);
glBufferData(GL_ARRAY_BUFFER, sizeof(transparentVertices), transparentVertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
//对于一个顶点着色器,可以有多个VAO去用它的位置,之前的画箱子平面也用了这两个位置,你可能会疑惑为什么同一个顶点着色器的location被重复使用了,但是你仔细想想,它并没有重复,因为每创建一个VAO,你就使用了顶点着色器里面的locaion,然后去解析了数据,然后就存在了VAO之中,顶点着色器里面的lacation斌并有用来存数据,他只是一个指示,来把数据解析然后存在VAO中,所以重复使用位置不是什么奇怪的事。
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

//加载纹理,并且把纹理存储到2号位
unsigned int transparentTexture = LoadImageToGPU("grass.png", GL_RGBA, GL_RGBA, 2);

//画平面
glBindVertexArray(transparentVAO);
glUniform1i(glGetUniformLocation(myShader->ID, "texture1"), 2);
for (unsigned int i = 0; i < grass.size(); i++)
{
model = glm::mat4(1.0f);
model = glm::translate(model, grass[i]);
glUniformMatrix4fv(glad_glGetUniformLocation(myShader->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 6);
}

这样就可以画出这样的样子:

但是我们加载图片的时候虽然加了alpha通道,但是没有告诉片段着色器如何去处理它,于是我们要改变一下片段着色器,GLSL有一个discard命令,一旦被调用,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲。有了这个指令,我们就能够在片段着色器中检测一个片段的alpha值是否低于某个值,如果是的话,则丢弃这个片段:

1
2
3
4
5
6
7
8
9
10
11
12
#version 330 core                                      
in vec2 texCoord;

out vec4 fragColor;

uniform sampler2D texture1;

void main(){
vec4 texColor = texture(texture1,texCoord);
if(texColor.a < 0.1) discard;
fragColor = texColor;
}

于是就可以得到比较好的效果:

但是如果只是很简单地丢弃,我们就做不出来透明效果,所以要启动混合,OpenGL中的混合是通过一个方程去混和:

片段着色器运行完成后,并且所有的测试都通过之后,这个混合方程就会应用到片段颜色输出与当前颜色缓冲中的值上。源颜色和目标颜色将会由OpenGL自动设定,但源因子和目标因子的值可以由我们来决定。首先把片段着色器还原:

1
2
3
4
5
6
7
8
9
10
#version 330 core                                      
in vec2 texCoord;

out vec4 fragColor;

uniform sampler2D texture1;

void main(){
fragColor = texture(texture1,texCoord);
}

开启混合:

1
2
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

然后把之前的草纹理换成玻璃,就可以得到这样的效果:

但是仔细看你会发现前面的玻璃居然能会挡住后面的玻璃,这是因为深度测试和混合一起使用会产生一些麻烦。当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会也会写入到深度缓冲中。这样一来透明物体后面的东西也无法被渲染出来。

所以我们不能随意地决定如何渲染窗户,让深度缓冲解决所有的问题,我们必须先手动将窗户按照最远到最近来排序,再按照顺序渲染。大致的顺序事是:先绘制所有不透明的物体,然后对所有透明的物体排序,最后再按顺序绘制所有透明的物体。

具体算法:我们从观察者视角获取物体的距离(通过计算摄像机位置向量和物体的位置向量之间的距离所获得),接下来我们把距离和它对应的位置向量存储到map数据结构中。map会自动根据键值(Key)对它的值排序,所以只要我们添加了所有的位置,并以它的距离作为键,它们就会自动根据距离值排序了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::map<float, glm::vec3> distanceSort;
for (unsigned int i = 0; i < grass.size(); i++)
{
float distance = glm::length(camera.Position - grass[i]);
distanceSort[distance] = grass[i];
}

//反向迭代器,由远到近渲染窗户
glBindVertexArray(windowVAO);
glUniform1i(glGetUniformLocation(myShader->ID, "texture1"), 3);
for (std::map<float, glm::vec3>::reverse_iterator it = distanceSort.rbegin(); it != distanceSort.rend(); ++it)
{
model = glm::mat4(1.0f);
model = glm::translate(model, it->second);
glUniformMatrix4fv(glad_glGetUniformLocation(myShader->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 6);
}

这样就可以得到我们想要的结果:

虽然这种方法对我们这个场景能够正常工作,但它并没有考虑旋转、缩放或者其它的变换,有一些奇怪形状的物体仅用一个位置向量不能也准确地描述它是否遮挡了别人。而且这个算法当场景中需要排序地物体太多时,它的开销就太大了。

还有一种方法就是在渲染半透明物体的时候把深度写入关了,渲染完毕之后再打开,这样就透明物体就不会受深度值的影响,这样也可以得到一样的效果:

这也许就需要去了解一下更优秀的技术,比如说次序无关透明度这种技术。后面会专门用一篇文章去记录一下我对这个技术的学习。


面剔除

面剔除就是用来剔除那些我们看不到的面,这样可以节省很多的时间。但需要告诉OpenGL哪些面是正向面(Front Face),哪些面是背向面(Back Face),这就是通过环绕顺序去实现的。

我们定义一组三角形顶点时,我们会以特定的环绕顺序来定义它们,可能是顺时针(Clockwise)的,也可能是逆时针(Counter-clockwise)的。默认情况下,逆时针顶点所定义的三角形将会被处理为正向三角形。

所以在实际操作中,你只需要定义好逆时针的顶点数据,然后用glEnable(GL_CULL_FACE)就可以舍弃背面。而对于舍弃也可以选择是正面还是背面,用glCullFace函数就可以实现。

glCullFace函数有三个可用的选项:

  • GL_BACK:只剔除背向面。
  • GL_FRONT:只剔除正向面。
  • GL_FRONT_AND_BACK:剔除正向面和背向面。

混合和面剔除的基础俺也学完了!!!


Author: cdc
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source cdc !
  TOC