10、OpenGL ES基础:EGL环境搭建、着色器编写、纹理渲染、FBO离屏渲染
各位同学,今天我们来啃一块硬骨头——OpenGL ES。说实话,很多做Android音视频开发的朋友,一听到OpenGL就头大。我当年也是这样,看着一堆矩阵、着色器代码,心里直犯嘀咕:这玩意儿是给人学的吗?
但后来我发现,只要你把几个核心概念理清楚,OpenGL ES其实没那么可怕。今天这一章,我们就聚焦四个关键点:EGL环境搭建、着色器编写、纹理渲染、FBO离屏渲染。把这四个搞明白,你就能在Android上玩转GPU了。
一、EGL环境搭建——OpenGL与窗口系统的桥梁
先问大家一个问题:OpenGL ES本身不知道屏幕长什么样,它怎么把图像画到你的手机屏幕上?答案就是EGL。EGL是OpenGL ES和本地窗口系统之间的中间层。说白了,没有EGL,你的着色器写得再漂亮,也显示不出来。
我个人习惯把EGL搭建分为三步走:
- 获取Display——连接本地显示设备
- 创建Surface——绑定到窗口或屏幕
- 创建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);
二、着色器编写——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);
}
三、纹理渲染——把图片贴到多边形上
纹理渲染,说白了就是把一张图片贴到你画的几何图形上。这个过程涉及三个关键步骤:
- 加载纹理数据——把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);
五、实战中的坑与经验
做音视频开发这几年,我在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音视频开发中绕不开的基础,也是后续学习滤镜、特效、视频编辑的基石。