第 8 讲:OpenGL 流水线与 GLSL
前七讲回答“该算什么”,这一讲回答“这些计算在 GPU 流水线里放在哪里”。现代 OpenGL 更像一台状态机加数据管线,而不是一个替你画好物体的高级场景库。
1. 一帧图像经过哪些阶段
应用程序在 CPU 上准备场景、相机、材质和命令,把顶点与索引上传到 GPU。随后:
- 顶点着色器逐顶点运行,最少要给出裁剪坐标。
- 图元装配把顶点组成点、线或三角形。
- 裁剪与光栅化把图元变成片元,并插值顶点输出。
- 片元着色器逐候选片元计算颜色等输出。
- 深度、模板与混合测试决定最终怎样写入 Framebuffer。
OpenGL 1.x 的固定功能流水线替用户决定很多步骤;OpenGL 2.0 引入可编程着色器,3.1 起核心模式移除旧固定功能。课件关注的是现代可编程管线。
2. 着色器怎样通信
GLSL 是类 C 语言,常见类型有 float、int、bool、vec2/3/4 和 mat2/3/4。矩阵按 OpenGL 约定存储,应用端与着色器端的行列约定必须一致。
三类变量最重要:
- in:当前阶段的输入。
- out:当前阶段的输出;顶点输出会在图元内部插值后送入片元阶段。
- uniform:一次 draw call 期间对所有调用保持相同,例如 MVP、光源位置和材质参数。
顶点着色器必须写 gl_Position。片元着色器输出颜色,也可显式写深度。向量可用 xyzw 或 rgba 分量名并支持 swizzle。
3. VBO、VAO 与 EBO 分别保存什么
源补充材料把三者区分得很清楚:
- VBO:GPU 缓冲区,真正保存位置、颜色、法线、UV 等顶点数据。
- EBO:保存图元索引,使多个三角形可以复用同一顶点。
- VAO:保存一组顶点属性解释方式和相关缓冲绑定;它不复制 VBO 数据。
典型初始化顺序是:
- 创建并绑定 VAO。
- 创建、绑定 VBO,用 glBufferData 上传数据。
- 如需索引绘制,在 VAO 绑定期间绑定 EBO。
- 用 glVertexAttribPointer 说明属性位置、分量数、类型、步长和偏移。
- 用 glEnableVertexAttribArray 启用属性。
步长是相邻两个同类顶点属性之间的字节距离,偏移是当前属性在单个顶点记录中的起点。二者混淆会让顶点数据像“随机数”。
glBufferData 的 usage 只是给驱动的使用提示:STATIC 表示很少改,DYNAMIC 表示经常改,STREAM 表示几乎每次绘制都变。
4. 着色器程序如何成为可执行对象
顶点和片元着色器分别经历:
- 提供源码。
- 编译并检查日志。
- 附加到 program。
- 链接并检查日志。
- 绘制前 glUseProgram。
链接后,应用必须把顶点属性 location 与 VBO 布局对应起来,并设置 uniform。可在 GLSL 中写 layout location,也可链接后按变量名查询。
5. 一次 draw call 发生了什么
应用绑定 program、VAO、纹理和其他状态,再调用 glDrawArrays 或 glDrawElements。前者按连续顶点顺序组装图元,后者按 EBO 索引取顶点。
OpenGL 是状态机:函数经常作用于“当前绑定的对象”。多物体、多纹理、多着色器切换时,忘记绑定正确的 VAO、program 或 texture,是最常见的上下文错误。调试时应把每次绘制依赖的状态列成清单,而不是只盯着 shader 源码。
6. 纹理如何进入管线
应用创建 Texture Object,上传图像并设置 wrap 与 min/mag filter;再把纹理绑定到某个 texture unit,把 sampler uniform 指向该单元。顶点着色器输出 UV,光栅化器插值后,片元着色器用 texture 采样。
纹理与几何走两条数据流:几何来自 VBO,图像来自 Texture Object,最终在片元着色器汇合。
7. Gouraud 与 Phong 放在哪
- Gouraud:顶点着色器计算光照,输出颜色;片元阶段只接收插值颜色。
- Phong shading:顶点着色器输出位置和法线,片元着色器逐片元计算光照。
后者计算更多,却能保留小高光。两种写法的区别正好展示了“同一算法放在不同流水线阶段”的性能与质量权衡。
把现代 OpenGL 想成三件事就够了:把数据放进 GPU、告诉 GPU 如何解释数据、用 shader 决定每个阶段怎样算。API 调用很多,底层模型并不神秘。