6、OpenGL ES 基础:EGL环境搭建、着色器、纹理渲染、FBO离屏渲染
说实话,OpenGL ES 这块内容,是 Android 音视频处理里绕不开的坎。很多做应用层的朋友一听到「着色器」「FBO」就头大,觉得这是图形学的东西,跟音视频没关系。其实你想想看,我们做视频滤镜、特效、转场,甚至视频编解码的 Surface 输入,底层哪个离得开 OpenGL ES?
我个人习惯把 OpenGL ES 的学习分成四个台阶:EGL 环境搭建 → 着色器编写 → 纹理渲染 → FBO 离屏渲染。今天我们就一个一个踩过去。
核心知识点一览
EGL 是 OpenGL ES 与窗口系统的桥梁;着色器是 GPU 上运行的小程序;纹理是 GPU 里的图片数据;FBO 让你能在离屏缓冲区里画画,而不是直接显示到屏幕上。
1. EGL 环境搭建——OpenGL ES 的「地基」
EGL 是什么?说白了,它是 OpenGL ES 和 Android 窗口系统(SurfaceFlinger)之间的中间层。没有 EGL,你的 OpenGL ES 指令就不知道往哪画。
我记得第一次接触 EGL 时,被那一堆 eglCreateWindowSurface、eglMakeCurrent 搞得晕头转向。后来我总结了一个口诀:「一显二配三创建,四上下文五当前」。
- 一显:
eglGetDisplay(EGL_DEFAULT_DISPLAY)获取默认显示设备 - 二配:
eglChooseConfig()选择匹配的 EGLConfig - 三创建:
eglCreateWindowSurface()创建渲染表面 - 四上:
eglCreateContext()创建 OpenGL ES 上下文 - 五当前:
eglMakeCurrent()把上下文绑定到当前线程
小技巧:EGL 上下文是线程相关的。你必须在渲染线程里调用 eglMakeCurrent,不能在主线程里搞。我曾经在项目里犯过这个错,结果画面死活不显示,查了半天才发现是线程问题。
// EGL 环境搭建核心代码片段
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, null, null);
EGLConfig config = chooseConfig(display); // 选择 RGBA8888 + 深度缓冲
EGLSurface surface = eglCreateWindowSurface(display, config, surfaceView, null);
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, null);
eglMakeCurrent(display, surface, surface, context);
// 现在可以调用 OpenGL ES 指令了
2. 着色器——GPU 上的「小程序」
着色器(Shader)是 OpenGL ES 里最核心的概念。你可以把它理解成一段跑在 GPU 上的代码。我们主要用两种:顶点着色器和片段着色器。
顶点着色器负责处理每个顶点的位置,片段着色器负责处理每个像素的颜色。嗯,这里要注意:片段着色器是逐像素执行的,所以性能很关键。
我在项目中遇到过一个问题:滤镜效果在低端机上卡成 PPT。后来发现是片段着色器里写了个 for 循环,GPU 受不了。所以我的建议是:着色器里尽量别写循环,能用数学公式解决的别用分支。
// 一个简单的顶点着色器
attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = aPosition;
vTexCoord = aTexCoord;
}
// 对应的片段着色器
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
void main() {
gl_FragColor = texture2D(uTexture, vTexCoord);
}
避坑指南:我曾经在片段着色器里忘记加 precision mediump float,结果在某些 GPU 上直接黑屏。这个声明指定了浮点精度,一定要写上。
3. 纹理渲染——把图片数据喂给 GPU
纹理说白了就是一张「GPU 能识别的图片」。在 Android 里,我们通常把 Bitmap 或者 YUV 数据上传到纹理,然后让着色器去处理。
纹理渲染的流程其实很简单:创建纹理 → 绑定纹理 → 上传数据 → 设置纹理参数 → 绘制。
你想想看,为什么视频帧要转成纹理?因为 GPU 只认纹理,不认 Bitmap。所以我们在做视频渲染时,第一步就是把解码后的 YUV 数据上传到纹理。
// 创建并上传纹理
int[] textures = new int[1];
glGenTextures(1, textures, 0);
glBindTexture(GL_TEXTURE_2D, textures[0]);
// 设置纹理参数(很重要!)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 上传 Bitmap 数据
GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
glBindTexture(GL_TEXTURE_2D, 0);
重点:纹理参数里的 GL_LINEAR 和 GL_NEAREST 区别很大。前者是双线性插值,画面平滑;后者是最近邻采样,画面有锯齿。做视频渲染时我一般用 GL_LINEAR。
4. FBO 离屏渲染——看不见的「画板」
FBO(Frame Buffer Object)是 OpenGL ES 里一个非常强大的工具。它允许你把渲染结果输出到一个「离屏缓冲区」里,而不是直接显示到屏幕上。
为什么要用 FBO?举个例子:你要给视频加一个高斯模糊滤镜。你不能直接在屏幕上画一遍模糊效果,因为用户看到的是中间过程。你需要先在 FBO 里渲染一遍,拿到模糊后的纹理,再把它显示到屏幕上。
我个人习惯把 FBO 叫做「画板」——你在画板上画完,再决定要不要贴到墙上(屏幕)。
// 创建 FBO
int[] fbo = new int[1];
glGenFramebuffers(1, fbo, 0);
glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]);
// 创建一个纹理作为 FBO 的附着点
int[] fboTexture = new int[1];
glGenTextures(1, fboTexture, 0);
glBindTexture(GL_TEXTURE_2D, fboTexture[0]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 把纹理附着到 FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fboTexture[0], 0);
// 检查 FBO 是否完整
int status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
// 处理错误
}
// 渲染到 FBO
glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]);
// ... 执行绘制指令 ...
// 渲染完成后,fboTexture[0] 里就是渲染结果
// 切换回默认帧缓冲(屏幕)
glBindFramebuffer(GL_FRAMEBUFFER, 0);
注意:FBO 创建后一定要检查 glCheckFramebufferStatus 的返回值。我曾经在某个机型上遇到 FBO 不完整的问题,原因是纹理尺寸超过了 GPU 支持的最大值。所以建议先查询 GL_MAX_TEXTURE_SIZE。
5. 综合实战:用 FBO 实现一个简单的滤镜链
好了,理论讲完了,我们来点实际的。假设你要实现一个「先灰度化再锐化」的滤镜链,怎么做?
思路是这样的:
- 创建两个 FBO,分别对应两个滤镜的输出
- 原始纹理 → FBO1(灰度化着色器)→ 得到灰度纹理
- 灰度纹理 → FBO2(锐化着色器)→ 得到最终纹理
- 最终纹理渲染到屏幕
你看,FBO 在这里起到了「中间缓存」的作用。没有 FBO,你没法把灰度化的结果再传给锐化着色器。
我的经验:滤镜链越长,FBO 切换次数越多,性能开销越大。实际项目中,我会尽量合并着色器,减少 FBO 数量。比如灰度+锐化可以写在一个片段着色器里,一步到位。
好了,今天的内容就到这里。EGL 环境搭建、着色器编写、纹理渲染、FBO 离屏渲染,这四个知识点是 OpenGL ES 的基石。你把这些搞明白了,后面做视频滤镜、特效、转场就会轻松很多。