1. 引言
OpenGL ES(OpenGL for Embedded Systems)是 OpenGL 在移动设备和嵌入式平台上的精简版本,广泛应用于 Android、iOS 等移动操作系统的 3D 图形渲染。无论是游戏开发、AR/VR 应用,还是数据可视化,OpenGL ES 都是移动端 3D 渲染的核心技术之一。
本教程将从零开始,带你逐步掌握 OpenGL ES 的 3D 渲染流程。我们会从基础概念讲起,通过可运行的代码示例,最终实现一个完整的 3D 立方体渲染程序。
2. OpenGL ES 基础概念
2.1 什么是 OpenGL ES
OpenGL ES 是一套跨平台的 3D 图形 API,由 Khronos Group 维护。它去掉了桌面版 OpenGL 中一些复杂且不适用于嵌入式设备的特性,专注于高效、低功耗的渲染。
2.2 渲染管线
OpenGL ES 的渲染管线(Pipeline)是理解 3D 渲染的关键。核心流程如下:
#mermaid-svg-FCWs0neisQdk981m{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FCWs0neisQdk981m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FCWs0neisQdk981m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FCWs0neisQdk981m .error-icon{fill:#552222;}#mermaid-svg-FCWs0neisQdk981m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FCWs0neisQdk981m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FCWs0neisQdk981m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FCWs0neisQdk981m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FCWs0neisQdk981m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FCWs0neisQdk981m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FCWs0neisQdk981m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FCWs0neisQdk981m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FCWs0neisQdk981m .marker.cross{stroke:#333333;}#mermaid-svg-FCWs0neisQdk981m svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FCWs0neisQdk981m p{margin:0;}#mermaid-svg-FCWs0neisQdk981m .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FCWs0neisQdk981m .cluster-label text{fill:#333;}#mermaid-svg-FCWs0neisQdk981m .cluster-label span{color:#333;}#mermaid-svg-FCWs0neisQdk981m .cluster-label span p{background-color:transparent;}#mermaid-svg-FCWs0neisQdk981m .label text,#mermaid-svg-FCWs0neisQdk981m span{fill:#333;color:#333;}#mermaid-svg-FCWs0neisQdk981m .node rect,#mermaid-svg-FCWs0neisQdk981m .node circle,#mermaid-svg-FCWs0neisQdk981m .node ellipse,#mermaid-svg-FCWs0neisQdk981m .node polygon,#mermaid-svg-FCWs0neisQdk981m .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FCWs0neisQdk981m .rough-node .label text,#mermaid-svg-FCWs0neisQdk981m .node .label text,#mermaid-svg-FCWs0neisQdk981m .image-shape .label,#mermaid-svg-FCWs0neisQdk981m .icon-shape .label{text-anchor:middle;}#mermaid-svg-FCWs0neisQdk981m .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FCWs0neisQdk981m .rough-node .label,#mermaid-svg-FCWs0neisQdk981m .node .label,#mermaid-svg-FCWs0neisQdk981m .image-shape .label,#mermaid-svg-FCWs0neisQdk981m .icon-shape .label{text-align:center;}#mermaid-svg-FCWs0neisQdk981m .node.clickable{cursor:pointer;}#mermaid-svg-FCWs0neisQdk981m .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FCWs0neisQdk981m .arrowheadPath{fill:#333333;}#mermaid-svg-FCWs0neisQdk981m .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FCWs0neisQdk981m .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FCWs0neisQdk981m .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FCWs0neisQdk981m .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FCWs0neisQdk981m .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FCWs0neisQdk981m .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FCWs0neisQdk981m .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FCWs0neisQdk981m .cluster text{fill:#333;}#mermaid-svg-FCWs0neisQdk981m .cluster span{color:#333;}#mermaid-svg-FCWs0neisQdk981m div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FCWs0neisQdk981m .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FCWs0neisQdk981m rect.text{fill:none;stroke-width:0;}#mermaid-svg-FCWs0neisQdk981m .icon-shape,#mermaid-svg-FCWs0neisQdk981m .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FCWs0neisQdk981m .icon-shape p,#mermaid-svg-FCWs0neisQdk981m .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FCWs0neisQdk981m .icon-shape .label rect,#mermaid-svg-FCWs0neisQdk981m .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FCWs0neisQdk981m .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FCWs0neisQdk981m .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FCWs0neisQdk981m :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 顶点数据
顶点着色器
图元装配
光栅化
片段着色器
逐片段操作
帧缓冲
- 顶点着色器(Vertex Shader):处理每个顶点的位置、颜色等属性。
- 图元装配(Primitive Assembly):将顶点组装成点、线、三角形等图元。
- 光栅化(Rasterization):将图元转换为屏幕上的片段(像素)。
- 片段着色器(Fragment Shader):计算每个片段的最终颜色。
- 逐片段操作(Per-Fragment Operations):包括深度测试、混合等。
2.3 着色器语言 GLSL
GLSL(OpenGL Shading Language)是编写着色器的语言,语法类似 C 语言。一个最简单的顶点着色器和片段着色器如下:
glsl
// 顶点着色器
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
}
glsl
// 片段着色器
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
}
3. 开发环境准备
3.1 Android 环境
在 Android 上使用 OpenGL ES,需要:
- 在
AndroidManifest.xml中声明 OpenGL ES 版本:
xml
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
- 在布局中添加
GLSurfaceView:
xml
<android.opengl.GLSurfaceView
android:id="@+id/gl_surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- 创建
GLSurfaceView.Renderer实现类。
3.2 iOS 环境
在 iOS 上,使用 EAGLContext 和 GLKView(或自定义 UIView)来搭建渲染环境:
objc
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
GLKView *view = [[GLKView alloc] initWithFrame:self.view.bounds context:context];
view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
[self.view addSubview:view];
4. 第一个 OpenGL ES 程序
4.1 创建 Renderer
以 Android 为例,创建一个渲染器类:
java
public class MyGLRenderer implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 设置清屏颜色为深灰色
GLES20.glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 设置视口
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
// 清屏
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
}
4.2 编译着色器
编写工具方法编译着色器:
java
private int compileShader(int type, String shaderCode) {
int shaderId = GLES20.glCreateShader(type);
GLES20.glShaderSource(shaderId, shaderCode);
GLES20.glCompileShader(shaderId);
// 检查编译状态
int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == 0) {
String error = GLES20.glGetShaderInfoLog(shaderId);
GLES20.glDeleteShader(shaderId);
throw new RuntimeException("着色器编译失败: " + error);
}
return shaderId;
}
4.3 链接着色器程序
java
private int linkProgram(int vertexShaderId, int fragmentShaderId) {
int programId = GLES20.glCreateProgram();
GLES20.glAttachShader(programId, vertexShaderId);
GLES20.glAttachShader(programId, fragmentShaderId);
GLES20.glLinkProgram(programId);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] == 0) {
String error = GLES20.glGetProgramInfoLog(programId);
GLES20.glDeleteProgram(programId);
throw new RuntimeException("着色器程序链接失败: " + error);
}
return programId;
}
5. 绘制 3D 立方体
5.1 定义顶点数据
一个立方体有 8 个顶点、6 个面。为了简单起见,我们使用三角形绘制每个面:
java
// 立方体顶点坐标(每个面由两个三角形组成)
float[] cubeVertices = {
// 前面
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
// 后面
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
// ... 其余四个面
};
5.2 设置投影矩阵
为了让 3D 场景有透视效果,需要设置投影矩阵:
java
private float[] projectionMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float aspectRatio = (float) width / height;
Matrix.perspectiveM(projectionMatrix, 0, 45f, aspectRatio, 0.1f, 100f);
}
5.3 设置视图矩阵
视图矩阵模拟相机位置:
java
private float[] viewMatrix = new float[16];
// 在 onDrawFrame 中设置
Matrix.setLookAtM(viewMatrix, 0,
0f, 0f, 3f, // 相机位置
0f, 0f, 0f, // 观察目标
0f, 1f, 0f); // 上方向
5.4 应用 MVP 矩阵
将投影矩阵和视图矩阵相乘,得到 MVP 矩阵,传入顶点着色器:
java
float[] mvpMatrix = new float[16];
Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
// 传入着色器
int mvpMatrixLocation = GLES20.glGetUniformLocation(programId, "u_MVPMatrix");
GLES20.glUniformMatrix4fv(mvpMatrixLocation, 1, false, mvpMatrix, 0);
5.5 完整绘制代码
java
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
// 使用着色器程序
GLES20.glUseProgram(programId);
// 绑定顶点数据
GLES20.glVertexAttribPointer(positionLocation, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
GLES20.glEnableVertexAttribArray(positionLocation);
// 绘制立方体(36 个顶点 = 12 个三角形)
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 36);
}
6. 添加旋转动画
为了让立方体动起来,可以在每帧更新旋转矩阵:
java
private float rotationAngle = 0f;
@Override
public void onDrawFrame(GL10 gl) {
// ... 清屏和绑定操作
// 更新旋转角度
rotationAngle += 1f;
// 创建旋转矩阵
float[] rotationMatrix = new float[16];
Matrix.setRotateM(rotationMatrix, 0, rotationAngle, 0f, 1f, 0f);
// 组合 MVP 矩阵
float[] tempMatrix = new float[16];
Matrix.multiplyMM(tempMatrix, 0, viewMatrix, 0, rotationMatrix, 0);
Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, tempMatrix, 0);
// 传入着色器并绘制
GLES20.glUniformMatrix4fv(mvpMatrixLocation, 1, false, mvpMatrix, 0);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 36);
}
7. 光照与材质
7.1 法线向量
要实现光照效果,需要为每个顶点提供法线向量:
java
// 立方体各面的法线
float[] normals = {
// 前面
0f, 0f, 1f,
0f, 0f, 1f,
0f, 0f, 1f,
0f, 0f, 1f,
// 后面
0f, 0f, -1f,
// ... 其余面
};
7.2 漫反射光照
在片段着色器中计算漫反射:
glsl
precision mediump float;
uniform vec3 u_LightDirection;
uniform vec3 u_LightColor;
varying vec3 v_Normal;
void main() {
// 归一化法线
vec3 normal = normalize(v_Normal);
// 计算漫反射强度
float diffuse = max(dot(normal, u_LightDirection), 0.0);
// 最终颜色
vec3 color = u_LightColor * diffuse;
gl_FragColor = vec4(color, 1.0);
}
8. 纹理映射
8.1 加载纹理
java
private int loadTexture(Context context, int resourceId) {
int[] textureIds = new int[1];
GLES20.glGenTextures(1, textureIds, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[0]);
// 设置纹理过滤参数
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);
// 解码图片并上传
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
return textureIds[0];
}
8.2 纹理坐标
为每个顶点指定纹理坐标(UV):
java
float[] textureCoords = {
// 前面
0f, 0f,
1f, 0f,
1f, 1f,
0f, 1f,
// ... 其余面
};
8.3 采样纹理
在片段着色器中使用纹理:
glsl
precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoord;
void main() {
gl_FragColor = texture2D(u_Texture, v_TexCoord);
}
9. 性能优化建议
9.1 减少状态切换
OpenGL ES 的状态切换(如切换着色器程序、绑定纹理)开销较大,应尽量减少:
- 将相同状态的绘制调用合并。
- 使用纹理图集(Texture Atlas)减少纹理绑定次数。
9.2 使用顶点缓冲对象(VBO)
将顶点数据上传到 GPU 显存,避免每帧重复传输:
java
int[] vboIds = new int[1];
GLES20.glGenBuffers(1, vboIds, 0);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]);
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexData.length * 4, vertexBuffer, GLES20.GL_STATIC_DRAW);
9.3 避免每帧分配对象
在 onDrawFrame 中避免创建新对象(如数组、矩阵),应提前分配并复用。
10. 总结
本教程从 OpenGL ES 的基础概念出发,带你走完了 3D 渲染的完整流程:
- 理解了渲染管线和 GLSL 着色器语言。
- 搭建了 Android/iOS 开发环境。
- 实现了第一个清屏程序。
- 绘制了带旋转动画的 3D 立方体。
- 添加了光照和纹理效果。
- 学习了性能优化技巧。
OpenGL ES 是一个庞大而强大的图形库,本教程只是入门。建议你继续深入学习:
- 高级光照模型(Blinn-Phong、PBR)
- 阴影映射(Shadow Mapping)
- 帧缓冲对象(FBO)与离屏渲染
- 实例化渲染(Instancing)
希望本教程能帮助你开启 3D 渲染的精彩之旅!