取消绑定 WebGL 缓冲区,值得吗?

Unbinding a WebGL buffer, worth it?

在各种来源中,我看到了 'unbinding' 使用后缓冲区的建议,即将其设置为空。我很好奇是否真的需要这个。例如

var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

// ... buffer related operations ...

gl.bindBuffer(gl.ARRAY_BUFFER, null); // unbinding

一方面,它可能更适合调试,因为您可能会收到更好的错误消息,但是解绑定缓冲区是否会一直显着降低性能?通常建议尽可能减少 WebGL 调用。

人们经常取消绑定缓冲区和其他对象的原因是为了尽量减少 functions/methods 的副作用。这是一个通用的软件开发原则,功能应该只执行它们的广告操作,并且没有任何意想不到的副作用。因此,通常的做法是,如果一个函数绑定了对象,它会在返回之前解除绑定。

让我们看一个典型的例子(没有特定的语言语法)。首先,我们定义一个创建纹理的函数,没有任何定义的内容:

function GLuint createEmptyTexture(int texWidth, int texHeight) {
    GLuint texId;
    glGenTextures(1, &texId);
    glBindTexture(GL_TEXTURE_2D, texId);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, texWidth, texHeight, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, 0);
    return texId;
}

那么,我们再来一个创建贴图的函数。但是这个用缓冲区中的数据填充纹理(我相信 WebGL 还不支持它,但它仍然有助于说明一般原理):

function GLuint createTextureFromBuffer(int texWidth, int texHeight,
                                        GLuint bufferId) {
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, bufferId);
    GLuint texId;
    glGenTextures(1, &texId);
    glBindTexture(GL_TEXTURE_2D, texId);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, texWidth, texHeight, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, 0);
    return texId;
}

现在,我可以调用这些函数,并且一切正常:

GLuint tex1 = createEmptyTexture(width, height);
GLuint tex2 = createTextureFromBuffer(width, height, bufferId);

但是如果我以相反的顺序调用它们,看看会发生什么:

GLuint tex1 = createTextureFromBuffer(width, height, bufferId);
GLuint tex2 = createEmptyTexture(width, height);

这一次,两个纹理都将填充缓冲区内容,因为像素解包缓冲区在第一个函数返回后仍然绑定,因此在调用第二个函数时。

避免这种情况的一种方法是在绑定它的函数末尾取消绑定像素解包缓冲区。并确保不会因为纹理仍然绑定而发生类似问题,它也可以解除绑定:

function GLuint createTextureFromBuffer(int texWidth, int texHeight,
                                        GLuint bufferId) {
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, bufferId);
    GLuint texId;
    glGenTextures(1, &texId);
    glBindTexture(GL_TEXTURE_2D, texId);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, texWidth, texHeight, 0,
                 GL_RGBA, GL_UNSIGNED_BYTE, 0);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    glBindTexture(GL_TEXTURE_2D, 0);
    return texId;
}

使用此实现,使用这两个函数的调用序列将产生相同的结果。

还有其他方法可以解决这个问题。例如:

  1. 每个函数记录其前提条件和副作用,调用者负责在调用具有副作用的函数后进行任何必要的状态更改以满足下一个函数的前提条件。
  2. 每个函数都完全负责设置它的所有状态。在上面的示例中,这意味着 createEmptyTexture() 函数必须取消绑定像素解包缓冲区,因为它依赖于 none 被绑定。

方法 1 的扩展性并不好,在较大的系统中维护起来会很痛苦。方法 2 也不尽如人意,因为 OpenGL 有很多状态,必须在每个函数中设置 所有 相关状态会很冗长且效率低下。

这实际上是一个更大问题的一部分:您如何在模块化软件架构中处理 OpenGL 基于状态的特性?缓冲区绑定只是您需要处理的状态示例之一。这在您自己编写的小程序中通常不是很难处理,但在较大的系统中可能是一个问题点。如果混合来自不同来源(例如不同供应商)的组件,情况会变得更糟。

我认为没有一种方法适用于所有可能的情况。重要的是你选择了一个明确定义的策略,并始终如一地使用它。如何在各种情况下最好地处理这个问题有点超出了这里的答案范围。

虽然取消绑定缓冲区应该相当便宜,但我不喜欢不必要的调用。所以我会尽量避免这些调用,除非你真的觉得你需要它们来为你正在编写的软件执行一个清晰一致的策略。