Creating 3D Tools with MFC
by Joe Houston

Introduction

Game tools have long been overlooked as an important piece of the development process. Many hobbyists and professional programmers have difficulty putting forth effort on a complete tool package, coming from a world where glorious hacks are applauded. Existing software can be supported through command line applications or file parsers, and artists can adapt to a few extra steps in the process. Many feel that their time is best spent doing "important" things, rather than squandering it all on a tool set that will never see the light of day. GUI layout and other "fluff" add tedium to the process, further frustrating the programmer. Further more, windows applications have a sort of mystique for those that are new to tool development, often times leading to intimidation and rationalization about the subject.

No problem. Using APIs like OpenGL or Direct3D (which most programmers are already familiar with) and MFC, tools can be written quickly and efficiently. More often than not code can be taken directly from the game engine and dropped right into the framework, not only saving time but maintaining consistency between the way scenes are displayed in-game and in the tools.

Who is This Article For?

The purpose of this article is to provide a lightning quick introduction to MFC and dive into 3D tools. Prior knowledge of MFC is not required, but a cursory knowledge of the MFC architecture is highly recommended. The accompanying source uses OpenGL for rendering, although other APIs can be similarly implemented. MFC is also heavily rooted in C++, so those that don’t subscribe to the object-oriented school of thought might want to read this article with a reference book in their lap. I also recommend referring back to the MSDN library throughout the course of all your projects, as I can’t possibly provide the amount of detail found therein.

Obviously this subject has the potential to be overwhelming and huge, so I’ll only be covering a fraction of the process here. However if there were an expressed interest in future articles along the same vein I’d be happy to comply.

Still here? Let the good times roll...

Introducing MFC

So after all that, what the hell am I talking about? For those that skipped their anachronism class in high school, MFC stands for the Microsoft Foundation Classes. As this is nearly as cryptic as the abbreviation was, I’ll elaborate. Simply put, MFC is a set of C++ classes that attempt to simplify the process of creating GUI applications for windows. Alone it’s not much to look at, but combined with an uncharacteristically helpful wizard and a useful set of editors it becomes a very powerful tool. For purists MFC is viewed as an unneeded abstraction, and ranks right up there with Mr. Rogers and Hitler. Despite arguments to the contrary MFC can save you weeks of tedious guess and check coding, so if you can leave your hang-ups at the door we’ll all get along better.

The following are some terms that are used freely when discussing MFC:

Control: A GUI object that allows the user to control or modify the document or application in some fashion (text field, checkbox, dropdown list, button, etc).

Document: All the data that is contained and created by your application (the text in a word processor, a scene in a 3D modeler, etc).

View: A particular way to display a document or set of controls in your application.

SDI: Single Document Interface - an MFC application with a single document and a varying number of views.

MDI: Multiple Document Interface – an MFC application with many different types of documents and a varying number of views.

In our case we’ll only be looking at SDI applications that use the document/view philosophy. Most applications can easily be split into a single document and multiple views – especially in the case of a one-track tool like a level editor. Although it’s possible to create an MFC application without using documents or views it is more time consuming and generally less effective unless your application is very simple. This is just a taste of MFC terminology, so I still recommend reading through some of the MSDN entries on the subject. We’ll also be using the class wizard and the resource editor to maintain our application during development, so make sure you’re familiar with them, as I won’t be describing them in depth. Once again the MSDN library has many helpful entries on both tools.

Before We Start...

For this article we will be creating a simple 3D-model viewer. The layout will include four viewports to display the model in either a variety of orthogonal views or a perspective view. There will also be a bar of controls used to change each viewport’s type. The file format will be *.md3 files (Quake III Arena model files), however there will be little time spent on the actual model loading and drawing, as the general architecture is our focus. I chose the *.md3 file format simply because I had some old code from another project lying around and I didn’t feel like spending the time on another file format. It’s recommended that you download the accompanying source package.

Getting Started

Creating the Project

When choosing your project type (before or after you’ve created your workspace, depending on the way you do things) select MFC AppWizard EXE. By choosing the EXE option rather than DLL we make sure we’re creating a standalone application. Choosing this option will launch the AppWizard (hardly surprising). Choose "single document", and accept the defaults. You can turn off "printing and printing preview", just make sure that you’re using the document/view architecture.

Compiling the Project

After you’ve completed the wizard you should find a dozen files or so in your workspace. Don’t panic, this is just the beginning! Just take a few deep breaths and relax, they’re mostly class definitions and the like and will become clear with time. Before you continue compile, build and execute your project. MFC uses a precompiled header file, which has to compiled and saved to disk. Making sure you perform a complete build at the beginning of development will help you avoid cryptic link errors later.

Are we having fun yet?

Creating the Layout

The first files we will be looking at will be MainFrm.h and MainFrm.cpp which contain your CMainFrame class. Like the name suggests, CMainFrame is the main frame window for your application. While your own applications will most likely spawn dialog boxes and child windows, the main frame is the base of all the action. This is where we need to add our splitter windows.

A CSplitterWnd class or splitter window is a way to split the screen into two or more different views or windows. A comparable example might be frames in HTML, or the different panes in Microsoft Developer Studio. A splitter window should be created during the initialization of the client area of the main frame window. In order to facilitate this we need to perform two steps.

First our CSplitterWnd objects are added as members of the CMainFrame class. Note that this doesn’t initialize or create them in anyway, it just gives us the option of doing so in the future. Since our layout plan has four viewports bordered by a single pane of controls we need to use two CSplitterWnd objects to accomplish our desired layout. The first splitter will be used to split the screen into a window for the control pane and a window for the viewports. The second splitter will be used to split the viewport window into four separate windows. Again this is similar to creating frames in HTML. Somewhere in the class definition in MainFrm.h we add the following code:

CSplitterWnd m_mainSplitter,                 // Splitter windows
             m_viewportSplitter;

Secondly we must create the splitters when the client area is created. In order to do this we have to add a message map to the CMainFrame class. A message map is the way messages are passed in MFC. Whenever the class that is assigned a message map receives a certain message, it executes a named function with parameters corresponding to the pertinent information in the message. In our case we use the ClassWizard to add a message map to CMainFrame for the OnCreateClient message. Looking in MainFrm.cpp we find the following method skeleton:

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
  // TODO: Add your specialized code here and/or call the base class

  return CFrameWnd::OnCreateClient(lpcs, pContext);
}

Not bad, we now have a place to create our splitter windows. Splitter windows come in two flavors, static and dynamic. A dynamic splitter can be split and collapsed by the user, while a static splitter cannot. We’ll only be dealing with static splitters since we generally don’t want to give the user that kind of control over the layout. First we create the main splitter window and check for errors:

if ( !m_mainSplitter.CreateStatic( this, 1, 2 ) )
{
  MessageBox( "Error setting up splitter frames!", "Init Error!",
              MB_OK | MB_ICONERROR );
  return FALSE;
}

The parameters are fairly straightforward; we pass in the parent window and the number of rows and columns in the splitter. Next we must create the views that we want to fill each pane in the splitter. When creating a static splitter you must create a view or nested splitter window for each pane before it is displayed. First we create the view for our control panel:

CRect cr;  // client rectangle -
           // used to calculate client sizes

GetClientRect( &cr );
if ( !m_mainSplitter.CreateView( 0, 1, RUNTIME_CLASS(CYourProjectNameView),
        CSize( INFOBAR_SIZE, cr.Height() ), pContext ) )
{
  MessageBox( "Error setting up splitter frames!", "Init Error!",
              MB_OK | MB_ICONERROR );
  return FALSE;
}

This one’s more of a mouthful. The first two parameters are fairly simple – just the row column (zero based) indices of the splitter where the view is to be created. The third parameter is the class type that will be created. The RUNTIME_CLASS statement is a macro that simplifies passing in a dynamically declared class. CYourProjectNameView is the default view that is created by the wizard. We will return to this code later and change this class to our own. The last two parameters are just the size of the pane to be created and the context that was passed into the OnCreateClient method. Next we create the viewport splitter, nested inside the main splitter:

if ( !m_viewportSplitter.CreateStatic( &m_mainSplitter, 2, 2, WS_CHILD |
            WS_VISIBLE, m_mainSplitter.IdFromRowCol( 0, 0 ) ) )
{
  MessageBox( "Error setting up splitter frames!", "Init Error!",
              MB_OK | MB_ICONERROR );
  return FALSE;
}

Since this is a nested splitter we have a few more things to worry about. Rather than passing this as the parent window we pass in the address of the parent splitter. Also rather than passing in the context we pass the ID from the pane where we want to insert the splitter. Views must then be created for every pane in this splitter in the exact same manner as before. For now make them all the default view type, we’ll change them later.

Note that the skeleton returns the parent class’ OnCreateClient method. This is typical behavior in message maps. Often times you’ll want to poke your head in the works, but afterwards have business proceed as usual. In this particular case this is not what we want however. If we call the parent method we’ll be creating the client area a second time, basically destroying all our hard work. To prevent this just return TRUE rather than the parent method.

If you execute your program at this point you should see the splitters in place, more or less how you expected them. However if you try resizing the window you’ll find that the splitters don’t change with your window. Obviously this isn’t behavior we want in our application. This can be fixed by adding another message map to CMainFrame for the WM_SIZE message:

void CMainFrame::OnSize(UINT nType, int cx, int cy)
{
  CFrameWnd::OnSize(nType, cx, cy);

  if (  nType != SIZE_MINIMIZED )
  {
    m_mainSplitter.SetRowInfo( 0, cy, 0 );
    m_mainSplitter.SetColumnInfo( 0, cx - INFOBAR_SIZE, 0 );
    m_mainSplitter.SetColumnInfo( 1, INFOBAR_SIZE, 0 );

    // etc...

    m_mainSplitter.RecalcLayout();
    m_viewportSplitter.RecalcLayout();
  }
}

Try running this and see if you get an error. Whoops, it looks like we get a WM_SIZE message before OnCreateClient is called! To remedy this we add a flag to the CMainFrame member variables and set it to FALSE in CMainFrame’s constructor and to TRUE in OnCreateClient. By checking it in OnSize we only adjust the splitters after they’ve been initialized:

if ( m_initSplitters && nType != SIZE_MINIMIZED )
{
  // etc...

OK! Pat yourself on the back and do something soothing to relax yourself (I like to pound my head on the keyboard – make sure you note where all the keys fly, you may need them). Take an Advil and then proceed...

Creating the Viewport Classes

The viewport, as a vital piece of our application, is one place in particular that requires careful planning. Running in headfirst will most likely turn up a bunch of hacks and a lot of redundant code. I hate having to type things twice twice.

A good place to start is a base class that contains all the functionality required to get up and running in our desired API. The rest of our viewport classes will inherit from this class, making a port from one API to another simple work. The sample code deals with OpenGL, but the process should be similar with other APIs.

COpenGLWnd – Our base class with the basic OpenGL setup stuff. For more information on the specifics of creating an OpenGL view with MFC check the accompanying code or the tutorial found here. Note that this class doesn’t have a message map for WM_SIZE. That will be covered later.

CPerspective – The perspective viewport. To get an MFC class with all the right trimmings we start by creating a class with the ClassWizard with CView as the parent. Following this make sure you change ALL references to CView to COpenGLWnd. At this point we’re ready to start handling WM_SIZE messages with a message map:

// Adjust the viewport after a window sizing.
void CPerspective::OnSize(UINT nType, int cx, int cy)
{
  COpenGLWnd::OnSize(nType, cx, cy);

  if ( 0 >= cx || 0 >= cy || nType == SIZE_MINIMIZED )
    return;

  // Change the perspective viewing volume to
  // reflect the new dimensions of the window.
  SetContext();
  glViewport( 0, 0, cx, cy );
  glMatrixMode( GL_PROJECTION );
  glLoadIdentity();
  gluPerspective(45.0f, (float)(cx)/(float)(cy), 0.01f, 1000.0f);
  glMatrixMode( GL_MODELVIEW );
}

This process should look familiar to those that use OpenGL. We simple adjust the viewing volume with the new dimensions of the screen. At this point our viewport is just a boring static window, so let’s shake it up a bit. First we need to add member values to track the camera’s position and rotation:

float m_zoom,
      m_xpos,
      m_ypos,
      m_xrot,
      m_yrot;

Next we need to catch user actions to move the camera. We’ll create a process similar to camera manipulation in Maya and many other 3D packages. If the user holds down the Ctrl key and clicks and drags with the left mouse button the camera rotates, the middle mouse button zooms, and the right mouse button pans. We accomplish this by adding a message map for the WM_MOUSEMOVE message:

Where:

int  m_lastMouseX,
     m_lastMouseY;

Are class members of CPerspective:

void CPerspective::OnMouseMove(UINT nFlags, CPoint point) 
{
  // Move the camera if control is being
  // pressed and the apropriate mouse
  // button is being held down.

  if ( nFlags & MK_CONTROL )
  {
    if ( nFlags & MK_LBUTTON )
    {
      // Left mouse button is being
      // pressed. Rotate the camera.
      if ( m_lastMouseX != -1 )
      {
        m_yrot += point.y - m_lastMouseY;
        m_xrot += point.x - m_lastMouseX;
        // Redraw the viewport.
        OnDraw( NULL );
      }
      m_lastMouseX = point.x;
      m_lastMouseY = point.y;
    }

    // etc...

    else
    {
      m_lastMouseX = -1;
      m_lastMouseY = -1;
    }
  }
  else
  {
    m_lastMouseX = -1;
    m_lastMouseY = -1;
  }

  COpenGLWnd::OnMouseMove(nFlags, point);
}

Now mouse movements under the correct conditions alter the member variables representing our camera. The final step is to implement our camera variables in our WM_DRAW message map:

void CPerspective::OnDraw(CDC* pDC)
{
  SetContext();
  glLoadIdentity();

  // Position the camera
  glTranslatef( m_xpos, -m_ypos, -m_zoom );

  // Adjust viewport for md3 models which
  // use a different coordinate system -
  // 3DSMAX format.
  glRotatef( -90.0f, 1.0f, 0.0f, 0.0f );
  glRotatef( -90.0f, 0.0f, 0.0f, 1.0f );

  // Rotate the camera
  glRotatef( m_xrot, 0.0f, 0.0f, 1.0f );
  glRotatef( m_yrot, 0.0f, 1.0f, 0.0f );
  RenderScene();
  SwapGLBuffers();
}

Huzzah! Go back to MainFrm.cpp and plug your new class into one or more of the viewport panes and put some drawing code into CPerspective::RenderScene(). I used a cube with colored faces for testing purposes. It feels good to be brilliant doesn’t it?

COrthographic – COrthographic inherits from COpenGLWnd, create it in the same manner you created CPerspective. While CPerspective was a finished viewport (more or less) COrthographic is one step away from being a finished viewport, with CFront, CSide, and CTop inheriting from it. COrthographic differs from CPerspective in the fact that as an orthogonal viewport it cannot be rotated by the user, and all positioning and zooming can be done in the WM_SIZE message map because of the nature of orthographic viewports:

Where:

float m_zoom,
      m_xpos,
      m_ypos;
int   m_lastMouseX,
      m_lastMouseY;

Are class members of COrthographic:

void COrthographic::OnSize(UINT nType, int cx, int cy) 
{
  COpenGLWnd::OnSize(nType, cx, cy);

  if ( 0 >= cx || 0 >= cy || nType == SIZE_MINIMIZED )
    return;

  // Change the orthographic viewing volume to
  // reflect the new dimensions of the window
  // and the zoom and position of the viewport.
  SetContext();
  glViewport( 0, 0, cx, cy );
  glMatrixMode( GL_PROJECTION );
  glLoadIdentity();
  glOrtho( (float)(cx)/(float)(cy)*-m_zoom-m_xpos, (float)(cx)/(float)(cy)*m_zoom-m_xpos,
           -m_zoom+m_ypos, m_zoom+m_ypos, -200.0f, 200.0f );
  glMatrixMode( GL_MODELVIEW );
}

If you have trouble understanding the reasoning behind the orthographic sizing I suggest reading up on the difference between perspective and orthogonal views to get a clear idea of exactly what’s going on here. Note that COrthographic has no member variables for rotation. Orthographic views can be rotated, but in the context of 3D editor or modeler you don’t want them to be rotated by the user. The message map for WM_MOUSEMOVE is identical to the map for CPerspective except there is no check for the left mouse button (rotation) and OnSize is called just before OnDraw to apply the changes. Check the accompanying source for more information.

CFront, CSide, CTop – All inherited from COrthographic each class differs in its WM_DRAW message map with the appropriate amount of rotation for its respective viewpoint.

Double Huzzah! Go back to MainFrm.cpp and add in your orthographic viewports. Now that we have sleek sexy viewports covering most of the screen that little control panel is starting to look pretty weak. Let’s see what we can do...

Taking Control

Adding controls to a window can be accomplished in a variety of ways. One of the easiest (and fastest) ways is to use a dialog template with a CFormView class. A dialog template is a resource file that is generated by the resource editor when you layout controls for a dialog. A CFormView class allows you to add a dialog template to a window by acting like a modeless dialog box imbedded into the window. A modeless dialog box is just a dialog that functions coincidentally with the main frame. A modal dialog box is the opposite, forcing the user to do what is necessary to close the box before the main frame becomes active again.

Creating the Dialog Template

Using the resource editor create a new dialog template with four dropdown lists arranged in a square. Each of these lists will represent a viewport and allow the user to change between a front, side, top, and perspective view. Do yourself a favor and change the control IDs to meaningful names, it will make life easier later on. Since these will be static, read-only dropdown lists go ahead and enter the data for the dropdown lists (Front, Top, Side, Persp). Just a side note, there are a few aspects of making dropdown lists with the resource editor that drove me nuts, they weren’t documented very well and were quite annoying. To enter a new line in the data field you need to press Ctrl + Enter, and you must also set the dropdown height by clicking on the arrow for the list. Make sure the ordering is the same for each list.


Get the point?

Creating the Form View Class

Double click on the dialog template in the resource editor. This should open the ClassWizard with a message regarding your new template. You can also do this manually from within the ClassWizard if you don’t get a message for whatever reason. Create a new class that inherits from CFormView and uses your new dialog template (the dialog ID is selected in the dropdown list under the parent class). We’ll call this new class CInfoPannel for the rest of this article. Go back to MainFrm.cpp and change the class of the remaining window to CInfoPannel. Run the program and make sure you see the controls in the panel as expected.

So far so good. Now if only it did something...

Adding Member Variables and Message Maps

In a normal dialog box controls are maintained by creating special member variables that are automatically updated with validated data from their associated controls. Changing those same member variables can change the controls in turn. Since a CFormView class (and classes that inherit from it) is not a true dialog box we have to take a few extra steps to use this same method. First, just as though we were dealing with a dialog box, we add the member variables using the ClassWizard. This is where it pays off if you used meaningful control IDs earlier.

In order for these variables to be updated from their respective controls we have to explicitly call UpdateData, a method that is normally called by the framework to update dialog controls and member variables. By passing this method TRUE, information from a control will be stored in a member variable. By passing it FALSE, information from a member variable will update a control. We assume that we would want to change control values during the class’ WM_UPDATE message map:

void CInfoPannel::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
{
  UpdateData( FALSE );
}

Finally we need to perform some action when the dropdown selection is changed. By double clicking one of the controls in the resource editor the wizard will add a message map that corresponds to that control to the parent class. Do so for each dropdown list.

Now we’re faced with a dilemma. To change our viewports we need to access our viewport splitter. At this point the easiest thing to do is to bend the OOP rules a little bit, although C++ zealots are welcome to flame me into a cinder for it:

Where:

CSplitterWnd *m_viewports;

Is a member variable of CInfoPannel.

In CMainFrame, just after CInfoPannel is created:

// Setup a pointer to the viewport splitter to be
// used later by the information bar to change
// viewports.
((CInfoPannel*)(m_mainSplitter.GetPane( 0, 1 )))->m_viewports = &m_viewportSplitter;

We’re now able to alter the viewport splitter to our heart’s content, which is done during each dropdown list’s message map:

// Message handlers for the info bar
// controls. Viewport dropdowns.

void CInfoPannel::OnEditchangeUpperleft()
{
  // Automatically update the form view
  // member variables with the values from
  // their associated controls.
  UpdateData( TRUE );

  // Delete the view in the appropriate
  // splitter pane and create a new one
  // based on the dropdown selection.

  CRuntimeClass *newViewClass;
  CRect cr;                     // client rectange

  switch ( m_upperLeft )
  {
    case FRONTVIEWPORT:
      newViewClass = RUNTIME_CLASS( CFront );
      break;
    case TOPVIEWPORT:
      newViewClass = RUNTIME_CLASS( CTop );
      break;
    case SIDEVIEWPORT:
      newViewClass = RUNTIME_CLASS( CSide );
      break;
    case PERSPVIEWPORT:
      newViewClass = RUNTIME_CLASS( CPerspective );
      break;
  }

  m_viewports->GetClientRect( &cr );
  m_viewports->DeleteView( 0, 0 );
  m_viewports->CreateView( 0, 0, newViewClass,
      CSize( cr.Width() / 2, cr.Height() / 2 ), NULL );

  // Add the new view to the application's
  // document.
  GetDocument()->AddView( (CView*)(m_viewports->GetPane( 0, 0 )) );

  // Recalculate the splitter window so the
  // changes are displayed.
  m_viewports->RecalcLayout();
}

Take minute to wrap your brain around that, there’s a bunch of stuff there. First we call UpdateData( TRUE ) to update the member variables associated with all the controls. Next the appropriate class type is chosen based on the dropdown selection. FRONTVIEWPORT, SIDEVIEWPORT, TOPVIEWPORT, and PERSPVIEWPORT are just constants that correspond to the appropriate dropdown index number. Following that the current view for the appropriate viewport is deleted and a new one is created. The document is then updated with the new view to avoid an issue further down the line.

So you run your program and find that, amazingly enough, nothing happens. After some detective work and a bunch of swearing you’ll find that the default behavior for a dropdown message map is to run when a dropdown has been edited (the actual text has been edited, rather than the selection). Since our dropdown lists are read-only our message maps will never be called. This can be fixed simply enough by editing some of the wizard-generated code:

Change:

BEGIN_MESSAGE_MAP(CInfoPannel, CFormView)
  //{{AFX_MSG_MAP(CInfoPannel)
  ON_CBN_EDITCHANGE(IDC_UPPERLEFT, OnEditchangeUpperleft)
  ON_CBN_EDITCHANGE(IDC_UPPERRIGHT, OnEditchangeUpperright)
  ON_CBN_EDITCHANGE(IDC_LOWERLEFT, OnEditchangeLowerleft)
  ON_CBN_EDITCHANGE(IDC_LOWERRIGHT, OnEditchangeLowerright)
  //}}AFX_MSG_MAP
END_MESSAGE_MAP()

To:

BEGIN_MESSAGE_MAP(CInfoPannel, CFormView)
  //{{AFX_MSG_MAP(CInfoPannel)
  ON_CBN_SELCHANGE(IDC_UPPERLEFT, OnEditchangeUpperleft)
  ON_CBN_SELCHANGE(IDC_UPPERRIGHT, OnEditchangeUpperright)
  ON_CBN_SELCHANGE(IDC_LOWERLEFT, OnEditchangeLowerleft)
  ON_CBN_SELCHANGE(IDC_LOWERRIGHT, OnEditchangeLowerright)
  //}}AFX_MSG_MAP
END_MESSAGE_MAP()

Run the program one more time and everything should be squared away. Looking good, it almost does something now...


Introducing the power of eCube, your one stop shop for displaying static-colored cubes.

Creating the Document

So now we have a flashy little app that doesn’t really do anything. If we were making a commercial product we’d be laughing our way to the bank by this time, but unfortunately we plan on using this tool so it’s going to have to do something. Up until this point we’ve dealt with views to a great extent, but now it’s time to put the "document" back into "document/view architecture". We’ll be looking at YourProjectNameDoc.cpp and YourProjectNameDoc.h from this point on.

Adding Data to the Document

The first (and most obvious step) is to add our data to our document class. In our case we’ll just add a model object, which will contain all the info the application needs to draw the *.md3 file. We also add some code to the constructor and destructor to handle memory allocation for the model:

Where:

CMd3Model *m_currentModel;

Is a member variable of CYourProjectNameDoc:

CYourProjectNameDoc:: CYourProjectNameDoc ()
{
  // No model has been loaded yet.
  m_currentModel = NULL;
}

CYourProjectNameDoc::~ CYourProjectNameDoc ()
{
  // If a model is currently loaded
  // delete it.
  if ( m_currentModel != NULL )
    delete m_currentModel;
}

Loading the Model

This step is fairly simple because all the message maps you need have already been created by the wizard. Simply add your loading code to the serialize method. This is also where you add code for saving, however we won’t be covering that here. The CArchive object that is passed to the method will be the file that the user is asking to load:

void CYourProjectNameDoc::Serialize(CArchive& ar)
{
  if (ar.IsStoring())
  {
    // TODO: add storing code here
  }
  else
  {
    // Load the MD3 file.

    if ( m_currentModel != NULL )
      delete m_currentModel;

    m_currentModel = new CMd3Model;
    if ( !LoadMd3( ar, m_currentModel ) )
    {
      MessageBox( NULL, "Not a valid md3 file!", "File Error!",
        MB_OK | MB_ICONERROR );
      return;
    }
  }
}

And that’s it. You can further customize this process by intersecting the menu message and applying file filters to the file dialog, etc. Refer to the accompanying source for more information on how to do this. At this point you can simply add your draw code to the RenderScene method of each of your viewports. The application document can be accessed by any of its child views with the function GetDocument. Note that I used a rather cryptic method for rendering scenes in the accompanying source. I find that method works best for me, so I included it for advanced users – however I won’t explain it here as it looks like voodoo chicken scratch ( void SetRenderFunc(void (*func) (CViewerDoc* )) { m_RenderScene = func; } ).


Everything you’ve always wanted in an *.md3 viewer, and less!

Conclusion

Despite popular opinion, creating proprietary tools for game development doesn’t have to be time consuming, tedious, or intimidating, (like writing articles on creating proprietary tools for game development). Through experience and code reuse new applications can be written in mere hours (my first MFC app took about two days to complete, my second took about two hours). Hobbyists and professionals alike no longer have an excuse to hack about, now that they have the proper tools.

Joe Houston

jhouston@agilitycom.com

If you’re interested in seeing other articles relating to this subject (picking objects/verts in 3D space, drawing marquees in 3D, etc) please let me know. I’m always interested in feedback, and I welcome all comments and corrections.

Discuss this article in the forums


Date this article was posted to GameDev.net: 5/1/2001
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
Stand-alone Tools

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!