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

各位同学,今天我们来啃一块硬骨头——OpenGL ES。说实话,很多做Android音视频开发的朋友,一听到OpenGL就头大。我当年也是这样,看着一堆矩阵、着色器代码,心里直犯嘀咕:这玩意儿是给人学的吗?

但后来我发现,只要你把几个核心概念理清楚,OpenGL ES其实没那么可怕。今天这一章,我们就聚焦四个关键点:EGL环境搭建、着色器编写、纹理渲染、FBO离屏渲染。把这四个搞明白,你就能在Android上玩转GPU了。

OpenGL ES 核心知识体系 OpenGL ES 渲染管线 EGL 环境搭建 Display → Surface → Context 着色器编写 顶点 + 片段着色器 纹理渲染 纹理坐标 → 采样 → 输出 FBO 离屏渲染 帧缓冲对象 → 离屏处理 四个环节环环相扣,缺一不可

一、EGL环境搭建——OpenGL与窗口系统的桥梁

先问大家一个问题:OpenGL ES本身不知道屏幕长什么样,它怎么把图像画到你的手机屏幕上?答案就是EGL。EGL是OpenGL ES和本地窗口系统之间的中间层。说白了,没有EGL,你的着色器写得再漂亮,也显示不出来。

我个人习惯把EGL搭建分为三步走:

  1. 获取Display——连接本地显示设备
  2. 创建Surface——绑定到窗口或屏幕
  3. 创建Context——设置OpenGL ES的工作环境

来看一段我常用的EGL初始化代码:

// 1. 获取默认Display
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, null, null);

// 2. 配置属性
int[] attribList = {
    EGL_RED_SIZE, 8,
    EGL_GREEN_SIZE, 8,
    EGL_BLUE_SIZE, 8,
    EGL_ALPHA_SIZE, 8,
    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
    EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
eglChooseConfig(display, attribList, configs, 1, numConfigs);

// 3. 创建Surface和Context
EGLSurface surface = eglCreateWindowSurface(display, configs[0], surfaceView, null);
int[] contextAttribs = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
EGLContext context = eglCreateContext(display, configs[0], EGL_NO_CONTEXT, contextAttribs);

// 4. 绑定
eglMakeCurrent(display, surface, surface, context);
⚠️ 避坑提醒: 我曾经在项目里忘了设置EGL_CONTEXT_CLIENT_VERSION,结果着色器死活编译不过。默认是OpenGL ES 1.x,不支持可编程管线。记得显式指定版本为2。

二、着色器编写——GPU的编程语言

着色器(Shader)是运行在GPU上的小程序。OpenGL ES 2.0开始,我们必须自己写两种着色器:顶点着色器和片段着色器。

顶点着色器负责处理每个顶点的位置。你想想看,一个三角形有三个顶点,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);
}
💡 个人经验: 刚开始写着色器时,我总把attribute和uniform搞混。记住一点:attribute是每个顶点不同的数据,uniform是所有顶点共享的数据。比如纹理坐标用attribute,纹理对象用uniform。

三、纹理渲染——把图片贴到多边形上

纹理渲染,说白了就是把一张图片贴到你画的几何图形上。这个过程涉及三个关键步骤:

  • 加载纹理数据——把Bitmap或YUV数据传给GPU
  • 设置纹理参数——控制放大缩小、重复方式等
  • 纹理采样——在片段着色器里用纹理坐标取颜色

来看纹理加载的代码:

int[] textureIds = new int[1];
glGenTextures(1, textureIds, 0);
int textureId = textureIds[0];

glBindTexture(GL_TEXTURE_2D, textureId);
// 设置纹理参数
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);

核心要点:纹理坐标的范围是0.0到1.0。左上角是(0,0),右下角是(1,1)。这个和屏幕坐标不一样,我刚开始做的时候经常搞反,导致纹理显示是倒的。

四、FBO离屏渲染——不显示到屏幕的渲染

FBO(Frame Buffer Object)是OpenGL ES里一个非常强大的工具。它允许你把渲染结果输出到一个纹理或缓冲区,而不是直接显示到屏幕。为什么要这么做?

  • 滤镜处理——先渲染到FBO,再读取做后处理
  • 多pass渲染——需要多次渲染同一场景
  • 截图功能——把渲染结果保存为图片

FBO的使用流程其实很简单:

// 1. 创建FBO
int[] fboIds = new int[1];
glGenFramebuffers(1, fboIds, 0);
int fboId = fboIds[0];
glBindFramebuffer(GL_FRAMEBUFFER, fboId);

// 2. 创建纹理并附着到FBO
int[] texIds = new int[1];
glGenTextures(1, texIds, 0);
int fboTextureId = texIds[0];
glBindTexture(GL_TEXTURE_2D, fboTextureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fboTextureId, 0);

// 3. 检查FBO是否完整
int status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
    // 处理错误
}

// 4. 渲染到FBO(所有绘制命令都会输出到纹理)
// ... 绘制代码 ...

// 5. 切换回默认帧缓冲
glBindFramebuffer(GL_FRAMEBUFFER, 0);
⚠️ 注意: 我曾经犯过一个低级错误——FBO的纹理尺寸和屏幕尺寸不一致,结果渲染出来的图像被拉伸了。FBO纹理的宽高一定要和你要渲染的内容尺寸匹配。

五、实战中的坑与经验

做音视频开发这几年,我在OpenGL ES上踩过的坑真不少。挑几个典型的说说:

常见问题 原因 解决方案
纹理显示为黑色 纹理未正确加载或绑定 检查glGenTextures和glBindTexture顺序
画面闪烁 EGL上下文切换频繁 确保在同一个线程操作EGL
FBO渲染无效 未检查FBO完整性 调用glCheckFramebufferStatus
着色器编译失败 语法错误或版本不匹配 使用glGetShaderInfoLog查看错误

嗯,这里还要强调一点:OpenGL ES的所有操作都必须在GL线程中执行。我见过不少同学在UI线程里调用OpenGL API,结果程序直接崩溃。记住,EGL上下文是和线程绑定的。

最后说一句,学习OpenGL ES没有捷径,多写多练是唯一的办法。刚开始可能觉得难,但当你真正理解渲染管线的工作方式后,你会发现它其实非常优雅。好了,这一章的内容就到这里,大家动手试试吧。

本章小结:EGL是OpenGL ES的运行基础,着色器是GPU编程的核心,纹理渲染是图像处理的关键,FBO则提供了离屏渲染的能力。这四个知识点是Android音视频开发中绕不开的基础,也是后续学习滤镜、特效、视频编辑的基石。

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