I have gone down the rabbit hole that is abstracting away a working game engine in OpenGL and C++. Before reaching the other side I'd like to ask about my current design, which is - the more I look at it - eventually in need of a major revision. I have separated windows, input and frame buffers from the concerns, but render operations are becoming a problem.

Currently all types of renderable objects, including frame buffers inherit from a common class: Renderable which has a shader program and a vertex array object. Next in the hierarchy is an Entity (yeah, I'd use better naming..), which has a model matrix, world coordinates, uniforms and associated functions with it.

After that are the specific types of objects. Namely Dummy and Model. They deal with the actual buffers and data. For this example I will focus on Model. It shall load model data from a file, create buffers and draw the model on command. I planned to use a buffer for each attribute and process meshes separately, drawing with index buffers.

I have also separated the concept of a buffer away from the renderable. I use a class for an index buffer and a vertex buffer (or interleaved buffer). The buffer manages data and e.g. binds GL buffers and deals with vertex array pointers and so on.

This all was fine until I asked this question about VAO and VBO granularity on GameDev. Since then more doubts have arisen.

Given that:

  • Only a few VAOs should be used in a whole program
  • Using modern techniques one can switch between the VBOs being used by a VAO when drawing
  • Using a single interleaved buffer for multiple meshes improves performance

I must ask:

  • Is my design sensible?
  • Is using separate classes for buffers a common design pattern?
  • What should be done to accommodate for textures, animations and such in the future?

It seems that particularly when introducing the need of combining multiple meshes and their multiple attributes into a single buffer my design becomes a bit complicated since a buffer needs to know about a VAO and a model needs to provide the buffers with information.

Any insight would be greatly appreciated!

  • 1
    I'm interested to see what comes of this discussion since I will also be solving a similar problem. I'm thinking that using the principles of Robert Martin's Clean Architecture can help. You may need to make sure that you don't mix your low-level rendering models with your high-level business (game) models. Somehow, a low-level rendering adapter can use the low-level models fed to it in a way it knows how to optimize. – Nick Miller Dec 6 at 14:23
up vote 3 down vote accepted

I've had bad experiences with too much abstracted rendering in the past. OpenGL is still very procedural at it's core, and the whole state management is very difficult to debug and extend if you apply a nice modern OO filter and make lots of small objects.

In addition, writing optimized rendering code often forces you to take a global view because the different steps to render a frame and even just the content management/preloading don't map nicely to the approaches used in other parts of an engine.

Therefore, I would separate the functionalities into layers as well as possible, so you can live in a 'looser' OO world with longer methods that deal with OpenGL state, and a nice clean world where you can work in a more abstracted, nice OO context. There's an link to DDD here, with seperate domains designed according to the needs of the domain.

  • Thanks for the comment! To concretise a bit: so you would divide the functionality into larger classes and longer functions, but make the management of those objects abstract? Would a more functional style be appropriate to reduce the amount of duplicated code, even inside the classes themselves? – Felix Mar 29 at 13:59
  • Well, for example your basic model. Seems like there should be a basic model class with just buffers for everything, a simple container with opengl indices to the data. Another object might be used to load data from disk into those buffer, and a third one would have the logic for the rendering. ModelRenderer is bound to be 'ugly' with a few longer, prodecural-style methods for dealing with OpenGL state. As OpenGL can be kind of finicky, I feel it is often clearer to clearly express every state change explicit in the rendering method(s), and not worry too much about duplicated code. – Wilbert Apr 3 at 5:46

If I can try to read between some lines of what you said and try to deduce where you're trying to move with the design, I believe I ventured down similar rabbit holes and came out cursing with more gray hairs.

In my particular case I wanted to abstract the rendering capabilities of all these rendering backends (OpenGL, OpenGL ES, DirectX, consoles, etc) in one abstract rendering interface to rule them all, like IRenderer, with the hope of minimizing porting grief by simply swapping one renderer subtype with another (ex: RendererGl with RendererDx). That's a bit different and probably more ambitious than what you are doing but I suspect there is a common theme to our design headaches.

What was fundamentally so difficult to design with that route beyond the different rendering capabilities between all these backends and the difficulty of trying to design a uniform abstraction for all of them was how I was trying to make the renderer oblivious about the rest of the game engine and world. I was trying to decouple the renderer away from everything else and have the game engine have knowledge of how to use the rendering interface instead of having the renderer have knowledge about the engine.

To put it simply my renderer interface was huge like:

class IRenderer
{
public:
    // boatload of functions related to rendering along
    // with auxiliary abstract interfaces like ones for
    // vertex buffers and textures.
};

And it was completely oblivious of higher-level abstract concepts in the engine like particle systems, models, bones, materials, lights, etc. Now I did still decouple the game logic from rendering code by injecting and associating renderable/rendering components to them, but fundamentally I was trying to make the renderer oblivious of the game world/scene.

What I found to be so, so, so much easier was to make the renderer have access to and understand the entire game world/scene and all the abstractions within. That made my renderer interface so simple like this if we use a more object-oriented approach (these days I use an ECS):

class IRenderer
{
public:
     virtual ~IRender() {}
     virtual void render(const IGameScene& scene) = 0;
};

And that does mean I have this epic implementation of that function to render scenes (though decomposed into various smaller functions and medium-sized objects), and it does mean my renderers cannot possibly be made to work anymore for any engine other than my engine. Each concrete renderer has to know how about the abstraction of my specific engine's particle system, for example, and how to render it is up to the concrete renderer to use whatever low-level mechanisms it needs to do so.

If you do it that way, then you can be as abstract as you want with the rest of the game engine and just think in terms of high-level logic without wrestling with the most effective to render the data and store and create and manage buffers and bind shaders and textures and so forth. That's all handled within the renderer itself.

For things like models which are loaded from a file, the renderer might encounter an abstract model in the scene which isn't loaded, load the file, then externally associate a VBO to that model to be used for subsequent rendering. All the gory details of rendering are inside the concrete renderer.

And that's still not easy to implement the renderer (it does help if you can generally attach data, like a component, to any entity that already exists, like a VBO component), but it was way simpler than what I was doing before as far as getting designs correct (the implementation was still hard) and I stopped having to fight with my own designs and interfaces trying to headbutt them into shape. These kinds of concerns you listed become rather moot in that case (in the sense of not tempting you to change your central designs) because you'll have all the breathing room you need in a particular renderer's implementation to get knee-deep in the specifics and peculiarities of OpenGL or any other backend to render the abstract objects/entities that make up your game world in an efficient way.

There's very generalized design sort of lesson here. If something, A (a subsystem, interface, collections of interfaces, say high-level game engine abstractions) is relatively easier to design correctly, while B (say the renderer) is extremely hard to design correctly, then consider not making A depend on B. Instead of like:

enter image description here

See if you can invert the flow of knowledge and dependencies and have B depend on A, like so:

enter image description here

Now the dependencies aren't flowing to B and you have all the breathing you need to change it without changing any central designs (a "central design" would be something lots of things depend upon which is extremely costly and difficult to change). Basically don't make things that are so hard to get right difficult to change. As straightforward as that seems, I think it's somewhat humanly counter-intuitive because I don't think most of us instinctively tend to abstract and flow dependencies in the direction of what's easiest to design correctly. It tends to be more intuitive to create abstractions in a way that yield the smallest implementations behind each abstraction and direct the dependencies that way instead of directing the dependencies away from what's most difficult to design correctly. Yet smaller implementations be damned if the designs on top constantly tempt us to reconsider them.

  • 1
    Good lesson! It looks like you are using the Dependency Inversion Principle. Your low-level, ugly, complex, hard-to-understand, & volatile rendering code depends on your high-level, more stable, application-level models (via the abstraction IGameScene) – Nick Miller Dec 6 at 14:44
  • I think fundamentally this is DIP, though in my case I originally had both the renderer (which might be considered the lower-level module) and the game objects both utilizing abstractions along with DI. In this particular case the abstraction was just too difficult for my mortal self to design correctly for the renderer. There's just too much to anticipate, too many underlying capabilities (and multiplied when we consider how to most efficiently utilize them). So – Dragon Energy Dec 6 at 14:56
  • So I found it helpful not to try to abstract the capabilities of the renderer beyond a simple render(scene) function which made it dead simple to design, and then the rest was just trying to implement this beast of a thing... and it still was a beast, but now no longer something where I was tempted to reconsider the design over and over. :-D – Dragon Energy Dec 6 at 14:58
  • In this peculiar case I found it too difficult to design a renderer interface where the system told it what to do. Abstracting all the possibilities here was just a nightmare to think about and had me reconsidering things every single time I gained some additional knowledge in hindsight. Meanwhile the design became dead simple the other way around -- which is rather than telling the renderer was to do, just let it figure out what to do by giving it all the information it needs to do what it needs to do. – Dragon Energy Dec 6 at 15:15
  • 1
    Ah thanks for the clarity. So it would seem that you're dodging the inherent complexity be NOT creating the abstraction (I admit, that sounds very challenging). I can't help but wonder if separating capabilities across different abstractions (ISP) makes the problem easier because you can compartmentalize some things - then maybe the concrete renderer can implement some or all of those abstractions. Also, I'm curious, what would you do differently now that you use ECS? I was creating an ECS architecture and was going to see if I could make it also a Clean Architecture. – Nick Miller Dec 6 at 15:16

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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