mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-22 20:25:28 +08:00
Update 04-03
This commit is contained in:
@@ -46,7 +46,7 @@ float texCoords[] = {
|
||||
|
||||
纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:
|
||||
|
||||
环绕方式(Wrapping) | 描述
|
||||
环绕方式 | 描述
|
||||
---|---
|
||||
<var>GL_REPEAT</var> | 对纹理的默认行为。重复纹理图像。
|
||||
<var>GL_MIRRORED_REPEAT</var> | 和<var>GL_REPEAT</var>一样,但每次重复图片是镜像放置的。
|
||||
@@ -64,7 +64,7 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
|
||||
```
|
||||
|
||||
第一个参数指定了纹理目标;我们使用的是2D纹理,因此纹理目标是<var>GL_TEXTURE_2D</var>。第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是`WRAP`选项,并且指定`S`和`T`轴。最后一个参数需要我们传递一个环绕方式,在这个例子中OpenGL会给当前激活的纹理设定纹理环绕方式为<var>GL_MIRRORED_REPEAT</var>。
|
||||
第一个参数指定了纹理目标;我们使用的是2D纹理,因此纹理目标是<var>GL_TEXTURE_2D</var>。第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是`WRAP`选项,并且指定`S`和`T`轴。最后一个参数需要我们传递一个环绕方式(Wrapping),在这个例子中OpenGL会给当前激活的纹理设定纹理环绕方式为<var>GL_MIRRORED_REPEAT</var>。
|
||||
|
||||
如果我们选择<var>GL_CLAMP_TO_BORDER</var>选项,我们还需要指定一个边缘的颜色。这需要使用<fun>glTexParameter</fun>函数的`fv`后缀形式,用<var>GL_TEXTURE_BORDER_COLOR</var>作为它的选项,并且传递一个float数组作为边缘的颜色值:
|
||||
|
||||
|
@@ -3,57 +3,51 @@
|
||||
原文 | [Blending](http://learnopengl.com/#!Advanced-OpenGL/Blending)
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | [Geequlim](http://geequlim.com)
|
||||
翻译 | Meow J, [Django](http://bullteacher.com/)
|
||||
校对 | 暂无校对
|
||||
|
||||
|
||||
在OpenGL中,物体透明技术通常被叫做**混合(Blending)**。透明是物体(或物体的一部分)非纯色而是混合色,这种颜色来自于不同浓度的自身颜色和它后面的物体颜色。一个有色玻璃窗就是一种透明物体,玻璃有自身的颜色,但是最终的颜色包含了所有玻璃后面的颜色。这也正是混合这名称的出处,因为我们将多种(来自于不同物体)颜色混合为一个颜色,透明使得我们可以看穿物体。
|
||||
OpenGL中,<def>混合</def>(Blending)通常是实现物体<def>透明度</def>(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。一个有色玻璃窗是一个透明的物体,玻璃有它自己的颜色,但它最终的颜色还包含了玻璃之后所有物体的颜色。这也是混合这一名字的出处,我们<def>混合</def>(Blend)(不同物体的)多种颜色为一种颜色。所以透明度能让我们看穿物体。
|
||||
|
||||

|
||||
|
||||
透明物体可以是完全透明(它使颜色完全穿透)或者半透明的(它使颜色穿透的同时也显示自身颜色)。一个物体的透明度,被定义为它的颜色的alpha值。alpha颜色值是一个颜色向量的第四个元素,你可能已经看到很多了。在这个教程前,我们一直把这个元素设置为1.0,这样物体的透明度就是0.0,同样的,当alpha值是0.0时就表示物体是完全透明的,alpha值为0.5时表示物体的颜色由50%的自身的颜色和50%的后面的颜色组成。
|
||||
透明的物体可以是完全透明的(让所有的颜色穿过),或者是半透明的(它让颜色通过,同时也会显示自身的颜色)。一个物体的透明度是通过它颜色的<def>aplha</def>值来决定的。Alpha颜色值是颜色向量的第四个分量,你可能已经看到过它很多遍了。在这个教程之前我们都将这个第四个分量设置为1.0,让这个物体的透明度为0.0,而当alpha值为0.0时物体将会是完全透明的。当alpha值为0.5时,物体的颜色有50%是来自物体自身的颜色,50%来自背后物体的颜色。
|
||||
|
||||
我们之前所使用的纹理都是由3个颜色元素组成的:红、绿、蓝,但是有些纹理同样有一个内嵌的aloha通道,它为每个纹理像素(Texel)包含着一个alpha值。这个alpha值告诉我们纹理的哪个部分有透明度,以及这个透明度有多少。例如,下面的[窗户纹理](../img/04/03/blending_transparent_window.png)的玻璃部分的alpha值为0.25(它的颜色是完全红色,但是由于它有75的透明度,它会很大程度上反映出网站的背景色,看起来就不那么红了),角落部分alpha是0.0。
|
||||
我们目前一直使用的纹理有三个颜色分量:红、绿、蓝。但一些材质会有一个内嵌的alpha通道,对每个纹素(Texel)都包含了一个<def>alpha</def>值。这个alpha值精确地告诉我们纹理各个部分的透明度。比如说,下面这个[窗户纹理](../img/04/03/blending_transparent_window.png)中的玻璃部分的alpha值为0.25(它在一般情况下是完全的红色,但由于它有75%的透明度,能让很大一部分的网站背景颜色穿过,让它看起来不那么红了),边框的alpha值是0.0。
|
||||
|
||||

|
||||
|
||||
我们很快就会把这个窗子纹理加到场景中,但是首先,我们将讨论一点简单的技术来实现纹理的半透明,也就是完全透明和完全不透明。
|
||||
我们很快就会将这个窗户纹理添加到场景中,但是首先我们需要讨论一个更简单的技术,来实现只有完全透明和完全不透明的纹理的透明度。
|
||||
|
||||
## 忽略片段
|
||||
## 丢弃片段
|
||||
|
||||
有些图像并不关心半透明度,但也想基于纹理的颜色值显示一部分。例如,创建像草这种物体你不需要花费很大力气,通常把一个草的纹理贴到2D四边形上,然后把这个四边形放置到你的场景中。可是,草并不是像2D四边形这样的形状,而只需要显示草纹理的一部分而忽略其他部分。
|
||||
有些图片并不需要半透明,只需要根据纹理颜色值,显示一部分,或者不显示一部分,没有中间情况。比如说草,如果想不太费劲地创建草这种东西,你需要将一个草的纹理贴在一个2D四边形(Quad)上,然后将这个四边形放到场景中。然而,草的形状和2D四边形的形状并不完全相同,所以你只想显示草纹理的某些部分,而忽略剩下的部分。
|
||||
|
||||
下面的纹理正是这样的纹理,它既有完全不透明的部分(alpha值为1.0)也有完全透明的部分(alpha值为0.0),而没有半透明的部分。你可以看到没有草的部分,图片显示了网站的背景色,而不是它自身的那部分颜色。
|
||||
下面这个纹理正是这样的,它要么是完全不透明的(alpha值为1.0),要么是完全透明的(alpha值为0.0),没有中间情况。你可以看到,只要不是草的部分,这个图片显示的都是网站的背景颜色而不是它本身的颜色。
|
||||
|
||||

|
||||
|
||||
所以,当向场景中添加像这样的纹理时,我们不希望看到一个方块图像,而是只显示实际的纹理像素,剩下的部分可以被看穿。我们要忽略(丢弃)纹理透明部分的像素,不必将这些片段储存到颜色缓冲中。在此之前,我们还要学一下如何加载一个带有透明像素的纹理。
|
||||
所以当添加像草这样的植被到场景中时,我们不希望看到草的方形图像,而是只显示草的部分,并能看透图像其余的部分。我们想要<def>丢弃</def>(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中。在此之前,我们还要学习如何加载一个透明的纹理。
|
||||
|
||||
加载带有alpha值的纹理我们需要告诉SOIL,去加载RGBA元素图像,而不再是RGB元素的。SOIL能以RGBA的方式加载大多数没有alpha值的纹理,它会将这些像素的alpha值设为了1.0。
|
||||
要想加载有alpha值的纹理,我们并不需要改很多东西,`stb_image`在纹理有alpha通道的时候会自动加载,但我们仍要在纹理生成过程中告诉OpenGL,我们的纹理现在使用alpha通道了:
|
||||
|
||||
```c++
|
||||
unsigned char * image = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGBA);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
|
||||
```
|
||||
|
||||
不要忘记还要改变OpenGL生成的纹理:
|
||||
同样,保证你在片段着色器中获取了纹理的全部4个颜色分量,而不仅仅是RGB分量:
|
||||
|
||||
```c++
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
|
||||
```
|
||||
|
||||
保证你在片段着色器中获取了纹理的所有4个颜色元素,而不仅仅是RGB元素:
|
||||
|
||||
```c++
|
||||
void main()
|
||||
{
|
||||
// color = vec4(vec3(texture(texture1, TexCoords)), 1.0);
|
||||
color = texture(texture1, TexCoords);
|
||||
// FragColor = vec4(vec3(texture(texture1, TexCoords)), 1.0);
|
||||
FragColor = texture(texture1, TexCoords);
|
||||
}
|
||||
```
|
||||
|
||||
现在我们知道了如何加载透明纹理,是时候试试在深度测试教程里那个场景中添加几根草了。
|
||||
既然我们已经知道该如何加载透明的纹理了,是时候将它带入实战了,我们将会在[深度测试](01 Depth testing.md)小节的场景中加入几棵草。
|
||||
|
||||
我们创建一个`std::vector`,并向里面添加几个`glm::vec3`变量,来表示草的位置:
|
||||
我们会创建一个vector,向里面添加几个`glm::vec3`变量来代表草的位置:
|
||||
|
||||
```c++
|
||||
vector<glm::vec3> vegetation;
|
||||
@@ -64,224 +58,220 @@ 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,以及设置合理的顶点属性指针。在我们绘制完地面和两个立方体后,我们就来绘制草叶:
|
||||
因为草的纹理是添加到四边形对象上的,我们还需要创建另外一个VAO,填充VBO,设置正确的顶点属性指针。接下来,在绘制完地板和两个立方体后,我们将会绘制草:
|
||||
|
||||
```c++
|
||||
glBindVertexArray(vegetationVAO);
|
||||
glBindTexture(GL_TEXTURE_2D, grassTexture);
|
||||
for(GLuint i = 0; i < vegetation.size(); i++)
|
||||
for(unsigned int i = 0; i < vegetation.size(); i++)
|
||||
{
|
||||
model = glm::mat4();
|
||||
model = glm::translate(model, vegetation[i]);
|
||||
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
|
||||
model = glm::translate(model, vegetation[i]);
|
||||
shader.setMat4("model", model);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
```
|
||||
|
||||
运行程序你将看到:
|
||||
|
||||

|
||||
|
||||
出现这种情况是因为OpenGL默认是不知道如何处理alpha值的,不知道何时忽略(丢弃)它们。我们不得不手动做这件事。幸运的是这很简单,感谢着色器,GLSL为我们提供了discard命令,它保证了片段不会被进一步处理,这样就不会进入颜色缓冲。有了这个命令我们就可以在片段着色器中检查一个片段是否有在一定的阈限下的alpha值,如果有,那么丢弃这个片段,就好像它不存在一样:
|
||||
出现这种情况是因为OpenGL默认是不知道怎么处理alpha值的,更不知道什么时候应该丢弃片段。我们需要自己手动来弄。幸运的是,有了着色器,这还是非常容易的。GLSL给了我们`discard`命令,一旦被调用,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲。有了这个指令,我们就能够在片段着色器中检测一个片段的alpha值是否低于某个阈值,如果是的话,则丢弃这个片段,就好像它不存在一样:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
in vec2 TexCoords;
|
||||
out vec4 FragColor;
|
||||
|
||||
out vec4 color;
|
||||
in vec2 TexCoords;
|
||||
|
||||
uniform sampler2D texture1;
|
||||
|
||||
void main()
|
||||
{
|
||||
{
|
||||
vec4 texColor = texture(texture1, TexCoords);
|
||||
if(texColor.a < 0.1)
|
||||
discard;
|
||||
color = texColor;
|
||||
FragColor = texColor;
|
||||
}
|
||||
```
|
||||
|
||||
在这儿我们检查被采样纹理颜色包含着一个低于0.1这个阈限的alpha值,如果有,就丢弃这个片段。这个片段着色器能够保证我们只渲染哪些不是完全透明的片段。现在我们来看看效果:
|
||||
这里,我们检测被采样的纹理颜色的alpha值是否低于0.1的阈值,如果是的话,则丢弃这个片段。片段着色器保证了它只会渲染不是(几乎)完全透明的片段。现在它看起来就正常了:
|
||||
|
||||

|
||||
|
||||
!!! Important
|
||||
|
||||
需要注意的是,当采样纹理边缘的时候,OpenGL在边界值和下一个重复的纹理的值之间进行插值(因为我们把它的放置方式设置成了GL_REPEAT)。这样就行了,但是由于我们使用的是透明值,纹理图片的上部获得了它的透明值是与底边的纯色值进行插值的。结果就是一个有点半透明的边,你可以从我们的纹理四边形的四周看到。为了防止它的出现,当你使用alpha纹理的时候要把纹理环绕方式设置为`GL_CLAMP_TO_EDGE`:
|
||||
注意,当采样纹理的边缘的时候,OpenGL会对边缘的值和纹理下一个重复的值进行插值(因为我们将它的环绕方式设置为了<var>GL_REPEAT</var>。这通常是没问题的,但是由于我们使用了透明值,纹理图像的顶部将会与底部边缘的纯色值进行插值。这样的结果是一个半透明的有色边框,你可能会看见它环绕着你的纹理四边形。要想避免这个,每当你alpha纹理的时候,请将纹理的环绕方式设置为<var>GL_CLAMP_TO_EDGE</var>:
|
||||
|
||||
`glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);`
|
||||
|
||||
`glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);`
|
||||
|
||||
你可以[在这里得到源码](http://learnopengl.com/code_viewer.php?code=advanced/blending_discard)。
|
||||
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
|
||||
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/3.1.blending_discard/blending_discard.cpp)找到源码。
|
||||
|
||||
## 混合
|
||||
|
||||
上述丢弃片段的方式,不能使我们获得渲染半透明图像,我们要么渲染出像素,要么完全地丢弃它。为了渲染出不同的透明度级别,我们需要开启**混合**(Blending)。像大多数OpenGL的功能一样,我们可以开启`GL_BLEND`来启用**混合(Blending)**功能:
|
||||
虽然直接丢弃片段很好,但它不能让我们渲染半透明的图像。我们要么渲染一个片段,要么完全丢弃它。要想渲染有多个透明度级别的图像,我们需要启用<def>混合</def>(Blending)。和OpenGL大多数的功能一样,我们可以启用<var>GL_BLEND</var>来启用混合:
|
||||
|
||||
```c++
|
||||
glEnable(GL_BLEND);
|
||||
```
|
||||
|
||||
开启混合后,我们还需要告诉OpenGL它该如何混合。
|
||||
启用了混合之后,我们需要告诉OpenGL它该**如何**混合。
|
||||
|
||||
OpenGL以下面的方程进行混合:
|
||||
OpenGL中的混合是通过下面这个方程来实现的:
|
||||
|
||||
$$
|
||||
\begin{equation}\bar{C}_{result} = \bar{\color{green}C}_{source} * \color{green}F_{source} + \bar{\color{red}C}_{destination} * \color{red}F_{destination}\end{equation}
|
||||
$$
|
||||
|
||||
* \(\bar{\color{green}C}_{source}\):源颜色向量。这是来自纹理的本来的颜色向量。
|
||||
* \(\bar{\color{red}C}_{destination}\):目标颜色向量。这是储存在颜色缓冲中当前位置的颜色向量。
|
||||
* \(\color{green}F_{source}\):源因子。设置了对源颜色的alpha值影响。
|
||||
* \(\color{red}F_{destination}\):目标因子。设置了对目标颜色的alpha影响。
|
||||
- \(\bar{\color{green}C}_{source}\):源颜色向量。这是源自纹理的颜色向量。
|
||||
- \(\bar{\color{red}C}_{destination}\):目标颜色向量。这是当前储存在颜色缓冲中的颜色向量。
|
||||
- \(\color{green}F_{source}\):源因子值。指定了alpha值对源颜色的影响。
|
||||
- \(\color{red}F_{destination}\):目标因子值。指定了alpha值对目标颜色的影响。
|
||||
|
||||
片段着色器运行完成并且所有的测试都通过以后,混合方程才能自由执行片段的颜色输出,当前它在颜色缓冲中(前面片段的颜色在当前片段之前储存)。源和目标颜色会自动被OpenGL设置,而源和目标因子可以让我们自由设置。我们来看一个简单的例子:
|
||||
片段着色器运行完成后,并且所有的测试都通过之后,这个<def>混合方程</def>(Blend Equation)才会应用到片段颜色输出与当前颜色缓冲中的值(当前片段之前储存的之前片段的颜色)上。源颜色和目标颜色将会由OpenGL自动设定,但源因子和目标因子的值可以由我们来决定。我们先来看一个简单的例子:
|
||||
|
||||

|
||||
|
||||
我们有两个方块,我们希望在红色方块上绘制绿色方块。红色方块会成为目标颜色(它会先进入颜色缓冲),我们将在红色方块上绘制绿色方块。
|
||||
我们有两个方形,我们希望将这个半透明的绿色方形绘制在红色方形之上。红色的方形将会是目标颜色(所以它应该先在颜色缓冲中),我们将要在这个红色方形之上绘制这个绿色方形。
|
||||
|
||||
那么问题来了:我们怎样来设置因子呢?我们起码要把绿色方块乘以它的alpha值,所以我们打算把\(F_{src}\)设置为源颜色向量的alpha值:0.6。接着,让目标方块的浓度等于剩下的alpha值。如果最终的颜色中绿色方块的浓度为60%,我们就把红色的浓度设为40%(1.0 – 0.6)。所以我们把\(F_{destination}\)设置为1减去源颜色向量的alpha值。方程将变成:
|
||||
问题来了:我们将因子值设置为什么?嘛,我们至少想让绿色方形乘以它的alpha值,所以我们想要将\(F_{src}\)设置为源颜色向量的alpha值,也就是0.6。接下来就应该清楚了,目标方形的贡献应该为剩下的alpha值。如果绿色方形对最终颜色贡献了60%,那么红色方块应该对最终颜色贡献了40%,即`1.0 - 0.6`。所以我们将\(F_{destination}\)设置为1减去源颜色向量的alpha值。这个方程变成了:
|
||||
|
||||
$$
|
||||
\begin{equation}\bar{C}_{result} = \begin{pmatrix} \color{red}{0.0} \\ \color{green}{1.0} \\ \color{blue}{0.0} \\ \color{purple}{0.6} \end{pmatrix} * \color{green}{0.6} + \begin{pmatrix} \color{red}{1.0} \\ \color{green}{0.0} \\ \color{blue}{0.0} \\ \color{purple}{1.0} \end{pmatrix} * \color{red}{(1 - 0.6)} \end{equation}
|
||||
$$
|
||||
|
||||
最终方块结合部分包含了60%的绿色和40%的红色,得到一种脏兮兮的颜色:
|
||||
结果就是重叠方形的片段包含了一个60%绿色,40%红色的一种脏兮兮的颜色:
|
||||
|
||||

|
||||
|
||||
最后的颜色被储存到颜色缓冲中,取代先前的颜色。
|
||||
最终的颜色将会被储存到颜色缓冲中,替代之前的颜色。
|
||||
|
||||
这个方案不错,但我们怎样告诉OpenGL来使用这样的因子呢?恰好有一个叫做`glBlendFunc`的函数。
|
||||
|
||||
`void glBlendFunc(GLenum sfactor, GLenum dfactor)`接收两个参数,来设置源(source)和目标(destination)因子。OpenGL为我们定义了很多选项,我们把最常用的列在下面。注意,颜色常数向量\(\bar{\color{blue}C}_{constant}\)可以用`glBlendColor`函数分开来设置。
|
||||
这样子很不错,但我们该如何让OpenGL使用这样的因子呢?正好有一个专门的函数,叫做<fun>glBlendFunc</fun>。
|
||||
|
||||
<fun>glBlendFunc(GLenum sfactor, GLenum dfactor)</fun>函数接受两个参数,来设置<def>源</def>和<def>目标因子</def>。OpenGL为我们定义了很多个选项,我们将在下面列出大部分最常用的选项。注意常数颜色向量\(\bar{\color{blue}C}_{constant}\)可以通过<fun>glBlendColor</fun>函数来另外设置。
|
||||
|
||||
选项 | 值
|
||||
---|---
|
||||
GL_ZERO | \(0\)
|
||||
GL_ONE | \(1\)
|
||||
GL_SRC_COLOR | 源颜色向量\(\bar{\color{green}C}_{source}\)
|
||||
GL_ONE_MINUS_SRC_COLOR | \(1 - \bar{\color{green}C}_{source}\)
|
||||
GL_DST_COLOR | 目标颜色向量\(\bar{\color{red}C}_{destination}\)
|
||||
GL_ONE_MINUS_DST_COLOR | \(1 - \bar{\color{red}C}_{destination}\)
|
||||
GL_SRC_ALPHA | \(\bar{\color{green}C}_{source}\)的\(alpha\)值
|
||||
GL_ONE_MINUS_SRC_ALPHA | \(1 -\) \(\bar{\color{green}C}_{source}\)的\(alpha\)值
|
||||
GL_DST_ALPHA | \(\bar{\color{red}C}_{destination}\)的\(alpha\)值
|
||||
GL_ONE_MINUS_DST_ALPHA | \(1 -\) \(\bar{\color{red}C}_{destination}\)的\(alpha\)值
|
||||
GL_CONSTANT_COLOR | 常颜色向量\(\bar{\color{blue}C}_{constant}\)
|
||||
GL_ONE_MINUS_CONSTANT_COLOR | \(1 - \bar{\color{blue}C}_{constant}\)
|
||||
GL_CONSTANT_ALPHA | \(\bar{\color{blue}C}_{constant}\)的\(alpha\)值
|
||||
GL_ONE_MINUS_CONSTANT_ALPHA | \(1 -\) \(\bar{\color{blue}C}_{constant}\)的\(alpha\)值
|
||||
`GL_ZERO` | 因子等于\(0\)
|
||||
`GL_ONE` | 因子等于\(1\)
|
||||
`GL_SRC_COLOR` | 因子等于源颜色向量\(\bar{\color{green}C}_{source}\)
|
||||
`GL_ONE_MINUS_SRC_COLOR` | 因子等于\(1 - \bar{\color{green}C}_{source}\)
|
||||
`GL_DST_COLOR` | 因子等于目标颜色向量\(\bar{\color{red}C}_{destination}\)
|
||||
`GL_ONE_MINUS_DST_COLOR` | 因子等于\(1 - \bar{\color{red}C}_{destination}\)
|
||||
`GL_SRC_ALPHA` | 因子等于\(\bar{\color{green}C}_{source}\)的\(alpha\)分量
|
||||
`GL_ONE_MINUS_SRC_ALPHA` | 因子等于\(1 -\) \(\bar{\color{green}C}_{source}\)的\(alpha\)分量
|
||||
`GL_DST_ALPHA` | 因子等于\(\bar{\color{red}C}_{destination}\)的\(alpha\)分量
|
||||
`GL_ONE_MINUS_DST_ALPHA` | 因子等于\(1 -\) \(\bar{\color{red}C}_{destination}\)的\(alpha\)分量
|
||||
`GL_CONSTANT_COLOR` | 因子等于常数颜色向量\(\bar{\color{blue}C}_{constant}\)
|
||||
`GL_ONE_MINUS_CONSTANT_COLOR` | 因子等于\(1 - \bar{\color{blue}C}_{constant}\)
|
||||
`GL_CONSTANT_ALPHA` | 因子等于\(\bar{\color{blue}C}_{constant}\)的\(alpha\)分量
|
||||
`GL_ONE_MINUS_CONSTANT_ALPHA` | 因子等于\(1 -\) \(\bar{\color{blue}C}_{constant}\)的\(alpha\)分量
|
||||
|
||||
为从两个方块获得混合结果,我们打算把源颜色的\(alpha\)给源因子,\(1 - alpha\)给目标因子。调整到`glBlendFunc`之后就像这样:
|
||||
为了获得之前两个方形的混合结果,我们需要使用源颜色向量的\(alpha\)作为源因子,使用\(1 - alpha\)作为目标因子。这将会产生以下的<fun>glBlendFunc</fun>:
|
||||
|
||||
```c++
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
```
|
||||
|
||||
也可以为RGB和alpha通道各自设置不同的选项,使用`glBlendFuncSeperate`:
|
||||
也可以使用<fun>glBlendFuncSeparate</fun>为RGB和alpha通道分别设置不同的选项:
|
||||
|
||||
```c++
|
||||
glBlendFuncSeperate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,GL_ONE, GL_ZERO);
|
||||
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
|
||||
```
|
||||
|
||||
这个方程就像我们之前设置的那样,设置了RGB元素,但是只让最终的alpha元素被源alpha值影响到。
|
||||
这个函数和我们之前设置的那样设置了RGB分量,但这样只能让最终的alpha分量被源颜色向量的alpha值所影响到。
|
||||
|
||||
OpenGL给了我们更多的自由,我们可以改变方程源和目标部分的操作符。现在,源和目标元素已经相加了。如果我们愿意的话,我们还可以把它们相减。
|
||||
OpenGL甚至给了我们更多的灵活性,允许我们改变方程中源和目标部分的运算符。当前源和目标是相加的,但如果愿意的话,我们也可以让它们相减。<fun>glBlendEquation(GLenum mode)</fun>允许我们设置运算符,它提供了三个选项:
|
||||
|
||||
`void glBlendEquation(GLenum mode)`允许我们设置这个操作,有3种可行的选项:
|
||||
- GL_FUNC_ADD:默认选项,将两个分量相加:\(\bar{C}_{result} = \color{green}{Src} + \color{red}{Dst}\)。
|
||||
- GL_FUNC_SUBTRACT:将两个分量相减: \(\bar{C}_{result} = \color{green}{Src} - \color{red}{Dst}\)。
|
||||
- GL_FUNC_REVERSE_SUBTRACT:将两个分量相减,但顺序相反:\(\bar{C}_{result} = \color{red}{Dst} - \color{green}{Src}\)。
|
||||
|
||||
* GL_FUNC_ADD:默认的,彼此元素相加:\(\bar{C}_{result} = \color{green}{Src} + \color{red}{Dst}\)
|
||||
* GL_FUNC_SUBTRACT:彼此元素相减: \(\bar{C}_{result} = \color{green}{Src} - \color{red}{Dst}\)
|
||||
* GL_FUNC_REVERSE_SUBTRACT:彼此元素相减,但顺序相反:\(\bar{C}_{result} = \color{red}{Dst} - \color{green}{Src}\).
|
||||
|
||||
通常我们可以简单地省略`glBlendEquation`因为GL_FUNC_ADD在大多数时候就是我们想要的,但是如果你如果你真想尝试努力打破主流常规,其他的方程或许符合你的要求。
|
||||
通常我们都可以省略调用<fun>glBlendEquation</fun>,因为<var>GL_FUNC_ADD</var>对大部分的操作来说都是我们希望的混合方程,但如果你真的想打破主流,其它的方程也可能符合你的要求。
|
||||
|
||||
## 渲染半透明纹理
|
||||
|
||||
现在我们知道OpenGL如何处理混合,是时候把我们的知识运用起来了,我们来添加几个半透明窗子。我们会使用本教程开始时用的那个场景,但是不再渲染草纹理,取而代之的是来自教程开始处半透明窗子纹理。
|
||||
既然我们已经知道OpenGL是如何处理混合的了,是时候将我们的知识运用到实战中了,我们将会在场景中添加几个半透明的窗户。我们将使用本节开始的那个场景,但是这次不再是渲染草的纹理了,我们现在将使用本节开始时的那个[透明的窗户](../img/04/03/blending_transparent_window.png)纹理。
|
||||
|
||||
首先,初始化时我们需要开启混合,设置合适和混合方程:
|
||||
首先,在初始化时我们启用混合,并设定相应的混合函数:
|
||||
|
||||
```c++
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
```
|
||||
|
||||
由于我们开启了混合,就不需要丢弃片段了,所以我们把片段着色器设置为原来的那个版本:
|
||||
由于启用了混合,我们就不需要丢弃片段了,所以我们把片段着色器还原:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
in vec2 TexCoords;
|
||||
out vec4 FragColor;
|
||||
|
||||
out vec4 color;
|
||||
in vec2 TexCoords;
|
||||
|
||||
uniform sampler2D texture1;
|
||||
|
||||
void main()
|
||||
{
|
||||
color = texture(texture1, TexCoords);
|
||||
{
|
||||
FragColor = texture(texture1, TexCoords);
|
||||
}
|
||||
```
|
||||
|
||||
这一次(无论OpenGL什么时候去渲染一个片段),它都根据alpha值,把当前片段的颜色和颜色缓冲中的颜色进行混合。因为窗子的玻璃部分的纹理是半透明的,我们应该可以透过玻璃看到整个场景。
|
||||
现在(每当OpenGL渲染了一个片段时)它都会将当前片段的颜色和当前颜色缓冲中的片段颜色根据alpha值来进行混合。由于窗户纹理的玻璃部分是半透明的,我们应该能通窗户中看到背后的场景了。
|
||||
|
||||

|
||||
|
||||
如果你仔细看看,就会注意到有些不对劲。前面的窗子透明部分阻塞了后面的。为什么会这样?
|
||||
如果你仔细看的话,你可能会注意到有些不对劲。最前面窗户的透明部分遮蔽了背后的窗户?这为什么会发生呢?
|
||||
|
||||
原因是深度测试在与混合的一同工作时出现了点状况。当写入深度缓冲的时候,深度测试不关心片段是否有透明度,所以透明部分被写入深度缓冲,就和其他值没什么区别。结果是整个四边形的窗子被检查时都忽视了透明度。即便透明部分应该显示出后面的窗子,深度缓冲还是丢弃了它们。
|
||||
发生这一现象的原因是,深度测试和混合一起使用的话会产生一些麻烦。当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会和其它值一样写入到深度缓冲中。结果就是窗户的整个四边形不论透明度都会进行深度测试。即使透明的部分应该显示背后的窗户,深度测试仍然丢弃了它们。
|
||||
|
||||
所以我们不能简简单单地去渲染窗子,我们期待着深度缓冲为我们解决这所有问题;这也正是混合之处代码不怎么好看的原因。为保证前面窗子显示了它后面的窗子,我们必须首先绘制后面的窗子。这意味着我们必须手工调整窗子的顺序,从远到近地逐个渲染。
|
||||
所以我们不能随意地决定如何渲染窗户,让深度缓冲解决所有的问题了。这也是混合变得有些麻烦的部分。要想保证窗户中能够显示它们背后的窗户,我们需要首先绘制背后的这部分窗户。这也就是说在绘制的时候,我们必须先手动将窗户按照最远到最近来排序,再按照顺序渲染。
|
||||
|
||||
!!! Important
|
||||
|
||||
对于全透明物体,比如草叶,我们选择简单的丢弃透明像素而不是混合,这样就减少了令我们头疼的问题(没有深度测试问题)。
|
||||
注意,对于草这种全透明的物体,我们可以选择丢弃透明的片段而不是混合它们,这样就解决了这些头疼的问题(没有深度问题)。
|
||||
|
||||
## 别打乱顺序
|
||||
## 不要打乱顺序
|
||||
|
||||
要让混合在多物体上有效,我们必须先绘制最远的物体,最后绘制最近的物体。普通的无混合物体仍然可以使用深度缓冲正常绘制,所以不必给它们排序。我们一定要保证它们在透明物体前绘制好。当无透明度物体和透明物体一起绘制的时候,通常要遵循以下原则:
|
||||
要想让混合在多个物体上工作,我们需要最先绘制最远的物体,最后绘制最近的物体。普通不需要混合的物体仍然可以使用深度缓冲正常绘制,所以它们不需要排序。但我们仍要保证它们在绘制(排序的)透明物体之前已经绘制完毕了。当绘制一个有不透明和透明物体的场景的时候,大体的原则如下:
|
||||
|
||||
先绘制所有不透明物体。
|
||||
为所有透明物体排序。
|
||||
按顺序绘制透明物体。
|
||||
一种排序透明物体的方式是,获取一个物体到观察者透视图的距离。这可以通过获取摄像机的位置向量和物体的位置向量来得到。接着我们就可以把它和相应的位置向量一起储存到一个map数据结构(STL库)中。map会自动基于它的键排序它的值,所以当我们把它们的距离作为键添加到所有位置中后,它们就自动按照距离值排序了:
|
||||
1. 先绘制所有不透明的物体。
|
||||
2. 对所有透明的物体排序。
|
||||
3. 按顺序绘制所有透明的物体。
|
||||
|
||||
排序透明物体的一种方法是,从观察者视角获取物体的距离。这可以通过计算摄像机位置向量和物体的位置向量之间的距离所获得。接下来我们把距离和它对应的位置向量存储到一个STL库的<fun>map</fun>数据结构中。<fun>map</fun>会自动根据键值(Key)对它的值排序,所以只要我们添加了所有的位置,并以它的距离作为键,它们就会自动根据距离值排序了。
|
||||
|
||||
```c++
|
||||
std::map<float, glm::vec3> sorted;
|
||||
for (GLuint i = 0; i < windows.size(); i++) // windows contains all window positions
|
||||
for (unsigned int i = 0; i < windows.size(); i++)
|
||||
{
|
||||
GLfloat distance = glm::length(camera.Position - windows[i]);
|
||||
float distance = glm::length(camera.Position - windows[i]);
|
||||
sorted[distance] = windows[i];
|
||||
}
|
||||
```
|
||||
|
||||
最后产生了一个容器对象,基于它们距离从低到高储存了每个窗子的位置。
|
||||
结果就是一个排序后的容器对象,它根据<var>distance</var>键值从低到高储存了每个窗户的位置。
|
||||
|
||||
随后当渲染的时候,我们逆序获取到每个map的值(从远到近),然后以正确的绘制相应的窗子:
|
||||
之后,这次在渲染的时候,我们将以逆序(从远到近)从map中获取值,之后以正确的顺序绘制对应的窗户:
|
||||
|
||||
```c++
|
||||
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
|
||||
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
|
||||
{
|
||||
model = glm::mat4();
|
||||
model = glm::translate(model, it->second);
|
||||
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
|
||||
model = glm::translate(model, it->second);
|
||||
shader.setMat4("model", model);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
```
|
||||
|
||||
我们从map得来一个逆序的迭代器,迭代出每个逆序的条目,然后把每个窗子的四边形平移到相应的位置。这个相对简单的方法对透明物体进行了排序,修正了前面的问题,现在场景看起来像这样:
|
||||
我们使用了<fun>map</fun>的一个反向迭代器(Reverse Iterator),反向遍历其中的条目,并将每个窗户四边形位移到对应的窗户位置上。这是排序透明物体的一个比较简单的实现,它能够修复之前的问题,现在场景看起来是这样的:
|
||||
|
||||

|
||||
|
||||
你可以[从这里得到完整的带有排序的源码](http://learnopengl.com/code_viewer.php?code=advanced/blending_sorted)。
|
||||
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/3.2.blending_sort/blending_sorted.cpp)找到带有排序的完整源代码。
|
||||
|
||||
虽然这个按照它们的距离对物体进行排序的方法在这个特定的场景中能够良好工作,但它不能进行旋转、缩放或者进行其他的变换,奇怪形状的物体需要一种不同的方式,而不能简单的使用位置向量。
|
||||
虽然按照距离排序物体这种方法对我们这个场景能够正常工作,但它并没有考虑旋转、缩放或者其它的变换,奇怪形状的物体需要一个不同的计量,而不是仅仅一个位置向量。
|
||||
|
||||
在场景中排序物体是个有难度的技术,它很大程度上取决于你场景的类型,更不必说会耗费额外的处理能力了。完美地渲染带有透明和不透明的物体的场景并不那么容易。有更高级的技术例如次序无关透明度(order independent transparency),但是这超出了本教程的范围。现在你不得不采用普通的混合你的物体,但是如果你小心谨慎,并知道这个局限,你仍可以得到颇为合适的混合实现。
|
||||
在场景中排序物体是一个很困难的技术,很大程度上由你场景的类型所决定,更别说它额外需要消耗的处理能力了。完整渲染一个包含不透明和透明物体的场景并不是那么容易。更高级的技术还有<def>次序无关透明度</def>(Order Independent Transparency, OIT),但这超出本教程的范围了。现在,你还是必须要普通地混合你的物体,但如果你很小心,并且知道目前方法的限制的话,你仍然能够获得一个比较不错的混合实现。
|
16
glossary.md
16
glossary.md
@@ -109,7 +109,7 @@
|
||||
- Wrapping:环绕方式
|
||||
- Clamp:约束
|
||||
- Nearest Neighbor Filtering:临近过滤
|
||||
- Texture Pixel,Texel:纹理像素
|
||||
- Texture Pixel,Texel:纹理像素,纹素
|
||||
- (Bi)linear Filtering:线性过滤
|
||||
- Magnify:放大
|
||||
- Minify:缩小
|
||||
@@ -303,6 +303,20 @@
|
||||
- Filter(后期处理):滤镜
|
||||
- Shadow Volume:阴影体积
|
||||
|
||||
## 04-03
|
||||
|
||||
- Blending:混合
|
||||
- Transparency:透明(度)
|
||||
- Solid Color:纯色
|
||||
- Alpha Value(透明度):Alpha值
|
||||
- Source Color Vector:源颜色向量
|
||||
- Destination Color Vector:目标颜色向量
|
||||
- Source Factor Value:源因子值
|
||||
- Destination Factor Value:目标因子值
|
||||
- Blend Equation:混合方程
|
||||
- Reverse Iterator:反向迭代器
|
||||
- Order Independent Transparency:次序无关透明度
|
||||
|
||||
## 06-01
|
||||
|
||||
- Debugging:调试
|
||||
|
Reference in New Issue
Block a user