9
\$\begingroup\$

In my game engine, a mesh can be made of many submeshes. These submeshes may or may not share vertex data with the rest of the mesh, if they don't they have their own vertex data array.

I've noticed that rendering a mesh with many submeshes that don't share vertices is incredibly slow. This is because each submesh has it's own VAO (and vertex/index VBO pair).

The alternative is to gather the vertices and indices into one pair of VBOs and update them every frame. But will this be much faster?

Is it faster to create one large VAO and update it every frame, or to render many small ones which don't need updating each frame?

\$\endgroup\$

3 Answers 3

10
\$\begingroup\$

I haven't done any benchmarking to compare but there's a relatively unknown feature introduced to core in 4.3 that might be of interest to you. glVertexAttribPointer has somewhat silently been deprecated in favor of a new suite of functions:

In 4.4 there's also a compound version of the latter function, glBindVertexBuffers, that does N consecutive binds at once (not necessarily consecutive vertex attribute locations!) to save you the call overhead.

In the common use case of drawing a lot of objects with the same vertex attributes but different buffers, this vastly simplifies the work you have to do (and potentially is faster!) Instead of keeping around a VAO for each object, you only need one for each different combination of vertex attributes.

Sample code is worth 1000 words, so here goes.

void buildVAO() {
    glBindVertexArray(vao);

    // layout(location = 0) in vec3 position;
    glEnableVertexAttribArray(0);
    glVertexAttribFormat(0, 3, GL_FLOAT, false, 0);
    glVertexAttribBinding(0, 0); // bind to first vertex buffer

    // layout(location = 2) in vec3 normal;
    glEnableVertexAttribArray(2);
    glVertexAttribFormat(2, 3, GL_FLOAT, false, 0);
    glVertexAttribBinding(2, 1); // bind to second vertex buffer

    glBindVertexArray(0);
}

The biggest difference you should notice here is that you no longer need a buffer bound to specify your vertex attributes. Hooray for decoupling!

struct Object {
    GLuint buffers[2]; // first provides position, second provides normal
    size_t offsets[2]; // beginning of vertex data in buffer
    size_t strides[2]; // stride of entire element in buffer

    unsigned int triangles;
};

void draw(unsigned int count, const struct Object *objects) {
    glBindVertexArray(vao);

    for(int i = 0; i < count; ++i) {
        // consecutive vertex buffer binding points are used here
        glBindVertexBuffers(0, 2, objects[i].buffers, objects[i].offsets, objects[i].strides);
        glDrawArrays(GL_TRIANGLES, 0, objects[i].triangles*3);
    }

    glBindVertexArray(0);
}

Here you can see why vertex buffer binding points are useful. Even though they don't contribute to consecutive vertex attribute locations, we can still bind consecutive buffers into those locations.

One last thing to note: if you have a GPU that supports OpenGL 4.5 there is a DSA equivalent for each of these functions.

\$\endgroup\$
2
\$\begingroup\$

The other answers here are good (maybe better) but I wanted to add my 2 cents.

One approach is for objects that have static verts to all share one VBO. Just pile on the data from each objects into one huge array and make it a VBO. Track offsets and sizes for each object within the VBO and then draw each individual one like this:

glDrawElements(GL_TRIANGLES, models[thisItem->model].indSize, GL_UNSIGNED_INT, (void *) (models[thisItem->model].indOffset * sizeof(GLuint)));

The issue there then becomes too many draw calls. But depending on your game it might not matter. I've done many successful projects this way with 60fps on the iPhone. But again these are all objects that have static verts that are not changing per frame.

For objects that DO change per frame, like a waving flag, I update the data each frame and draw without a VBO.

glVertexAttribPointer(glPrograms[currentProgram].glAttributes[A_vec3_vertexPosition], 3, GL_FLOAT, GL_FALSE, sizeof(FlagVertex), &testFlag.m_Vertices[0].pos.x);
glEnableVertexAttribArray(glPrograms[currentProgram].glAttributes[A_vec3_vertexPosition]);

glVertexAttribPointer(glPrograms[currentProgram].glAttributes[A_vec2_vertexTexCoord0], 2, GL_FLOAT, GL_FALSE, sizeof(FlagVertex), &testFlag.m_Vertices[0].texCoords.x);
glEnableVertexAttribArray(glPrograms[currentProgram].glAttributes[A_vec2_vertexTexCoord0]);

glDrawElements(GL_TRIANGLES, testFlag.m_NumIndices, GL_UNSIGNED_INT, testFlag.m_Indices);

For 2d sprites or fonts I used to draw each quad in it's own transform/draw call and got good results because I was sharing a VBO and not having to rebind textures. But another probably better approach is to combine multiple objects in a single mesh but take care of the offset math on the verts in the array instead of doing 1000 transforms. Then you can combine 1000 draw calls into one.

The approach you finally pick really has to do with your situation. And probably is a mix of techniques based on a set of priorities:

  • Reduce draw calls as much as possible by combining objects
  • combine objects into less VBOs
  • batch items that share materials to reduce texture bindings
  • avoid dynamic objects/arryas when you can

But 1 and 4 are opposites. Because combining moving objects means having dynamic arrays. So which guideline to give priority to can often only be decided on with testing.

\$\endgroup\$
1
\$\begingroup\$

Merge materials to use a single material per object as much as possible. Even sharing materials between objects if possible (eg: 1 leaf material, 1 ground material).

You should batch up materials & VBOs to avoid state changes between opaque render calls.

And you should not need to update most VBOs on every frames, the goal of VBOs is to leave the data on the GPU avoiding expensive bus transfers.

With vertex shaders you can do skinning entirely on the GPU avoiding VBO updates.

To limit data transfers for particles you can use 2 VBOs: one with the vertex values that change on every frame (position, alpha) and one with static values (uvs, color) that is if you cannot use geometry shaders to do point sprite expansion.

There are many web pages explaining best VBO & render practices.

( https://developer.apple.com/library/ios/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html )

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .