22
\$\begingroup\$

For the architectural side of rendering, there's two main ways: having each object render itself, and having a single renderer which renders everything.

I'm currently aiming for the second idea, for the following reasons:

  • The list can be sorted to only use shaders once. Else each object would have to bind the shader, because it's not sure if it's active. The objects could be sorted and grouped.

  • Easier to swap APIs. With a few macro lines, it can be easy to swap between a DirectX renderer and an OpenGL renderer (not a reason for my project, but still a good point)

  • Easier to manage rendering code

Of course, if anyone has strong recommendations for the first method, I will listen to them.

But I was wondering how make this work.

First idea
The renderer has a list of pointers to the renderable components of each entity, which register themselves on RenderCompoent creation. However, I'm worrying that this may end up as a lot of extra pointer weight. But I can sort the list of pointers every so often.

Second idea
The entire list of entities is passed to the renderer each render call. The renderer then sorts the list (each call, or maybe once?) and gets what it wants. That's a lot of passing and/or sorting, however.

Other ideas
???
PROFIT

Anyone got ideas? Thank you.

\$\endgroup\$
2
  • \$\begingroup\$ "Else each object would have to...cannot find word activate? the shader." I believe the word you're looking for is "bind". \$\endgroup\$
    – jpaver
    Sep 1, 2010 at 22:33
  • 4
    \$\begingroup\$ Pointer weight is really not much of a consideration here. One pointer per renderable object will run out of rendering capacity far earlier than it would run out of any processing or memory capacity by adding 4(8) bytes of pointer to each object. And in fact having the renderer's list of items be a nice flat array of pointers is far easier to traverse than, say, having the renderer iterate from the root of your actual object hierarchy and find renderable objects based on some check. \$\endgroup\$
    – MrCranky
    Sep 3, 2010 at 9:48

4 Answers 4

18
\$\begingroup\$

Third idea

Each entity submits an encoded integer key, possibly with an associated value to the renderer for each item to be rendered. The renderer can then sort the keys using any fast integer sorting method, and submit each item to be rendered.

By encoding the key in the order you want each component sorted, from MSB to LSB, you get the benefits of your second idea with a much faster sorting step.

There's a huge amount of flexibility here in terms of how you encode your key, from something as minimal as:

|MaterialID|Depth|MeshID|
|         4|   20|      8|  (16 materials, 256 models)

To a more complex 64-bit key & pointer value:

|Fullscreen|Viewport|Layer|Blend|Depth|MaterialID|  :  ptr_to_data
|         2|       3|    3|    2|   23|        30|

There are several additional benefits to using integer keys; for example, if you have a static section of the scene you can cache the resulting keys and simply re-submit the prepared list (if used often, pre-sort and use a suitable sorting algorithm).

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

Have your renderer persistently manage all the renderable data

When a new renderable instance is created the renderer passes back a handle. The handle allows the client code to make basic changes to the renderable, eg. make it invisible, change its transform, per instance material properties (eg. alpha) etc.

Benefits of this architecture as I see them:

  • allows the renderable to remain in a predetermined bucket as determined by it's material/shader properties (see Jason's post for how to possibly classify these buckets) at creation time, not at run-time.
  • It also allows the renderer to keep these buckets/lists compact from frame to frame, allowing the renderer to manage the memory footprint of each bucket in a nice coherent way.
  • Allows the renderer to gather renderable instances to improve per-draw call overhead using instancing.
  • Allows the renderer to double buffer these lists, and dispatch jobs to multiple processors (eg. using DX11 deferred contexts, or asynchronous per-bucket sorting) without revealing the complexity of the implementation to the client code.
\$\endgroup\$
2
  • 3
    \$\begingroup\$ Would the person(s) that marked this answer down please add a comment explaining why so we can all benefit from your viewpoint? :) \$\endgroup\$
    – jpaver
    Nov 15, 2010 at 19:26
  • \$\begingroup\$ I'm very interested in this approach. Is there any open source library/engine that uses this approach? Or maybe some material/books that treat this kind of architecture. I know that is used in the Destiny's engine. But I would want to see a real code example. \$\endgroup\$
    – Ciberman
    Jan 25, 2022 at 6:58
4
\$\begingroup\$

I asked a similar question over at Stack Overflow which got some decent responses that might help you: "What's wrong with the architecture of a game object drawing and updating itself?". I think reading this question and its answers will help you validate that yes, you should have a central renderer (though you already seem to get that), and hopefully clarify how to do so.

\$\endgroup\$
0
\$\begingroup\$

In practice I use both. I try to keep the number of items types sufficiently low (e.g. particles, sprites and GUI images have a hell of a lot in common with each other and can share much code) but where it helps I break things out (e.g. some types of model can be drawn with instancing which is sufficiently different to standard drawing that I feel centralizing this would over-complicate the design).

For the common stuff it's just a matter of deciding what properties they all share and designing some wrappers around them. For particles, sprites and GUI images I did a simple "Quad drawing" interface that I can just hand a list of vertexes to, as well as a texture and a set of shaders to use. The interface then handles changing state as needed, appending to a dynamic vertex buffer, drawing in batches, etc. It ended up looking a lot like ID3DXSprite in fact, but setup to meet my own specific requirements rather than in a more general fashion.

I've another layer beneath this that's used by both generalized and specialized drawing, which handles lower-level stuff like flushing buffers, updating shader parameters, etc.

\$\endgroup\$

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