2. OpenGL ES基础回顾
在进入多屏渲染工程之前,有必要把 OpenGL ES 的核心对象与执行链路重新梳理一遍。多屏渲染并不是简单地“多开几个窗口”,它要求同一份渲染逻辑能够被多个 EGL 上下文、多个 Surface 复用,因此对管线阶段、着色器程序、纹理与缓冲区对象的生命周期、以及 EGL 上下文与线程的绑定关系必须有清晰认识。本节从渲染管线出发,串联着色器、纹理、缓冲区对象与 EGL 上下文管理,形成一张可复用的知识地图。
一、渲染管线:从顶点到像素的固定链路
OpenGL ES 的渲染管线是一条由固定阶段与可编程阶段交替组成的流水线。顶点数据从缓冲区对象出发,经过顶点着色器完成坐标变换,再进入图元装配与裁剪,随后由光栅化把图元离散为片元,片元着色器计算颜色,最后经过逐片元操作(深度测试、混合、抖动)写入帧缓冲。理解这条链路的关键在于:可编程阶段只有顶点着色器和片元着色器,其余阶段由驱动按状态配置执行。
在多屏场景中,管线本身不变,变化的是“输出目标”。同一套顶点与片元着色器,可以分别渲染到不同的 Framebuffer Object(FBO)或 EGLSurface 上。因此,管线是逻辑,Surface 是物理出口,二者通过 EGL 上下文解耦。
二、着色器:可编程阶段的语言与编译流程
OpenGL ES 使用 GLSL ES 编写着色器。顶点着色器负责把顶点从模型空间变换到裁剪空间,片元着色器负责计算最终颜色。两者通过 varying(ES 3.0 中为 in/out)传递插值数据。着色器的编译流程是固定的:创建对象、加载源码、编译、检查日志、链接成程序、检查链接日志,最后获取 uniform 与 attribute 位置。
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, &vsrc, NULL);
glCompileShader(vs);
GLint ok = 0;
glGetShaderiv(vs, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[512];
glGetShaderInfoLog(vs, 512, NULL, log);
LOGE("VS compile error: %s", log);
}
GLuint prog = glCreateProgram();
glAttachShader(prog, vs);
glAttachShader(prog, fs);
glLinkProgram(prog);
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
在多屏工程中,常见做法是一套着色器程序被多个上下文共享。但要注意:着色器程序对象本身不能跨上下文直接使用,除非两个上下文通过 eglCreateContext 的 share_context 参数建立了共享关系。共享的是纹理、缓冲区等资源对象,程序对象仍需各自创建或通过共享组管理。
三、纹理与缓冲区对象:资源生命周期与共享
纹理对象和缓冲区对象是渲染数据的载体。纹理通过 glGenTextures、glBindTexture、glTexImage2D 创建并上传像素数据;缓冲区对象通过 glGenBuffers、glBindBuffer、glBufferData 上传顶点或索引数据。两者都属于“命名对象”,其名字只在所属上下文的共享组内有效。
| 对象 | 创建接口 | 典型用途 | 多屏共享注意点 |
|---|---|---|---|
| Texture | glGenTextures / glTexImage2D | 颜色贴图、离屏渲染附件 | 共享组内可直接复用,需同步访问 |
| VBO | glGenBuffers / glBufferData | 顶点位置、法线、UV | 共享组内共享,避免重复上传 |
| IBO | glGenBuffers / glBufferData | 索引绘制 | 与 VBO 同组管理 |
| FBO | glGenFramebuffers | 离屏渲染目标 | 通常每上下文独立,或共享纹理附件 |
| Shader Program | glCreateProgram / glLinkProgram | 渲染逻辑 | 不跨上下文共享,需各自创建 |
四、EGL 上下文管理:多屏渲染的调度核心
EGL 是 OpenGL ES 与本地窗口系统之间的桥梁。它负责创建 Context、Surface 与 Config,并通过 eglMakeCurrent 把上下文绑定到当前线程。多屏渲染的本质,就是多个 Context 与多个 Surface 的配对与切换。
典型流程如下:初始化 eglGetDisplay、eglInitialize、eglChooseConfig;为每个屏幕创建 EGLSurface;创建 Context 时通过 share_context 建立共享组;渲染线程中调用 eglMakeCurrent 切换上下文,执行绘制,再 eglSwapBuffers 提交。若多个线程同时渲染,每个线程必须拥有自己的 current context,且同一 Context 不能同时绑定到多个线程。
常见误区:把 eglMakeCurrent 当成“全局切换”,忽略线程绑定关系。实际上 current context 是线程局部的,跨线程使用同一 Context 会导致未定义行为。多屏渲染中推荐“一线程一 Context”或“主线程串行切换”,并配合 eglReleaseThread 清理。
实践建议:共享组内资源(纹理、VBO)只创建一次,通过 share_context 让多个 Context 可见;FBO 与程序对象按上下文独立管理;每次切换 Context 后重新绑定必要的状态,避免状态泄漏。
五、案例复盘:双屏同源渲染的上下文布局
假设一块设备连接两个 HDMI 屏幕,需要把同一场景分别渲染到两个屏幕。推荐布局是:创建一个共享 Context 作为“资源上下文”,负责创建纹理与 VBO;再为每个屏幕创建一个渲染 Context,共享该资源上下文。渲染时,线程 A 绑定 Context 1 渲染屏 1,线程 B 绑定 Context 2 渲染屏 2,两者共享纹理与顶点数据,但各自维护 FBO 与程序状态。这样既避免了资源重复上传,又保证了上下文隔离。