Decoupling the Graphics System (Render Engine)

Started by
5 comments, last by robert.leblanc 10 years, 2 months ago

I'm fairly new to 3D programming but I've got a grasp on some of the basics. My last attempt at it fizzled out mainly because my ability to manage assets and the code in general was compromised due to poor program architecture. I've been reading about various design patterns and it has spawned a few good ideas (nothing overly concrete though). I'm programming with c++/Direct3D11 API (Win 8 sdk) and I've created a class that I'm calling a RenderEngine (I'm not entirely convinced that I actually know what I render engine is but you'll see soon enough what I'm leaning toward). The idea of having a 'game object' that consists of components like 'graphics', 'physics', 'control', etc seems appealing for its flexibility. In my mind at least part of the idea behind a RenderEngine is that the engine could do things like optimization for one, but also allow you to program objects without worrying about putting a bunch of rendering calls in the game object itself.

So for example, say you have a game object that is a ball (or whatever). I'm interested in what would be considered its graphical part. Also I'm interested in how the RenderEngine would interpret the graphics part. For example, would you have some member variables for say,

texture,

verticies,

color,

sprite,

etc (what other useful things would go here ?)

Then somewhere in a game loop you'd have some type of GameObjectManager that would give a game object to the renderEngine and ask it to render it. Presumably the GraphicsComponent would be a predefined interface so that when the renderEngine looks at the GraphicsComponent, it "knows" it can call something like getVerticies(), getColor(), .... Some of these things could be null obviously because not every GraphicsComponent would be the same. That's at least part of how I thought a rendering engine would work (probably a very small part--but I'm only going for simple game at this point).

However, I don't see how the object could be truly decoupled from the Direct3D API because there are so many specific things that need to be set up in to bind the buffers, and layouts, to the pipeline. For example, vertex descriptions, layout descriptions, multisampling, etc, etc. So that makes me wonder, does it make sense to include that in the graphicsComponent as well? So along with texturs, verticies, colors, etc, you'd have DESC_ type structures that would get passed along to the RenderEngine? I don't see how the engine could be programmed to figure that stuff out.

Or does it make sense to have a initialize class that would have all these specific needs coded for the gameObject in question. The initializingComponent could pass this data along to the RenderEngine and the Engine could store it in some kind of STL container. Perhaps an objectID would also be passed along so that the next time the game asks the render engine to render that gameObject, it would use the gameID to locate the stored data in the container and fire that into the pipeline.

The question for me is this: in a initializing class as described above, am I maintaining a decoupled state even though I'd have a ton of highly specific commands to create buffer descriptions and the like? You certainly couldn't write the initializeComponent and GraphicsComponent separately. Well you could write the GraphicsComponent probably but someone (me) would have to make sure the stuff in the initializeComponent made sense for what was in the graphicsComponent (which seems to me is not decoupled)

Also, suppose I went the initialization route, when things like vertex buffer is created, would it make more sense for the render engine to store that? Or should that be fired back into the GraphicsComponent? If in the renderEngine, you'd need to tell the renderEngine if an object get's destroyed so you could release that memory? If in the GraphicsComponent you'd need to add many place holder variables in the interface for things that could possibly get initialized. I have no idea if either of those options are good, or if they even make much sense.

So what parts of anything I just wrote made sense? What am I out to lunch on? And where might I find examples of this type of architecture that I could learn from?

Advertisement

In my code when a level loads or the game starts I load all models in a list into graphics memory. Each level might have different models so to save VRAM I only load used models for that level and unload the rest. My models are stored in a list in a data file. Each model has a reference (file name, ID, etc.) to a material data file, vertex data file, all the stuff you want exported from your modeling software could go here. I have a map that maps the filenames to an ID for easy reference. This creates a library where you could ask for the correct model at render time and draw it using only an integer ID. The ID is a pointer of sorts to the graphics engine. I use an entity component system so one of my components is GraphicsComponent and simply stores an integer ID to the graphics engine. At draw time, the physics code is asked for the model position and orientation and then the graphics engine transforms it and renders the ID stored in the GraphicsComponent. I send a message to the graphics engine containing the ID, and the graphics engine looks up the ID in the model library and draws the assosiated data in the proper way. You will need to make something to load the different types of models and store the data properly, but that's easy enough. The way you render the model doesn't matter to the rest of the engine, as the rest of the engine sees it only as an integer ID. You could store each model as a vertex buffer internally, or you could store the model as a vertex array, the engine will never know and will never need to know.

Valve does something similar in the Source Engine. When you create a prop which is basically a model attached to a bounding box, you assign it a model by file name which I assume internally is linked to an integer ID. The model is a special type of file which has the vertex data and material data encoded either by reference or by raw data, I'm not sure but I'm assuming the latter. When you go to assign the model itself to the prop, you get to scroll through a list of available models which is their model library. You could even store some physics data with the material for things like bullet penetration, just as long as you delegate the proper data to the proper subsystem at initialization.

However, I don't see how the object could be truly decoupled from the Direct3D API because there are so many specific things that need to be set up in to bind the buffers, and layouts, to the pipeline. For example, vertex descriptions, layout descriptions, multisampling, etc, etc.

The graphics module and model module are separate things.
The graphics module contains CVertexBuffer, CIndexBuffer, CShader, CTexture2D, etc., and a bunch of wrappers around Direct3D/OpenGL/Maple calls.

A model never touches Direct3D/Maple/OpenGL/Glide/whatever directly.

A CVertexBuffer handles layouts etc.
A CTexture2D, CSampler, CShader, CRenderTarget, etc., all know how to bind themselves to the device. A model never needs to worry about this.
Multi-sampling has nothing to do with models at all. This is done at a higher level when the user selects the properties of the back buffer. Also done through wrappers provided by the engine.



A model is a collection of meshes.
A mesh is a collection of mesh subsets.
A mesh subset holds its own CVertexBuffer/CIndexBuffer pair and some kind of structure containing render states that need to be set to render the mesh subset. Materials, texture pointers, culling modes, etc. There is one of each of these structures for each pass of rendering needed to render the mesh subset.
None of the values in this structure are actual Direct3D/OpenGL/OpenGL ES 2.0 enumerated values; they are engine constants that are translated (at compile-time or sometimes run-time) into values understood by Direct3D/OpenGL/etc.


And now the model has no idea what Direct3D or OpenGL or Glide or Maple or GR is. It has been completely separated from the underlying graphics API.


Or does it make sense to have a initialize class that would have all these specific needs coded for the gameObject in question.

Objects need to initialize all the structures they own that hold all the state settings, textures, shaders, vertex buffers, and index buffers, but why would that be a class other than the model itself?
These settings all come from a model data file. Why would any other class be messing with it?


The initializingComponent could pass this data along to the RenderEngine and the Engine could store it in some kind of STL container. Perhaps an objectID would also be passed along so that the next time the game asks the render engine to render that gameObject, it would use the gameID to locate the stored data in the container and fire that into the pipeline.

Extremely inefficient. Models can do just fine by holding their own structures with all their render settings.


The question for me is this: in a initializing class as described above, am I maintaining a decoupled state even though I'd have a ton of highly specific commands to create buffer descriptions and the like?

No, you are making a headache for yourself while not having decoupled anything from anything else.


Also, suppose I went the initialization route, when things like vertex buffer is created, would it make more sense for the render engine to store that?

Why?
The vertex buffers and index buffers used by models are part of their model file formats. They belong to the models no matter how you look at it.
You keep them separate from Direct3D by using a CVertexBuffer and CIndexBuffer class provided by the graphics module.


What am I out to lunch on?

A pepperoni pizza with extra cheese.




You have misunderstood decoupling graphics.
In a normal hierarchy a graphics module is not “coupled” to anything but a math module, thread module, memory module, template module, and a foundation module (provides basic types such as u32 as well as system macros such as E_WIN32 or E_IOS, plus generic functions such as ByteSwap()).
That’s what a graphics module needs to survive and that is all to which it links.

Anything that needs graphics (the building module, model module, terrain module, etc.) link to the graphics module and use the features it exposes.
This doesn’t mean they necessarily do their own drawing.
The engine module sits on top of everything. It provides a scene manager in which models, buildings, terrain, cameras, lights, etc. can live.
As the highest-level module it is responsible for high-level rendering.
Gathering objects that are in view, preparing render targets, creating shadow maps, picking LOD levels, deciding if and when to billboard an object, etc.
Part of rendering means the scene manager, after culling non-visible objects, gathers those structures that each model has (one for each pass that tells what states, textures, shaders, vertex buffers, etc., to use), passes them off to a render queue for sorting, and at some point at its own convenience renders them in sorted order.

And thus the graphics module is not coupled to models, the scene, terrain, vegetation, water, particles, etc.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

None of the values in this structure are actual Direct3D/OpenGL/OpenGL ES 2.0 enumerated values; they are engine constants that are translated (at compile-time or sometimes run-time) into values understood by Direct3D/OpenGL/etc.

Can we just dissect a little bit pseudocode of extremely simple code to see if I can get this a little more.. it will likely help others who are new like me...

MyGameObject( .. ) : GameObject{

...
init( .. )
inputModule
physicsModule
modelModule
graphicsModule
otherModules
...
}

I don't think this was exactly what you were describing so you can clear up my misconceptions hopefully. It seems there is some degree of coupling between the graphics and model modules as you'd have to get the vertices or the mesh or whatever from the model and pass it to the graphics to be turned into buffers of various sorts. I guess this makes sense. Currently I'm working with the simplest thing possible a few vertices in the shape of a cube so I guess that would be the model. In the future I imagine I'll have create models using software like Blender (I'm too poor for the like of 3D Studio Max). So then you'd be parsing a file (I used Assimp or whatever it's called in the past... but back then I wasn't any decoupling whatsoever so it was messy just like this run on sentence with poor use of commas ;-) The model I suppose would also contain references to materials, texture files, and other stuff that would be the starting point of what would eventually get put on the screen. Most of the periods used in this paragraphs could be replaced with questions marks, except for this one.

modelModule{

init( .. path-to-model-file/ID-of-Model-to-look-up-/Something-that-lets-this-class-load-model) {

do the loading stuff by parsing... or whatever this implementation decided on
}

verticies
Mesh
textures
materials
general-model-data-variables
etc
}

How the graphicsModule gets its hands on this I'm not quite sure.. Maybe this is a implementation of an interface and the graphics module gets a handle to this during the MyGameObjects initialization? Then the graphics can call things like getMesh(), getWhateverItNeeds() knowing those functions will be there because they were virtually defined in the modelModule interface... In any case... some black magic happens and the graphicsModule maybe could look like....

graphicsModule : someInterfaceThatMakesSenseForTheRenderEngine{
init( handle-to-modelModule, whatever-else-it-needs) {
//This is what I'm wondering... does the following sort of stuff go in here ...
//for example

//Build some shaders based on the data from the model (presumably models define their own shaders )-- The engine can deal
//with the result
D3DCompileFromFile( szFileName, nullptr, nullptr, szEntryPoint, szShaderModel, dwShaderFlags, 0, ppBlobOut, &pErrorBlob );



// Define the input layout
D3D11_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
UINT numElements = ARRAYSIZE( layout );

//Vertext Buffer Description
D3D11_BUFFER_DESC bd;
ZeroMemory( &bd, sizeof(bd) );
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof( SimpleVertex ) * 3;
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory( &InitData, sizeof(InitData) );
InitData.pSysMem = vertices;
//description ready to be passed along to the engine
...
...
}
}
To make an actual vertex buffer requires a handle to the D3DDevice. I guess creating a VB maybe isn't considered rendering?? So does it make sense to just get a handle to the engine and give it the buffer desc and get back a VB? Then just hang on to it in this class?
You had several classes mentioned CVertexBuffer, CInputLayout, .... Are these classes within the graphicsModule? Are the initialized during the parsing of the model data?
If any of what I have written is even close to on the right track (and I doubt it is) I picture the Engine using the interface when a GameObjectManager calls RenderEngine.render(MyGameObject) to access all the predefined methods like, getVB, getInputLayouts, getShaders, getWhateverStates, getWhateverElseIsDefinedByTheInterface. BTW is there a difference in the use of the word component vs. module? Also I'm mostly interested in simple ideas here that allow me to decouple for organization. I'm currently (although I recognize its importance) not terribly interested in optimizing speed or fancy dancy scene management. I will do that when I get a bunch of simpler objects interacting and doing the basic things I want them to do. I figure even get there I need at least a semblance of structure to my code. That's why I'm going this route. Ex: I'm not building a commercial rendering engine here


… It seems there is some degree of coupling between the graphics and model modules as you'd have to get the vertices or the mesh or whatever from the model and pass it to the graphics to be turned into buffers of various sorts. ...

I rephrase the second last paragraph of L. Spiro's above answer a bit. As ever, details and naming things may differ a bit from case to case...

The usual way (as long as we speak of modern engine architecture) is to have 2 levels of rendering, neglecting here that a mesh generator level may be placed on top of it. Let us name the upper level "graphics rendering" and the lower level "graphics device".

The graphics rendering iterates through the scene and uses some mechanism on each of the found renderable objects to determine whether it could be visible (e.g. it does frustum culling or the like). For each object passing this, the graphics rendering creates a graphics rendering job. This job is an instance of a structure that becomes filled with the draw call (what kind of drawing routine is to be used, but in an engine manner, not using graphics API specific enums or codes) and perhaps which ranges are to be used, which index buffer and vertex buffers and constant buffers and so on, and which drawing state (an abbreviation for blending and other modes, as well as textures and so on). For each visible sub-mesh an own job is created (perhaps more than one; think of L. Spiro's "one for each pass"), and put into a queue.

The graphics device then works on the queue, sorts the jobs as necessary, and invokes the underlaying D3D/OpenGL/... library first to set the modes as prescribed by the current job's drawing state, and then the API's routine matching the current job's draw call.

Notice that the graphics device gets told what to do by the graphics rendering. Neither does the graphics rendering need to know anything about D3D/OpenGL/..., nor does the graphics device know anything about models. Of course, both levels must agree how index buffer, vertex buffer, and so on are built up, and for sure will their structure be borrowed from what D3D/OpenGL/... use. But that is just a convention due to performance reasons ;)

I love that L. Spiro mentioned Glide!!!!

Code makes the man

A CVertexBuffer handles layouts etc.
A CTexture2D, CSampler, CShader, CRenderTarget, etc., all know how to bind themselves to the device. A model never needs to worry about this.

So do they take a device type parameter or a render type class? Here's an example I found of a CVertextBuffer class. Is this similar to what you meant? It's D3D9 but I'm assuming the ideas are the same. I'm less interested in having the flexibility to change 3D API's although I know that is a big advantage of this approach. I'm more interested in how it helps structure programming itself. Maybe that's the wrong reason to go this route.

Also this VertexBuffer Class

This topic is closed to new replies.

Advertisement