OpenGL ES 教程:从零开始掌握 3D 渲染

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,需要:

  1. 在 AndroidManifest.xml 中声明 OpenGL ES 版本:
xml 复制代码
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
  1. 在布局中添加 GLSurfaceView:
xml 复制代码
<android.opengl.GLSurfaceView
    android:id="@+id/gl_surface_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
  1. 创建 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 渲染的完整流程:

  1. 理解了渲染管线和 GLSL 着色器语言。
  2. 搭建了 Android/iOS 开发环境。
  3. 实现了第一个清屏程序。
  4. 绘制了带旋转动画的 3D 立方体。
  5. 添加了光照和纹理效果。
  6. 学习了性能优化技巧。

OpenGL ES 是一个庞大而强大的图形库,本教程只是入门。建议你继续深入学习:

  • 高级光照模型(Blinn-Phong、PBR)
  • 阴影映射(Shadow Mapping)
  • 帧缓冲对象(FBO)与离屏渲染
  • 实例化渲染(Instancing)

希望本教程能帮助你开启 3D 渲染的精彩之旅!

相关推荐
袋鼠云数栈2 小时前
非结构化数据也能“周期调度“:离线开发BatchWorks的多模态数据同步实践
大数据·人工智能·多模态数据·离线开放
算了吧95692 小时前
GEO服务商选型指南:2026年企业评估框架与决策路径
大数据·人工智能·物联网
智塑未来2 小时前
App竞品数据平台选型指南:从入门到精通的全维度研判方法
大数据
回眸不遇2 小时前
Elasticsearch嵌套类型nested使用指南<第七十二篇>
大数据·elasticsearch·jenkins
龙亘川2 小时前
城市运管服平台下综合办公数字化建设实践与思考
大数据·人工智能·智慧城市·开源软件·数据可视化
SM_YHJ3 小时前
RFID通道门禁与资产台账联动的自动登记技术实现方案
大数据·数据库·人工智能
liliangcsdn4 小时前
PMI参数的解读与应用分析
大数据
AI编程教父5 小时前
WorkBuddy企业培训怎么选?腾讯云公开课程与红烁AI内训对比
大数据·人工智能
大大大大晴天5 小时前
湖仓时代的数据生命周期治理:让数据留得有价值、删得有依据
大数据