Sunday, October 17, 2010

Model Data

First it Was Easy
My Junior game, Æscher, didn't have any complex asset requirements, so my graphics engine just had managers for meshes, textures, and shaders. It then used a bucketed system to reduce the amount of context switching so that everything with the same shader was rendered, and in that context, everything with the same texture was rendered. Hardware instancing was used to make sure all object with the same mesh were rendered at once.

In a stupid-simplified version, think of it like this:
typedef std::vector<mesh> MeshBuckets;
typedef std::vector<meshbuckets> TextureBuckets;
typedef std::vector<texturebuckets> EffectBuckets;

//...
class GraphicsManager
{
  //...
  EffectBuckets mBuckets;
};

Then, you could loop through the hierarchy and keep the context switching to a minium (i.e. it's most expensive to switch shaders, followed by textures, then meshes):
void GraphicsManager::Render()
{
  for(unsigned e = 0; e < mBuckets.size(); ++e)
  {
    Effect   &lEffect = mEffectManager.GetByID(e);

    //Start shading:
    unsigned lPasses = lEffect.Begin();

    //For all TextureBuckets associated with Effect 'e':
    for(unsigned t = 0; t < mBuckets[e].size(); ++t)
    {
      //Specify the current texture:
      Texture  &lTexture = mTextureManager.GetByID(t);
      lEffect.SetTexture(lTexture.GetTexture());

      //For all MeshBuckets associated with Texture 't':
      for(unsigned m = 0; m < mBuckets[e][t].size(); ++m)
      {
        //Get the mesh for HW instancing:
        Mesh &lMesh = mMeshManager.GetByID(m);

        //Set up HW instancing stuff

        //Render each pass:
        for(unsigned p = 0; p < lPasses; ++p)
        {
          lEffect.BeginPass(p);
          
          //DirectX draw call

          lEffect.EndPass();
        }//End Passes
      } //End Meshes
    } //End Textures

    lEffect.End();
  }//End Effects
}

Not so Fast
However, with the new complexities I'm introducing, it's not as easy as that. Now I have skeletons and animations to manage as well as normal maps, per-instance materials, etc. I'll cross each bridge as I come to it, but right now I'm stuck on how I should handle my new mesh system.

My game only supports one mesh per object. I could extrapolate this to multiple meshes in an object, but that's something that can come later. It simplifies things for me to make this assumption.

A skeleton is something that each mesh needs to be skinned (if it's not static). Since there can be static objects, not all meshes have skeletons. However, for the objects that do have skeletons, I can't decide if it should even be in the Mesh data-structure or not.

Now, conceptually, yes, I should just toss it in there, because it would be silly to code the ability to "switch" skeletons on a mesh without the necessity for it. But that's not my point, my point is that for optimizations, I think it makes more sense for it to be in a separate manager (like my MeshManager) so that the HW instancing buffers can be more efficiently constructed instead of uselessly skipping over the skeleton data and nuking your cache.

Even more complex than that, is what about the animation data? I'm not sure I'm settled on it, but the solution I'm going for right now is to have the skeleton data with the mesh and have an AnimationManager. While technically this is worse for the HW instancing, it helps cache in the other areas when I need to access the skeleton for the skinning code right along side the mesh.

I should really do some profiling and try multiple ways, but first I should get it working one of the ways and then determine if it's even a bottle-neck.

So what's more important? 1) Cache, 2) memory overhead of another manager, 3) keeping things together that should conceptually be together? I know there's no true answer other than, "It depends." And that's the kicker.

1 comment:

  1. Your skeleton will likely be an intrusive list/tree of bones right? Might as well just give the root node to the mesh as it is only a pointer. You can simply put the root node at the end of the structure and pass the struct as 4 bytes smaller than it actually is, if you want to avoid the pointer (If your struct is a POD).

    ReplyDelete