Playing with Avalon and 3D Models

I recently decided to hone up on some of the foundation technologies
within WinFX. Being a person that is drawn to UI work, I immediately
targeted Windows Presentation Foundation (previously Avalon). After
installing the SDK, I opened up a build shell and started building and
playing around with the samples. Since I'm an avid gamer and wannabe
game programmer, I decided to take a look at the 3D support within
Avalon. When I ran the Animate3DRotation demo (found in
%PROGRAMFILES%\Microsoft
SDKs\WinFX\samples\Allsamples\Avalon\GraphicsMM\3D\Animate3DRotation\XAML)
I went to investigate how the 3D models were being loaded. If you look
in the file MyApp.xaml you will see an element named MeshGeometry3D
with several attributes containing a long string of numbers. The bad
news: you have to create those numbers by hand for your mesh. The good
news: I hate bad news.

As I already mentioned, I am a wannabe games programmer and frequent
https://www.garagegames.com whenever I can (and I do own all 3 game
engines available on that site but that's a different story). Since I
am somewhat familiar with games programming, I have also written my
fair share of 3D modelling software exporters (I even wrote a exporter
for a GameBoy Advance game engine). What follows is my little foray
into the world of 3D programming with Avalon.

MilkShape is an easy to use low polygon 3D modelling package. It's
cheap and supports a large plethora of game model formats...except the
format used by XAML. So, with a little boredom setting in yesterday, I
downloaded the MilkShape SDK and began writing a XAML exporter (links
at bottom of post). The SDK is fairly simple to use. I created a WIN32
DLL application, added the MilkShape header path to my "Additional
Include Directories" and added the MilkShape lib path to my "Additional
Library Directories". When you create a MilkShape plugin there are 3
main things you have to do:

  1. Make sure your dll name starts with 'ms'. I just named my dll
    XAMLExporter.dll and for the life of me could not figure out why it
    wouldn't load. I just changed the name to msXAMLExporter.dll and it
    worked.
  2. Export a function named CreatePlugin that returns an instance to a class derived from cMsPlugIn.
  3. Implement the virtual methods defined in cMsPlugIn.

There are 3 methods defined in cMsPlugIn that the exporter implements.
The GetType method returns an enumerated value denoting whether your
plugin is a exporter, importer or tool (and a few others). The GetTitle
method returns a string that is the display string in the Export menu.
Finally, the Execute method which takes a msModel* parameter is the
main function you use to write out your custom 3D file format.

If you look at the MeshGeometry3D schema, you will notice quite a few
attributes that Avalon uses to construct the mesh. The ones I decided
to deal with are Positions, Normals, and TriangleIndices. Positions in this case refers to the actual 3D points or vertices in
your mesh. The Normals and TriangleIndices should be obvious. The
code below is my Execute method. It first makes sure there's a model to
export. If there is, it opens a save file dialog to get a filename to
save to. Next, it begins enumerating through all the meshes present in
MilkShape. Each shape you create is a seperate mesh unless you weld the
vertices of 2 meshes together to create a single mesh. For each mesh it
finds, it enumerates all the vertices and outputs them to the Positions attribute. It also enumerates all the normals and triangle
indices in the same manner. Its not rocket science, but it sure beats
having to do all this mesh work by hand. Here's my Execute method (link
to the actual project is at the end of this post and yes, it is C++ code).

 int msXAMLExporter::Execute (msModel *pModel){    if (!pModel)        return -1;    //    // check, if we have something to export    //    if (msModel_GetMeshCount (pModel) == 0)    {        ::MessageBox (NULL, "The model is empty!  Nothing exported!", "XAML Exporter", MB_OK | MB_ICONWARNING);        return 0;    }    //    // choose filename    //    OPENFILENAME ofn;    memset (&ofn, 0, sizeof (OPENFILENAME));        char szFile[MS_MAX_PATH];    char szFileTitle[MS_MAX_PATH];    char szDefExt[32] = "txt";    char szFilter[128] = "XAML Files (*.xaml)\0*.xaml\0All Files (*.*)\0*.*\0\0";    szFile[0] = '\0';    szFileTitle[0] = '\0';    ofn.lStructSize = sizeof (OPENFILENAME);    ofn.lpstrDefExt = szDefExt;    ofn.lpstrFilter = szFilter;    ofn.lpstrFile = szFile;    ofn.nMaxFile = MS_MAX_PATH;    ofn.lpstrFileTitle = szFileTitle;    ofn.nMaxFileTitle = MS_MAX_PATH;    ofn.Flags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST;    ofn.lpstrTitle = "Export XAML";    if (!::GetSaveFileName (&ofn))        return 0;    //    // export    //    FILE *file = fopen (szFile, "wt");    if (!file)        return -1;    int i, j;    char szName[MS_MAX_NAME];    for (i = 0; i < msModel_GetMeshCount (pModel); i++)    {      msMesh *pMesh = msModel_GetMeshAt (pModel, i);        msMesh_GetName (pMesh, szName, MS_MAX_NAME);     if (strlen(szName) == 0)          strcpy(szName, "myModel");      fprintf( file, "<MeshGeometry3D x:Key='%s'", szName );        //        // vertices        //         fprintf( file, " \r\nPositions='");        for (j = 0; j < msMesh_GetVertexCount (pMesh); j++)        {            msVertex *pVertex = msMesh_GetVertexAt (pMesh, j);            msVec3 Vertex;            msVertex_GetVertex (pVertex, Vertex);            fprintf (file, "%f,%f,%f ", Vertex[0], Vertex[1], Vertex[2] );        }        //        // vertex normals        //      fprintf( file, "' \r\nNormals='");         for (j = 0; j < msMesh_GetVertexNormalCount (pMesh); j++)        {            msVec3 Normal;            msMesh_GetVertexNormalAt (pMesh, j, Normal);            fprintf (file, "%f,%f,%f \n", Normal[0], Normal[1], Normal[2]);        }        //        // triangles        //        fprintf( file, "' \r\nTriangleIndices='");        for (j = 0; j < msMesh_GetTriangleCount (pMesh); j++)        {            msTriangle *pTriangle = msMesh_GetTriangleAt (pMesh, j);                        word nIndices[3];            msTriangle_GetVertexIndices (pTriangle, nIndices);                        fprintf (file, "%d %d %d \n", nIndices[0], nIndices[1], nIndices[2] );        }        fprintf( file, "'/>\r\n");    }          fclose (file);    // dont' forget to destroy the model    msModel_Destroy (pModel);    return 0;}

When you build your DLL, make sure you place it in the installation
directory of MilkShape. Not being a simple 'cube' type of guy, I opted
to use the terrain generator (Tools->Terrain Generator). After
clicking the 'Add Terrain' button, I had to scale the mesh down so the
camera in my sample XAML project can view it properly. I used the Scale
All tool twice using 10% as

the scale value. Once this was done, I clicked on
File->Export->XAML Export and saved it to a XAML file. Here's a
screenshot of MilkShape and my terrain:

Milkshape Landscape Mesh

I should note that the exporter itself does not output ready to use
XAML (i.e. you can't open it up in XAMLPad). I just had the exporter
ouput a MeshGeometry3D element. After I exported the file, I copied the
contents of that file and inserted it as a child of the
<Application.Resources> element found in the MyApp.xaml file of
my XAML project. Next, I noted the name of the 'key' attribute (in this
case it was 'terrain') and opened the Windows1.xaml file and looked for
the model it was loading. I changed the GeometryModel3D element by
using the attribute value "{StaticResource terrain}" defined in the
Geometry attribute. The word 'terrain' corresponds to the Key
attribute my exporter defined when it output the MeshGeometry3D
element. With all that finished, I hit build, ran it and saw the
following:

XAML Landscape

Believe it or not, my exporter worked on the 1st try (that rarely
happens). I haven't gotten into any of the material definitions and the
lighting that I told Avalon to use was taken directly from the
Animate3DRotation demo. If I get bored again one day, I might expand
the exporter to export material definitions in the exported XAML file
(I think I already know how to do that but I have other fish to fry if
you know what I mean).

So there you go. Avalon does 3D but it makes it much easier if you
have a 3D modelling tool that will export the XAML code for you. Here's
a link to the projects:

  • msXAMLExporter.zip : If you
    actually want to build and play around with the exporter, do the
    following:
    • Download Milkshape and install it
    • Download the Milkshape SDK and extract the content to the Milkshape installation directory (giving you a ms3dsdk directory)
    • Extract
      the msXAMLExporter project into the SDK directory and build away. If
      you do it right, you shouldn't have to update any paths.
  • Animate3DRotation.zip :
    You should be able to just extract the contents of this project
    anywhere. Open it in Visual Studio 2005 Beta 2 and build or open a
    WinFX build command prompt and type msbuild in the same location as the
    solution file. Right now the project will load the landscape mesh I
    talked about in this post. This is just a modified version of the original Animate3DRotation sample found in the WinFX SDK.

Enjoy!