OpenGL的“绑定”函数背后的概念

59
我正在学习OpenGL,参考的是这个教程。 我的问题与具体的函数或主题无关,而是涉及规范的总体内容。 当看到如下代码时:
glGenBuffers(1, &positionBufferObject);
glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
我对在设置缓冲数据之前和之后调用绑定函数的效用感到困惑。由于我在OpenGL和计算机图形方面缺乏经验,所以这似乎对我来说是多余的。
手册上说:
glBindBuffer让你创建或使用命名的缓冲区对象。将目标设置为GL_ARRAY_BUFFER、GL_ELEMENT_ARRAY_BUFFER、GL_PIXEL_PACK_BUFFER或GL_PIXEL_UNPACK_BUFFER,并将缓冲区设置为新缓冲区对象的名称时,调用glBindBuffer会将缓冲区对象名称绑定到目标上。当缓冲区对象绑定到目标上时,该目标的以前绑定会自动断开连接。
那么,“将某物”绑定到“目标”上的概念/效用是什么?

6
记住一件事情...当有人说"bind"(绑定)时...通常情况下。b=5 就是将 5 绑定到了 b 上。c="asdf" 就是将 "asdf" 绑定到了 c 上。你可以通过“名字”将“内存”绑定为漂亮(或丑陋)的名字。 - n611x007
1
@n611x007,这正是我在寻找的东西。我一直在阅读文档,看到了“bind”这个词,但却不知道它的含义。 - KANJICODER
5个回答

45

在OpenGL中的命令并不是孤立存在的,它们需要存在一个上下文。可以这样理解,背景中存在一个OpenGL对象,这些函数是该对象的方法。

因此,当您调用函数时,它所执行的操作取决于参数和OpenGL的内部状态-即上下文/对象。

bind非常清晰明了,它表示“将此设置为当前X”。然后稍后的函数修改“当前X”(其中X可以是缓冲区,例如)。[更新:]正如您所说,被设置的事物(对象中的属性或“数据成员”)是绑定的第一个参数。因此,GL_ARRAY_BUFFER命名了要设置的特定对象。

回答第二个问题 - 将其设置为0只是清除该值,以免意外地在其他地方进行未计划的更改。


7
如果我正确理解了你的回答,那么我认为GL_ARRAY_BUFFER可以被视为上下文的“数据成员”,我将positionBufferObject分配为数组vertexPositions的句柄。正确吗? - manasij7479
4
"stateful" 这个词可以在这里被使用。 - n611x007
听起来你对"bind"这个词有一些概念,但无法准确地向他人描述它。 - Andy Ray
1
我有一个问题,为什么我们不直接跳过“绑定”步骤,只是将句柄作为额外参数提供给glBufferData呢?换句话说,为什么OpenGL API不接受句柄作为参数? - wdanxna

37

OpenGL技术可能非常晦涩难懂。我知道!我已经用基于OpenGL的3D引擎写了多年(断断续续)。在我的情况下,问题的一部分是,我编写引擎来隐藏底层的3D API(OpenGL),所以一旦我使某些东西工作起来,我再也不看OpenGL代码了。

但是这里有一个技巧可以帮助我的大脑理解“OpenGL方式”。我认为这种思考方式是正确的(但不是全部)。

想想硬件图形/GPU卡。它们在硬件中实现了某些功能。例如,GPU可能只能同时更新(写入)一个纹理。尽管如此,GPU内存中必须包含许多纹理,因为CPU内存和GPU内存之间的传输非常缓慢。

因此,OpenGL API创建了“活动纹理”的概念。然后,当我们调用OpenGL API函数将图像复制到纹理时,我们必须这样做:

1:  generate a texture and assign its identifier to an unsigned integer variable.
2:  bind the texture to the GL_TEXTURE bind point (or some such bind point).
3:  specify the size and format of the texture bound to GL_TEXTURE target.
4:  copy some image we want on the texture to the GL_TEXTURE target.

如果我们想在另一个纹理上绘制图像,我们必须重复相同的过程。

当我们最终准备在显示器上呈现某些东西时,我们的代码需要使我们创建和复制图像的一个或多个纹理成为我们的片段着色器可以访问的。

事实证明,片段着色器可以通过访问多个“纹理单元”(每个纹理单元一个纹理)同时访问多个纹理。因此,我们的代码必须将我们想要可用于纹理单元的纹理绑定到我们的片段着色器期望它们绑定到的纹理单元中。

所以我们必须像这样做:

glActiveTexture (GL_TEXTURE0);
glBindTexture (GL_TEXTURE_2D, mytexture0);

glActiveTexture (GL_TEXTURE1);
glBindTexture (GL_TEXTURE_2D, mytexture1);

glActiveTexture (GL_TEXTURE2);
glBindTexture (GL_TEXTURE_2D, mytexture2);

glActiveTexture (GL_TEXTURE3);
glBindTexture (GL_TEXTURE_2D, mytexture3);

现在,我必须说我喜欢OpenGL的许多原因,但这种方法让我疯狂。这是因为多年来我编写的所有软件看起来都像这样:

error = glSetTexture (GL_TEXTURE0, GL_TEXTURE_2D, mytexture0);
error = glSetTexture (GL_TEXTURE1, GL_TEXTURE_2D, mytexture1);
error = glSetTexture (GL_TEXTURE2, GL_TEXTURE_2D, mytexture2);
error = glSetTexture (GL_TEXTURE3, GL_TEXTURE_2D, mytexture3);

只需指定要附加纹理的纹理单元、指示如何访问纹理的纹理类型以及要附加到纹理单元的纹理ID,就可以避免一遍又一遍地设置所有这些状态。此外,要将图像复制到指定纹理中,也不需要将其绑定为活动纹理,只需提供要复制的纹理ID即可。为什么它需要被绑定呢?

嗯,这是强制OpenGL按照目前的方式进行结构化的关键。因为硬件和软件驱动程序分别执行某些操作,并且由于在哪里执行的操作是一个变量(取决于GPU卡),所以他们需要某种方式来控制复杂性。他们的解决方案实际上是对于每种实体/对象仅有一个绑定点,并要求我们将我们的实体绑定到这些绑定点,然后调用操纵它们的函数。其次,绑定实体是使它们对GPU和在GPU中执行的各种着色器可用的方法。


至少这就是我保持“OpenGL方式”始终清晰的方法。老实说,如果有人真正、真正、真正地了解OpenGL必须以其方式进行结构化的所有原因,我会很高兴看到他们发表自己的回复。我认为这是一个重要的问题和主题,但是其基本原理很少被描述,更不用说以我这样微不足道的大脑可以理解的方式描述了。


2
确实,不过我想澄清一件事情,关于这个声明:1:生成纹理并将其标识符分配给无符号整数变量。虽然标识符是从名称池中取出的(在OpenGL 3.0+中,该池对于每种类型的对象是唯一的,在旧版本中它不必是),但在绑定对象的第一次之前,它实际上并不是一个真正的纹理。我知道这很苛刻,但GL充满了这样的怪癖 :P 另外,“glSetTexture (...)”类型的语法确实存在,以“GL_EXT_direct_state_access”的形式存在,但扩展似乎被永久困在了“EXT”状态中 :( - Andon M. Coleman
1
实际上,它被称为 glBindMultiTextureEXT (...)。因此,OpenGL确实有一个解决愚蠢的绑定-修改语义的方法,但ARB并没有急于正式解决这个问题。好消息是,GL_EXT_direct_state_access已经由大多数平台上的所有主要供应商实现。我们甚至可以通过OpenGL 4.4中的新的无绑定纹理功能GL_ARB_bindless_texture完全避开这个问题,其中您可以直接将句柄传递给GLSL着色器,而不是绑定纹理。 - Andon M. Coleman

23

来自 介绍:什么是OpenGL? 部分:

像结构体这样的复杂聚合物在OpenGL中从不直接暴露。任何这样的构造都藏在API后面。这使得将OpenGL API暴露给非C语言变得更容易,而无需复杂的转换层。

在C ++中,如果您想要一个包含整数、浮点数和字符串的对象,您可以创建并像这样访问它:

struct Object
{
    int count;
    float opacity;
    char *name;
};

//Create the storage for the object.
Object newObject;

//Put data into the object.
newObject.count = 5;
newObject.opacity = 0.4f;
newObject.name = "Some String";
在OpenGL中,您将使用一个API,看起来更像是这样:
//Create the storage for the object
GLuint objectName;
glGenObject(1, &objectName);

//Put data into the object.
glBindObject(GL_MODIFY, objectName);
glObjectParameteri(GL_MODIFY, GL_OBJECT_COUNT, 5);
glObjectParameterf(GL_MODIFY, GL_OBJECT_OPACITY, 0.4f);
glObjectParameters(GL_MODIFY, GL_OBJECT_NAME, "Some String");

当然,这些都不是实际的OpenGL命令。这只是一个展示这种对象接口的示例。

OpenGL拥有所有OpenGL对象的存储空间。由于这个原因,用户只能通过引用访问对象。几乎所有的OpenGL对象都被表示为一个无符号整数(GLuint)。对象是由一个形式为glGen*的函数创建的,其中*是对象的类型。第一个参数是要创建的对象数量,第二个参数是一个GLuint*数组,用于接收新创建的对象名称。

要修改大多数对象,它们必须首先绑定到上下文中。许多对象可以绑定到上下文中的不同位置;这允许以不同的方式使用相同的对象。这些不同的位置称为目标;所有对象都有一个有效目标列表,有些只有一个。在上面的示例中,虚构的目标“GL_MODIFY”是绑定对象名称的位置。

这是大多数OpenGL对象的工作方式,而缓冲区对象是“大多数OpenGL对象”之一。

如果以上内容还不够清晰,本教程在第1章:跟随数据中再次介绍了它们。

void InitializeVertexBuffer()
{
    glGenBuffers(1, &positionBufferObject);

    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}
第一行创建了缓冲对象,将对象句柄存储在全局变量positionBufferObject中。尽管对象现在存在,但它还没有拥有任何内存。这是因为我们还没有为此对象分配任何内存。
glBindBuffer函数将新创建的缓冲对象绑定到GL_ARRAY_BUFFER绑定目标上。如介绍所述,在OpenGL中,通常必须将对象绑定到上下文中才能让它们起作用,而缓冲对象也不例外。
glBufferData函数执行两个操作。它为当前绑定到GL_ARRAY_BUFFER上的缓冲区分配内存,即我们刚刚创建和绑定的缓冲区。我们已经有了一些顶点数据;问题在于它们在我们的内存中而不是OpenGL的内存中。sizeof(vertexPositions)使用C++编译器确定vertexPositions数组的字节大小。然后,我们将这个大小作为要为此缓冲区对象分配的内存大小传递给glBufferData。因此,我们分配了足够的GPU内存来存储我们的顶点数据。
glBufferData执行的另一个操作是将数据从我们的内存数组复制到缓冲对象中。第三个参数控制这个过程。如果该值不为NULL,如本例所示,glBufferData将把指针引用的数据复制到缓冲对象中。调用此函数后,缓冲对象存储的内容与vertexPositions完全相同。
第四个参数是我们将在未来的教程中讨论的内容。
第二个绑定缓冲区调用只是清理工作。通过将缓冲对象0绑定到GL_ARRAY_BUFFER上,我们使先前绑定到该目标的缓冲对象从中解绑。在这种情况下,零很像空指针。这并不是严格必要的,因为稍后对此目标的任何绑定都将简单地解除已经存在的绑定。但是,除非您对渲染有非常严格的控制,否则通常最好取消绑定您绑定的对象。

9
我已经阅读了它。 我提出问题的唯一原因是因为对我来说不太清楚。 特别是像“glBindBuffer函数将新创建的缓冲对象绑定到GL_ARRAY_BUFFER绑定目标”这样的句子实际上意味着什么。 - manasij7479
4
"they must first be bound to the context"的意思是“它们必须先与上下文绑定”。 "不同位置的上下文"中的locations指的是位置,targets指的是目标吗? 我知道进一步详细说明可能会事倍功半,但有些类比并没有伤害。 (如果您是作者,请允许我感谢您的写作,做得很好。尽管我在构建时遇到了很多麻烦。) - manasij7479
5
我知道。 在(上下文,目标,位置)等语境中,我不理解其意义。 我不知道其他人怎么样,但在刚开始学习OpenGL时,那些晦涩难懂的术语让人感到非常沮丧。 - manasij7479
1
只是态度不好而已。这有点粗鲁。 - Michael Bishop
3
如果他很难理解教程中的内容,那么不加重新解释直接在这里复制粘贴也帮助不大。 - wardd
显示剩余4条评论

7
将缓冲区绑定到目标上就像设置全局变量一样。随后的函数调用将操作该全局数据。在OpenGL中,所有这些“全局变量”共同形成一个GL上下文。几乎所有GL函数都从该上下文中读取数据或以某种方式进行修改。
glGenBuffers()调用有点像malloc(),它会分配一个缓冲区;我们使用glBindBuffer()将全局指向它;我们调用一个操作该全局的函数(glBufferData()),然后我们将全局设置为NULL,这样它就不会意外地再次使用glBindBuffer()操作该缓冲区了。

为什么glGenBuffers()需要分配更多的内存?难道我已经存储数据的数组不能被重用吗? - manasij7479
5
glGenBuffers函数不会分配内存,它生成一个句柄来标识缓冲区,您可以使用该句柄稍后绑定缓冲区,使用glBindBuffer函数。 - datenwolf
3
此外,OpenGL缓冲区需要存储在GPU端,因此在CPU端分配的数据“无用”,只能用作传输的源。 - MaKo

2
OpenGL是一个“状态机”,因此OpenGL有几个“绑定目标”,每个目标一次只能绑定一个对象。绑定其他对象将替换当前绑定的对象,从而改变其状态。因此,通过绑定缓冲区,您正在(重新)定义状态机的状态。
作为状态机,您所绑定的任何信息都会影响状态机的下一个输出,在OpenGL中,这是它的下一个绘制调用。一旦完成,您可以绑定新的顶点数据、绑定新的像素数据、绑定新的目标等,然后启动另一个绘制调用。如果您想在屏幕上创建移动的假象,当您满意地绘制了整个场景(这是3D引擎的概念,而不是OpenGL的概念)时,您将翻转帧缓冲区。

1
如果你知道什么是状态机,这段内容会非常有用。否则可能没那么有用。如果你没有意识到自己写了一个状态机,“bind”也不是一个非常有意义的词。 - n611x007
API基本上就是一个被吹嘘的打印机。你只需要将一些数据加载到机器中,就像右键单击纹理并点击“打印”一样,你正在将该数据“绑定”到你的喷墨打印机中,只需将其复制到打印机编程查看的任何存储芯片/位置中,在绑定之后的某个时刻,你发送另一个命令,打印机或OpenGL会将数据运行通过一些预定义的过程,无论是所有的GLSL着色器将渲染放在你的帧缓冲区上,还是你的激光打印机进行颜色分级和棕褐色效果等其他操作,最终呈现在一张纸上。 - Russell Barlow
只是提醒一下,这个类比只有在打印机内置这些效果的情况下才有效。使用某些程序先制作一个带有棕褐色滤镜的新图像,然后再发送它是完全不同的事情,更像是“纹理烘焙”。但是,例如,一台可以在打印机硬件上将原始图像转换为灰度的打印机与“像素着色器”非常相似,实际上在宽泛意义上它就是一个像素着色器。片段和顶点着色器不太适合这个类比,但它们是相同的负载机器、开火/发信号、得到结果的过程。 - Russell Barlow
1
你一直在说“绑定”,但从未解释过绑定是什么。你知道吗? - Andy Ray

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接