OpenGL 的缓冲区是如何工作的?

Posted

技术标签:

【中文标题】OpenGL 的缓冲区是如何工作的?【英文标题】:How do OpenGL's buffers work? 【发布时间】:2013-12-11 11:31:52 【问题描述】:

我不明白 OpenGL 的缓冲区是如何工作的。我通过 OpenGL Redbook 8th edition 学习 OpenGL。 例如,我有一个位置数组、一个颜色数组和一个索引数组:

static const GLfloat strip_position[] =
    
        -4.0f,  0.0f, -1.0f, 1.0f,  //0
        -3.5f, -1.0f, -1.0f, 1.0f,  //1
        -3.0f,  0.0f, -1.0f, 1.0f,  //2
        -2.5f, -1.0f, -1.0f, 1.0f,  //3
        -2.0f,  0.0f, -1.0f, 1.0f,  //4
        -1.5f, -1.0f, -1.0f, 1.0f,  //5
        -1.0f,  0.0f, -1.0f, 1.0f,  //6
        -0.5f, -1.0f, -1.0f, 1.0f,  //7
         0.0f,  0.0f, -1.0f, 1.0f   //8
    ;
static const GLfloat strip_colors[] =
    
        1.0f, 1.0f, 1.0f, 1.0f,
        1.0f, 1.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 1.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 1.0f, 0.0f, 1.0f,
    ;

static const GLushort strip_indices[] =

    0, 1, 2, 3, 4, 5, 6, 7, 8
;

好。那我创建顶点数组对象如下:

    GLuint vao[1]; // vertex array object
    glGenVertexArrays(1, vao);
    glBindVertexArray(vao[0]);

在我的理解中,第一个参数 (GLsizei n) 是位置数组的数量(或我的对象的一个​​顶点的坐标)。 然后我创建Element Array Buffer如下:

GLuint ebo[1]; // element buffer object
glGenBuffers(1, ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]);
glBufferData(
             GL_ELEMENT_ARRAY_BUFFER, 
             sizeof(strip_indices), 
             strip_indices, 
             GL_STATIC_DRAW
);

然后我创建Vertex Buffer Object如下:

GLuint vbo[1]; // vertex buffer object
glGenBuffers(1, vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
    glBufferData(
                 GL_ARRAY_BUFFER, 
                 sizeof(strip_position) + sizeof(strip_colors), 
                 NULL, 
                 GL_STATIC_DRAW
    );
    glBufferSubData(
                    GL_ARRAY_BUFFER, 
                    0,                      //offset
                    sizeof(strip_position), //size date
                    strip_position          //data
    );
    glBufferSubData(
                    GL_ARRAY_BUFFER, 
                    sizeof(strip_position), //offset
                    sizeof(strip_colors),   //size data
                    strip_colors               //data
    );

接下来我调用glVertexAttribPointer()如下:

glVertexAttribPointer(
                      0,         //index
                      4,         //size
                      GL_FLOAT,  //type
                      GL_FALSE,  //normalized
                      0,         //stride
                      NULL       //pointer
);
glVertexAttribPointer(
                      1, 
                      4, 
                      GL_FLOAT, 
                      GL_FALSE, 
                      0, 
                      (const GLvoid*)sizeof(strip_position)
);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);

它有什么作用?(glVertexAttribPointer()glEnableVertexAttribArray()) 好的。我完成了我的数据的初始化。现在我可以画出如下:

glBindVertexArray(vao[0]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]);
glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, NULL);

OpenGL 是如何理解的,需要使用哪个缓冲区以及它在哪里?单词“绑定”是指关系?即某物与某物绑定?如果我想显示一个两个对象,我该怎么办? 例如,我有两个位置数组,一个两个位置数组和一个两个索引数组?

static const GLfloat TWOstrip_colors[] =

    1.0f, 1.0f, 1.0f, 1.0f,
    1.0f, 1.0f, 0.0f, 1.0f,
    1.0f, 0.0f, 1.0f, 1.0f,
    1.0f, 0.0f, 0.0f, 1.0f,
    0.0f, 1.0f, 1.0f, 1.0f,
    0.0f, 1.0f, 0.0f, 1.0f,
    0.0f, 0.0f, 1.0f, 1.0f,
    1.0f, 0.0f, 1.0f, 1.0f,
    1.0f, 1.0f, 0.0f, 1.0f
;
static const GLfloat TWOstrip_colors[] =
    
        1.0f, 1.0f, 1.0f, 1.0f,
        1.0f, 1.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 1.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 1.0f, 0.0f, 1.0f,
    ;

static const GLushort TWOstrip_indices[] =

    0, 1, 2, 3, 4, 5, 6, 7, 8
;

这是怎么做到的?

【问题讨论】:

【参考方案1】:

OpenGL 有所谓的对象 的概念。这些不是模型或几何对象,而是内部状态的封装。如果你熟悉面向对象的编程,C++ STL OpenGL 对象可以被认为是类实例。

调用glGenBuffers(count, out_names) 可以粗略地解释为类似

std::map<GLuint, openglobject*> bufferobjects;

glGenBuffers(GLuint count, std::vector<GLuint> *out_names)

    out_names->resize(count);
    for(int i=0; i < count; i++) 
        GLuint name = get_next_free_handle_ID();
        bufferobjects[name] = NULL;
        out_names.set(i, name);
    

所以它的作用是,它保留一个句柄 ID(OpenGL 称它们为名称)并在句柄和缓冲区对象实例指针之间的内部映射中为其分配一个槽。

调用glBindBuffer实际上创建了缓冲区对象,类似的东西

glBindBuffer(GLenum target, GLuint name)


    openglobject *objinstance = NULL;

    if( name != 0 ) 
        if( !bufferobjects.has_key(name) ) 
            push_openglerror( INVALID_NAME );
            return;
        

        objinstance = bufferobjects[name];

        if( NULL == bufferobjects[name] ) 
            switch(target) 
            case GL_ARRAY_BUFFER:
                objinstance = new OpenGLArrayBuffer; break;

            case GL_ELEMENT_ARRAY_BUFFER:
                objinstance = new OpenGLElementArrayBuffer; break;

            /* ... and so on */    

            default:
                push_openglerror( INVALID_TARGET ); return;
            

            bufferobjects[name] = objinstance;

            
        
    

    if( objinstance != NULL && target_of(objinstance) != target ) 
         opengl_pusherror( INVALID_TARGET );
    

    switch( target ) 
    case GL_ARRAY_BUFFER:
         /* this would be a static function of the subclass setting 
          * global singleton instance pointer
          */
         OpenGLArrayBuffer::make_current(objinstance);
         break;

         /* ... and so on */
    

我想你可以看到这是怎么回事:缓冲区target 指定了你正在使用的实例的子类类型及其静态成员。

glBufferData 然后实际分配特定对象的内存,并可以使用传递给它的缓冲区的内容对其进行初始化。 glBufferSubData 只是将数据复制到内部存储中。

缓冲区对象就这么多了(其中有好几种)。


另一部分是顶点数组对象。这些是特殊的 OpenGL 对象,它们在 顶点属性 之间创建关联,这些对象是根据属性索引传递给着色器的每个顶点数据,以及从中获取此属性的 数组缓冲区 对象。数据是需要的。

当你调用 glGenVertexArray 时会发生这样的事情:

std::map<GLuint, openglobject*> vertexarrayobjects;

glGenVertexArrays(GLuint count, std::vector<GLuint> *out_names)

    out_names->resize(count);
    for(int i=0; i < count; i++) 
        GLuint name = get_next_free_handle_ID();
        vertexarrayrobjects[name] = NULL;
        out_names.set(i, name);
    

看起来很熟悉,不是吗?唯一的区别是使用了不同的映射结构。 glBindVertexArray 分配实例等等。

现在调用glEnableVertexAttributeglVertexAttribPointer 可以认为如下:

glEnableVertexAttribute(GLuint idx)

    ((OpenGLVertexArrayObject*)currentvertexarray)->add_attribute(idx);


glVertexAttribPointer(GLuint idx, ..., void *ptr)

    ((OpenGLVertexArrayObject*)currentvertexarray)->bind_attribute(
          idx,
          OpenGLArrayBuffer::get_current(),
          (off_t)ptr );

好的,最后一点需要解释一下。您将指针传递给 glVertexAttribPointer 是 OpenGL-1.1 的遗留问题,其中没有 OpenGL 缓冲区对象,而是直接指向程序的内存。然后引入了缓冲区对象,这些对象在绑定时不需要指针而是字节大小的偏移量。因此,OpenGL 开发人员走上了肮脏的道路,只是对编译器撒了谎。 I did explain the details in my answer to the question "What is the result of NULL + int?"

请注意,OpenGL-4 引入了一个新的、更强大、更灵活的 API 来创建 VAO 属性 ←→ VBO 绑定。

【讨论】:

看到您对此没有更多赞成票,有点失望,超级可靠的答案。【参考方案2】:

glBindBuffer(target, id) 设置的每个目标总是有一个“当前缓冲区”,大多数缓冲区操作都知道要对其进行操作。

openGL 使用glEnableVertexAttribArray 来知道它应该寻找哪些属性,如果不调用,那么openGL 将不会使用数据。

glVertexAttribPointer 告诉 openGL 在当前绑定的GL_ARRAY_BUFFER 中必须找到当前顶点数组的属性。在您的示例中:(假设vbo[0] 仍绑定到GL_ARRAY_BUFFER

    索引0 的属性在vbo[0] 中找到,4 floats 每个顶点tightly packed 并从0 开始 索引1 的属性在vbo[0] 中找到,4 floats 每个顶点tightly packed 并从sizeof(strip_position) 开始

这些绑定在 glBindBuffer 调用中持续存在,因此如果您想重新绑定,则需要绑定另一个缓冲区调用 glVertexAttribPointer,然后您可以再次取消绑定

我建议您始终使用 0 缓冲区调用 glBindBuffer,以便 openGL 知道您不想再使用当前缓冲区并避免奇怪的行为

要创建第二个对象,您可以在每次切换对象时重新填充各种缓冲区

或者您可以创建 2 组缓冲区:

GLuint vao[2]; // vertex array object
glGenVertexArrays(2, vao);
glBindVertexArray(vao[0]);

GLuint ebo[2]; // element buffer object
glGenBuffers(2, ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]);
glBufferData(
             GL_ELEMENT_ARRAY_BUFFER, 
             sizeof(strip_indices), 
             strip_indices, 
             GL_STATIC_DRAW
);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[1]);
glBufferData(
             GL_ELEMENT_ARRAY_BUFFER, 
             sizeof(strip_indices), 
             TWO_strip_indices, 
             GL_STATIC_DRAW
);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

GLuint vbo[2]; // vertex buffer object
glGenBuffers(2, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(
             GL_ARRAY_BUFFER, 
             sizeof(strip_position) + sizeof(strip_colors), 
             NULL, 
             GL_STATIC_DRAW
);
glBufferSubData(
                GL_ARRAY_BUFFER, 
                0,                      //offset
                sizeof(strip_position), //size date
                strip_position          //data
);
glBufferSubData(
                GL_ARRAY_BUFFER, 
                sizeof(strip_position), //offset
                sizeof(strip_colors),   //size data
                strip_colors               //data
);
//fill other buffer (assuming the first TWOstrip_colors was actually TWOstrip_position
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(
             GL_ARRAY_BUFFER, 
             sizeof(TWOstrip_position) + sizeof(TWOstrip_colors), 
             NULL, 
             GL_STATIC_DRAW
);
glBufferSubData(
                GL_ARRAY_BUFFER, 
                0,                      //offset
                sizeof(TWOstrip_position), //size date
                strip_position          //data
);
glBufferSubData(
                GL_ARRAY_BUFFER, 
                sizeof(TWOstrip_position), //offset
                sizeof(TWOstrip_colors),   //size data
                strip_colors               //data
);
glBindBuffer(GL_ARRAY_BUFFER, 0);


glBindVertexArray(vao[0]);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0])
glVertexAttribPointer(
                      0,         //index
                      4,         //size
                      GL_FLOAT,  //type
                      GL_FALSE,  //normalized
                      0,         //stride
                      NULL       //pointer
);
glVertexAttribPointer(
                      1, 
                      4, 
                      GL_FLOAT, 
                      GL_FALSE, 
                      0, 
                      (const GLvoid*)sizeof(strip_position)
);

glBindVertexArray(vao[1]);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(
                      0,         //index
                      4,         //size
                      GL_FLOAT,  //type
                      GL_FALSE,  //normalized
                      0,         //stride
                      NULL       //pointer
);
glVertexAttribPointer(
                      1, 
                      4, 
                      GL_FLOAT, 
                      GL_FALSE, 
                      0, 
                      (const GLvoid*)sizeof(TWOstrip_position)
);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

然后绘制:

glEnableVertexAttribArray(0); 
glEnableVertexAttribArray(1);

glBindVertexArray(vao[0]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]);
glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, NULL);

glBindVertexArray(vao[1]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[1]);
glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, NULL);

【讨论】:

以上是关于OpenGL 的缓冲区是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

实现 pyglet 会破坏我曾经工作的帧缓冲区 OpenGL 代码

如何在 1D 缓冲区中生成 2D 纹理并将其加载到 OpenGL 中?

来自 glClear() 的 OpenGL 帧缓冲区错误

如何在 iPhone OpenGL ES 2.0 中在渲染和呈现帧缓冲区之间切换?

我可以设置 openGL 缓冲区以按照与 OBJ 文件类似的原理工作吗?

VBO/FBO/DisplayLists 如何在 Haskell 的 OpenGL 绑定中工作?