Blending 混合
Advanced-OpenGL/Blending
OpenGL中的混合通常被称为在对象中实现透明的技术。透明指的是物体(或它们的一部分)没有纯色,而是由物体本身和它后面的其他物体的不同强度的颜色组合而成。彩色玻璃窗是一种透明的物体;玻璃有自己的颜色,但产生的颜色也包含了玻璃后面所有对象的颜色。这也是名称混合的来源,因为我们混合了几个像素颜色(从不同的对象)到一个单一的颜色。因此,透明让我们能够看透物体。
透明对象可以是完全透明(允许所有颜色通过)或部分透明(允许颜色通过,但也允许它自己的一些颜色通过)。对象的透明度是由其颜色的alpha值定义的。alpha颜色值是颜色向量的第4个分量,你现在可能经常看到。在本章之前,我们一直将第4个组件的值设为1.0,让对象的透明度为0.0。alpha值0.0将会导致对象完全透明。alpha值为0.5告诉我们对象的颜色由它自己的颜色的50%和对象后面的颜色的50%组成。
到目前为止,我们使用的纹理都是由三种颜色组成:红色,绿色和蓝色,但有些纹理也有一个内嵌的alpha通道,它包含每个texel的alpha值。这个alpha值告诉我们纹理的哪些部分是透明的,有多少是透明的。例如,下面的窗口纹理window texture在其玻璃部分的alpha值为0.25,在其角的alpha值为0.0。玻璃部分通常是完全红色的,但由于它有75%的透明度,它很大程度上通过它显示了页面的背景,使它看起来少了很多红色:
我们将很快在深度测试章节中添加这个窗口纹理到场景中,但是首先我们将讨论一种更简单的技术来实现像素的透明,无论是完全透明还是完全不透明。
Discarding fragments 丢弃的片段
有些效果并不关心局部的透明度,而是根据纹理的颜色值想要显示一些东西或者什么都不显示。认为草;为了创建一些像草一样的东西,你通常粘贴一个草纹理到一个2D方块上,并将这个方块放置到你的场景中。然而,草的形状并不完全像2D的正方形,所以你只需要显示草的某些部分的纹理,而忽略其他部分。
下面的纹理texture 就是这样的纹理,要么是完全不透明的(alpha值为1.0),要么是完全透明的(alpha值为0.0),中间没有任何东西。您可以看到,在没有草的地方,图像显示的是页面的背景色,而不是它自己的颜色。
所以当我们在一个场景中添加植被时,我们不想看到一个草的方形图像,而只是显示实际的草,并看到图像的其余部分。我们想丢弃那些显示纹理透明部分的片段,而不是将这些片段存储到颜色缓冲区中。
在我们进入之前,我们首先需要学习如何加载透明纹理。要加载带有alpha值的纹理,我们不需要做太多修改。stb_image自动加载图像的alpha通道,如果它是可用的,但我们需要告诉OpenGL我们的纹理现在使用一个alpha通道在纹理生成过程:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
也要确保你在fragment shader中检索所有4种颜色的纹理组件,而不仅仅是RGB组件:
void main()
{
// FragColor = vec4(vec3(texture(texture1, TexCoords)), 1.0);
FragColor = texture(texture1, TexCoords);
}
现在我们知道了如何加载透明纹理,是时候通过在深度测试一章中介绍的基本场景中添加一些草叶子来进行测试了。
我们创建一个小的向量数组,在其中添加几个glm::vec3向量来表示草叶的位置:
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));
每一个草对象都被渲染为一个单独的带有草纹理的四边形。这并不是一个完美的草的3D表现,但它比加载和渲染大量复杂的模型要高效得多。通过一些技巧,比如添加随机旋转和缩放,你可以用四边形得到非常令人信服的结果
因为草纹理将显示在一个四轴对象上,我们需要再次创建另一个VAO,填充VBO,并设置适当的顶点属性指针。在我们渲染了地板和两个立方体之后,我们要渲染草叶子:
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);
}
运行应用程序现在看起来有点像这样:
这是因为OpenGL在默认情况下不知道如何处理alpha值,也不知道什么时候丢弃它们。我们必须自己动手做这件事。幸运的是,由于使用了着色器,这是相当容易的。GLSL为我们提供了丢弃命令,该命令(一旦被调用)确保片段不会被进一步处理,因此不会最终进入颜色缓冲区。感谢这个命令,我们可以检查一个片段是否有一个alpha值低于某个阈值,如果是,丢弃的片段,就像它从来没有被处理:
#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的alpha值,如果是这样,丢弃片段。这个片段着色器确保我们,它只渲染片段不是(几乎)完全透明。现在它看起来应该是:
注意,当采样纹理的边界时,OpenGL用纹理的下一个重复值来插值边界值(因为我们默认将其包装参数设置为GL_REPEAT)。这通常是可以的,但因为我们使用的是透明值,纹理图像的顶部得到了它的透明值与底部边框的纯色值的插值。结果是一个稍微半透明的彩色边界,你可以看到你的纹理四边。为了防止这种情况,当你使用不希望重复的alpha纹理时,将纹理缠绕方法设置为GL_CLAMP_TO_EDGE:
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
您可以在这里here. 找到源代码。
Blending
尽管丢弃碎片很好,但它并没有给我们提供渲染半透明图像的灵活性;我们要么渲染片段,要么完全丢弃它。为了渲染不同透明度级别的图像,我们必须启用混合。像OpenGL的大多数功能一样,我们可以通过启用GL_BLEND来启用混合:
glEnable(GL_BLEND);
既然我们已经启用了混合,我们需要告诉OpenGL它应该如何混合。
在OpenGL中混合的过程如下:
- 源颜色矢量。这是片段着色器的颜色输出。
- :目标颜色向量。这是当前存储在颜色缓冲区中的颜色向量。
- : the source factor value. Sets the impact of the alpha value on the source color.
- : the destination factor value. Sets the impact of the alpha value on the destination color.
在片段着色器运行并且所有测试都通过之后,这个混合方程将在片段的颜色输出和当前在颜色缓冲区中的任何东西上释放出来。源和目标颜色会被OpenGL自动设置,但是源和目标因子可以被设置为我们选择的值。让我们从一个简单的例子开始:
我们有两个方块,我们想在红色方块的上面画一个半透明的绿色方块。红色方块将是目标颜色(因此应该是颜色缓冲区的第一个),我们现在要在红色方块上绘制绿色方块。
然后问题就出现了:我们应该将因子值设置为什么?我们至少要把绿色方块和它的alpha值相乘所以我们要让FsrcFsrc等于源颜色向量的alpha值也就是0。6。然后,让目标方块的贡献等于alpha值的余数是有意义的。如果绿色方块占最终颜色的60%,我们希望红色方块占最终颜色的40%,例如1.0 - 0.6。所以我们设置FdestinationFdestination等于1减去源颜色向量的alpha值。方程为:
结果是组合的正方形片段包含了60%的绿色和40%的红色:
然后,产生的颜色被存储在颜色缓冲区中,替换以前的颜色。
这很好,但我们如何告诉OpenGL使用这样的因子呢?很凑巧,这里有一个函数叫做glBlendFunc。
glBlendFunc(GLenum sfactor, GLenum dfactor)函数需要两个参数来设置源和目标因子的选项。OpenGL为我们定义了很多选项,我们将在下面列出最常见的选项。请注意,常量颜色向量 可通过glBlendColor函数单独设置。
Option | Value |
---|---|
GL_ZERO | Factor is equal to 00 . |
GL_ONE | Factor is equal to 11 . |
GL_SRC_COLOR | Factor is equal to the source color vector C¯sourceC¯source . |
GL_ONE_MINUS_SRC_COLOR | Factor is equal to 11 minus the source color vector: 1−C¯source1−C¯source . |
GL_DST_COLOR | Factor is equal to the destination color vector C¯destinationC¯destination |
GL_ONE_MINUS_DST_COLOR | Factor is equal to 11 minus the destination color vector: 1−C¯destination1−C¯destination . |
GL_SRC_ALPHA | Factor is equal to the alphaalpha component of the source color vector C¯sourceC¯source . |
GL_ONE_MINUS_SRC_ALPHA | Factor is equal to 1−alpha1−alpha of the source color vector C¯sourceC¯source . |
GL_DST_ALPHA | Factor is equal to the alphaalpha component of the destination color vector C¯destinationC¯destination . |
GL_ONE_MINUS_DST_ALPHA | Factor is equal to 1−alpha1−alpha of the destination color vector C¯destinationC¯destination . |
GL_CONSTANT_COLOR | Factor is equal to the constant color vector C¯constantC¯constant . |
GL_ONE_MINUS_CONSTANT_COLOR | Factor is equal to 11 - the constant color vector C¯constantC¯constant . |
GL_CONSTANT_ALPHA | Factor is equal to the alphaalpha component of the constant color vector C¯constantC¯constant . |
GL_ONE_MINUS_CONSTANT_ALPHA | Factor is equal to 1−alpha1−alpha of the constant color vector C¯constantC¯constant . |
为了得到我们的小二方示例的混合结果,我们想用源颜色矢量的alphaalpha作为源因子,用相同颜色矢量的1 - alpha作为目标因子。翻译成glBlendFunc如下:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
它也可以为RGB和alpha通道分别使用glBlendFuncSeparate设置不同的选项:
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
这个函数像我们之前设置的那样设置RGB组件,但是只让产生的alpha组件受源的alpha值的影响。
OpenGL为我们提供了更大的灵活性,它允许我们在这个等式的源和目标部分之间更改操作符。现在,源组件和目标组件被加在一起,但是如果我们想的话,我们也可以减去它们。glBlendEquation(GLenum模式)允许我们设置此操作,有5种可能的选项:
- GL_FUNC_ADD:默认,添加两种颜色:
- GL_FUNC_SUBTRACT:互相减去两种颜色:
- GL_FUNC_REVERSE_SUBTRACT:减去两种颜色,但颠倒顺序:
- GL_MIN:取两种颜色的组件最小值:
- GL_MAX:取组件最大的两种颜色:
通常我们可以简单地省略对glBlendEquation的调用,因为GL_FUNC_ADD是大多数操作的首选混合方程,但如果您真的想尽力打破主流电路,那么任何其他方程都可以满足您的需要。
Rendering semi-transparent textures 呈现半透明的材质
既然我们已经了解了OpenGL如何在混合中工作,现在就来通过添加几个半透明窗口来测试一下我们的知识。我们将使用与本章开始时相同的场景,但是我们将从本章开始使用透明窗口transparent window 纹理,而不是渲染草的纹理。
首先,在初始化过程中,我们启用混合并设置适当的混合函数:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
因为我们启用了混合,没有必要丢弃碎片,所以我们将重置碎片着色器到它的原始版本:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
FragColor = texture(texture1, TexCoords);
}
这一次(每当OpenGL渲染一个片段时),它会根据FragColor的alpha值将当前片段的颜色与当前颜色缓冲区中的片段颜色结合起来。由于窗户纹理的玻璃部分是半透明的,我们应该能够通过这扇窗户看到场景的其余部分。
然而,如果你仔细看,你可能会注意到有些东西不对。 前窗的透明部分遮挡了背景中的窗户。为什么会发生这种情况?
这样做的原因是深度测试工作起来有点棘手的混合。当写入深度缓冲区时,深度测试并不关心片段是否透明,因此透明部分作为任何其他值写入深度缓冲区。结果是背景窗口像其他不透明对象一样在深度上进行测试,忽略透明度。即使透明部分应该显示其背后的窗户,深度测试却丢弃了它们。
所以我们不能简单地以我们想要的方式渲染窗口并期望深度缓冲为我们解决所有的问题;这也是混合起来会有点讨厌的地方。为了确保窗口显示后面的窗口,我们必须首先在背景中绘制窗口。这意味着我们必须手动将窗口从最远的到最近的排序,并自己相应地绘制它们。
请注意,对于完全透明的对象,比如草叶,我们可以选择丢弃透明片段而不是混合它们,这样就省去了一些令人头疼的问题(没有深度问题)。
Don't break the order 不要破坏秩序
为了使混合工作为多个对象,我们必须首先绘制最远的对象,最后绘制最近的对象。普通的非混合对象仍然可以使用深度缓冲区正常绘制,所以它们不需要排序。在绘制(排序的)透明对象之前,我们必须确保它们是首先绘制的。当绘制带有非透明和透明物体的场景时,大致轮廓通常如下:
- 首先绘制所有不透明对象。
- 对所有透明对象进行排序。
- 绘制所有透明对象排序。
排序透明对象的一种方法是从观察者的角度检索对象的距离。这可以通过取相机的位置向量和物体的位置向量之间的距离来实现。然后我们将这个距离和对应的位置向量存储在STL库中的map数据结构中。map会根据键值自动对其值进行排序,所以一旦我们添加了所有位置和距离作为键值,它们就会自动根据距离值进行排序:
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];
}
结果是一个排序的容器对象,它根据窗口的距离键值从最低到最高的距离存储每个窗口的位置。
然后,这一次渲染的时候,我们把每个地图的值颠倒过来(从最远到最近),然后按照正确的顺序画出相应的窗口:
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);
}
我们从映射中获取一个反向迭代器,以反向顺序遍历每个项,然后将每个窗口四轴转换为相应的窗口位置。这个相对简单的方法来排序透明的对象修复了之前的问题,现在的场景是这样的:
您可以在这里here. 找到完整的排序源代码。
虽然这种根据距离对物体进行排序的方法在这个特定的场景中效果很好,但它没有考虑旋转、缩放或任何其他变换,而且形状古怪的物体需要一个不同的度量,而不仅仅是一个位置向量。
在你的场景中排序对象是一个困难的壮举,这很大程度上取决于你拥有的场景类型,更不用说它所花费的额外处理能力。完全渲染一个带有实体和透明对象的场景并不是那么容易。还有一些更高级的技术,比如顺序独立透明,但这些都不在本章的讨论范围之内。目前,您必须正常地混合您的对象,但如果您小心并知道限制,您可以得到相当不错的混合实现。