第11章 混合与透明度:Alpha混合,渲染顺序问题

各位同学,今天我们来聊聊OpenGL ES里一个特别有意思的话题——混合与透明度。

说实话,我刚开始学OpenGL那会儿,觉得混合就是个简单的「半透明叠加」。直到我在一个AR项目里做玻璃杯效果,怎么调都像块塑料,后来才发现是渲染顺序搞的鬼。嗯,今天咱们就把这块彻底讲透。

11.1 什么是Alpha混合?

Alpha混合,说白了就是让两个颜色「掺和」在一起。你想想看,一个半透明的红色玻璃放在蓝色背景前,你看到的既不是纯红也不是纯蓝,而是红蓝混合后的紫色。这就是混合在干活。

在OpenGL ES里,每个像素除了RGB三个颜色通道,还有第四个通道——Alpha(透明度)。Alpha值范围是0.0到1.0:

  • Alpha = 1.0:完全不透明
  • Alpha = 0.0:完全透明
  • Alpha = 0.5:半透明

混合的核心公式其实很简单:

最终颜色 = 源颜色 × 源因子 + 目标颜色 × 目标因子

这里的「源颜色」就是你正在绘制的片段,「目标颜色」是帧缓冲区里已经存在的颜色。两个因子决定了混合的方式。

11.2 启用混合与设置混合函数

在OpenGL ES里启用混合只需要一行代码:

glEnable(GL_BLEND);

然后设置混合函数。最常用的组合是:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

这个组合的意思是:

  • 源因子 = 源颜色的Alpha值
  • 目标因子 = 1 - 源颜色的Alpha值

举个例子,一个Alpha=0.3的红色(1.0, 0.0, 0.0, 0.3)叠加在蓝色(0.0, 0.0, 1.0)背景上:

最终R = 1.0 × 0.3 + 0.0 × 0.7 = 0.3
最终G = 0.0 × 0.3 + 0.0 × 0.7 = 0.0
最终B = 0.0 × 0.3 + 1.0 × 0.7 = 0.7

结果就是(0.3, 0.0, 0.7),一个偏蓝的紫色。嗯,符合直觉。

我的小经验: 如果你做的是玻璃、烟雾这类效果,GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA基本够用。但做发光效果时,可以试试GL_SRC_ALPHA, GL_ONE,叠加效果会更亮。

11.3 混合方程的其他模式

除了glBlendFunc,OpenGL ES还提供了glBlendFuncSeparate,可以分别设置RGB和Alpha的混合方式:

glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE);

这个设置的意思是:

  • RGB通道:用源Alpha混合
  • Alpha通道:直接相加

为什么要分开?我在做烟雾效果时遇到过一个问题——多个半透明层叠在一起,Alpha通道越叠越不透明,看起来像一团浆糊。分开设置后,Alpha通道用加法,透明度就能保持住。

混合模式 glBlendFunc参数 适用场景
标准透明 GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA 玻璃、UI元素
叠加发光 GL_SRC_ALPHA, GL_ONE 粒子、光晕
相乘 GL_ZERO, GL_SRC_COLOR 阴影、纹理叠加
变亮 GL_ONE, GL_ONE 火焰、闪电

11.4 渲染顺序——半透明的老大难

好,现在到了最关键的环节。为什么你的半透明效果总是不对?十有八九是渲染顺序的问题。

我举个例子你就明白了。假设场景里有三个物体:

  • 物体A:红色,Alpha=0.5,距离相机最近
  • 物体B:绿色,Alpha=0.5,在中间
  • 物体C:蓝色,Alpha=0.5,最远

正确的渲染顺序应该是:先画C,再画B,最后画A。也就是从远到近。

为什么?因为混合公式是「源颜色叠加到目标颜色上」。如果你先画近处的A,再画远处的C,C会叠在A上面——但C明明在远处啊!这就违反了物理直觉。

我曾经踩过的坑: 在一个室内场景里,我同时渲染了玻璃窗和窗外的树。玻璃是半透明的,树是不透明的。我按物体ID顺序渲染,结果玻璃把树挡住了,看起来像玻璃上贴了张树的照片。后来改成先画不透明物体,再画透明物体,从远到近排序,效果才正常。

11.5 半透明渲染的标准流程

经过多次项目实践,我总结了一套半透明渲染的标准流程:

  1. 先画所有不透明物体——关闭混合,正常渲染
  2. 再画半透明物体——开启混合,从远到近排序

代码实现大概是这样的:

// 第一步:渲染不透明物体
glDisable(GL_BLEND);
renderOpaqueObjects();

// 第二步:排序半透明物体(从远到近)
sortTransparentObjectsByDistance(cameraPosition);

// 第三步:渲染半透明物体
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (auto& obj : transparentObjects) {
    renderObject(obj);
}

排序的依据是物体中心到相机的距离。注意,如果物体很大,用中心点排序可能不够精确,这时候可以用物体包围盒的最近点。

11.6 实现玻璃效果

玻璃效果的核心是:半透明 + 反射 + 折射。在OpenGL ES里,我们通常用环境贴图来模拟反射和折射。

一个简单的玻璃着色器:

// 顶点着色器
attribute vec4 aPosition;
attribute vec3 aNormal;
uniform mat4 uMVPMatrix;
uniform mat4 uModelMatrix;
varying vec3 vNormal;
varying vec3 vPosition;

void main() {
    gl_Position = uMVPMatrix * aPosition;
    vNormal = mat3(uModelMatrix) * aNormal;
    vPosition = (uModelMatrix * aPosition).xyz;
}

// 片段着色器
precision mediump float;
uniform samplerCube uEnvironmentMap;
uniform vec3 uCameraPosition;
uniform float uAlpha;  // 透明度
varying vec3 vNormal;
varying vec3 vPosition;

void main() {
    vec3 normal = normalize(vNormal);
    vec3 viewDir = normalize(uCameraPosition - vPosition);
    
    // 反射方向
    vec3 reflectDir = reflect(-viewDir, normal);
    vec4 reflectColor = textureCube(uEnvironmentMap, reflectDir);
    
    // 折射方向(简单模拟)
    vec3 refractDir = refract(-viewDir, normal, 0.66);
    vec4 refractColor = textureCube(uEnvironmentMap, refractDir);
    
    // 混合反射和折射
    vec4 glassColor = mix(refractColor, reflectColor, 0.3);
    glassColor.a = uAlpha;
    
    gl_FragColor = glassColor;
}

这个着色器里,我用了mix函数把反射和折射混合起来,比例是3:7。为什么是3:7?嗯,这个比例是我在项目里调出来的,看起来比较像普通玻璃。不同玻璃的折射率不一样,你可以根据需求调整。

11.7 实现烟雾效果

烟雾和玻璃不太一样。烟雾是体积效果,通常用粒子系统来实现。

烟雾粒子的关键参数:

  • 位置:随机分布在空间里
  • 大小:随时间增大
  • Alpha:随时间从0.6逐渐降到0.0
  • 颜色:灰白色,带一点随机变化

粒子更新逻辑:

struct SmokeParticle {
    vec3 position;
    vec3 velocity;
    float size;
    float alpha;
    float life;
};

void updateSmokeParticle(SmokeParticle& p, float deltaTime) {
    p.life -= deltaTime;
    if (p.life <= 0.0f) {
        // 粒子死亡,重置
        resetParticle(p);
        return;
    }
    
    // 烟雾上升并扩散
    p.position += p.velocity * deltaTime;
    p.velocity.y += 0.1f * deltaTime;  // 上升加速度
    p.velocity.x += (rand() - 0.5f) * 0.5f * deltaTime;  // 随机飘散
    
    // 大小增大,透明度降低
    p.size += 0.5f * deltaTime;
    p.alpha = 0.6f * (1.0f - p.life / 3.0f);  // 假设生命周期3秒
}

渲染烟雾粒子时,混合模式我推荐用GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA。但要注意,粒子之间也要排序,从远到近渲染。

一个小技巧: 烟雾粒子用纹理时,纹理的Alpha通道要预乘。也就是说,纹理的RGB值乘以Alpha值再存进去。这样混合出来的效果更自然,边缘不会有暗边。

11.8 深度测试与混合的冲突

这里有个坑,我必须提醒你。深度测试和混合一起用的时候,容易出问题。

深度测试的原理是:每个像素记录一个深度值,新来的片段如果深度更小(更近),就覆盖;否则丢弃。但半透明物体不应该完全覆盖后面的物体,它应该让后面的颜色透过来。

解决方案:

  • 不透明物体:正常深度测试,写入深度缓冲
  • 半透明物体:深度测试开启,但不写入深度缓冲

代码实现:

// 渲染不透明物体
glDepthMask(GL_TRUE);
renderOpaque();

// 渲染半透明物体
glDepthMask(GL_FALSE);
renderTransparent();

glDepthMask(GL_FALSE)的意思是:深度测试仍然进行(防止半透明物体画到更近的物体前面),但不更新深度缓冲(让后面的半透明物体也能显示)。

注意: 如果场景里半透明物体之间互相遮挡,这个方法就不够用了。这时候需要把半透明物体按深度排序,然后从远到近渲染。如果两个半透明物体交叉在一起,那就更复杂了——可能需要分割几何体。我在一个VR项目里遇到过这种问题,最后用了「深度剥离」技术才解决。

11.9 知识体系总览

下面这张图总结了本章的核心知识点和它们之间的关系:

Alpha混合与半透明渲染知识体系 Alpha混合 混合函数设置 渲染顺序(从远到近) 深度测试冲突 glBlendFunc glBlendFuncSeparate 不透明物体优先 透明物体排序 glDepthMask(GL_FALSE) 玻璃效果 烟雾效果 粒子系统 核心原则:先不透明后透明,透明物体从远到近排序,深度测试只读不写

11.10 实战中的注意事项

最后,我总结几个实战中容易踩的坑:

  • 纹理预乘:半透明纹理的RGB值最好预乘Alpha,否则边缘会出现暗色光晕。我见过不少新手在这个问题上卡好几天。
  • 排序精度:物体距离相机的排序,用浮点数比较时要注意精度。两个物体距离非常接近时,排序结果可能不稳定,导致闪烁。
  • 性能开销:每帧对半透明物体排序,如果物体数量多(比如上千个粒子),排序本身会消耗不少CPU。可以考虑用空间分区优化。
  • 多重混合:如果场景里既有玻璃又有烟雾,还有水面,它们的混合模式可能不一样。记得在渲染不同物体时切换glBlendFunc

好了,关于Alpha混合和半透明渲染,今天就讲到这里。这些内容看起来简单,但真正用好需要大量实践。我建议你从玻璃杯和烟雾粒子两个案例入手,把今天讲的知识点都过一遍。遇到问题别慌,回头看看这张知识体系图,理清思路再调试。


公众号:蓝海资料掘金营,微信deep3321