3、GPU基础与OpenGL ES:渲染管线、着色器语言、第一个三角形程序
好,咱们进入正题。这一章是GPU编程的起点,也是你理解8155图形性能的关键。说白了,OpenGL ES就是嵌入式设备上的图形API标准,8155的Adreno GPU对它支持得非常好。我当年第一次在8155上跑通三角形程序时,那种感觉——嗯,就像你第一次点亮一个LED灯一样兴奋。
3.1 渲染管线:GPU到底在干什么?
很多人觉得渲染管线很玄乎。其实没那么复杂。你想想看,GPU就像一条流水线工厂,原料是顶点数据,成品是屏幕上的像素。中间经过几个固定工位,每个工位干一件事。
我习惯把渲染管线分成三个阶段来讲:
- 应用阶段:CPU准备好数据,比如顶点坐标、颜色、纹理。然后调用glDrawArrays告诉GPU开工。
- 几何阶段:GPU处理顶点,做变换、裁剪、光栅化。这里跑的是顶点着色器。
- 光栅化阶段:把三角形变成像素碎片,然后跑片段着色器,决定每个像素的颜色。
核心要点:整个管线是并行的。GPU有几百个核心同时干活,所以画几万个三角形和画几十万个三角形,时间差不了太多。我在8155上测试过,只要不触发带宽瓶颈,三角形数量翻10倍,帧率只掉20%。
这里有个坑,我曾经踩过:顶点缓存。如果你每次绘制都重新上传顶点数据,那CPU和GPU之间的带宽就成了瓶颈。8155的Adreno 640有L2缓存,但数据量大了照样扛不住。解决方案?用VBO(顶点缓冲对象),把数据留在GPU显存里。
3.2 着色器语言:GLSL入门
着色器语言GLSL,说白了就是跑在GPU上的小程序。它不像C语言那么自由,有很多限制。我刚开始写GLSL时,总觉得这也不让那也不让,后来才明白——这些限制是为了并行效率。
一个典型的OpenGL ES程序至少需要两个着色器:
- 顶点着色器:处理每个顶点,输出gl_Position。
- 片段着色器:处理每个像素,输出gl_FragColor。
来看一个最简单的顶点着色器:
// 顶点着色器
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
}
这个着色器啥也没干,就是把传入的顶点坐标直接输出。实际项目中你会加上模型矩阵、视图矩阵、投影矩阵的变换。我在8155上做HMI时,经常把矩阵运算放在顶点着色器里,而不是CPU端——因为GPU算矩阵乘法比CPU快得多。
再看片段着色器:
// 片段着色器
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
}
这里有个细节:precision mediump float。为什么要有精度修饰符?因为移动GPU的浮点单元有不同精度,高精度(highp)慢,低精度(lowp)快。我建议在8155上,颜色计算用mediump就够了,位置计算用highp。别一上来就全用highp,性能会打折扣。
个人经验:在8155上调试着色器时,可以用glGetShaderInfoLog查看编译错误。我曾经花了两小时找bug,最后发现是分号写成了中文全角——嗯,这种低级错误,谁还没犯过呢?
3.3 第一个三角形程序
好,理论说完了,咱们动手。在8155上画一个三角形,其实就几步:
- 初始化EGL(OpenGL ES和窗口系统的桥梁)
- 创建着色器并编译
- 准备顶点数据
- 绘制
先看EGL初始化。这部分比较固定,我一般写成工具函数:
// EGL初始化(简化版)
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, NULL, NULL);
EGLConfig config;
EGLint numConfigs;
EGLint 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
};
eglChooseConfig(display, attribList, &config, 1, &numConfigs);
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, NULL);
EGLSurface surface = eglCreateWindowSurface(display, config, nativeWindow, NULL);
eglMakeCurrent(display, surface, surface, context);
然后编译着色器。我习惯把着色器源码写成字符串数组:
// 编译着色器
const char* vertexShaderSource =
"attribute vec4 a_Position;\n"
"void main() {\n"
" gl_Position = a_Position;\n"
"}\n";
const char* fragmentShaderSource =
"precision mediump float;\n"
"void main() {\n"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
// 创建、编译、链接...(代码略,你懂的)
顶点数据就三个点:
// 三角形顶点(归一化设备坐标)
GLfloat vertices[] = {
0.0f, 0.5f, 0.0f, // 上
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f // 右下
};
最后绘制:
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);
eglSwapBuffers(display, surface);
注意:在8155上,eglSwapBuffers会触发垂直同步。如果你发现帧率被锁在60fps,那是正常的。想跑更高帧率?可以尝试eglSwapInterval(0)关闭垂直同步,但小心画面撕裂。
3.4 在8155上跑起来的注意事项
嗯,这里有几个坑,我替你们踩过了:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| EGL版本不匹配 | eglCreateContext返回EGL_NO_CONTEXT | 检查EGL_RENDERABLE_TYPE是否设为EGL_OPENGL_ES2_BIT |
| 着色器编译失败 | 三角形不显示,控制台无输出 | 调用glGetShaderInfoLog查看错误信息 |
| 顶点数据错误 | 三角形变形或消失 | 检查顶点坐标是否在[-1,1]范围内 |
| 窗口尺寸变化 | 三角形被拉伸 | 调用glViewport更新视口 |
我曾经在8155上遇到一个诡异问题:三角形显示正常,但颜色不对。查了半天,发现是片段着色器里用了highp精度,而8155的Adreno 640对highp支持有限制。改成mediump就好了。所以,精度选择不是小事。
调试技巧:在8155上调试OpenGL ES,我推荐用高通自己的Snapdragon Profiler。它能实时看到GPU占用率、着色器指令数、带宽使用情况。比瞎猜强多了。
3.5 从三角形到实际项目
画三角形只是第一步。实际项目中,你会面对成百上千个三角形,还要考虑纹理、光照、阴影。但核心思想不变:
- CPU负责逻辑和提交数据
- GPU负责并行渲染
- 着色器决定最终效果
在8155上做HMI(人机交互界面)时,我经常用OpenGL ES 2.0,因为它兼容性好,性能也够用。如果你需要更高级的效果,比如曲面细分、计算着色器,那就得上OpenGL ES 3.0或3.1。8155的Adreno 640支持到OpenGL ES 3.2,所以不用担心兼容性。
最后说一句:别被“第一个三角形”这个说法骗了。能画三角形,你就能画任何东西。三角形是GPU的原子操作,所有3D模型最终都会被拆成三角形。所以,把这个例子跑通,你就打开了GPU编程的大门。
下一章,咱们聊聊纹理映射和帧缓冲——嗯,那才是真正出效果的地方。