17、OpenGL ES入门:OpenGL ES简介、EGL环境搭建、着色器基础(Vertex/Fragment)、绘制三角形
好,咱们开始进入OpenGL ES的世界。
说实话,很多做Android视频渲染的朋友,一听到OpenGL ES就觉得头大。矩阵、着色器、缓冲区……一堆抽象概念。我当年刚接触时也一样,对着屏幕上的黑框发呆,心想这玩意儿到底怎么把图像画出来的?
但你别怕。今天这一章,咱们只做一件事:在屏幕上画一个三角形。就这一个目标,把OpenGL ES的整个流程跑通。等你亲手看到那个三角形亮起来,后面的路就好走了。
核心要点:OpenGL ES不是魔法,它就是一个状态机。你给它数据,告诉它怎么处理,它就把像素画到屏幕上。就这么简单。
17.1 OpenGL ES是什么?
OpenGL ES,全称是OpenGL for Embedded Systems。说白了,就是OpenGL的“轻量版”,专门给手机、平板这类嵌入式设备用的。
你想想看,桌面版的OpenGL功能太庞大了,手机GPU扛不住。所以Khronos组织(就是制定这个标准的组织)把它裁剪了一下,去掉了一些复杂但少用的功能,保留了核心渲染能力。这就是OpenGL ES。
Android从1.0开始就支持OpenGL ES 1.0,后来发展到2.0、3.0、3.1、3.2。现在主流应用基本都基于OpenGL ES 2.0或3.0开发。我个人建议,新项目直接上OpenGL ES 3.0,因为2.0有些限制(比如不支持多个渲染目标),3.0向下兼容,用起来更顺手。
| 版本 | 主要特性 | 我的建议 |
|---|---|---|
| OpenGL ES 1.0/1.1 | 固定管线,不支持着色器 | 别用了,太老 |
| OpenGL ES 2.0 | 可编程管线,支持着色器 | 兼容性最好,但功能有限 |
| OpenGL ES 3.0 | 新增多目标渲染、纹理压缩等 | 推荐,向下兼容2.0 |
| OpenGL ES 3.1/3.2 | 计算着色器、高级特性 | 新设备支持,看项目需求 |
17.2 EGL环境搭建——让OpenGL ES和Android窗口“握手”
OpenGL ES本身不直接和Android的Surface打交道。它需要一个“中间人”——EGL。
EGL的作用是什么?说白了,就是把OpenGL ES的渲染结果,显示到Android的窗口(Surface)上。没有EGL,你画得再好,用户也看不见。
我在项目中遇到过一个问题:有同事直接在SurfaceView的onDraw里调用OpenGL ES命令,结果画面死活不显示。原因就是没初始化EGL。嗯,这个坑我踩过,所以今天咱们把EGL的流程讲清楚。
EGL环境搭建的四个步骤
- 获取EGL显示连接(Display)——连接EGL和本地窗口系统。
- 初始化EGL——告诉EGL我们要干活了。
- 配置EGL配置(Config)——选择颜色格式、深度缓冲等参数。
- 创建EGL上下文(Context)和渲染表面(Surface)——绑定到当前线程。
来看代码。这是我在项目中常用的EGL初始化模板:
// 1. 获取默认Display
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (eglDisplay == EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("无法获取EGL Display");
}
// 2. 初始化EGL
int[] version = new int[2];
if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
throw new RuntimeException("EGL初始化失败");
}
// 3. 配置Config——我要RGBA8888,还要深度缓冲
int[] attribList = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 16,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, 1, numConfigs, 0);
// 4. 创建Context和Surface
int[] contextAttribs = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, // 使用OpenGL ES 2.0
EGL14.EGL_NONE
};
EGLContext eglContext = EGL14.eglCreateContext(eglDisplay, configs[0], EGL14.EGL_NO_CONTEXT, contextAttribs, 0);
// 假设surface是SurfaceView的Surface
EGLSurface eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, configs[0], surface, null, 0);
// 绑定到当前线程
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
小提示:EGL14是Android 4.2(API 17)引入的,如果你要兼容更老的设备,可以用EGL10。但我个人建议直接用EGL14,代码更清晰。
17.3 着色器基础——Vertex和Fragment
OpenGL ES 2.0之后,引入了可编程管线。什么意思呢?就是你可以自己写小程序,控制GPU怎么处理顶点和像素。这些小程序就叫着色器(Shader)。
着色器有两种:
- 顶点着色器(Vertex Shader):处理每个顶点的位置、颜色等属性。说白了,就是决定“这个点画在哪儿”。
- 片段着色器(Fragment Shader):处理每个像素的颜色。说白了,就是决定“这个点是什么颜色”。
你想想看,一个三角形有三个顶点。顶点着色器处理这三个点,确定它们的位置。然后GPU自动把这三个点连起来,填充中间的所有像素。每个像素的颜色,由片段着色器决定。
一个最简单的顶点着色器
// 顶点着色器
attribute vec4 aPosition; // 输入:顶点位置
void main() {
gl_Position = aPosition; // 输出:标准化设备坐标
}
这里有个关键点:gl_Position是OpenGL ES内置的输出变量,范围是[-1, 1]。也就是说,你传入的坐标必须在-1到1之间,超出这个范围就看不见了。
一个最简单的片段着色器
// 片段着色器
precision mediump float; // 设置精度
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 输出:红色
}
gl_FragColor是片段着色器的输出,RGBA四个分量,范围都是0.0到1.0。上面这个着色器,会把所有像素都画成红色。
注意:着色器代码是字符串,在Java/Kotlin中需要以字符串形式传入。编译时如果出错,OpenGL ES会返回错误日志。我曾经花了一下午调试一个着色器,最后发现是少了个分号……所以,写着色器时一定要仔细检查语法。
17.4 绘制三角形——把一切串起来
好,环境搭好了,着色器写好了,现在咱们来画三角形。
流程是这样的:
- 定义三角形的三个顶点坐标。
- 创建顶点缓冲区(VBO),把数据传给GPU。
- 编译着色器,创建着色器程序。
- 绑定顶点数据到着色器的attribute变量。
- 调用
glDrawArrays绘制。
来看完整代码:
// 1. 顶点数据(标准化设备坐标)
float[] vertices = {
0.0f, 0.5f, 0.0f, // 上顶点
-0.5f, -0.5f, 0.0f, // 左下顶点
0.5f, -0.5f, 0.0f // 右下顶点
};
// 2. 创建VBO
int[] vbo = new int[1];
GLES20.glGenBuffers(1, vbo, 0);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]);
ByteBuffer vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4);
vertexBuffer.order(ByteOrder.nativeOrder());
vertexBuffer.asFloatBuffer().put(vertices);
vertexBuffer.position(0);
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertices.length * 4, vertexBuffer, GLES20.GL_STATIC_DRAW);
// 3. 编译着色器(假设已有compileShader方法)
int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
int program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
GLES20.glUseProgram(program);
// 4. 绑定attribute
int positionHandle = GLES20.glGetAttribLocation(program, "aPosition");
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, 0);
// 5. 绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
看到没?glDrawArrays的第三个参数是3,表示画3个顶点。OpenGL ES会自动把它们连成一个三角形。
关键点:绘制完成后,记得调用eglSwapBuffers把渲染结果显示到屏幕上。否则你画了半天,用户看到的还是黑屏。
17.5 知识体系总览
下面这张图,把咱们今天讲的内容串起来了。你可以看到,从EGL环境到着色器,再到绘制,整个流程是环环相扣的。
这张图里,从上到下就是咱们今天做的所有事情。你跟着这个流程走一遍,三角形就出来了。
17.6 避坑指南
最后,分享几个我踩过的坑:
- 线程问题:OpenGL ES的Context是线程绑定的。你必须在同一个线程里调用所有OpenGL ES命令。我曾经在子线程创建了Context,然后在主线程调用绘制,结果直接崩溃。
- 坐标范围:标准化设备坐标是[-1, 1],超出这个范围就看不见了。如果你画了三角形但屏幕是黑的,先检查坐标。
- 着色器编译错误:调用
glGetShaderInfoLog查看错误日志。我习惯在编译后立即检查,避免问题堆积。 - 忘记SwapBuffers:这是新手最容易犯的错误。画完了不交换缓冲区,用户看到的永远是上一帧的画面。
好了,今天的内容就到这里。你跟着代码敲一遍,看到屏幕上出现那个红色三角形的时候,那种成就感……嗯,我到现在还记得。
公众号:蓝海资料掘金营,微信deep3321