一.混合
混合是实现透明度的一种技术,透明是说把物体的颜色和后面物体的颜色结合在一起。分为半透明和透明两种。一个物体的透明度是通过它颜色的aplha值来决定的。Alpha颜色值是颜色向量的第四个分量。
前一直使用的纹理有三个颜色分量:红、绿、蓝。但一些材质会有一个内嵌的alpha通道,对每个纹素(Texel)都包含了一个alpha值。这个alpha值精确地告诉我们纹理各个部分的透明度。
如下,红色玻璃区域的alpha值为0.25,边框为1.0
1.丢弃片段
利用纹理的alpha通道,我们可以将一个图片选择性的加载到我们的场景中。
例如,下面的草图片,绿色部分的alpha值为1.0,背景部分的alpha值为0.0(完全透明)
将这些草加载到场景中有一下步骤:
- 加载有alpha值的纹理
stb_image在纹理有alpha通道时会自动加载,但仍要在纹理生成时告诉Opengl加载了纹理。
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
同时,更新片段着色器。
void main()
{
// FragColor = vec4(vec3(texture(texture1, TexCoords)), 1.0);
FragColor = texture(texture1, TexCoords);
}
2.添加草的位置
vector<glm::vec3> vegetation;
vegetation.push_back(glm::vec3(-1.5f, 0.0f, -0.48f));
vegetation.push_back(glm::vec3( 1.5f, 0.0f, 0.51f));
vegetation.push_back(glm::vec3( 0.0f, 0.0f, 0.7f));
vegetation.push_back(glm::vec3(-0.3f, 0.0f, -2.3f));
vegetation.push_back(glm::vec3( 0.5f, 0.0f, -0.6f));
3.绘制草
glBindVertexArray(vegetationVAO);
glBindTexture(GL_TEXTURE_2D, grassTexture);
for(unsigned int i = 0; i < vegetation.size(); i++)
{
model = glm::mat4(1.0f);
model = glm::translate(model, vegetation[i]);
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
4.设置阈值
OpenGL默认是不知道怎么处理alpha值的,更不知道什么时候应该丢弃片段。需要自己手动来弄。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;
}
效果如下:
2.混合
虽然直接丢弃片段很好,但它不能让我们渲染半透明的图像。我们要么渲染一个片段,要么完全丢弃它。要想渲染有多个透明度级别的图像,我们需要启用混合。
- 启用混合
glEnable(GL_BLEND);
OpenGl启用混合方程:
当片段着色器上的颜色输和缓冲上的颜色确定后,混合方程才会确定,并且源因子和目标因子的值可以由我们来确定。
例如:我们有两个方块,一个是红色,一个是绿色。要把这个半透明绿色绘制在红色方形上,红色目标会是目标颜色。
我们设置一个源颜色的贡献值和目标颜色的贡献值。
结果得到了一个混合的颜色:
- glBlendFunc(GLenum sfactor, GLenum dfactor)函数接受两个参数,来设置源和目标因子。
二.渲染半透明纹理
现在我们来渲染那个窗户
- 启用混合,设置混合函数
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
2.恢复片段着色器
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
FragColor = texture(texture1, TexCoords);
}
注意:深度测试和混合一起使用的时候,当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会和其它值一样写入到深度缓冲中。结果就是窗户的整个四边形不论透明度都会进行深度测试。即使透明的部分应该显示背后的窗户,深度测试仍然丢弃了它们。解决这个问题需要我们将这些窗户进行排序,按从远到近的顺序来渲染它。
三.渲染顺序
要想让混合在多个物体上工作,我们需要最先绘制最远的物体,最后绘制最近的物体。
普通不需要混合的物体仍然可以使用深度缓冲正常绘制,所以它们不需要排序。但我们仍要保证它们在绘制(排序的)透明物体之前已经绘制完毕了。
当绘制一个有不透明和透明物体的场景的时候,大体的原则如下:
- 先绘制所有不透明的物体。
- 对所有透明的物体排序。
- 按顺序绘制所有透明的物体。
排序透明物体的一种方法是,从观察者视角获取物体的距离。这可以通过计算摄像机位置向量和物体的位置向量之间的距离所获得。
接下来我们把距离和它对应的位置向量存储到一个STL库的map
数据结构中。map
会自动根据键值(Key)对它的值排序,所以只要我们添加了所有的位置,并以它的距离作为键,它们就会自动根据距离值排序了。
std::map<float, glm::vec3> sorted;
for (unsigned int i = 0; i < windows.size(); i++)
{
float distance = glm::length(camera.Position - windows[i]);
sorted[distance] = windows[i];
}
结果就是一个排序后的容器对象,它根据distance
键值从低到高储存了每个窗户的位置。
之后,这次在渲染的时候,我们将以逆序(从远到近)从map中获取值,之后以正确的顺序绘制对应的窗户:
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
model = glm::mat4();
model = glm::translate(model, it->second);
shader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
我们使用了map的一个反向迭代器(Reverse Iterator),反向遍历其中的条目,并将每个窗户四边形位移到对应的窗户位置上。
效果如图: