2. OpenGL ES基础回顾

在进入多屏渲染工程之前,有必要把 OpenGL ES 的核心对象与执行链路重新梳理一遍。多屏渲染并不是简单地“多开几个窗口”,它要求同一份渲染逻辑能够被多个 EGL 上下文、多个 Surface 复用,因此对管线阶段、着色器程序、纹理与缓冲区对象的生命周期、以及 EGL 上下文与线程的绑定关系必须有清晰认识。本节从渲染管线出发,串联着色器、纹理、缓冲区对象与 EGL 上下文管理,形成一张可复用的知识地图。

一、渲染管线:从顶点到像素的固定链路

OpenGL ES 的渲染管线是一条由固定阶段与可编程阶段交替组成的流水线。顶点数据从缓冲区对象出发,经过顶点着色器完成坐标变换,再进入图元装配与裁剪,随后由光栅化把图元离散为片元,片元着色器计算颜色,最后经过逐片元操作(深度测试、混合、抖动)写入帧缓冲。理解这条链路的关键在于:可编程阶段只有顶点着色器和片元着色器,其余阶段由驱动按状态配置执行。

在多屏场景中,管线本身不变,变化的是“输出目标”。同一套顶点与片元着色器,可以分别渲染到不同的 Framebuffer Object(FBO)或 EGLSurface 上。因此,管线是逻辑,Surface 是物理出口,二者通过 EGL 上下文解耦。

OpenGL ES 渲染管线与多屏输出关系 顶点缓冲区 VBO / IBO 顶点着色器 可编程 · 坐标变换 图元装配/裁剪 固定功能 光栅化 片元生成 片元着色器 可编程 · 纹理采样 逐片元操作 深度/混合 纹理对象 Texture 采样器 uniform · 多级渐远 · 环绕方式 缓冲区对象 VBO/IBO 顶点属性指针 · 索引绘制 EGL 上下文管理 eglCreateContext · eglMakeCurrent · eglSwapBuffers Context A → 屏1 Context B → 屏2 共享 Context FBO 离屏

二、着色器:可编程阶段的语言与编译流程

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 与程序状态。这样既避免了资源重复上传,又保证了上下文隔离。