Frage

I'm making a 2D batch renderer in OpenGL inspired by the XNA/MonoGame interface, but I've run into a small design problem and I'm looking for some input. Currently, you can submit vertex data in four general ways:

void Render(const Sprite& sprite);
void Render(const Shape& shape);
void Render(const Vertex* vertices, unsigned int length);
void Render(const Vertex* vertices, unsigned int length, const Texture* texture);

A sprite contains four vertices, color and texture coordinates while the other three can contain an arbitrary number (the sprite and shape have unique transformations). Everything can be textured or untextured. I want to batch everything to reduce the number of state changes and OpenGL draw calls. I feel it reasonable to assume that most submissions will have shared vertices so that I can use glDrawElements instead of glDrawArrays, but I have trouble figuring out how to batch things properly given what I described above.

The XNA/MonoGame sprite batchers work because they work solely with textured quads/triangles and not arbitrary shapes. Alternatively, I could do like the SFML renderer and issue a draw call for each drawable object, but that defeats the purpose of batch rendering.

I feel like my renderer is trying to "do everything" which is something I want to avoid since it usually quickly becomes too complex in my experience.

What I'm asking is: How could I redesign my renderer? Could I keep separate batch lists for different submissions? Could I modularize my renderer somehow? Should I just allow only textured objects as done in XNA/MonoGame?

War es hilfreich?

Lösung

Alright, so we need to minimize the number of state changes and draw calls. I'm assuming you're using modern OpenGL, including Vertex Buffer Objects and shaders.

One approach is to ensure that all vertex data has the same format. For example, each vertex has a position, color and texture coordinate (xyz, rgba, uv). If we interleave our vertex data in a VBO, we only need a single call to glVertexAttribPointer and glEnableVertexAttribArray, before rendering.

This means some redundant data for untextured objects, but we get to cram everything into a single batch, which is nice.

To handle the untextured objects, you could either bind a blank white texture and treat it as a textured object. Or, you could have a uniform variable (a float between 0 and 1) in your fragment shader, and blend between texture color and vertex color using the mix function.

To batch sprites and shapes we should first handle the transformations on the CPU, so that we always upload "world"-coordinates to the GPU. This saves us from having to set a transformation uniform for each sprite, which would each be require individual draw calls.

Furthermore, we need to sort by texture whenever possible, as texture bindings are among the more expensive operations you can do.

Our approach basically boils down to the following:

  • Maintain a single Vertex- and Index Buffer Object to store the data
  • Keep all vertex data in a single format and interleave the data in the VBO
  • Sort by texture
  • Flush the data (draw elements/arrays) in the buffers whenever we change texture (or set the texture-blend uniform, if we go with that option)

Getting the data from the CPU to GPU memory can be done in different ways. For example, by first allocating a large enough, empty memory buffer on the GPU, and using glBufferSubData to upload a subset of vertex/index data, whenever you do one of your Render calls.

Remember to profile!

It is very important to do profiling when doing this kind of work. For example, to compare the performance between batching and individual draw calls, or glDrawArrays vs glDrawElements. I recommend using gDebugger, which is a free and very good OpenGL profiler.

Also note that too big of a VBO can hurt your performance. So keep it to a reasonable size, and flush it with a draw call whenever it fills up.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top