13、OpenGL ES基础:渲染管线、着色器语言、纹理映射、EGL环境
好,咱们今天聊点硬核的——OpenGL ES。做Android音视频开发,这玩意儿是绕不过去的坎。说白了,它就是一套在嵌入式设备上跑3D图形的API。你手机里那些酷炫的相机滤镜、视频特效,底层基本都是它在干活。
我个人习惯把OpenGL ES比作一个「图形工厂」。你给它原材料(顶点坐标、纹理图片),它按照一套固定的流水线(渲染管线)加工,最后产出屏幕上的像素。这套流水线,就是咱们今天要啃的第一块骨头。
13.1 渲染管线:图形工厂的流水线
渲染管线,听起来玄乎,其实就是一套固定的处理步骤。我刚开始学的时候,总想跳过它直接写代码,结果代码跑出来全是黑屏。后来才明白,不理解管线,你连bug都不知道怎么调。
OpenGL ES 2.0以后的管线,是可编程的。这意味着中间有几个关键环节,你可以自己写代码(着色器)来控制。整个流程大致如下:
核心流程: 顶点数据 → 顶点着色器 → 图元装配 → 光栅化 → 片段着色器 → 逐片段测试 → 帧缓冲区
我来拆解一下这几个关键步骤:
- 顶点着色器:处理每个顶点的位置、颜色。你想想看,一个三角形有三个顶点,这个阶段就是决定这三个点最终在屏幕上的位置。我在项目中遇到过,如果顶点坐标没处理好,整个画面会扭曲变形。
- 图元装配:把顶点组装成点、线、三角形。说白了就是告诉GPU,这些点要连成什么形状。
- 光栅化:把几何图形转换成像素片段。嗯,这里要注意,光栅化之后的数据是离散的像素点,不再是连续的图形了。
- 片段着色器:决定每个像素最终的颜色。这是最灵活的阶段,滤镜、混合、纹理采样都在这里做。
- 逐片段测试:深度测试、模板测试、裁剪测试。我曾经在这里踩过一个坑——深度测试没开,结果后面的物体把前面的挡住了,画面看起来像鬼片。
避坑指南: 我曾经在项目里遇到一个诡异的问题——画面闪烁。查了两天才发现,是顶点着色器里坐标变换矩阵算错了。记住,管线里任何一个环节出问题,最终画面都会崩。调试时建议从后往前查,先确认片段着色器没问题,再往前推。
13.2 着色器语言:GLSL 入门
着色器是用GLSL(OpenGL Shading Language)写的。这语言语法跟C语言很像,但有几个关键区别。我刚开始写的时候,总习惯用printf调试,结果发现GLSL里根本没有这玩意儿。
一个典型的顶点着色器长这样:
// 顶点着色器
attribute vec4 aPosition; // 顶点位置
attribute vec2 aTexCoord; // 纹理坐标
uniform mat4 uMVPMatrix; // 变换矩阵
varying vec2 vTexCoord; // 传递给片段着色器的纹理坐标
void main() {
gl_Position = uMVPMatrix * aPosition;
vTexCoord = aTexCoord;
}
片段着色器长这样:
// 片段着色器
precision mediump float; // 精度限定符
uniform sampler2D uTexture; // 纹理采样器
varying vec2 vTexCoord; // 从顶点着色器传来的纹理坐标
void main() {
gl_FragColor = texture2D(uTexture, vTexCoord);
}
这里有几个关键点:
- attribute:每个顶点都不同的数据,比如位置、颜色。我习惯把attribute看作「每个顶点的身份证」。
- uniform:对所有顶点都相同的数据,比如变换矩阵、时间。说白了就是全局变量。
- varying:从顶点着色器传到片段着色器的数据。光栅化阶段会自动插值,让像素之间的过渡更平滑。
- precision:精度限定符。移动端GPU性能有限,精度太高会拖慢速度,太低又会有画面瑕疵。我个人习惯用mediump,大部分场景够用。
注意: GLSL里没有指针,没有动态内存分配,没有递归。如果你在着色器里写了个while(true),GPU会直接卡死。我曾经见过同事把CPU上的算法直接搬进着色器,结果手机烫得能煎鸡蛋。
13.3 纹理映射:给图形穿上衣服
纹理映射,说白了就是把一张图片贴到3D模型上。你想想看,一个纯白色的三角形有什么好看的?贴上纹理之后,它就能变成照片、视频、或者任何你想要的图案。
纹理映射的核心是纹理坐标。每个顶点对应一个纹理坐标(u, v),范围是0.0到1.0。左上角是(0,0),右下角是(1,1)。
加载纹理的代码大致如下:
// 生成纹理对象
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
int textureId = textures[0];
// 绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
// 设置纹理参数
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
// 上传纹理数据
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
// 解绑
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
避坑指南: 我曾经在项目里遇到纹理显示为纯色的问题。查了半天,发现是bitmap的格式不对——OpenGL ES要求纹理图片的宽高必须是2的幂次方(比如256x256、512x512)。虽然现在大部分设备支持非2的幂次方,但为了兼容性,我建议还是按这个规矩来。
13.4 EGL环境:OpenGL ES与窗口系统的桥梁
EGL,全称是Embedded Graphics Library。它负责管理OpenGL ES的上下文(Context)和显示表面(Surface)。说白了,没有EGL,你的OpenGL ES代码就不知道往哪里画。
在Android上,EGL的典型使用流程是这样的:
- 获取默认显示设备:
EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) - 初始化EGL:
EGL14.eglInitialize(display, majorVersion, minorVersion) - 配置属性:选择颜色格式、深度缓冲位数等。我习惯用RGBA8888,颜色精度高一些。
- 创建EGL上下文:
EGL14.eglCreateContext(display, config, sharedContext, attribList) - 创建EGL表面:
EGL14.eglCreateWindowSurface(display, config, surface, attribList) - 绑定上下文:
EGL14.eglMakeCurrent(display, drawSurface, readSurface, context) - 开始渲染:调用OpenGL ES API
- 交换缓冲区:
EGL14.eglSwapBuffers(display, surface)
这里有个关键点——共享上下文。如果你要在多个线程里操作OpenGL ES(比如一个线程渲染,一个线程加载纹理),就需要共享上下文。我在做视频播放器的时候,就是靠这个机制让解码线程和渲染线程协同工作的。
注意: EGL上下文是线程相关的。你不能在A线程创建上下文,然后在B线程直接使用。我曾经犯过这个错——在子线程创建了EGL上下文,然后在主线程调用OpenGL ES API,结果程序直接崩溃。记住,eglMakeCurrent必须在同一个线程调用。
嗯,到这里,OpenGL ES的基础框架就搭起来了。渲染管线是骨架,着色器是灵魂,纹理是血肉,EGL是连接外界的桥梁。这四个东西搞明白了,后面那些滤镜、特效、视频渲染,都是在此基础上添砖加瓦。