2. OpenGL ES基础

OpenGL ES 是嵌入式与移动平台上的精简版图形 API,它把 3D 场景变成屏幕像素的过程抽象为一条可编程的图形渲染管线。理解这条管线的阶段划分、着色器职责、坐标变换链路以及纹理采样机制,是后续在 Qt 中集成 OpenGL ES 的前提。本节从整体管线出发,逐层拆解顶点着色器、图元装配、光栅化、片段着色器与逐片段操作,并给出坐标系变换与纹理映射的关键参数。

一、图形渲染管线的阶段划分

OpenGL ES 2.0 及以后版本采用可编程管线,核心阶段如下:

  • 顶点着色器(Vertex Shader):每个顶点执行一次,负责把顶点从模型空间变换到裁剪空间,并计算法线、纹理坐标等可传递数据。
  • 图元装配(Primitive Assembly):把顶点按绘制模式组合成点、线或三角形。
  • 光栅化(Rasterization):把图元离散为片段,并插值顶点着色器输出的 varying 变量。
  • 片段着色器(Fragment Shader):每个片段执行一次,计算最终颜色,可进行纹理采样、光照计算与混合准备。
  • 逐片段操作(Per-Fragment Operations):包括裁剪测试、深度测试、模板测试、混合与抖动,最终写入帧缓冲。

其中顶点着色器和片段着色器是开发者必须编写的两个可编程阶段,其余阶段由 GPU 固定功能完成。下图展示了这条管线的数据流与两个着色器的位置关系。

OpenGL ES 可编程渲染管线数据流 顶点数组 位置/法线/UV 顶点着色器 MVP 变换 输出 varying 图元装配 点/线/三角形 光栅化 片段插值 片段着色器 纹理采样 颜色计算 逐片段操作 深度/模板/混合 写入帧缓冲 帧缓冲 Framebuffer 颜色/深度/模板附件 纹理对象 采样器 sampler2D 纹理采样

二、顶点着色器与片段着色器的职责

顶点着色器接收顶点属性(attribute)、统一变量(uniform)和常量,输出裁剪空间坐标 gl_Position,以及可选的 varying 变量。片段着色器接收插值后的 varying,输出 gl_FragColor 或自定义输出变量。两者通过 varying 传递数据,但 varying 在光栅化阶段会被线性插值,因此适合传递纹理坐标、颜色和法线。

// 顶点着色器示例
attribute vec3 aPos;
attribute vec2 aUV;
uniform mat4 uMVP;
varying vec2 vUV;
void main() {
    vUV = aUV;
    gl_Position = uMVP * vec4(aPos, 1.0);
}

// 片段着色器示例
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vUV;
void main() {
    gl_FragColor = texture2D(uTexture, vUV);
}
在 Qt 中,QOpenGLShaderProgram 负责编译和链接着色器,attribute 与 uniform 的位置可通过 program.attributeLocation() 和 uniformLocation() 获取,避免硬编码。

三、坐标系与变换链路

顶点从模型空间到屏幕空间需要经过一系列坐标变换,这是图形学中最容易混淆的部分。完整链路为:模型空间 → 世界空间 → 观察空间 → 裁剪空间 → 归一化设备坐标(NDC)→ 窗口空间。其中模型矩阵、观察矩阵和投影矩阵通常合并为 MVP 矩阵,在顶点着色器中一次性完成。

变换阶段 输入空间 输出空间 典型矩阵
模型变换 模型空间 世界空间 Model Matrix
观察变换 世界空间 观察空间 View Matrix
投影变换 观察空间 裁剪空间 Projection Matrix
透视除法 裁剪空间 NDC GPU 自动完成
视口变换 NDC 窗口空间 glViewport 参数

在 Qt 中,QMatrix4x4 提供了 perspective()、lookAt()、translate()、rotate() 等便捷方法,可以快速构建 MVP 矩阵。注意 OpenGL ES 的 NDC 范围是 [-1, 1],深度范围默认也是 [-1, 1],而 Qt 的 QMatrix4x4::perspective() 默认生成符合 OpenGL 约定的矩阵。

四、纹理映射核心概念

纹理映射是把二维图像贴到三维表面的过程。核心步骤包括:创建纹理对象、绑定纹理、设置过滤与环绕参数、上传像素数据、在片段着色器中采样。纹理坐标通常记为 (s, t) 或 (u, v),范围是 [0, 1],原点位置取决于图像加载方式。

  • 过滤方式:GL_NEAREST 适合像素风,GL_LINEAR 适合平滑缩放;Mipmap 可配合 GL_LINEAR_MIPMAP_LINEAR 使用。
  • 环绕方式:GL_REPEAT 平铺,GL_CLAMP_TO_EDGE 边缘拉伸,GL_MIRRORED_REPEAT 镜像重复。
  • 纹理单元:通过 glActiveTexture 激活,uniform1i 指定采样器对应的单元编号。
  • 像素对齐:glPixelStorei(GL_UNPACK_ALIGNMENT, 1) 可避免非 4 字节行宽导致的图像错位。
常见误区:在 Qt 中使用 QImage 加载纹理时,若未调用 mirror() 或 convertToFormat(),图像可能上下颠倒或通道顺序错误。OpenGL 纹理原点在左下角,而 QImage 原点在左上角,需要手动翻转或调整纹理坐标。

五、案例与排障复盘

假设在 Qt 中绘制一个带纹理的旋转立方体,但屏幕全黑。可按以下顺序排查:

  1. 检查着色器编译日志,确认 attribute 和 uniform 是否被优化掉。
  2. 确认 MVP 矩阵是否已正确传入,尤其是投影矩阵的远近平面是否合理。
  3. 检查纹理是否成功绑定,采样器单元编号是否与 uniform 一致。
  4. 确认深度测试是否开启,glEnable(GL_DEPTH_TEST) 是否在绘制前调用。
  5. 检查顶点数据是否上传到 VBO,glVertexAttribPointer 的偏移与步长是否正确。

通过这条管线视角,可以把“黑屏”问题定位到具体阶段:是顶点没进裁剪空间,还是片段颜色被深度测试丢弃,抑或纹理采样返回了全黑。掌握每个阶段的输入输出与状态开关,才能在 Qt 与 OpenGL ES 集成时快速定位问题。