Direct3D Engine Design

Started by
11 comments, last by mumpo 19 years, 6 months ago
Hi, I am writting a design for a directx engine. My question is mainly about direct 3d and how I could make classes easy to use. I have a device namespace that handles all the rendering. My problem is about objects and how to put them in classes. Should I have a base class called Objects and then another class like Rectangles, MeshObjects, or Sprites that do different things but they do share some similar things like world position and rendering graphics. And then with my object I could just add it to my device manager to do the rendering of the object itself. I just don't know what to include in the base class, and what classes I could create from that class. Can anybody please help me deciding what to do, because I'm going in different directions each day. Thanks.
Advertisement
You could have a base class called Renderable instead of Object. Since Object can be really almost anything as well. Renderable would have the basic functionality of anything tht can be seen on the screen ie:

virtual Render();
virtual SetWorldPos();
virtual GetMaterialAndEffects();
virtual GetBounding[whatever]();
...
...
etc...

But also keep in mind that the Render function shouldn't actually render anything. Rendering on a per object basis is considered bad practice. So the Render function should actually pass it's vertices and indices along to a RenderManager of the sort. The RenderManager then sorts the renderable objects (based on certain criteria obtained via the GetMaterialAndEffects function), and then batches whatever it can out to the gfx pipeline.

It sounds really nice and managable on paper, but it gets very complex when you go and implement the above. But anyway, that's one way to do stuff...
[size=2]aliak.net
What do you mean by Rendering on a per object basis is considered bad practice?
Quote:Original post by andyb716
What do you mean by Rendering on a per object basis is considered bad practice?

It's not necessarily bad practise, but it introduces unwanted overhead as well as tight coupling to the rendering system.

If your rendering back-end batches calls and sets up buffers you still have lots of function calls for each object (e.g. in case of multi-pass rendering). IMHO this approach is better suitable for immediate mode (which D3D 8+ doesn't offer anymore).

Regards,
Pat.
Wouldn't I want to render all my objects though. I don't understand rendering per object.
What they mean is that you don't want to render objects as unrelated render calls. It may be tempting to have a class of Renderables that contain all the needed vertices and textures etc and then just call Renderable->Render() to draw them to the screen. But this will always come with uneeded overhead and will quickly sink performance as you increase in scene complexity. Consider what code my look like with that design.

[SOURCE]Engine::RenderWorld(){Car* Car1;Tree* Tree1;Car* Car2;Engine::SetD3DRenderProperties(Car1->GetMatrlAndTexture());Car1->Render();Engine::SetD3DRenderProperties(Tree1->GetMatrlAndTexture());Tree1->Render();Engine::SetD3DRenderProperties(Car2->GetMatrlAndTexture());Car2->Render();}[/SOURCE]


It is likely that Car1 and Car2 will use the same vertices and textures yet because of this design, materials and textures will be set more times than they are really needed.

Now consider an alternative

[SOURCE]//Somewhere in codeCar* Car1;Tree* Tree1;Car* Car2;Engine::AddToRenderList(Car1);Engine::AddToRenderList(Tree1);Engine::AddToRenderList(Car2);Engine::RenderWorld(){for(each batch in RenderList){    RenderList[batch]->Render();}}[/SOURCE]


When adding to the RenderList, it can group objects that share identical textures and/or vertex data. That way, it eliminates unneeded state changing calls.
President: Video Game Development Club of UCIhttp://spirit.dos.uci.edu/vgdc
Quote:Original post by andyb716
Can anybody please help me deciding what to do, because
I'm going in different directions each day.
Engine design (and graphics engine design, especially), can be complex and confusing enough to make your head spin. Lay your thoughts out on paper, but don't over-engineer or over-analyzing. That's when you start going around in circles. Simply put, the best way to learn what works and what doesn't (in the long run), it to implement it and find out.

With that said, make sure you account for a flexible material system. A material contains anything that has to do with how the object looks (ie the shader, textures, constants, renderstates, ect...). You can also extend the material properties to include physics and sounds, if you so desire.

When rendering, the objects should be batched by their material. First, sort by the shader, then the textures, and finally any constants. This will minimize calls to any of the costly functions listed here.

Here is a great thread on the whole material concept.
Dustin Franklin ( circlesoft :: KBase :: Mystic GD :: ApolloNL )
[note: some of this is just a copy and paste from what I posted over in the Engineering forum]

Sometimes batching by object isn't enough. A game object could potentially have multiple objects it needs to render (think about a person, a gun, other attachments, etc.). Those other objects might be duplicated many times in the world also. I happen to think that it is best to approach the rendering of a 3D engine at a low level. Think about rendering triangles, not objects (even a mesh is just a collection of triangles).

This is an approach to the problem that I learned from the book "3d Game Engine Programming". The game objects (like a Sprite, or a Mesh), simply pass all the vertices they need to rendered to a VertexManager. So, the objects don't actually do any rendering themselves. They still perhaps implement the common Render() function, but the Render() function simply passes the vertices that need to be rendered to the VertexManager.

This method makes it a lot easier to "batch" and organize your drawing calls if possible. Trying to batch and organize the drawing calls (like organizing the drawing of vertices by texture) becomes a lot more painful if all the rendering is done by each object. This is because there is no "watch dog" to actually do the organizing (especially if one object will use multiple textures (as is usually the case)).

Personally, I do it by having a VertexManager that is accesible through the renderer. Something similar to:

public abstract class RenderDevice{     public abstract void BeginScene();     public abstract void EndScene();     public abstract IVertexManager GetVertexManager();}public class D3DRenderDevice : RenderDevice{     public override void BeginScene()      {          // begin scene on D3D device     }     public override void EndScene()     {          // present scene on D3D device     }     public override IVertexManager GetVertexManager()     {          return this.d3dVertexManager();     }}


Your VertexManager would then have Render() methods that basically add the vertices to a VertexCache that caches the vertices and doesn't draw them right away. This allows the VertexManager to prioritize and organize the vertices any way in which you choose and the game objects don't even have to know that anything is happenning. For all the game objects know, the VertexManager is drawing the vertices right away (which it isn't).

So, a boiled-down and simple VertexManager might look like the following (very boiled-down mind you):

public interface IVertexManager {     public void Render(Skin skin, VertexType vertexType, ArrayList vertices, ArrayList indices);}public class D3DVertexManager : IVertexManager{     public void Render(Skin skin, VertexType, vertexType, ArrayList vertices, ArrayList indices)     {          // begin: loop through all vertex caches          //    if the skin and the vertexType are the same as the cache          //       add vertices to cache          //       return          //    end if          // end: loop          //          // -- no matching vertex caches found so.....          // if there was an empty cache          //    initalize cache          //    add vertices to cache          //    return          // end if          //          // -- no empty caches so flush fullest cache          // fullestCache.Flush()          // initialize cache          // add vertices to cache          // return     }}


Please note, there are many opinions on this topic. I think the important thing is to build your engine in such a way that it will be easy for you to change. That's one of the reasons I like the approach specified here.

Not only does it allow you to easily change the drawing algorithm (if you decide to change how to order your drawing calls), it also allows you to more easily abstract the engine away from the rendering-specific technology. For instance, in the above example, all DirectX specific classes (i.e. D3DVertexManager, D3DRenderDevice, etc.) would be in a library included at run-time. The game would use the abstract engine classes and would hook them in at compile time. This allows you to easily plug in in OpenGL renderer instead of a Direct3D renderer (or vice versa). So, changing renderers can even be done at run-time and doesn't require a single change to any of your game objects.

Remember, abstraction and architecture are your friends :).

I hope that helps at least a little bit. The best of luck to you :). (And don't forget to enjoy yourself :D). Happy Coding!
Jason Olson - Software Developer[ Managed World ]
Quote:Original post by IFooBar
virtual Render();
virtual SetWorldPos();
virtual GetMaterialAndEffects();
virtual GetBounding[whatever]();
...
...
etc...


These are good examples of the sorts of functions that you might want in your base class. However, when you put functions into a base class, you should always consider carefully before adding anything. Try not to think of what ought to generally be in the base class; rather add a function when there is a specific place you want to use it. For example, if you have a list of Renderable objects and want to render them in a loop without needing to first determine what type of object each is, then add a Render() function to the Renderable base class. However, it will make programming the system easier if you do not try to add other functions to the base class, like SetWorlPos(), unless you know of an instance where you will want to try to access it without knowing which child class of Renderable the object you want to set the world position for belongs to. Even if a function will be shared by all classes derived from a base class, it is not necessary to try to generalize it by putting it in the base class itself unless you need to be able to call that function from a place where you don't know the specific type of the object. It will make your code cleaner if you do generalize it, but IMHO it is better to get the code written and working and then go back and move functions into the base class if you can. Somebody let me know if that came out garbled; I couldn't think of a concise way to say it.
no that wasn't garbled. It came out perfect (but I just had two cups of coffee so, I'd probably understand anything right now - except for nebula2). Anyway, definetly you shouldnt be putting anything into a class unless you know that you'll be using it. Over generalizing classes gets you into trouble. The second/third/forth/etc time you create the same class however, you tend to know what you're going to need this time. So classes like the above can only be made properly if you have experience. First time you go about making something generic, you just don't know enough. So you *are* going to mess it up quite a bit and forget to take a lot of things into account (because you've never faced them before).

So yeah, I agree with mumpo, specific always gets you farther.

This topic is closed to new replies.

Advertisement