If this is for desktop OpenGL or OpenGL|ES 3 and you're doing a common 2D sprite-style engine, you won't have a lot of repeat geometry. Everything is quads. You only need a single vertex buffer and index buffer (describing a quad). Use instancing to put all the transformation matrices for your scene objects into a single buffer and then draw that single quad X times (where X is the number of sprites you're drawing) with a single draw call. No need for dynamic buffers, no need to store buffers with "sprites," etc. Even with a fairly complex multi-part sprite animation system, each part is typically just a single quad, all arranged in a tree. You can still render multiple sprites/parts with a single draw call.
Generalizing to 3D (and some more complex 2D needs), each mesh would store the VBO of its attributes and an index buffer. Your renderer would try to group these together to use instancing; that is, if you're drawing 20 copies of the same mesh, only call draw once but use instancing to make that draw call draw all 20 copies. In 2D, there may be some use for this for more complicated objects, like paths or shapes that you need to triangulate, though usually sprites are enough.
For older OpenGL support or OpenGL|Es 2 you can't use instancing. In this case, just keep a single "large enough" VBO for a decent number of sprites. When your scene draws, first insert a transformed quad into the buffer for each object. That is, transform the vertices of a standard (-1,-1),(+1,+1)
quad on the CPU to get the world positions of the quad representing that object, then copy those vertices into the VBO (and update the index buffer too, of course). Then draw that single buffer. You may need to split up rendering if your buffer can only hold X objects but you have X+Y objects to draw. E.g., if your buffer can draw 1000 but you have 2456, you'd make three draw calls: one for the first 1000, another for the second 1000, and a third for the remaining 456. In your VBO building loop, just check if the buffer is full after adding each object, and if so, draw and flush the buffer.
Just remember to discard the buffer before reusing it that same frame using glBufferData(vbo_id, 0, NULL, GL_DYNAMIC_DRAW)
otherwise you'll stall the pipeline (you can't update a buffer while the GPU is rendering from it). You that call or the newer GL API to discard buffers (I forgot what it was), not glDeleteBuffer
, which is something else entirely (recreating a new VBO every time is much slower than discarding a buffers contents).