0
\$\begingroup\$

I am currently trying to write a (very simple) queued renderer using OpenGL. My current idea looks like this:

struct RenderCommand {
    GLUint  vao;
    GLUint  texture;
    GLUint  shader;
    GLInt   zOrder;
};


void Renderer::addCommand(const RenderCommand& cmd) { /* add to queue */}

void Renderer::render() {
    /* sort the command queue to try and z-sort, batch textures, etc. */

    for(const auto& cmd: queue_) {
        // Bind shader/vao/texture when necessary, issue draw calls
    }
}

Now my problem is this: for sprites for example, I only have one set of quad vertices on the GPU, and I send the sprite vertex shader the sprite size using a uniform. Same goes for the model transform matrix for literally any object. That's okay when render calls issued when iterating over scene objects, but becomes a problem when everything is queued: since most sprites will use the same shader, how do I set the uniform?

I've had a few ideas for solutions, but none of them feels right:

  • Add data in RenderCommand for size, model matrix, etc. The big problem with that is flexibility: it supports the engine's default sprite (or 3D model or text) shader, but custom shaders with other uniforms won't work.

  • Add a custom data pointer to RenderCommand to point to uniform data to send before rendering. This is probably the most flexible option.

  • Add a UniformCommand type, which only uploads a uniform to the currently bound shader. It'll have to be handled carefully when the queue is sorted, to ensure it stays contiguous with whatever shader and model it corresponds to, but that sounds feasible.

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

This code suffers from over-generalization, the sensibility that you need to account for every possible rendering task. This is not useful from a performance perspective.

For example, take your VAO here. Each rendered object gets its own VAO. Why? Lots of rendered objects share the same vertex format, and many of them will share the same bound buffers. When rendering such objects, you shouldn't change the VAO between them.

Consider your sprite example. You create a single quad, and render different sprites via separate uniforms. This is terrible from a performance standpoint; that's dozens if not hundreds of draw calls per-frame, with state changes between the draws. You'll get better performance by just writing each sprite's data to a buffer object (using appropriate streaming techniques, of course), and then rendering them all at once with a single draw command. Oh sure, each sprite will have duplicate color information for its four vertices. But so what? Even a 16MB double-buffer (8MB per buffer) should be able to store 400,000+ sprites.

You shouldn't be sticking all objects into a queue. You should be separating your rendering into hard-coded types of objects: skinned meshes, non-skinned meshes, sprites, lines, etc. Each type of object has its own effective queue, and each queue has its own kind of data.

For example, all sprites should source any texture data from a single texture atlas, and they should all render with the same shader. As such, there doesn't need to be any individual per-sprite data; that's all stored in the buffer object. So the only thing the sprite queue needs to know is which part of the buffer to read from and how many vertices the rendering command ought to have.

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

Option A) is not flexible enough for real cases. Option C), depends on the current OpenGL context status, which I think that is what you were trying to remove on the first place.

Therefore, I would go for option B). You may also want to look at https://www.khronos.org/opengl/wiki/Uniform_Buffer_Object With UBOs you can switch all the uniform data with just one OpenGL call.

However, are you sure you want to implement this kind of generalized queued renderer? Keep in mind that there are many OpenGL state that you are currently omitting: bound framebuffer, multiple textures, culling, clipping, Z testing, Z writing, blending...

For real applications I wouldn't write a generalized queue renderer. Although I would sort objects of the same type by their center Z to reduce pixel shader invokations by taking advantage of the early Z test. However, this is not always possible, for example, with transparency.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.