6、OpenGL ES 基础:EGL环境搭建、着色器、纹理渲染、FBO离屏渲染

说实话,OpenGL ES 这块内容,是 Android 音视频处理里绕不开的坎。很多做应用层的朋友一听到「着色器」「FBO」就头大,觉得这是图形学的东西,跟音视频没关系。其实你想想看,我们做视频滤镜、特效、转场,甚至视频编解码的 Surface 输入,底层哪个离得开 OpenGL ES?

我个人习惯把 OpenGL ES 的学习分成四个台阶:EGL 环境搭建 → 着色器编写 → 纹理渲染 → FBO 离屏渲染。今天我们就一个一个踩过去。

核心知识点一览

EGL 是 OpenGL ES 与窗口系统的桥梁;着色器是 GPU 上运行的小程序;纹理是 GPU 里的图片数据;FBO 让你能在离屏缓冲区里画画,而不是直接显示到屏幕上。

OpenGL ES 基础四步走 EGL 环境搭建 着色器编写 纹理渲染 FBO 离屏渲染 创建 EGLDisplay 配置 EGLConfig 顶点着色器 纹理坐标映射 创建 FBO 对象 每一步都是下一步的基础 没有 EGL → 无法创建 OpenGL ES 上下文 没有着色器 → 无法处理像素数据 没有 FBO → 无法做离屏特效处理

1. EGL 环境搭建——OpenGL ES 的「地基」

EGL 是什么?说白了,它是 OpenGL ES 和 Android 窗口系统(SurfaceFlinger)之间的中间层。没有 EGL,你的 OpenGL ES 指令就不知道往哪画。

我记得第一次接触 EGL 时,被那一堆 eglCreateWindowSurfaceeglMakeCurrent 搞得晕头转向。后来我总结了一个口诀:「一显二配三创建,四上下文五当前」

  • 一显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_LINEARGL_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 实现一个简单的滤镜链

好了,理论讲完了,我们来点实际的。假设你要实现一个「先灰度化再锐化」的滤镜链,怎么做?

思路是这样的:

  1. 创建两个 FBO,分别对应两个滤镜的输出
  2. 原始纹理 → FBO1(灰度化着色器)→ 得到灰度纹理
  3. 灰度纹理 → FBO2(锐化着色器)→ 得到最终纹理
  4. 最终纹理渲染到屏幕

你看,FBO 在这里起到了「中间缓存」的作用。没有 FBO,你没法把灰度化的结果再传给锐化着色器。

我的经验:滤镜链越长,FBO 切换次数越多,性能开销越大。实际项目中,我会尽量合并着色器,减少 FBO 数量。比如灰度+锐化可以写在一个片段着色器里,一步到位。

好了,今天的内容就到这里。EGL 环境搭建、着色器编写、纹理渲染、FBO 离屏渲染,这四个知识点是 OpenGL ES 的基石。你把这些搞明白了,后面做视频滤镜、特效、转场就会轻松很多。


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