Managed Direct3D:
Using a Frame Hierarchy

A scene in DirectX Graphics consists of a hierarchy of frames. This enables you to access parts of a geometrical object and manipulate them independently. Frames are organized in a hierarchical structure with parents, children, and sibling frames. Any manipulation of a parent frame is also applied to it's children and their children and so on.

Imagine that you are modelling a tank that consists of a body, a turret, and a gun. Imagine further that you model it using the body frame as root, the turret as a child frame to the body, and the gun frame as a child to the body frame. If you want to move your tank about in a landscape you can do this by translating and rotating the body frame, the turret and gun will move and rotate together with the rest of the tank. You can further rotate the turret and gun by just applying a rotation on the turret, the gun will rotate together with the turret. You can adjust the elevation of the gun by applying the proper transformation to the gun frame independently of the other frames.

Tank

The DirectX (.x) File

This article assumes that we are loading our scene from a DirectX (.x) file as these are supported by the classes supplied with the SDK. Let us look at how the tank described earlier might be represented in a DirectX file.

xof 0303txt 0032
...

Frame Body
{
  FrameTransformMatrix
  {
    1.000000,0.000000,0.000000,0.000000,
    0.000000,1.000000,0.000000,0.000000,
    0.000000,0.000000,1.000000,0.000000,
    -1.401197,0.000000,0.279802,1.000000;;
  }

  Mesh
  {
    20;
    -29.527559;0.000000;-49.212597;,
    ...
  }

  Frame Turret
  {
    FrameTransformMatrix
    {
      1.000000,0.000000,0.000000,0.000000,
      0.000000,1.000000,0.000000,0.000000,
      0.000000,0.000000,1.000000,0.000000,
      1.435083,29.904209,-0.126913,1.000000;;
    }

    Mesh
    {
      116;
      0.000000;0.000000;0.000000;
      ...
    }

    Frame Gun
    {
      FrameTransformMatrix
      {
        1.000000,0.000000,0.000000,0.000000,
        0.000000,0.000000,-1.000000,0.000000,
        0.000000,1.000000,0.000000,0.000000,
        1.016502,4.063328,21.468393,1.000000;;
      }

      Mesh
      {
        116;
        0.000000;0.000000;0.000000;,
        ...
      }
    }
  }
}

There are a whole lot more to DirectX files than this, but the elements above are relevant to the discussion of frames. Let us look at the different parts of the document above.

Frame Body {}

This declares a frame named Body. Note that the other two frames, Turret and Gun are nested within each other, creating a frame hierarchy.

FrameTransformMatrix {}

The transformation matrix for the current frame declares how the coordinates of the frame's mesh and the meshes of all child frames is to be scaled, moved, and rotated in order for the frame to be positioned correctly relative to the other frames of the geometrical object. Consider for instance the turret of the tank above. It might be modelled as a cylinder located at origo. If no transformation was applied to it, it would be positioned inside the body. Together with a transformation matrix we have enough information to position the turret correctly.

Mesh {}

This contains the coordinates for the vertices that make up the frame. Together with the transformation matrix of the frame we have enough information to render the frame correctly relative to the other frames of the object.

Loading a Frame Hierarchy

Let us begin designing a class that will encapsulate the operations done on a frame hierarchy. We begin by loading a DirectX file and create the objects that represent the frame hierarchy in memory. The in-memory representation of the scene will be stored in an AnimationRootFrame object. An AnimationRootFrame lets us store a frame hierarchy in it's FrameHierarchy property. AnimationRootFrame can perform other operations on a frame hierarchy, but we ignore these for now.

We further need some code that takes a DirectX file and creates the frame hiearchy to store in our AnimationRootFrame object. We do this with the help of the static method Mesh.LoadHierarchyFromFile. This method requires us to declare classes that will perform the actual allocation of the parts that make up the frame hierarchy. We need to derive classes from AllocateHierarchy, Frame, and MeshContainer, all of which are abstract classes. In this article we call our derived classes CustomAllocateHierarchy, CustomFrame, and CustomMeshContainer.

Our first version of the class looks as follows:

public class GraphicsObject
{
  // Path to the x-file containing the frame hierarchy for this class.
  protected string xFilePath;

  // Container for the frames
  protected AnimationRootFrame rootFrame;

  public GraphicsObject(string xFilePath, Device device)
  {
    this.xFilePath = xFilePath;
    this.device = device;
  }

  // Load the frame hierarchy from file.
  public virtual void Initialize()
  {
    this.rootFrame = Mesh.LoadHierarchyFromFile(this.xFilePath,
      MeshFlags.Managed,
      this.device,
      new CustomAllocateHierarchy(this.xFilePath),
      null);
  }
}

The static Mesh.LoadHierarchyFromFile takes a DirectX file and returns an AnimationRootFrame object, which contains the first frame hierarchy in the file. The first parameter is the path of the DirectX file. The second parameter determines how the meshes in the frame hierarchy will be created. The third parameter is a reference to the device used for rendering. The fourth parameter is a reference to our custom object that will handle the allocation of frames and mesh objects as these are encountered in the DirectX file. Let us ignore the fifth parameter for now.

Mesh.LoadHierarchyFromFile expects a reference to an object derived from the abstract class AllocateHierarchy. The class must define the methods CreateFrame, which will be called for every frame that is found in the DirectX file, and CreateMeshContainer, which will be called with information about the meshes found in the file. Both these methods return abstract classes, Frame for the CreateFrame method, and MeshContainer for the CreateMeshContainer object, so we will have to define classes that derive from these as well.

The AllocateHierarchy Class

The declaration of the CustomAllocateHierarchy class is listed below.

public class CustomAllocateHierarchy : AllocateHierarchy
{
  // We need to store the path to the DirectX file   // for later   private string xFilePath;

  public CustomAllocateHierarchy(string xFilePath) : base()
  {
    this.xFilePath = xFilePath;
  }

  public override Frame CreateFrame(string name)
  {
    return new CustomFrame(name);
  }

  public override MeshContainer CreateMeshContainer(string name,
    MeshData meshData,
    ExtendedMaterial[] materials,
    EffectInstance[] effectInstances,
    GraphicsStream adjacency,
    SkinInformation skinInfo)
  {
    CustomMeshContainer mc = new CustomMeshContainer(this.xFilePath, name, meshData, materials,
    effectInstances, adjacency, skinInfo);
    mc.Initialize();
    return mc;
  }
}

Our CustomAllocateHierarchy keeps the path to the DirectX file that we are loading stored in the xFilePath field. We need this later when we are creating our mesh objects. The CreateFrame method creates an instance of the CustomFrame class that we will take a look at shortly. The CreateMeshContainer method creates an instance of our CustomMeshContainer class.

The Frame Class

Our specialized Frame class looks like this:

public class CustomFrame : Frame
{
  // This transformation matrix allows us to add a transformation
  // to an individual frame within the frame hierarchy.
  protected Matrix customTransform = Matrix.Identity;

  public CustomFrame() : base()
  {
  }

  public CustomFrame(string name) : base()
  {
    this.Name = name;
  }

  public Matrix CustomTransform
  {
    get
    {
      return this.customTransform;
    }

    set
    {
      this.customTransform = value;
    }
  }
}

Our CustomFrame class defines a transformation matrix for the current frame. This will later be used to perform custom transformation to individual branches in the frame hierarchy. Consider, for instance, the turret in the tank model. With the information in the transformation matrices stored in the DirectX file for the turret frame and it's parents it is possible to render the turret correctly. With the addition of a custom matrix we also have the possibility to rotate the turret and it's gun. The customTransform matrix is initialized to the identity matrix, so no transformation is performed as a default. We will look at how this is used when we look at the rendering code later.

The MeshContainer Class

The declaration of the MeshContainer class is listed below.

public class CustomMeshContainer : MeshContainer
{
  private string xFilePath;
  private Texture[] textures;

  public CustomMeshContainer(string xFilePath, string name,
    MeshData meshData, ExtendedMaterial[] materials,
    EffectInstance[] effectInstances, GraphicsStream adjacency,
    SkinInformation skinInfo) : base()
  {
    this.Name = name;
    this.MeshData = meshData;
    this.SetMaterials(materials);
    this.SetEffectInstances(effectInstances);
    this.SetAdjacency(adjacency);
    this.SkinInformation = skinInfo;
    this.textures = null;
    this.xFilePath = xFilePath;
  }

  public void Initialize()
  {
    // Load the textures for the mesh.
    ExtendedMaterial[] m = this.GetMaterials();
    this.textures = new Texture[m.Length];

    for(int i=0; i<m.Length; i++)
    {
      if(m[i].TextureFilename != null)
      {
        string path =
          Path.Combine(Path.GetDirectoryName(this.xFilePath),
            m[i].TextureFilename);
        this.textures[i] = TextureLoader.FromFile(
          this.MeshData.Mesh.Device, path);
      }
    }
  }

  public Texture[] Textures
  {
    get
    {
      return this.textures;
    }
  }
}

The constructor takes the arguments passed to it and sets the properties accordingly. I think this is easier to use than having to set each property individually. The class also exposes an initialization method, in which we use the information in the DirectX file to load the textures of the mesh. The code in the Initialize method assumes that the textures are stored in the same directory as the DirectX file. The class also exposes the textures as a property. This will be needed when we are rendering the scene.

This is all code needed to load a frame hierarchy. Next, we will add the code to render it.

Rendering the Scene

We begin by adding a public render method to the class. Before we actually render anything we should give any user of our object the possibility to adjust the custom transformation matrices of the individual frames to perform animations etc. I choose to do this using an event. Any event handler for this event will be notified when it is time to perform rendering of this object, and may adjust the custom transform matrices of the object accordingly.

// Event used to perform pre-render operations public event EventHandler SetupCustomTransform;
// Render the frame hierarchy.
public bool Render()
{
  // Allow any event listeners to adjust the custom transformation
  // matrixes for the individual frames in the hierarchy.
  if(this.SetupCustomTransform != null)
  {
    this.SetupCustomTransform(this, null);
  }

  // Begin recusively rendering the frames.
  // Use the identity matrix as the custom transform for the entire
  // frame hierarchy.
  this.RenderFrame(this.rootFrame.FrameHierarchy,
    Matrix.Identity);

  return true;
}

The Render method then proceeds with calling the protected RenderFrame method, which will recursively set up the transformation matrices for the frames and render them. The RenderFrame method takes the frame to render and the transformation matrix of the parent frame as in parameters. For the root frame we pass the identity matrix.

protected void RenderFrame(Frame frame, Matrix parentTransformationMatrix)
{
  // First, render all sibling frames at this level passing the parent's
  // aggregated transformation matrix.
  if(frame.FrameSibling != null)
  {
    this.RenderFrame(frame.FrameSibling, parentTransformationMatrix);
  }

  // Aggregate the transformation matrix to be used for this frame.
  // 1. Apply the frames transformation as specified in the x-file.
  // 2. Apply the custom transformation for this individual frame.
  // 3. Apply the parent's aggregated transformation matrix.
  Matrix tm =
    frame.TransformationMatrix *
    ((CustomFrame)frame).CustomTransform *
    parentTransformationMatrix;

  // Go on and render the children of this frame, passing the transformation
  // we just aggregated.
  if(frame.FrameFirstChild != null)
  {
    this.RenderFrame(frame.FrameFirstChild, tm);
  }

  // TODO: Adjust for the possibility of a mesh container hierarchy.

  // Perform the actual rendering for this frame.
  if(frame.MeshContainer != null)
    this.DoRender(frame, tm);
}

For each frame in the hierarchy, the RenderFrame method first calls itself with all of the current frame's siblings recursively, passing the parent's transformation matrix. It then proceeds to combine the transformation matrices of the parent, the current frame, and the custom matrix of the current frame. It then calls itself recursively for all of the current frame's children, passing the combined matrix as the transformation to be applied. Then at last it calls DoRender which performs the actual rendering of the frame.

protected void DoRender(Frame frame, Matrix m)
{
  this.device.Transform.World = m;

  ExtendedMaterial[] em = frame.MeshContainer.GetMaterials();
  Texture[] t = ((CustomMeshContainer)(frame.MeshContainer)).Textures;
  for(int i=0; i < em.Length; i++)
  {
    this.device.Material = em[i].Material3D;
    if(t[i] != null)
    {
      this.device.SetTexture(0, t[i]);
    }

    frame.MeshContainer.MeshData.Mesh.DrawSubset(i);
  }
}

Retrieving Frames

If we are going to be able to apply transformations to individual frames in the frame hierarchy, we need a way to retrieve frames from it. We add the method GetFrame, which returns a frame by name:

// Allow caller to get a hold of a named frame within the
// frame hierarchy.
public CustomFrame GetFrame(string name)
{
  return (CustomFrame)Frame.Find(this.rootFrame.FrameHierarchy, name);
}

Using the Class

Let us see how the class we have just created is used by creating a specialized class for the tank model. We declare a Tank class that adds properties for the position, bearing, roll, and pitch of the tank, as well as the angle of the turret.

We override the Initialize class of GraphicsObject and hook up an event handler for the SetupCustomTransform event. This will give our class the opportunity to adjust the custom transform when we render the object. The Tank_SetupCustomTransform event handler finds the body frame and calculates it's CustomTransform from the bearing, pitch, roll, and position properties. It then finds the turret frame and calculates it's CustomTransform from the turret angle specified.

public class Tank : GraphicsObject
{

    ...

  public override void Initialize()
  {
    base.Initialize();
    this.SetupCustomTransform +=
      new EventHandler(this.Tank_SetupCustomTransform);
  }

  public void Tank_SetupCustomTransform(object sender, EventArgs e)
  {
    // Apply the transformation on the whole tank.
    this.GetFrame("Body").CustomTransform =
      Matrix.RotationYawPitchRoll(this.bearing, this.pitch, this.roll)*
      Matrix.Translation(this.position);

    // Apply this transformation on the turret only.
    this.GetFrame("Turret").CustomTransform =
      Matrix.RotationY(this.turretAngle);
  }

    ...

}

A Complete Example

I thought I would end with a complete working sample. There are a lot of stuff missing from it such as error handling, device recovery, and correct disposing of resources. But it should get you started. There are also a lot of performance optimizations that could be made to the code, but these would make it less readable. The DirectX file used can be found here. The application assumes it is located in the same directory as the .exe file.

using System;
using System.Drawing;
using System.Windows.Forms;
using System.IO;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace Joakim.DirectX
{
  public class CustomFrame : Frame
  {
    // This transformation matrix allows us to add a transformation
    // to an individual frame within the frame hierarchy.
    protected Matrix customTransform = Matrix.Identity;

    public CustomFrame() : base()
    {
    }

    public CustomFrame(string name) : base()
    {
      this.Name = name;
    }

    public Matrix CustomTransform
    {
      get
      {
        return this.customTransform;
      }

      set
      {
        this.customTransform = value;
      }
    }
  }

  public class CustomMeshContainer : MeshContainer
  {
    private string xFilePath;
    private Texture[] textures;

    public CustomMeshContainer(string xFilePath, string name, MeshData meshData, ExtendedMaterial[] materials,
      EffectInstance[] effectInstances, GraphicsStream adjacency, SkinInformation skinInfo) : base()
    {
      this.Name = name;
      this.MeshData = meshData;
      this.SetMaterials(materials);
      this.SetEffectInstances(effectInstances);
      this.SetAdjacency(adjacency);
      this.SkinInformation = skinInfo;
      this.textures = null;
      this.xFilePath = xFilePath;
    }

    public void Initialize()
    {
      // Load the textures for the mesh.
      ExtendedMaterial[] m = this.GetMaterials();
      this.textures = new Texture[m.Length];

      for(int i=0; i<m.Length; i++)
      {
        if(m[i].TextureFilename != null)
        {
          string path =
            Path.Combine(Path.GetDirectoryName(this.xFilePath), m[i].TextureFilename);
          this.textures[i] = TextureLoader.FromFile(this.MeshData.Mesh.Device, path);
        }
      }
    }

    public Texture[] Textures
    {
      get
      {
        return this.textures;
      }
    }
  }

  public class CustomAllocateHierarchy : AllocateHierarchy
  {
    private string xFilePath;
    public CustomAllocateHierarchy(string xFilePath) : base()
    {
      this.xFilePath = xFilePath;
    }

    public override Frame CreateFrame(string name)
    {
      return new CustomFrame(name);
    }

    public override MeshContainer CreateMeshContainer(string name, MeshData meshData,
      ExtendedMaterial[] materials, EffectInstance[] effectInstances, GraphicsStream adjacency,
      SkinInformation skinInfo)
    {
      CustomMeshContainer mc = new CustomMeshContainer(this.xFilePath, name, meshData, materials,
        effectInstances, adjacency, skinInfo);
      mc.Initialize();
      return mc;
    }
  }

  public class GraphicsObject
  {
    // Path to the x-file containing the frame hierarchy for this GO.
    protected string xFilePath;

    // Container for the frames
    protected AnimationRootFrame rootFrame;

    // The device for the scene
    protected Device device;

    // Event used to perform pre-render operations
    public event EventHandler SetupCustomTransform;

    public GraphicsObject(string xFilePath, Device device)
    {
      this.xFilePath = xFilePath;
      this.device = device;
    }

    // Allow caller to get a hold of a named frame within the
    // frame hierarchy.
    public CustomFrame GetFrame(string name)
    {
      return (CustomFrame)Frame.Find(this.rootFrame.FrameHierarchy, name);
    }

    // Load the frame hierarchy from file.
    public virtual void Initialize()
    {
      this.rootFrame = Mesh.LoadHierarchyFromFile(this.xFilePath,
        MeshFlags.Managed,
        this.device,
        new CustomAllocateHierarchy(this.xFilePath), // AllocateHierarchy
        null); // LoadUserData
    }

    // Render the frame hierarchy.
    public bool Render()
    {
      // Allow any event listeners to adjust the custom transformation
      // matrixes for the individual frames in the hierarchy.
      if(this.SetupCustomTransform != null)
      {
        this.SetupCustomTransform(this, null);
      }

      // Begin recusively rendering the frames.
      // Use the identity matrix as the custom transform for the entire
      // frame hierarchy.
      this.RenderFrame(this.rootFrame.FrameHierarchy,
        Matrix.Identity);

      return true;
    }

    protected void RenderFrame(Frame frame, Matrix parentTransformationMatrix)
    {
      // First, render all sibling frames at this level passing the parent's
      // aggregated transformation matrix.
      if(frame.FrameSibling != null)
      {
        this.RenderFrame(frame.FrameSibling, parentTransformationMatrix);
      }

      // Aggregate the transformation matrix to be used for this frame.
      // 1. Apply the frames transformation as specified in the x-file.
      // 2. Apply the custom transformation for this individual frame.
      // 3. Apply the parent's aggregated transformation matrix.
      Matrix tm =
        frame.TransformationMatrix *
        ((CustomFrame)frame).CustomTransform *
        parentTransformationMatrix;

      // Go on and render the children of this frame, passing the transformation
      // we just aggregated.
      if(frame.FrameFirstChild != null)
      {
        this.RenderFrame(frame.FrameFirstChild, tm);
      }

      // TODO: Adjust for the possibility of a mesh container hierarchy.

      // Perform the actual rendering for this frame.
      if(frame.MeshContainer != null)
        this.DoRender(frame, tm);
    }

    protected void DoRender(Frame frame, Matrix m)
    {
      this.device.Transform.World = m;

      ExtendedMaterial[] em = frame.MeshContainer.GetMaterials();
      Texture[] t = ((CustomMeshContainer)(frame.MeshContainer)).Textures;
      for(int i=0; i < em.Length; i++)
      {
        this.device.Material = em[i].Material3D;
        if(t[i] != null)
        {
          this.device.SetTexture(0, t[i]);
        }

        frame.MeshContainer.MeshData.Mesh.DrawSubset(i);
      }
    }
  }

  public class Tank : GraphicsObject
  {
    protected float turretAngle = 0.0F;
    protected float bearing = 0.0F;
    protected Vector3 position = new Vector3(0,0,0);
    protected float pitch = 0.0F;
    protected float roll = 0.0F;

    public Tank(string xFilePath, Device device) : base(xFilePath, device)
    {
    }

    public override void Initialize()
    {
      base.Initialize();
      this.SetupCustomTransform += new EventHandler(this.Tank_CalcCustomTransform);
    }

    public void Tank_CalcCustomTransform(object sender, EventArgs e)
    {
      this.GetFrame("Body").CustomTransform =
        Matrix.RotationYawPitchRoll(this.bearing, this.pitch, this.roll)*
        //Matrix.RotationY(this.bearing) *
        Matrix.Translation(this.position);

      this.GetFrame("Turret").CustomTransform = Matrix.RotationY(this.turretAngle);
    }

    public float TurretAngle
    {
      get
      {
        return this.turretAngle;
      }

      set
      {
        this.turretAngle = value;
      }
    }

    public float Bearing
    {
      get
      {
        return this.bearing;
      }

      set
      {
        this.bearing = value;
      }
    }

    public Vector3 Position
    {
      get
      {
        return this.position;
      }

      set
      {
        this.position = value;
      }
    }

    public float Pitch
    {
      get
      {
        return this.pitch;
      }

      set
      {
        this.pitch = value;
      }
    }

    public float Roll
    {
      get
      {
        return this.roll;
      }
      set
      {
        this.roll = value;
      }
    }
  }

  public class Form1 : Form
  {
    private Device device;
    private Tank tank;

    public Form1()
    {
    }

    [STAThread]
    static void Main()
    {
      Form1 app = new Form1();
      app.Initialize();
      app.Show();
      while(app.Created)
      {
        app.UpdateGraphics();
        app.Render();
        Application.DoEvents();
      }
    }

    private void Initialize()
    {
      this.SetupDevice();

      this.tank = new Tank(@".\tank1.x", this.device);
      this.tank.Initialize();
      this.tank.Position = new Vector3(0, -50, 250);
      this.tank.Bearing = (float)(Math.PI/2.0);
    }

    private void SetupDevice()
    {
      PresentParameters presParams = new PresentParameters();
      presParams.Windowed = true;
      presParams.SwapEffect = SwapEffect.Discard;
      presParams.AutoDepthStencilFormat = DepthFormat.D16;
      presParams.EnableAutoDepthStencil = true ;

      this.device = new Device(0, DeviceType.Hardware, this,
        CreateFlags.SoftwareVertexProcessing, presParams);
      this.device.RenderState.CullMode = Cull.CounterClockwise;
      this.device.RenderState.ZBufferEnable = true;
      this.device.RenderState.Lighting = false;

      this.device.Transform.View = Matrix.LookAtLH( new Vector3(0, 0.5F, -10),
        new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0));
      this.device.Transform.Projection =
        Matrix.PerspectiveFovLH(( float )Math.PI/4.0F, 1.0F, 1.0F, 10000.0F);

      device.RenderState.Lighting = true ;
      device.Lights[0].Diffuse = Color.White;
      device.Lights[0].Specular = Color.White;
      device.Lights[0].Type = LightType.Directional;
      device.Lights[0].Direction = new Vector3(-1, -1, 3);
      device.Lights[0].Enabled = true ;

      device.RenderState.Ambient = System.Drawing.Color.White;
    }

    protected void UpdateGraphics()
    {
      float elapsed = Environment.TickCount;

      this.tank.Bearing = elapsed/1000.0F;
      Vector3 pos = new Vector3(0,0,1);
      pos.TransformCoordinate(Matrix.RotationY(this.tank.Bearing));
      this.tank.Position += pos;

      this.tank.TurretAngle = elapsed/750.0F;
    }

    private void Render()
    {
      this.device.Clear(
        ClearFlags.Target | ClearFlags.ZBuffer,
        Color.Fuchsia, 1.0F, 0);

      this.device.BeginScene();

      this.tank.Render();

      this.device.EndScene();

      try
      {
        this.device.Present();
      }
      catch(DeviceLostException)
      {
      }
    }
  }
}