OpenGL ES教程——混合

185 阅读3分钟

OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。

简而言之,就是透明物体与它背后物体的颜色混合所呈现的效果。

透明的物体可以是完全透明的(让所有的颜色穿过),或者是半透明的(它让颜色通过,同时也会显示自身的颜色)。

物体的透明度即alpha,alpha值为0.0时,则是完全透明的,值为1.0时,则物体是完全不透明的。

1、混合原理

OpenGL中使用如下方式来开启混合:

glEnable(GL_BLEND);

开启混合之后,我们还要告诉OpenGL如何混合。

OpenGL中的混合是通过下面这个方程来实现的:

image.png

  • C source:源颜色向量。这是源自纹理的颜色向量。
  • F source:源因子值。指定了alpha值对源颜色的影响。
  • C des:目标颜色向量。这是当前储存在颜色缓冲中的颜色向量。
  • F des:目标因子值。指定了alpha值对目标颜色的影响。

片段着色器运行完成后,并且所有的测试都通过之后,才会执行混合操作。其中源颜色向量和目标颜色向量都是OpenGL自行指定,开发者能指定的是各自的因子值。

通过如下方法来指定因子值:

glBlendFunc(GLenum sfactor, GLenum dfactor);

这个函数的两个参数分别为源和目标因子,OpenGL定义了很多的因子常量:

选项
GL_ZERO因子等于0
GL_ONE因子等于1
GL_SRC_COLOR因子等于源颜色向量C_source
GL_ONE_MINUS_SRC_COLOR因子等于1−C_source
GL_DST_COLOR因子等于目标颜色向量C_des
GL_ONE_MINUS_DST_COLOR因子等于1−C_des
GL_SRC_ALPHA因子等于C_source的alpha分量
GL_ONE_MINUS_SRC_ALPHA因子等于1− C_source的alpha分量
GL_DST_ALPHA因子等于C_destination的alpha分量
GL_ONE_MINUS_DST_ALPHA因子等于1− C_destination的alpha分量
GL_CONSTANT_COLOR因子等于常数颜色向量C_constant
GL_ONE_MINUS_CONSTANT_COLOR因子等于1−C_constant
GL_CONSTANT_ALPHA因子等于C_constant的alpha分量
GL_ONE_MINUS_CONSTANT_ALPHA因子等于1− C_constant的alpha分量

为了获得之前两个图形形的混合结果,我们需要使用源颜色向量的alpha作为源因子,使用1−alpha作为目标因子。这将会产生以下的glBlendFunc:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

另外,混合还可以修改片段着色器来丢弃片段:

void main() {
    vec4 txColor = texture(firId, v_texCoord);
    if(txColor.a < 0.1)
        discard;
    outColor = txColor;
}

2、混合示例

image.png

如上所示,我们需要绘制一个地面、两个立方体,5个透明窗户。从颜色上看,窗户能显示背后的物体及颜色。

绘制地面,相当于绘制一张图片,使用一个纹理即可解决。

绘制窗户,也是绘制一张图片,用一个纹理解决,只是需要绘制5次而已

绘制立文体也是这样,我们绘制两次立文体即可,整体也只需要用一个纹理。

整体的绘制代码如下所示:

glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

std::vector<glm::vec3> vegetation
        {
                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)
        };

objectShader.use();
glBindVertexArray(mVao);
auto rat = MyGlRenderContext::getInstance()->getWidth() * 1.0f /
           MyGlRenderContext::getInstance()->getHeight();
glm::mat4 projection = glm::perspective(glm::radians(45.0f), rat, 0.1f, 100.0f);
glm::mat4 view = camera.getViewMatrix();
glm::mat4 model = glm::mat4(1.0f);
objectShader.setMat4("projection", projection);
objectShader.setMat4("view", view);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mFirstId);
objectShader.setInt("firId", 0);
model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f));
objectShader.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));
objectShader.setMat4("model", model);
glBindTexture(GL_TEXTURE_2D, mFirstId);
glDrawArrays(GL_TRIANGLES, 0, 36);

glBindVertexArray(mFloorVao);
glBindTexture(GL_TEXTURE_2D, mFloorId);
model = glm::mat4(1.0f);
objectShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 6);

std::map<float, glm::vec3> sorted;
for (int i = 0; i < vegetation.size(); ++i) {
    float distance = glm::length(camera.position - vegetation[i]);
    sorted[distance] = vegetation[i];
}

glBindVertexArray(mGrassVao);
glBindTexture(GL_TEXTURE_2D, mGrassId);

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);
    objectShader.setMat4("model", model);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

要想让混合在多个物体上工作,我们需要最先绘制最远的物体,最后绘制最近的物体。普通不需要混合的物体仍然可以使用深度缓冲正常绘制,所以它们不需要排序。但我们仍要保证它们在绘制(排序的)透明物体之前已经绘制完毕了。当绘制一个有不透明和透明物体的场景的时候,大体的原则如下:

  1. 先绘制所有不透明的物体。
  2. 对所有透明的物体排序。
  3. 按顺序绘制所有透明的物体。

上述代码中,我对5个窗户进行了排序,

std::map<float, glm::vec3> sorted;
for (int i = 0; i < vegetation.size(); ++i) {
    float distance = glm::length(camera.position - vegetation[i]);
    sorted[distance] = vegetation[i];
}

map自动对内部的元素根据key进行排序,我们计算窗户的远近,然后以远近为key添加到map中,这样map里的数据便是从近到远排序了。

取出时,我们逆序取出,即是先绘制最远的,再绘制最近的了:

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);
    objectShader.setMat4("model", model);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

最后,将实现上述效果。