第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明明在远处啊!这就违反了物理直觉。
11.5 半透明渲染的标准流程
经过多次项目实践,我总结了一套半透明渲染的标准流程:
- 先画所有不透明物体——关闭混合,正常渲染
- 再画半透明物体——开启混合,从远到近排序
代码实现大概是这样的:
// 第一步:渲染不透明物体
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。但要注意,粒子之间也要排序,从远到近渲染。
11.8 深度测试与混合的冲突
这里有个坑,我必须提醒你。深度测试和混合一起用的时候,容易出问题。
深度测试的原理是:每个像素记录一个深度值,新来的片段如果深度更小(更近),就覆盖;否则丢弃。但半透明物体不应该完全覆盖后面的物体,它应该让后面的颜色透过来。
解决方案:
- 不透明物体:正常深度测试,写入深度缓冲
- 半透明物体:深度测试开启,但不写入深度缓冲
代码实现:
// 渲染不透明物体
glDepthMask(GL_TRUE);
renderOpaque();
// 渲染半透明物体
glDepthMask(GL_FALSE);
renderTransparent();
glDepthMask(GL_FALSE)的意思是:深度测试仍然进行(防止半透明物体画到更近的物体前面),但不更新深度缓冲(让后面的半透明物体也能显示)。
11.9 知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系:
11.10 实战中的注意事项
最后,我总结几个实战中容易踩的坑:
- 纹理预乘:半透明纹理的RGB值最好预乘Alpha,否则边缘会出现暗色光晕。我见过不少新手在这个问题上卡好几天。
- 排序精度:物体距离相机的排序,用浮点数比较时要注意精度。两个物体距离非常接近时,排序结果可能不稳定,导致闪烁。
- 性能开销:每帧对半透明物体排序,如果物体数量多(比如上千个粒子),排序本身会消耗不少CPU。可以考虑用空间分区优化。
- 多重混合:如果场景里既有玻璃又有烟雾,还有水面,它们的混合模式可能不一样。记得在渲染不同物体时切换
glBlendFunc。
好了,关于Alpha混合和半透明渲染,今天就讲到这里。这些内容看起来简单,但真正用好需要大量实践。我建议你从玻璃杯和烟雾粒子两个案例入手,把今天讲的知识点都过一遍。遇到问题别慌,回头看看这张知识体系图,理清思路再调试。
公众号:蓝海资料掘金营,微信deep3321