2. OpenGL ES基础
在移动端 UI 渲染工程中,OpenGL ES 是连接应用层与 GPU 的底层图形接口。理解它的版本差异、管线阶段、坐标变换和着色器语法,是后续实现高性能控件、动画和特效的前提。本节从工程视角梳理 OpenGL ES 的核心基础,并给出一个可运行的最小渲染示例。
一、版本演进:从 ES 1.x 到 ES 3.x
OpenGL ES 是 OpenGL 的嵌入式子集,按版本可划分为三条主线:
- OpenGL ES 1.x:固定管线,顶点变换、光照、纹理混合由硬件固定实现,开发者通过
glEnable、glMatrixMode等接口配置状态。开发简单,但灵活性差,现代项目已基本淘汰。 - OpenGL ES 2.0:引入可编程管线,必须提供顶点着色器和片元着色器。它是 Android/iOS 长期兼容的基线版本,绝大多数 UI 渲染框架仍以 ES 2.0 为最低要求。
- OpenGL ES 3.0/3.1/3.2:在 2.0 基础上增加实例化渲染、多重渲染目标、纹理数组、统一缓冲对象、计算着色器(3.1+)等能力,适合复杂 UI 特效和 GPU 通用计算。
工程建议:若目标设备覆盖 Android 5.0 以上,可优先使用 ES 3.0;若需兼容老旧机型,则以 ES 2.0 为基线,通过扩展检测按需启用高级特性。
二、图形管线:从顶点到像素
OpenGL ES 2.0 及以上的可编程管线可简化为以下阶段:顶点数据 → 顶点着色器 → 图元装配 → 光栅化 → 片元着色器 → 逐片元操作 → 帧缓冲。其中顶点着色器负责坐标变换,片元着色器负责颜色计算,二者是开发者可编程的核心。
三、坐标系:从模型空间到屏幕空间
UI 渲染中最容易出错的是坐标变换。顶点从模型空间出发,依次经过模型矩阵、视图矩阵、投影矩阵,得到裁剪空间坐标,再经透视除法与视口变换映射到屏幕像素。常见坐标系包括:
| 坐标系 | 含义 | 典型变换 |
|---|---|---|
| 模型空间 | 顶点在自身模型中的局部坐标 | 模型矩阵 M |
| 世界空间 | 模型放入场景后的统一坐标 | 视图矩阵 V |
| 观察空间 | 以相机为原点的坐标 | 投影矩阵 P |
| 裁剪空间 | 齐次坐标,范围 [-w, w] | 透视除法 |
| 屏幕空间 | 最终像素坐标,原点在左下或左上 | 视口变换 |
注意:OpenGL ES 默认裁剪空间 z 范围为 [-1, 1],而 Direct3D 为 [0, 1]。若从其他图形 API 迁移,需调整投影矩阵,否则会出现深度异常。
四、GLSL ES 核心语法
GLSL ES 是 OpenGL ES 的着色器语言,语法类似 C,但增加了向量、矩阵和纹理采样类型。核心要点如下:
- 版本声明:ES 2.0 使用
#version 100或省略;ES 3.0 使用#version 300 es,且必须写在首行。 - 变量限定符:
attribute(顶点属性,仅顶点着色器)、uniform(全局常量)、varying(顶点到片元的插值变量)。ES 3.0 中分别改为in、uniform、out/in。 - 精度限定符:片元着色器必须声明浮点精度,如
precision mediump float;,否则编译失败。 - 内置变量:顶点着色器输出
gl_Position,片元着色器输出gl_FragColor(ES 2.0)或自定义out变量(ES 3.0)。
// 顶点着色器(ES 2.0)
attribute vec4 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uMVP;
varying vec2 vTexCoord;
void main() {
gl_Position = uMVP * aPosition;
vTexCoord = aTexCoord;
}
// 片元着色器(ES 2.0)
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;
void main() {
gl_FragColor = texture2D(uTexture, vTexCoord);
}
五、最小渲染示例:绘制一个纹理矩形
下面给出一个最小可运行的渲染流程,展示从初始化到绘制的关键步骤。代码以 Android GLSurfaceView 为例,省略了 Activity 生命周期细节。
// 1. 初始化:编译着色器、链接程序
int program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
// 2. 准备顶点数据:位置 + 纹理坐标
float[] vertices = {
-1f, -1f, 0f, 0f, 0f,
1f, -1f, 0f, 1f, 0f,
-1f, 1f, 0f, 0f, 1f,
1f, 1f, 0f, 1f, 1f
};
// 3. 上传到 VBO
glGenBuffers(1, vbo, 0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
// 4. 设置属性指针
int posLoc = glGetAttribLocation(program, "aPosition");
int texLoc = glGetAttribLocation(program, "aTexCoord");
glEnableVertexAttribArray(posLoc);
glVertexAttribPointer(posLoc, 3, GL_FLOAT, false, 20, 0);
glEnableVertexAttribArray(texLoc);
glVertexAttribPointer(texLoc, 2, GL_FLOAT, false, 20, 12);
// 5. 绘制
glUseProgram(program);
glUniformMatrix4fv(uMVPLoc, 1, false, mvp, 0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
六、常见误区与复盘
- 精度缺失:片元着色器未声明
precision,导致编译报错或颜色异常。建议统一在片元着色器首行声明precision mediump float;。 - 坐标原点混淆:OpenGL ES 纹理坐标原点在左下,而 Android Bitmap 原点在左上,直接采样会导致图像上下颠倒。可通过翻转纹理坐标或使用
glPixelStorei调整。 - 状态泄漏:忘记解绑 VBO、纹理或程序,导致后续绘制使用错误状态。建议在
onDrawFrame末尾统一清理。 - 版本混用:ES 3.0 着色器使用
attribute或gl_FragColor会编译失败,必须使用in/out和自定义输出变量。
掌握以上基础后,后续的 UI 渲染工程将围绕“如何高效组织顶点数据、如何复用着色器程序、如何管理纹理与帧缓冲”展开。建议先手写一个最小三角形,再逐步叠加纹理、混合和变换,形成可调试的渲染骨架。