Issues With Sprite Batching

Started by
7 comments, last by the_Predator 8 years, 4 months ago

Hey guys,

I am taking some first steps using SDL and GLEW and I've hit a snag.

In short, I am trying to batch sprites. I group them by their shaders, and within those groups I group them by texture. I create one VBO for each shader, then stick all of that into a VAO. Drawing that VAO subsequently results in (apparently) only the last VBO being used.
This is the relevant VAO generation code:


void SpriteBatch::CreateVertexArray()
    {
        if (Sprites.empty())
        {
            return;
        }
        Log::Print(V_LOG, "Creating a vertex array with %d sprite(s)", Sprites.size());
        SortSprites();
        if (VertexArrayID == 0)
        {
            glGenVertexArrays(1, &VertexArrayID);
        }

        glBindVertexArray(VertexArrayID);

        CreateRenderBatches();

        glBindVertexArray(0);
    }

    void SpriteBatch::CreateRenderBatches()
    {
        //Gotta iterate through all the sprites, create a vbo batch for every program, then reset vertices and offsets, but keep sprite index.
        VBOBatches.clear();
        int CurrentSprite = 0;
        do
        {
            CreateSingleVBO(CurrentSprite);
        } while (CurrentSprite < Sprites.size());

        bIsDirty = false;
    }

    void SpriteBatch::CreateSingleVBO(int& SpriteIndex)
    {
        
        std::vector<Vertex> Vertices;
        int CurrentVertex = 0;
        GLuint Offset = 0;



       //RenderBatch is a struct with the following:
        //       GLuint Offset;
        //       GLuint NumVerticies;
        //       GLuint TextureID;
        //       GLint UniformTimeID;
        //       GLint UniformProjectionMatrixID;
        //       GLint UniformTextureID;

        std::vector<RenderBatch> SingleVBOBatches;

        GLuint ProgramID = Sprites[SpriteIndex]->GetProgramID();
        GLuint CurrentTextureID = Sprites[SpriteIndex]->GetTextureID();

        SingleVBOBatches.emplace_back(Offset, 6,
            Sprites[SpriteIndex]->GetTextureID(),
            Sprites[SpriteIndex]->GetUniformTimeID(),
            Sprites[SpriteIndex]->GetUniformProjectionMatrixID(),
            Sprites[SpriteIndex]->GetUniformTextureID());

        std::vector<Vertex>& SpriteVerts = Sprites[SpriteIndex]->GetVertexData();
        Vertices.insert(std::end(Vertices), std::begin(SpriteVerts), std::end(SpriteVerts));
        CurrentVertex += 6;
        Offset += 6;

        SpriteIndex++;

        for (; SpriteIndex < Sprites.size(); SpriteIndex++)
        {
            if (Sprites[SpriteIndex]->GetProgramID() != ProgramID)
            {

                //New program, this VBO is ready, create it and move on to next
                break;
            }
            else if(Sprites[SpriteIndex]->GetTextureID() != CurrentTextureID)
            {

                //Same program different texture. Create new batch.
                SingleVBOBatches.emplace_back(Offset, 6,
                    Sprites[SpriteIndex]->GetTextureID(),
                    Sprites[SpriteIndex]->GetUniformTimeID(),
                    Sprites[SpriteIndex]->GetUniformProjectionMatrixID(),
                    Sprites[SpriteIndex]->GetUniformTextureID());
            }
            else
            {

                //Same program same texture, just gonna append the vertices below.
                SingleVBOBatches.back().NumVerticies+=6;
            }

            std::vector<Vertex>& SpriteVerts = Sprites[SpriteIndex]->GetVertexData();
            Vertices.insert(std::end(Vertices), std::begin(SpriteVerts), std::end(SpriteVerts));
            CurrentVertex += 6;
            Offset += 6;

        }

        GLuint NewVBO = 0;
        glGenBuffers(1, &NewVBO);
        glBindBuffer(GL_ARRAY_BUFFER, NewVBO);



         //SNipped out GenerateVBO below.
        Sprites[SpriteIndex - 1]->GenerateVBO(NewVBO);

        glBufferData(GL_ARRAY_BUFFER, Vertices.size() * sizeof(Vertex), Vertices.data(), GL_DYNAMIC_DRAW);
        //glBufferData(GL_ARRAY_BUFFER, Vertices.size() * sizeof(Vertex), nullptr, GL_DYNAMIC_DRAW);
        //glBufferSubData(GL_ARRAY_BUFFER, 0, Vertices.size() * sizeof(Vertex), Vertices.data());
        VBOBatches.emplace_back(NewVBO, ProgramID, SingleVBOBatches);

        Log::Print(V_LOG, "Generated a VBO %d", NewVBO);

        return;

    }


///////////////////////////

    void Sprite::GenerateVBO(const GLuint& InVertexBufferID)
    {
        VertexBufferID = InVertexBufferID;

        for (int i = 0; i < Attributes.size(); i++)
        {
            glEnableVertexAttribArray(i);
        }

        //Position attribute pointer
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, VertexPosition));

        //Color attribute pointer
        glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex), (void*)offsetof(Vertex, VertexColor));

        //UV attribute pointer
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, VertexUV));
    }
Finally, when I try to render it, I get this:

void SpriteBatch::RenderBatches()
    {

        //Log::Print(V_LOG, "Number of batches: %d", VBOBatches.size());
        glBindVertexArray(VertexArrayID);
        
        glActiveTexture(GL_TEXTURE0);

        for (auto& VBO : VBOBatches)
        {
            Log::Print(V_LOG, "Using program %d", VBO.ProgramID);
            glUseProgram(VBO.ProgramID);

            for (auto& Batch : VBO.VBOBatches)
            {
                glBindTexture(GL_TEXTURE_2D, Batch.TextureID);
                SetUniforms(Batch);
                glDrawArrays(GL_TRIANGLES, Batch.Offset, Batch.NumVerticies);
            }

        }

The result is weird. If I have one program but multiple textures, it works just fine. However, the moment I have more programs, it seems as if only the last VBO added is being used, as all my sprites are grouped on wherever the last sprite is positioned.

I'm pretty sure I've omitted something crucial but I can't put my finger on it. Perhaps I am going about this the wrong way?

Advertisement

The VAO knows the bindings from your glVertexAttribPointer as well as glEnableVertexAttribArray to the vertex-buffer, so you need a different VAO per batch. Only the last batch created will be the "active" VAO state, as it will overwrite the previous batches created (the glVertexAttribPointer for the same vertex-attrib indices for each batch will overwrite those made for the last batch).

The first long answer here seems to give good explanations: https://stackoverflow.com/questions/26552642/when-is-what-bound-to-a-vao

Ah, so calls to glEnableVertexAttribArray and glVertexAttribPointer are VAO specific, not VBO specific? If that's the case... multiple programs in the same VAO work only if they are essentially be on the same object / same set of vertices? Thanks a lot for the link, I am still having some issues wrapping my head around all the various concepts an workflows.

Yes, multiple programs with the same VAO must have the same inputs (vertex attributes), and they must have the same (or compatible) indices.

So if one program has "position" bound to attribute index 0 and another has it on attribute index 1, then it will go wrong.

In general I would have one VAO for each combination of (shaderprogram, vertexbuffer, indexbuffer), or you could share between shaders if you know they use the same attributes.

I've been reading the stuff you've posted and some more things, but the weird thing is, my shaders (at least the vertex shaders) DO share the same attributes and attribute indices. It still only sees the last VBO that's been bound. Or actually, more weirdly (after some tinkering), regardless of what order I batch the sprites (and consequently in which order I create and bind the VBOs), the shaders that is COMPILED last will get used.


It still only sees the last VBO that's been bound.

Either way you always need one VAO per VBO. But if the same VBO is drawn twice with different shaders then it can share the VAO if the shaders are compatible.

(Though technically a VAO can use more than one VBO, if you don't use interleaved vertex attributes and get some of them from a different VBO, but a VAO will always have "attribute 0" from exactly the last glVertexAttribPointer used on index 0 with the VAO active, any new call for an index will overwrite the old setting for that index)

So all the different "multiple VBOs per VAO" strickly have to refer to the same vertices / the same object? I think I understand now,could you give me a real world example on how this might be useful, just so I can concretize the idea. Thanks in advance!

I don't really know of one... always use interleaved arrays :) But you can have all positions in one VBO and all texture coordinates in another VBO if you really want to.

It could be good in special cases, for example if you have a vertex that is very large, say 128 bytes per vertex, and part of that vertex, say 8 of the bytes, have to be dynamic and be updated for every vertex every frame, whereas the other 120 bytes are static.

If you have 1000 vertices in your model, then you can either use one VBO and update 128k data each frame, or you can use two VBOs and separate the small part of the vertex that needs to be dynamic into its own VBO, and only update the small VBO with 8k data per frame while keeping 120k static.

Another usage can be instancing (though I've never tried it like this). This link talks about it under instanced arrays https://www.opengl.org/wiki/Vertex_Specification

The dynamic / static vertex example made it crystal clear.

Now to read up on interleaved arrays! Yey for moving forward!

Thanks a ton, this was one of the biggest hurdles I've come across so far.

Edit:

Oh, I already do this with my vertices. I suppose with multiple VBOs I'd want to group the dynamic parts separate from the static ones.

This topic is closed to new replies.

Advertisement