GPU多工位机床:OpenGL状态机解析
2026/9/9 18:42:08 网站建设 项目流程

一个类比:GPU 是一台"多工位机床"

想象 OpenGL 上下文是一台大型加工机床,机床上有很多固定的工位(插槽)

工位上的牌子(Target 名字)是刻死的,不能改:

┌───────────────────────────────────────────┐ │ GPU 加工机床 │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 顶点数据工位 │ │ 索引数据工位 │ │ │ │ ARRAY_BUFFER│ │ELEMENT_ARRAY│ │ │ │ [ 空 ] │ │ [ 空 ] │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 纹理工位0 │ │ 纹理工位1 │ │ │ │ TEXTURE0 │ │ TEXTURE1 │ │ │ │ [ 空 ] │ │ [ 空 ] │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ │ │ │ Shader工位 │ │ │ │ [ 空 ] │ │ │ └─────────────┘ │ │ │ │ [ 加工按钮:glDrawXXX ] │ │ │ └───────────────────────────────────────────┘

现在你有一堆原料(GL 对象),放在旁边的仓库里:

仓库: 📦 顶点数据 A(id=1) 📦 顶点数据 B(id=2) 📦 索引数据(id=3) 🖼️ 纹理"墙面"(id=10) 🖼️ 纹理"法线图"(id=11) 📜 Shader "标准着色"(id=100)

加工一个物体的流程

假设你要画一堵墙。整个流程是:

步骤 1:往工位上"摆原料"

// 把"顶点数据 A" 摆到"顶点数据工位"上glBindBuffer(GL_ARRAY_BUFFER,1);

机床状态:

┌─────────────┐ │ 顶点数据工位 │ │ ARRAY_BUFFER│ │ 📦 id=1 │ ← 摆上了 └─────────────┘

继续摆:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,3);// 索引数据摆到索引工位glActiveTexture(GL_TEXTURE0);// "我要操作 0 号纹理工位"glBindTexture(GL_TEXTURE_2D,10);// 摆"墙面纹理"到 0 号工位glActiveTexture(GL_TEXTURE1);// "我要操作 1 号纹理工位"glBindTexture(GL_TEXTURE_2D,11);// 摆"法线图"到 1 号工位glUseProgram(100);// 摆 Shader

现在机床满员:

┌─────────────────────────────────────┐ │ GPU 加工机床 │ │ │ │ 顶点数据工位: 📦 id=1 │ │ 索引数据工位: 📦 id=3 │ │ 纹理工位 0 : 🖼️ 墙面(id=10) │ │ 纹理工位 1 : 🖼️ 法线(id=11) │ │ Shader 工位: 📜 id=100 │ │ │ │ [ 加工按钮 ] │ └─────────────────────────────────────┘

步骤 2:按下"加工按钮"

glDrawElements(GL_TRIANGLES,...);

机床不需要你告诉它用哪些原料。它就用当前所有工位上摆着的东西开始加工:

  • 从"顶点数据工位"读顶点
  • 从"索引数据工位"读索引顺序
  • 用"Shader 工位"上的 shader 处理
  • Shader 里的sampler2D从"纹理工位"里取纹理
  • 加工完成 → 一堵墙画好了 ✅

关键理解 1:工位牌子是固定的,不能同时摆两个

一个工位只能摆一个东西。你摆新的,旧的就下来了:

glBindBuffer(GL_ARRAY_BUFFER,1);// 工位摆着 id=1glBindBuffer(GL_ARRAY_BUFFER,2);// 现在工位是 id=2,id=1 被顶下去了

这就是 OpenGL 状态机的核心。工位是全局共享的,谁最后 Bind 谁生效。


关键理解 2:同一个"原料"可以摆到不同工位

一个 Buffer 对象(比如 id=1),并不写死用途。它可以:

// 场景 A:当作顶点数据用glBindBuffer(GL_ARRAY_BUFFER,1);// 场景 B:当作 Uniform 数据用(虽然实际不会这么干,但语法允许)glBindBuffer(GL_UNIFORM_BUFFER,1);

Buffer 对象本身只是一块内存,怎么用取决于你摆在哪个工位。这是 OpenGL 的灵活性所在。


关键理解 3:纹理工位很特殊,是"两层结构"

其他工位是"一级"的:一个牌子对应一个位置。

但纹理工位是多个编号 + 每个编号里还有子分类:

┌─────────────────────────────────────────┐ │ 纹理工位组 │ │ │ │ ┌── 工位 0 (GL_TEXTURE0) ──┐ │ │ │ ├ TEXTURE_2D: 🖼️ │ │ │ │ ├ TEXTURE_3D: [ 空 ] │ │ │ │ └ TEXTURE_CUBE: [ 空 ] │ │ │ └──────────────────────────┘ │ │ │ │ ┌── 工位 1 (GL_TEXTURE1) ──┐ │ │ │ ├ TEXTURE_2D: 🖼️ │ │ │ │ ├ TEXTURE_3D: [ 空 ] │ │ │ │ └ TEXTURE_CUBE: [ 空 ] │ │ │ └──────────────────────────┘ │ │ │ │ ┌── 工位 2 (GL_TEXTURE2) ──┐ │ │ ... │ └─────────────────────────────────────────┘ [当前操作指针] → 指向工位 0

glActiveTexture(GL_TEXTURE1)的意思是:“我下一步操作的是 1 号工位”。它像一个指针,先指过去,再操作。

glActiveTexture(GL_TEXTURE1);// 把"操作指针"指向 1 号工位glBindTexture(GL_TEXTURE_2D,11);// 把纹理摆到"1 号工位的 2D 子格子"

如果忘了glActiveTexture,操作指针停留在原处,新纹理就摆错工位了。这是超级常见的 bug。


关键理解 4:Shader 里的 sampler 是怎么找到纹理的?

这里是最绕的地方,我用图讲清楚。

Shader 里写:

uniform sampler2D u_diffuse; uniform sampler2D u_normal;

这两个 sampler并不直接引用纹理对象。它们各自持有一个编号,指向"去哪个纹理工位取纹理"。

CPU 端设置这个编号:

glUniform1i(location_diffuse,0);// u_diffuse:去 0 号工位取glUniform1i(location_normal,1);// u_normal :去 1 号工位取

流程图:

Shader 里: u_diffuse ──→ (整数 0) ──┐ │ u_normal ──→ (整数 1) ──┼──→ 去对应工位取纹理 │ ↓ ┌───────────────────────┐ │ 纹理工位 0: 🖼️墙面 │ ← u_diffuse 采样这里 │ 纹理工位 1: 🖼️法线 │ ← u_normal 采样这里 └───────────────────────┘

关键理解:

  • Shader 里的sampler2D是"一个门牌号"
  • CPU 通过glUniform1i告诉它"你的门牌号是 0"
  • 采样时,GPU 说:“哦,你门牌号 0,那我去 0 号工位取纹理”

一个完整场景演示:画两个物体

假设要连续画两个物体:一堵墙 + 一个桌子。

画墙:

// 摆原料glBindBuffer(GL_ARRAY_BUFFER,wall_vbo);glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D,wall_tex);glUseProgram(shader_A);// 加工glDrawElements(...);

机床状态:

顶点工位: 📦 wall_vbo 纹理工位0: 🖼️ wall_tex Shader: 📜 shader_A

画桌子:

情况 1:换所有原料

glBindBuffer(GL_ARRAY_BUFFER,table_vbo);// 换顶点glBindTexture(GL_TEXTURE_2D,table_tex);// 换纹理(注意,还在 Unit 0)glUseProgram(shader_B);// 换 shaderglDrawElements(...);

换的每一样都是一次"工位切换",都有开销

情况 2:只换一点点(如果桌子用同样的 shader、纹理,只换顶点)

glBindBuffer(GL_ARRAY_BUFFER,table_vbo);// 只换顶点glDrawElements(...);// 直接画

只切换一个工位,开销更小


现在,你能推导出很多 Unity 的性能规律了

规律 1:相同材质的物体连续画,性能最好

为什么?因为不需要切换 Shader 工位 + 纹理工位。这就是 Unity 排序渲染队列的原因。

规律 2:合批的本质是"共用工位"

静态合批:把多个物体的顶点合并到一个大 VBO 里 → 只需摆一次"顶点工位"

GPU Instancing:所有实例用同一个 VBO + 同一个 Shader → 工位完全不切,只通过特殊机制传"每个实例不同的数据"

规律 3:SetPass Call 数量比 DrawCall 数量更能反映性能

为什么?因为 SetPass = “换 Shader / 换关键状态”。而"按加工按钮"(DrawCall)本身很便宜,贵的是换工位

规律 4:切换 RenderTarget 特别贵

为什么?因为切换 Framebuffer 工位,在移动端(TBDR)会触发 Tile 数据的 Store/Load(还记得我之前讲的 TBDR 吗?)。这是跨越两层元知识的推导。


一句话总结

OpenGL 上下文就是一台带很多工位的机床。你的所有工作分两种:“往工位上摆原料” 和 “按加工按钮”。摆原料是慢的,加工按钮本身很快。所以性能优化的核心 = 少摆几次原料,多按几次按钮。


最后,给你一个"内心自检"

如果你能自己回答下面几个问题,说明彻底懂了:

  1. 为什么glActiveTextureglBindTexture分两步调用,不能合成一个?
  2. 一个 Buffer 对象 id=5,同时glBindBuffer(GL_ARRAY_BUFFER, 5)glBindBuffer(GL_UNIFORM_BUFFER, 5),会冲突吗?
  3. Shader 里两个sampler2D都通过glUniform1i设成了 0,会怎样?
  4. 为什么 VAO 能"打包一堆状态"?它到底记录了哪些工位?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询