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 http://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!