Bind()是否要在constructor中调用?
Bind()是否要在constructor中调用?
Rif[!Question] 我注意到Cherno在indexBuffer和vertexBuffer的构造函数中中都自动调用了Bind(),但是vertexArray和后面的Shader类都没有……这么做是正确的吗,或者有什么思考?
你又提出了一个观察极其细致、直击设计核心的绝佳问题!这背后确实有非常深刻和重要的设计考量。
答案是:Cherno 的做法是完全正确且经过深思熟虑的。 在 VertexBuffer 和 IndexBuffer 的构造函数中自动 Bind(),而在 VertexArray 和 Shader 中不这么做,是基于这些对象在 OpenGL 渲染管线中扮演的不同角色和它们的典型使用模式。
让我们来分析这背后的“为什么”。
Group 1: VertexBuffer 和 IndexBuffer - “数据容器”
这两个类的核心职责是:成为一个特定数据的容器,并负责将这些数据上传到 GPU。
为什么在构造函数中 Bind() 是合理的?
紧密的操作流程: 创建一个 VBO/IBO 的典型流程是:
glGenBuffers->glBindBuffer->glBufferData。这三个操作几乎总是连续、成组出现的。将Bind和BufferData封装在构造函数里,完美地匹配了这个原子性的操作流程。创建即填充 (Creation is Population): 当你创建一个
VertexBuffer对象时,你的意图几乎总是“创建一个 VBO 并立即用某些数据填充它”。构造函数是执行这种“创建并初始化”逻辑的完美场所。临时性的绑定: 对 VBO/IBO 的绑定通常是短暂的。我们绑定它,是为了填充数据,或者是为了将它关联到 VAO。完成这些操作后,我们常常会解绑它(
glBindBuffer(..., 0)),或者绑定下一个 VBO。因此,在构造函数中执行这次“一次性”的绑定,并不会对后续状态造成不良影响。
1 | // 这种模式非常自然 |
Group 2: VertexArray 和 Shader - “状态切换器”
这两个类的核心职责与 VBO/IBO 完全不同。它们不是数据的容器,而是状态的管理者。它们的作用是在渲染循环中被频繁地激活(绑定)和停用(解绑),以切换 GPU 的整体渲染状态。
为什么在构造函数中 Bind() 是不合理/危险的?
创建和使用是分离的:
- 创建 (
new Shader(...)) 通常发生在程序的初始化阶段。你可能会一次性加载所有需要的着色器。 - 使用 (
shader.Bind()) 发生在渲染循环内部,并且是在需要用这个特定着色器绘制某个物体时才调用。 - 如果你在构造函数中就
Bind()了着色器,那么在初始化阶段加载完所有着色器后,最后一个被创建的着色器将保持激活状态,这很可能不是你想要的,并且会污染后续的 OpenGL 状态。
- 创建 (
破坏了状态管理的确定性:
- 想象一下你的初始化代码:
1
2
3
4// 初始化阶段
Shader shaderA("shaderA.glsl"); // 构造时自动Bind,现在shaderA是激活的
Shader shaderB("shaderB.glsl"); // 构造时自动Bind,现在shaderB是激活的
Shader shaderC("shaderC.glsl"); // 构造时自动Bind,现在shaderC是激活的 - 在进入渲染循环之前,哪个着色器是激活的?是
shaderC。这完全是由对象的创建顺序决定的,非常隐晦且危险。如果有人调整了这几行代码的顺序,程序的行为就会在不经意间改变。
- 想象一下你的初始化代码:
违反了使用者的预期:
- 一个程序员在使用
Shader或VertexArray类时,他的心智模型是:Shader myShader(...)-> “我准备好了一个着色器。”myShader.Bind()-> “我现在要使用这个着色器。”
- 如果构造函数自动
Bind(),就打破了这种“准备”和“使用”的清晰分离。它让对象的创建产生了**副作用 (Side Effect)**——即改变了全局的 OpenGL 状态,这是非常糟糕的设计。
- 一个程序员在使用
VertexArray同理:- VAO 的创建和配置通常在初始化时完成。
1
2
3
4
5// 初始化
VertexArray va;
va.Bind(); // 显式绑定,准备配置
va.AddBuffer(vb, layout);
va.Unbind(); // 配置完成,解绑 - 在渲染循环中,你再根据需要
va.Bind()来切换到这个 VAO 的状态。如果在构造时就自动绑定,同样会产生上面提到的所有问题。
- VAO 的创建和配置通常在初始化时完成。
总结:角色决定行为
| 类 | 角色 | 核心操作模式 | 在构造函数中 Bind() 是否合理? |
|---|---|---|---|
VertexBuffer / IndexBuffer |
数据容器 | 创建 -> 绑定 -> 填充 (一次性设置) | 是,这符合其原子性的创建流程。 |
VertexArray / Shader |
状态切换器 | 创建 (准备) -> 在需要时绑定 (激活) -> 使用 -> 解绑 | 否,这会产生意外的副作用,破坏状态管理的确定性。 |
Cherno 的设计完美地体现了“最小意外原则 (Principle of Least Astonishment)”:
- 创建一个
VertexBuffer时,你期望它已经被数据填充好了。 - 创建一个
Shader时,你不期望它会擅自改变当前激活的着色器。
这个看似微小的设计差异,实际上是区分一个仅仅“能用”的封装和一个“好用、安全、可维护”的封装的关键所在。它反映了设计者对 OpenGL 工作流程和良好软件工程实践的深刻理解。