A dotXSI Programming Primer
By Wessam Bahnassi
This tutorial is divided into the following sections:
Introduction
So you’ve got your models ready to live in your 3D game engine!
And you’ve just exported them to dotXSI from your favorite 3D authoring tool (XSI rules!)...
All what’s left is just to know how to translate these dumb bytes saved in the
so-called dotXSI file, into a living 3D thing! And that’s why we’re here today...
In The Beginning (Let There be Light)
Throughout this tutorial, we’ll be building a simple dotXSI application,
which reads dotXSI files and displays meshes saved in them.
The application will be based on Direct3D 9 as its output 3D API...
Why not OpenGL?
For several reasons, first of all, my personal experience
is with Direct3D. Second, translating dotXSI files to Direct3D involves more work
than that for OpenGL; the thing which I consider will give you a better idea
on how to adopt dotXSI to your specific engine.
Though I promise to keep Direct3D code to a minimum, and point out steps that differ
in OpenGL when possible.
That said; let’s review a check list of things you need
to start developing with this tutorial:
Requirements
dotXSI's nuts'n'bolts (the file structure)
dotXSI is a generic, template-based, ASCII file format...
Generic means it can theoretically hold any kind of data, from simple variable
types to multi-dimensional arrays.
Template-based because its data is organized within templates.
A template is much like a struct in C/C++. Templates are defined by a name, and
one or more parameters. Parameters have types. These are the same common types used in C/C++,
like int, float, char*, bool... etc. After you define a template,
you can declare as many instances of it as your heart desires! Instances can be nested within others.
The last thing, dotXSI is an ASCII-based file format,
which means that you can read the contents of the file easily using any text editor (e.g. Notepad).
However, this has changed with newer versions of dotXSI. As of dotXSI 3.0, you have the opportunity
to save files in binary format, which saves you a helluva space in comparison with the ASCII format.
However, don’t expect any speed differences between the two in load-time.
Binary files are not readable by us mere humans; opening them in Notepad will display
complete rubbish, which makes it very hard to tweak such files by hand.
dotXSI is much similar to Microsoft’s Direct3D X file format.
In fact, earlier versions of dotXSI used the same templates defined by D3D’s X files
(also called: Retained Mode Templates). But when it’s down to data access, Softimage’s API
(i.e. the FTK) differs widely from Microsoft’s COM-based API.
As of dotXSI 3.6.0, developers can manipulate the files using two APIs, or layers:
The core layer, which gives you direct/low-level access to the templates and their parameters,
and the second layer, called the semantic layer, allows you to logically access the same data set,
freeing you from the worries of how parameters are laid inside a template.
Of course, the semantic layer is built upon the core layer.
Good news is that the semantic layer doesn’t require any additional set of memory,
since it’s just a thin wrapper around the core layer, so you won’t be wasting memory
with redundant allocations.
Now the bad news is, that we won’t be using the semantic
layer throughout this tutorial, because if you’re able to use the core layer,
then you can definitely use the semantic layer; but the reverse is not strictly correct.
The following samples might help you get a better idea on how things work:
// A dotXSI template's definition
SI_Scene
{
char* Timing,
float start,
float end,
float frameRate,
}
// An instance of a dotXSI template
SI_Scene myGreatScene
{
"FRAMES",
1.000000,
100.000000,
29.970030,
}
// Hierarchies in dotXSI
SI_ShapeAnimation SHPANIM-Morpheus
{
// Parent template's parameters come first
"LINEAR", // Interpolation method
10, // Number of key shapes
// Children templates come next
SI_Shape SHP-Morpheus-0
{
// ...
}
// ...
SI_Shape SHP-Morpheus-9
{
// ...
}
SI_FCurve Morpheus-SHPANIM-1
{
// ...
}
}
Some dotXSI templates can magically grow their number of parameters. SI_Shape is a fine
example of this, as it adds a new array of floats for each kind of vertex element when it exists.
For example:
SI_Shape SHP-Bogus-ORG
{
2, // Number of shape arrays
"ORDERED", // Ordered means this isn't a key shape of a shape animation
3, // First array's elements count
"POSITION",
0.164448,-8.263903,-0.94169,
0.905932,-7.565758,0.607855,
1.435514,-5.374897,1.758833,
4, // Second array's elements count
"NORMAL",
0.138088,-0.923712,0.357335,
0.350603,-0.761782,0.544762,
0.533405,-0.203967,0.8209,
0.350603,0.761782,-0.544762,
}
When developing with dotXSI, it’s a good habit to export simple dotXSI files from XSI,
and observe how templates are laid out to express the scene’s model hierarchy
(use XSIDump to view the files textually). Notice how SI_Models get nested...
OK, now we have quite a background, we’re ready to set sail on our dotXSI journey...
Preparing a dotXSI Project
Make sure you have the dotXSI 3.6.1 FTK installed correctly, along with the
DirectX 9.0 SDK.
Now we're gonna do some once-and-for-all settings to the Development Environment.
Open Visual Studio.NET, in the Tools menu, choose the Options command...
Choose the Projects folder from the dialog’s left pane...
Click on the VC++ Directories sub-item to bring up the options for VC++’s default search directories...
Show the directories for Include files, and add in the two new entries:
- %XSIFTK_ROOT%\export\h\Core
- %XSIFTK_ROOT%\export\h\FTK
Then, show the directories for Library files, and add this entry:
- %XSIFTK_ROOT%\export\lib\Core
Dismiss the dialog by choosing OK. Now that the FTK’s directories setup correctly,
we can start building dotXSI applications cleanly.
Let’s start a new DirectX project, which will be used to load and output our dotXSI files.
Open the New Project dialog, and select the DirectX 9 Visual C++ Wizard from the available
projects templates (if you don’t see the DirectX 9 Wizard option, then make sure
you install it from the DirectX 9 SDK).
In the DirectX Wizard, choose Project Settings, and clear all DirectX components except Direct3D.
Make sure you choose a Single document window application. You also might want to clear
the Registry access option since this is a basic tutorial that shouldn’t mangle our registry database.
Head up to Direct3D Options, and make sure you clear support for 3D meshes (.x files) (Yuck!).
That’s it! Click on Finish to let the wizard generate our project’s files.
Of all the files generated, we’re only concerned in two of them.
These are the main application’s files EnterDotXSI.cpp and EnterDotXSI.h
(ok, and EnterDotXSI.rc since we’ll be adding a new menu option).
I know it’s starting to get a little boring, but we still have to change a couple
of those damn Project Settings. So right-click the project’s name in the Solution
Explorer window, and choose Properties from the pop-up menu.
In the left pane of the dialog, open the C/C++ folder, and choose the Code Generation item.
Set the Runtime Library option to Multi-threaded DLL for Release Configuration,
and for Debug Configuration, use Multi-threaded Debug DLL.
OK, we’re about to finish (err, the settings, that is). Navigate the left pane to the Linker folder,
and choose the Input item. There, let's add the following two entries in the Additional Dependencies
field (do it for both Debug and Release Configurations):
Great, you can dismiss this dialog now and forever, just click OK and we’re ready to rock!
One last catch (ouch!), you’ll have to copy the file XSIFtk.dll to your project’s directory
before you can run any dotXSI application. This file must also ship along with your executable,
and should reside in the same directory your executable is in (unless you put it in the Windows
System folder). This file can be fetched from %XSIFTK_ROOT%\export\bin.
Tip: You might want to add the FTK’s headers to your Visual C++ solution.
By doing this, you get that nice and helpful tool called IntelliSense working,
so Visual C++ can auto-complete all dotXSI-relevant code entities for you.
To do this, add a new folder to your project in the Solution Explorer.
Name it something meaningful like XSI FTK Headers.
Right-click the newly created folder, and choose Add->Add Existing Item...
In the dialog that appears next, navigate to your FTK’s directory, and choose all header
(.h) files that exist in %XSIFTK_ROOT%\export\h\Core.
Repeat the same operation for all headers inside %XSIFTK_ROOT%\export\h\FTK.
Now we’re really ready for some smooth dotXSI development! Let the journey begin... Set sail!
The Code: Warming Up
Now allow me to summarize the steps we’re gonna do
in the source code for our great dotXSI application:
- Add a menu command for opening dotXSI files.
- Implement some general helper functions.
- Design a “scene-graph” for our dotXSI scenes.
- Implement dotXSI file I/O routine.
- Parse dotXSI templates into our Direct3D world.
So, let’s go ahead...
First, I’m gonna introduce some global helping functions that will aid us in our work, waddya say?
Open up our big-time file EnterDotXSI.cpp, and add the following code to its start,
right after the #includes section:
//// Tutorial Code Start ////
#define GET_XSI_PARAM_VAL(pParams,Index,Value) pParams->Item(Index)->GetValue(&Value)
#define POS_TO_LEFTHANDED(Vector) Vector.z = -Vector.z
#define ROT_TO_LEFTHANDED(Vector) Vector.x = -Vector.x; Vector.y = -Vector.y
#define ANGLE_TO_RADIAN(a) a = D3DXToRadian(a)
char* ExcludePrefix(char* pInstanceName,const char szPrefix[4])
{
if ((pInstanceName == NULL) || ((char*)szPrefix == NULL))
return pInstanceName;
if (strlen(pInstanceName) < 5)
return pInstanceName;
for (BYTE i=0;i<4;i++)
if (pInstanceName[i] != szPrefix[i])
return pInstanceName;
return (pInstanceName+4);
}
//// Tutorial Code End ////
As you see, there are 4 macros, and 1 global function. I’ll skip on the first macro for now,
until we delve into the actual dotXSI code.
The second macro, POS_TO_LEFTHANDED() simply takes a vector representing a position in
right-handed 3D coordinate system, and converts it to D3D’s left-handed coordinate system.
We’re gonna use this macro a lot because, in general, we’ll be opening right-handed dotXSI files.
The conversion happens by simply multiplying the z coordinate by -1.0f. Easy, isn’t it?
However, this is different for 3D vectors representing rotations. In that case,
we’ll use ROT_TO_LEFTHANDED(), which converts right-handed rotations to left-handed by
multiplying both the x and y components by -1.0f (Still nothing hard, I suppose).
And last but not least, we need something to convert angles expressed in degrees into radians.
So there is ANGLE_TO_RADIAN(), we’re gonna need it because dotXSI generally expresses
angles in degrees, while both Direct3D and OpenGL take angles in radians.
We’re done with the macros, now the function ExcludePrefix(), the function takes two parameters,
both are strings, it excludes the second string from the beginning of the first (if it exists),
and returns that result.
For example, we’ll face many instances named in a pattern
like MSH-Manta. So, with ExcludePrefix(), you pass "MSH-Manta" as the first parameter,
and "MSH-" as the second, and the function will return "Manta". If the prefix doesn’t match,
then the function will just return the first string untouched.
The Code: Silly Win32 API Routines
In this section, we’re gonna write the code that adds a menu item which
facilitates opening dotXSI files. Mostly, this is all vanilla Win32 API code,
so I won’t be discussing it in detail. Consult your Win32 API documentation if you need more info.
Open your project’s Resource View (if it’s not visible, press Ctrl+Shift+E),
and open the IDR_MENU menu resource.
In the File menu, add the following entry right after the Config Display command:
&Open dotXSI file...\tCtrl+O
The item will be automatically assigned to an ID, which is ID_FILE_OPENDOTXSIFILE.
Alrighty, now we’ll have to write a handler for this new command.
Open EnterDotXSI.cpp, and navigate to the
function named CMyD3DApplication::MsgProc().
Add the following code inside the main switch block:
//// Tutorial Code Start ////
case WM_COMMAND:
if (LOWORD(wParam) == ID_FILE_OPENDOTXSIFILE)
{
// Show an 'Open File' common dialog...
OPENFILENAME ofn;
ZeroMemory(&ofn,sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = m_hWnd;
ofn.hInstance = g_hInst;
ofn.lpstrFilter = "dotXSI Files (*.xsi)\0*.xsi\0All Files (*.*)\0*.*\0\0";
ofn.nFilterIndex = 1;
ofn.lpstrFile = m_szFileName;
ofn.nMaxFile = 500;
ofn.lpstrFileTitle = m_szFileTitle;
ofn.nMaxFileTitle = 500;
ofn.lpstrTitle = "Open dotXSI File...";
ofn.Flags = OFN_FILEMUSTEXIST|OFN_HIDEREADONLY|OFN_PATHMUSTEXIST;
if (GetOpenFileName(&ofn) == FALSE)
break;
// Clear all previous entities...
ClearMeshes();
// Open the new file
OpenDotXSIFile(m_szFileName,m_szFileTitle);
}
break;
//// Tutorial Code End ////
As you’ve already noticed, nothing new here. Just plain Win32 API code.
We just display a common ‘Open File’ dialog box in response for our new menu command.
This dialog box prompts the user to specify a dotXSI file to be loaded in our application.
Yes, I can hear you saying: What are these functions ClearMeshes() and OpenDotXSIFile()?
Well, you asked for it captain, 'cuz we’re gonna implement those two babies soon.
Clearly, ClearMeshes() is a function that clears all meshes already loaded and viewed in our app.
OpenDotXSIFile() is very self explanatory that I’m gonna let you guess what it does (You do know, don’t you?).
Both ClearMeshes() and OpenDotXSIFile() will be implemented as member functions
of the main app’s class CMyD3DApplication.
The Code: A Simple Scene Graph
Oh! I forgot to tell you, if you’re really intending to load each and every piece
of info from dotXSI files, then you’d better have a REALLY damn scene graph system
in your engine. Now since the Direct3D Framework doesn’t provide such a thing,
I’ll implement a really compact version of a scene graph that it can hardly be named one.
We’ll write a small, fixed array of structs. Each struct represents a single dotXSI mesh
loaded from the specified file.
With each mesh, we save its transformation data (SRT values) so we can display it correctly
in our app. Let’s have a first look at this structure:
struct XSIMESH
{
bool bInitialized; // Is this a valid entry?
char szModelName[256]; // The model's name
D3DXVECTOR3 vTranslation; // Model's translation
D3DXVECTOR3 vRotation; // Model's rotation
D3DXVECTOR3 vScaling; // Model's scaling
D3DXMATRIX matWorld; // Object-to-World transformation matrix (SRT)
LPD3DXMESH pMesh; // Actual mesh data (indices, positions, normals...etc)
};
Yep! Most of the fields are self explanatory that I’m not gonna bother by speaking about them.
However, there is a couple that requires explanation.
First, bInitialized, this is a bool variable that acts as a flag that
signifies whether this mesh has been correctly
loaded and initialized or not.
The second mystical field is pMesh. This is an instance of an ID3DXMesh
object (read the DirectX documentation for more info on it). Basically, this object holds the mesh’s
indices and other vertex information such as positions, normals, colors...etc.
All indices are grouped in an index buffer, and all vertex information is stuffed into one vertex buffer.
OK, back to coding. Open up the header file EnterDotXSI.h, and get to CMyD3DApplication
class declaration. Add the following code fragment in the class’s public section:
//// Tutorial Code Start ////
FLOAT m_fWorldScale; // World scale state
DWORD m_dwTemplatesProcessed;
DWORD m_dwMeshesCount;
char m_szFileName[500]; // Device restoration purposes
char m_szFileTitle[500]; // Device restoration purposes
// Define a simple struct for holding dotXSI meshes info
struct XSIMESH
{
bool bInitialized;
char szModelName[256];
D3DXVECTOR3 vTranslation;
D3DXVECTOR3 vRotation;
D3DXVECTOR3 vScaling;
D3DXMATRIX matWorld;
LPD3DXMESH pMesh;
};
XSIMESH m_XSIMeshes[MAX_XSIMESHES_COUNT]; // For a real app, this has to be a growing list
//// Tutorial Code End ////
I’ve added the m_fWorldScale variable to enable the user to zoom-in or out the objects he sees
in addition to rotating them around.
Next comes m_dwTemplatesProcessed. This variable will hold the number of dotXSI templates
we successfully processed. Don’t worry about this so much for now.
m_dwMeshesCount will clearly hold the number of meshes that we managed to load from the dotXSI file.
Note how m_XSIMeshes is declared. For the sake of simplicity, it’s declared as a fixed array
of MAX_XSIMESHES_COUNT entries. We’ll have to #define MAX_XSIMESHES_COUNT later,
so let's keep it in our minds.
Next, declare the following member functions in the class’s public section:
//// Tutorial Code Start ////
// Generic helper functions
void ClearMeshes(void);
XSIMESH* GetNextEmptyMesh(void);
XSIMESH* FindXSIMesh(char* pName);
// dotXSI-specific functions
BOOL OpenDotXSIFile(LPSTR szFileName,LPSTR szFileTitle);
SI_Error ParseTemplates(CXSIParser *pParser,CdotXSITemplates *pTemplates);
// dotXSI Templates Handlers
SI_Error dotXSILoadFileInfo(CdotXSITemplate* pTemplate);
SI_Error dotXSILoadModel(CdotXSITemplate* pTemplate);
SI_Error dotXSILoadTransform(CdotXSITemplate* pTemplate);
SI_Error dotXSILoadTriangleList(CdotXSITemplate* pTemplate);
//// Tutorial Code End ////
Yes, I know we’re getting ahead of ourselves, but hold on your breath until we
implement the new functions. We’re getting closer.
Let’s see what ClearMeshes() actually does (add the following code in EnterDotXSI.cpp):
void CMyD3DApplication::ClearMeshes(void)
{
for (DWORD i=0;i<MAX_XSIMESHES_COUNT;i++)
{
if (m_XSIMeshes[i].pMesh != NULL)
{
m_XSIMeshes[i].pMesh->Release();
m_XSIMeshes[i].pMesh = NULL;
}
}
ZeroMemory(m_XSIMeshes,sizeof(XSIMESH)*MAX_XSIMESHES_COUNT);
m_dwMeshesCount = 0;
}
Simple enough? It just iterates our meshes array, releasing any ID3DXMesh it faces in its way,
and it resets m_dwMeshesCount to 0 along the walk.
Let’s check on the next function:
inline CMyD3DApplication::XSIMESH* CMyD3DApplication::GetNextEmptyMesh(void)
{
// Find us an empty mesh slot
for (DWORD i=0;i<MAX_XSIMESHES_COUNT;i++)
if (!m_XSIMeshes[i].bInitialized)
return &m_XSIMeshes[i];
// Sorry we're out of stock today!
return NULL;
}
It performs a quick scan on our meshes list, finding us an empty slot for our personal usage.
If it doesn’t find one, it just return’s NULL.
On to the third function:
inline CMyD3DApplication::XSIMESH* CMyD3DApplication::FindXSIMesh(char *pName)
{
for (DWORD i=0;i<MAX_XSIMESHES_COUNT;i++)
{
if (!m_XSIMeshes[i].bInitialized)
continue; // Skip uninitalized models
if (strcmp(m_XSIMeshes[i].szModelName,pName) == 0)
return &m_XSIMeshes[i]; // This is it!
}
// Sorry we're out of stock today!
return NULL;
}
With this function, you can search the meshes list for a specific mesh by its name.
This function will prove to be very handy within the next minutes.
And to wrap up this section, navigate EnterDotXSI.h to the struct UserInput
(near the top of the file), add two lines so the struct becomes like this:
// Struct to store the current input state
struct UserInput
{
// TODO: change as needed
BOOL bRotateUp;
BOOL bRotateDown;
BOOL bRotateLeft;
BOOL bRotateRight;
//// Tutorial Code Start ////
BOOL bZoomIn;
BOOL bZoomOut;
//// Tutorial Code End ////
};
And now, for the dotXSI meat... Ahoyyy!
The Code: dotXSI Parse Loop
Now get to EnterDotXSI.cpp again, and add the following #includes at its start,
right before the #include "EnterDotXSI.h" directive:
.
.
.
#include "D3DFont.h"
#include "D3DUtil.h"
#include "resource.h"
//// Tutorial Code Start ////
#define __XSI_APPLICATION_
#include <XSIParser.h>
#include <dotXSIDefines.h>
#define MAX_XSIMESHES_COUNT 24
//// Tutorial Code End ////
#include "EnterDotXSI.h"
The first #define will tell the FTK that we’re building a dotXSI application,
not something else (like a plug-in or the library itself).
Note that we didn’t #define XSI_STATIC_LINK as it’s done in the FTK’s documentation.
This is logical as we’re not linking to the FTK statically anymore.
We’re dynamically linking with XSIFtk.dll instead. So that flag is obsolete.
Next, we #include the FTK’s main header XSIParser.h. This will
#include the rest of the FTK’s headers internally, so we can use classes like
CXSIParser and CSIBCString.
The other #include, dotXSIDefines.h is a very handy header that’s gonna
relief us from keeping track of the parameters’ indices inside their relevant templates,
which in-turn, will keep our code sort of version-independent, which is a good thing.
Lastly, we #define MAX_XSIMESHES_COUNT as 24 (remember this one?).
You can set this to any value you want. Personally, I like to stick to 24!
Let’s head to the constructor of our class, and initialize some variables.
Add the following code to the function CMyD3DApplication::CMyD3DApplication() :
//// Tutorial Code Start ////
// Init our meshes list
ZeroMemory(m_XSIMeshes,sizeof(XSIMESH)*MAX_XSIMESHES_COUNT);
ZeroMemory(m_szFileName,sizeof(m_szFileName));
ZeroMemory(m_szFileTitle,sizeof(m_szFileTitle));
m_dwMeshesCount = 0;
m_dwTemplatesProcessed = 0;
m_fWorldScale = 1.0f;
//// Tutorial Code End ////
Alrighty, let’s implement CMyD3DApplication::OpenDotXSIFile(),
basically all what this functions does is, open a dotXSI file,
then start a recursive operation for parsing the templates.
Check out the code below:
BOOL CMyD3DApplication::OpenDotXSIFile(LPSTR szFileName,LPSTR szFileTitle)
{
SI_Error siError=0; // To hold error codes
CXSIParser Parser; // Main Parser Object
// Since we won't be writing dotXSI files now, then
// set file's open mode to 'Read'
Parser.SetOpenMode(OPEN_READ);
// The FTK accepts strings as CSIBCString instances, so construct one
// and initialize it with the file name we recieved from the Open File
// dialog box...
CSIBCString szFileNameSI(szFileName);
// Attempt to open the file... Save the result in our error code variable...
siError = Parser.Open(szFileNameSI);
if (siError != SI_SUCCESS)
{
DXTRACE("ERROR: Failed to open file %s...\n",szFileTitle);
return FALSE;
}
// Now that the file is open, we'll attempt to read in its data
siError = Parser.Read();
// Since we've loaded the data from the file, then we no more need to keep it open
Parser.Close();
// Check out whether we succeeded in loading the file or not
if (siError != SI_SUCCESS)
{
DXTRACE("ERROR: Failed to read dotXSI file: %s\n",szFileTitle);
return FALSE;
}
// Dump file info
DXTRACE("INFO: %s header information:\n",szFileTitle);
DXTRACE(" File version : %d.%d\n",Parser.GetdotXSIFileVersionMajor(),Parser.GetdotXSIFileVersionMinor());
// Start parsing the templates
m_dwTemplatesProcessed = 0;
m_dwMeshesCount = 0;
ParseTemplates(&Parser,Parser.dotXSITemplate());
DXTRACE("INFO: Processed %u/%u templates successfully\n",m_dwTemplatesProcessed,Parser.dotXSITemplate()->GetTotalCount());
DXTRACE(" %u Meshes loaded\n",m_dwMeshesCount);
return TRUE;
}
Nothing fancy here, the code is heavily commented. Just one note about DXTRACE(),
this macro acts just like printf(), except that it’s output is shown in the debug
output window instead of the console.
So if you want to track these messages, you should keep an eye on your IDE's output window.
Let’s see how CMyD3DApplication::ParseTemplates() does its magic:
SI_Error CMyD3DApplication::ParseTemplates(CXSIParser *pParser,CdotXSITemplates *pTemplates)
{
SI_Error eResult = SI_SUCCESS; // Error code variable
SI_Int iLoop;
CdotXSITemplate* pTemplate;
CSIBCString szTemplateName;
// Loop the top-level templates, and we'll recurse for
// their children
for (iLoop=0;iLoop<pTemplates->GetCount();iLoop++)
{
pTemplates->Item(iLoop, &pTemplate);
if(pTemplate == NULL)
{
DXTRACE("WARNING: Detected a problematic template\n");
continue;
}
// The large filter.. Based on what this template is, choose the appropriate handler function
szTemplateName = pTemplate->Name();
if (szTemplateName == "SI_FileInfo")
dotXSILoadFileInfo(pTemplate);
else if (szTemplateName == "SI_Model") // SI_Model is just a container..
dotXSILoadModel(pTemplate);
else if (szTemplateName == "SI_Transform") // Coordinate-System Dependent
dotXSILoadTransform(pTemplate);
else if (szTemplateName == "SI_TriangleList") // Coordinate-System Dependent
dotXSILoadTriangleList(pTemplate);
// Recurse for children...
eResult = ParseTemplates(pParser,&pTemplate->Children());
} // Main templates iLoop
return eResult;
}
Very straight forward, isn’t it?
Note how the function recurses for child templates.
In this tutorial, we’re gonna parse only four templates.
These will be enough to get you up and running on implementing the rest on your own.
The Code: First Look on a Template
Let's start implementing the first and easiest template at all, the SI_FileInfo
template. Actually, this template doesn’t contain very important info. It just tells
the name of the file’s creator, the time it was created (the file, not the creator!), and so on...
Open the FTK’s Developer’s Guide, and read the description of SI_FileInfo.
This will give you an idea of what parameters this template supports.
Now, let’s do the coding:
SI_Error CMyD3DApplication::dotXSILoadFileInfo(CdotXSITemplate *pTemplate)
{
// This template just outputs debug info...
CdotXSIParams *pParams;
SI_TinyVariant vValue;
pParams = &pTemplate->Params();
GET_XSI_PARAM_VAL(pParams,SI_FILEINFO_PROJ_NAME,vValue);
DXTRACE(" Project Name: %s\n",vValue.p_cVal);
GET_XSI_PARAM_VAL(pParams,SI_FILEINFO_USER_NAME,vValue);
DXTRACE(" User Name: %s\n",vValue.p_cVal);
GET_XSI_PARAM_VAL(pParams,SI_FILEINFO_SAVED_TIME,vValue);
DXTRACE(" Last Time Modified: %s\n",vValue.p_cVal);
GET_XSI_PARAM_VAL(pParams,SI_FILEINFO_ORIGINATOR,vValue);
DXTRACE(" Built in: %s\n",vValue.p_cVal);
// Increment processed templates count
m_dwTemplatesProcessed++;
return SI_SUCCESS;
}
Well well... we do have a lot of info to cover. Let’s start by outlining the main
operations we need to perform to parse any dotXSI template in general.
You start by retrieving a pointer to the parameters collection of the relevant template.
This collection is encapsulated within a CdotXSIParams object.
Next, we request a parameter from this collection by index. That is, by calling
CdotXSIParams::Item() and passing the index of the parameter you want.
So, to get the projectName parameter for SI_FileInfo, you write
CdotXSIParams::Item(0).
Similarly, you write CdotXSIParams::Item(3) to get the originator parameter.
Well, this is indeed an ugly thing! Why? Because once the template is updated with
new parameters, your indices might point to completely different parameters.
A good solution to this problem, is to use the semantic layer.
But I stated that I won’t be using it in this tutorial!
Don’t feel bad though, I won’t let you down! This is why I asked you to
#include<dotXSIDefines.h> at the start of the file.
If you sneak a peek at the contents of this file, you’ll find a bunch of #defines
that you can use to get rid of manually specifying the index of each parameter.
So, with that in mind, we can get the projectName this way:
CdotXSIParam *pParam; // Will hold the returned parameter
pParam = pParams->Item(SI_FILEINFO_PROJ_NAME);
And similarly, you can write:
// Get the 'originator' parameter
pParam = pParams->Item(SI_FILEINFO_ORIGINATOR);
OK, now that you have a CdotXSIParam that represents the parameter you requested,
you need to query for its value.
The FTK uses a unified structure that can hold
the parameter’s value no matter of what type it is. This is called SI_TinyVariant,
and I advice you to check out the FTK's Developer's Guide for more details on it,
because it's too lengthy to list here.
Just one additional note, the variantType member of the structure holds the type
of the value it represents.
But mostly, you should be aware of the type you’re expecting in application design time.
Now guess what! We’ve already written a little cool macro that simplifies all
this stuff. It’s in the beginning of the file, and its name is GET_XSI_PARAM_VAL().
What this macro does, is to take a pointer to the parameters collection,
the parameter’s index, and an SI_TinyValue, then it fills the variant with the
corresponding parameter's value. Cool, huh?
So, in the end, we can write the following single line to perform all the above hassle:
// Given that pParams has been initialized correctly, get the projectName
GET_XSI_PARAM_VAL(pParams,SI_FILEINFO_PROJ_NAME,vValue);
This is all great! Now that we’re done with the basics, we can concentrate on
something more interesting. Let’s check on the next template we want to support.
The Code: Parsing SI_Model
If you take a look at the FTK’s Developer's Guide, you’ll notice that SI_Model
is an empty template. It doesn’t contain any parameters inside it.
However, this is the template that expresses the scene hierarchy,
so you’ll have to parse it if you want to correctly import the scene hierarchy from XSI.
Let’s take a look at CMyD3DApplication::dotXSILoadModel():
SI_Error CMyD3DApplication::dotXSILoadModel(CdotXSITemplate *pTemplate)
{
// Currently, we don't handle model hierarchies. Just one level of depth
// Get the instance's name
CSIBCString szInstanceName = pTemplate->InstanceName();
// Remove the 'MDL-' prefix if it exists..
char *pModelName;
pModelName = ExcludePrefix(szInstanceName.GetText(),"MDL-");
// Add it to our models list
XSIMESH *pXSIMesh = GetNextEmptyMesh();
if (pXSIMesh == NULL)
{
DXTRACE("ERROR: Failed to get an empty XSI mesh container\n");
return SI_ERR_ALLOC_PROBLEM;
}
// Mark it as allocated
pXSIMesh->bInitialized = true;
// Save the model's name in it (the raw name)
strcpy(pXSIMesh->szModelName,pModelName);
// Increment processed templates count
m_dwTemplatesProcessed++;
return SI_SUCCESS;
}
I’ll guide you to a very nice habit when programming with the dotXSI FTK.
Use XSIDump before parsing any template! This way, you can observe the types
of parameters, their values, their names, how XSI exports them, and occasionally,
catch some undocumented behavior (it’s rare, but happens)!
Here, the code for parsing an SI_Model is simple because it doesn’t handle model
hierarchies yet. It gets the name of this model (the clean name, without the
'MDL-' prefix), finds an empty mesh slot, and initializes it with the model’s
name, so we can later bind the mesh to its relevant model.
OK, on to the next template.
The Code: It's the Matrix, Morpheus!
Next in our list, is SI_Transform. Now reading in the FTK’s Developer's Guide,
you’ll notice that this is a template with many parameters inside of it.
Those parameters express the transformation information for the relevant model,
expressed in SRT (Scaling-Rotation-Translation) form. Let’s sneak on the code:
SI_Error CMyD3DApplication::dotXSILoadTransform(CdotXSITemplate* pTemplate)
{
// SI_Transform templates are always nested within
// SI_Model templates
CdotXSITemplate* pParent = pTemplate->Parent();
CSIBCString szInstanceName = pTemplate->InstanceName();
CSIBCString szParentInstanceName = pParent->InstanceName();
// First find the model owner of this transform..
char *pModelName = ExcludePrefix(szParentInstanceName.GetText(),"MDL-");
XSIMESH* pXSIMesh = FindXSIMesh(pModelName);
if (pXSIMesh == NULL)
{
// Error.. An SI_Transform outside of an SI_Model
DXTRACE("ERROR: Instance of SI_Transform with no parent SI_Model.. Instance Name: %s .. Ignoring..\n",szInstanceName.GetText());
return SI_ERR_ELEM_NOTFOUND;
}
// This is an SI_Transform..
// Expected:
// 3 Floats Scale
// 3 Floats Rotation
// 3 Floats Translation
CdotXSIParams *pParams;
SI_TinyVariant vValue;
pParams = &pTemplate->Params();
GET_XSI_PARAM_VAL(pParams,SI_TRANSFORM_SCALX,vValue);
pXSIMesh->vScaling.x = vValue.fVal;
GET_XSI_PARAM_VAL(pParams,SI_TRANSFORM_SCALY,vValue);
pXSIMesh->vScaling.y = vValue.fVal;
GET_XSI_PARAM_VAL(pParams,SI_TRANSFORM_SCALZ,vValue);
pXSIMesh->vScaling.z = vValue.fVal;
GET_XSI_PARAM_VAL(pParams,SI_TRANSFORM_ROTX,vValue);
pXSIMesh->vRotation.x = vValue.fVal;
GET_XSI_PARAM_VAL(pParams,SI_TRANSFORM_ROTY,vValue);
pXSIMesh->vRotation.y = vValue.fVal;
GET_XSI_PARAM_VAL(pParams,SI_TRANSFORM_ROTZ,vValue);
pXSIMesh->vRotation.z = vValue.fVal;
// Convert to left-handed coordinate system (not required for OpenGL)
ROT_TO_LEFTHANDED(pXSIMesh->vRotation);
// Convert the angles from degrees (XSI default) to radians (D3D's default)
ANGLE_TO_RADIAN(pXSIMesh->vRotation.x);
ANGLE_TO_RADIAN(pXSIMesh->vRotation.y);
ANGLE_TO_RADIAN(pXSIMesh->vRotation.z);
GET_XSI_PARAM_VAL(pParams,SI_TRANSFORM_TRANSX,vValue);
pXSIMesh->vTranslation.x = vValue.fVal;
GET_XSI_PARAM_VAL(pParams,SI_TRANSFORM_TRANSY,vValue);
pXSIMesh->vTranslation.y = vValue.fVal;
GET_XSI_PARAM_VAL(pParams,SI_TRANSFORM_TRANSZ,vValue);
pXSIMesh->vTranslation.z = vValue.fVal;
// Convert to left-handed coordinate system (not required for OpenGL)
POS_TO_LEFTHANDED(pXSIMesh->vTranslation);
// Increment processed templates count
m_dwTemplatesProcessed++;
return SI_SUCCESS;
}
See how easy and straight forward it is?
We just look for the XSIMESH that the
parent SI_Model has initialized, when we get it, we start filling it with information
from this template’s parameters, which are very clear I think. Later, during rendering,
we’ll construct a matrix that groups all these transformations, and apply it to the
mesh we’re rendering. But we’re getting ahead of ourselves again, so let's get back to work!
Notice the use of the coordinate-system conversion macros. If you’re an OpenGL
buccaneer, you should omit this operation, because XSI exports using the right-handed
coordinate system by default.
Now we’re left with the big-time function. The mesh loader! So hang on and hold
your breath, we're going down!
The Code: Sensible Geometry
Parsing mesh data is the hardest thing we’ll do in this tutorial. I’m considering it hard
because we’ll be actually parsing two templates simultaneously! The first is SI_Shape,
which contains the mesh’s vertices positions, normals and other information. The second
template is SI_TriangleList. This template indices the information in SI_Shape
to compose a list of triangles composing the exported mesh. Very intuitive!
Because this is the way today’s 3D APIs work. For example, Direct3D takes a vertex buffer
(SI_Shape) and an index buffer (SI_TriangleList) which makes things really
easy to implement.
So for now, open up the FTK’s Developer’s Guide, and read what Softimage says about both
SI_Shape and SI_TriangleList. Also, make sure to say ahoy! to SI_Mesh
along the way so it doesn’t get mad on us. Once your done, examine a couple of dotXSI
files that you just exported from XSI with XSIDump, notice how mesh data is laid
inside SI_Model and SI_Mesh. Pay attention to how they relate to each
other, and how they’re named when exported. This information is vital for
our loading function.
It’s a bit long, but that’s really easy stuff:
SI_Error CMyD3DApplication::dotXSILoadTriangleList(CdotXSITemplate *pTemplate)
{
CSIBCString szInstanceName = pTemplate->InstanceName();
// Find the SI_Shape for this mesh..
// First, get the parent SI_Mesh container
if (pTemplate->Parent() == NULL)
{
DXTRACE("ERROR: SI_TriangleList(%s) with no parent SI_Mesh.. Ignoring\n",szInstanceName.GetText());
return SI_ERR_ELEM_NOTFOUND;
}
CdotXSITemplate *pShpTemplate = NULL;
CSIBCString szParentInstanceName = pTemplate->Parent()->InstanceName(); // SI_Mesh instance name
CSIBCString szShapeTemplateName("SI_Shape");
CSIBCString szShapeInstanceName;
// The SI_Shape instance name is: SHP-ModelName-ORG
// while the parent SI_Mesh instance name is: MSH-ModelName
szShapeInstanceName = szParentInstanceName;
// Convert the 'MSH' prefix to 'SHP'
szShapeInstanceName.GetText()[0] = 'S';
szShapeInstanceName.GetText()[1] = 'H';
szShapeInstanceName.GetText()[2] = 'P';
// Add the -ORG to identify the shape
szShapeInstanceName.Concat("-ORG");
if (!pTemplate->Parent()->Children().Find(&szShapeTemplateName,&szShapeInstanceName,&pShpTemplate))
{
DXTRACE("ERROR: SI_TriangleList(%s) with no SI_Shape definition.. Ignoring\n",szInstanceName.GetText());
return SI_ERR_ELEM_NOTFOUND;
}
CdotXSIParams *pTriParams,*pShpParams;
pTriParams = &pTemplate->Params();
pShpParams = &pShpTemplate->Params();
SI_TinyVariant vValue1,vValue2;
UINT uTrisCount=0;
bool bHasNormals=false;
// Get the number of triangle in the list
GET_XSI_PARAM_VAL(pTriParams,SI_TRIANGLELIST_NBTRIANGLES,vValue1);
uTrisCount = (UINT)vValue1.nVal;
if (uTrisCount == 0)
{
// An empty mesh.. Ignore it..
DXTRACE("WARNING: SI_TriangleList(%s) doesn't contain any triangles.. Ignoring\n",szInstanceName.GetText());
return SI_SUCCESS;
}
// Get the components of the shape's vertices
GET_XSI_PARAM_VAL(pTriParams,SI_TRIANGLELIST_INFORMATION,vValue1);
// Check if we have normals...
if (strstr(vValue1.p_cVal,"NORMAL") != NULL)
bHasNormals = true;
// Compose D3D's FVF (vertex descriptor)...
UINT uVertexSize = 0;
DWORD dwMeshFVF = D3DFVF_XYZ; // Each vertex contains position information
dwMeshFVF |= bHasNormals?D3DFVF_NORMAL:0; // And maybe normals
uVertexSize = D3DXGetFVFVertexSize(dwMeshFVF);
// Allocate Vertex Buffer and Index Buffer
DWORD dwVerticesCount = uTrisCount*3;
BYTE *pVB = new BYTE[uVertexSize*dwVerticesCount];
WORD *pIB = new WORD[uTrisCount*3];
if ((pVB == NULL) || (pIB == NULL))
{
SAFE_DELETE_ARRAY(pVB);
SAFE_DELETE_ARRAY(pIB);
DXTRACE("ERROR: SI_TriangleList(%s) failed to allocate memory for vertex data.. Ignoring\n",szInstanceName.GetText());
return SI_ERR_ALLOC_PROBLEM;
}
// Fill in the VB with data..
// First, vertex position
// Get the indices from the SI_TriangleList template
GET_XSI_PARAM_VAL(pTriParams,SI_TRIANGLELIST_VERTICES_ARRAY,vValue1);
// And the actual values from the SI_Shape template
GET_XSI_PARAM_VAL(pShpParams,SI_SHAPE_ARRAYx(0),vValue2);
DWORD i;
float *pVBData;
float fRTL=-1.0f; // Right-Handed to Left-Handed coordinate system
// conversion variable. For OpenGL, set this value to 1.0f
for (i=0;i<dwVerticesCount;i++)
{
// Set the pointer to the next vertex position
pVBData = (float*)(pVB+(i*uVertexSize));
// Calculate the current vertex index from SI_TriangleList
DWORD dwIndex = vValue1.p_nVal[i]*3;
// 3 Floats: X,Y,Z
*pVBData = vValue2.p_fVal[dwIndex];
*(pVBData+1) = vValue2.p_fVal[dwIndex+1];
*(pVBData+2) = vValue2.p_fVal[dwIndex+2]*fRTL; // Coordinate-System dependence
}
if (bHasNormals)
{
// Normals follow position information in the VB
DWORD dwNormalsOffset = 3*sizeof(float); // Position then Normals
// In dotXSI 3.6.1, normals follow positions in SI_TriangleList
GET_XSI_PARAM_VAL(pTriParams,SI_TRIANGLELIST_VERTICES_ARRAY+1,vValue1);
// Get to the shape's normals (right after the positions array)
GET_XSI_PARAM_VAL(pShpParams,SI_SHAPE_ARRAYx(1),vValue2);
// Do the copy
for (i=0;i<dwVerticesCount;i++)
{
// Set the pointer to the next vertex normal
pVBData = (float*)(pVB+(i*uVertexSize)+dwNormalsOffset);
// Calculate the current vertex index from SI_TriangleList
DWORD dwIndex = vValue1.p_nVal[i]*3;
// 3 Floats: nX,nY,nZ
*pVBData = vValue2.p_fVal[dwIndex];
*(pVBData+1) = vValue2.p_fVal[dwIndex+1];
*(pVBData+2) = vValue2.p_fVal[dwIndex+2]*fRTL; // Coordinate-System dependence
}
}
// Now, it's time to build the Index Buffer
// For now, we'll build a linear IB (i.e. 0,1,2,3,4...)
// VB requires vertices swap for accurate culling in D3D
// v0,v1,v2 => v0,v2,v1 .. Do it through the IB
for (WORD j=0;j<uTrisCount*3;j+=3)
{
// Fill in the IB.. Sequential Vertices
pIB[j] = j;
pIB[j+1] = j+2; // D3D
pIB[j+2] = j+1; // D3D
//pIB[j+1] = j+1; // OpenGL
//pIB[j+2] = j+2; // OpenGL
}
// Find our model's container
XSIMESH *pXSIMesh = FindXSIMesh(ExcludePrefix(szParentInstanceName.GetText(),"MSH-"));
if (pXSIMesh == NULL)
{
SAFE_DELETE_ARRAY(pVB);
SAFE_DELETE_ARRAY(pIB);
DXTRACE("ERROR: Failed to find a model for SI_Mesh(%s).. Ignoring\n",szParentInstanceName.GetText());
return SI_ERR_ELEM_NOTFOUND;
}
// Should we use Software Vertex Processing?
DWORD dwSWVP = m_pd3dDevice->GetSoftwareVertexProcessing()?D3DXMESH_SOFTWAREPROCESSING:0;
// Create the D3DX mesh
HRESULT Retval;
Retval = D3DXCreateMeshFVF(
uTrisCount, // NumFaces
dwVerticesCount, // NumVertices
D3DXMESH_MANAGED|D3DXMESH_WRITEONLY|dwSWVP, // Options
dwMeshFVF, // FVF
m_pd3dDevice, // pDevice
&pXSIMesh->pMesh); // ppMesh
if (FAILED(Retval))
{
SAFE_DELETE_ARRAY(pVB);
SAFE_DELETE_ARRAY(pIB);
DXTRACE("ERROR: Failed to create D3DX mesh for SI_Mesh(%s).. Ignoring\n",szParentInstanceName.GetText());
return Retval;
}
// Fill in the data
BYTE *pData = NULL;
pXSIMesh->pMesh->LockVertexBuffer(0,(void**)&pData);
memcpy(pData,pVB,uVertexSize*dwVerticesCount);
pXSIMesh->pMesh->UnlockVertexBuffer();
pXSIMesh->pMesh->LockIndexBuffer(0,(void**)&pData);
memcpy(pData,pIB,uTrisCount*3*sizeof(WORD)); // We use 16-bit indices (WORD)
pXSIMesh->pMesh->UnlockIndexBuffer();
// Done with the temp buffers
SAFE_DELETE_ARRAY(pVB);
SAFE_DELETE_ARRAY(pIB);
// Increment processed templates count
m_dwTemplatesProcessed++;
m_dwMeshesCount++;
return SI_SUCCESS;
}
//// Tutorial Code End ////
OK, if we’re really to understand how dotXSILoadTriangleList() really works,
we need to get an abstract view of the operations it performs before we delve into
the details.
In order to correctly parse an SI_TriangleList, we need to perform the following steps:
- Get the mesh's SI_Shape template which contains the actual vertex data.
- Get the mesh’s SI_TriangleList which indices the required vertex components from SI_Shape.
- Allocate memory to fit in the Vertex Buffer data we’ll be passing to Direct3D.
- Allocate memory for the Index Buffer which will be passed to Direct3D also.
- With the info from SI_TriangleList, read in the correct vertex element values from SI_Shape,
and save them into the Vertex Buffer.
- Perform any coordinate-system conversion operations required for correct Direct3D display.
- Compose the Index Buffer (very easy!).
- Create the Direct3D mesh, and save it in our scene graph for rendering.
Let’s begin with first step: getting the relevant SI_Shape.
As you might have noticed, Both SI_Shape and SI_TriangleList must be children
of an SI_Mesh container. And the SI_Mesh instance name is always prefixed with
'MSH-', like 'MSH-Parrot'. And the child SI_Shape gets prefixed
with 'SHP-' and postfixed with '-ORG'. So, for the previous SI_Mesh
instance, the corresponding SI_Shape instance will be named 'SHP-Parrot-ORG'.
So, what we’ll do is to ask SI_Mesh MSH-Parrot to find us a child that’s named
SI_Shape SHP-Parrot-ORG. The following code just does this (forget about the parrot thing):
// Find the SI_Shape for this mesh..
// First, get the parent SI_Mesh container
if (pTemplate->Parent() == NULL)
{
DXTRACE("ERROR: SI_TriangleList(%s) with no parent SI_Mesh.. Ignoring\n",szInstanceName.GetText());
return SI_ERR_ELEM_NOTFOUND;
}
CdotXSITemplate *pShpTemplate = NULL;
CSIBCString szParentInstanceName = pTemplate->Parent()->InstanceName(); // SI_Mesh instance name
CSIBCString szShapeTemplateName("SI_Shape");
CSIBCString szShapeInstanceName;
// The SI_Shape instance name is: SHP-ModelName-ORG
// while the parent SI_Mesh instance name is: MSH-ModelName
szShapeInstanceName = szParentInstanceName;
// Convert the 'MSH' prefix to 'SHP'
szShapeInstanceName.GetText()[0] = 'S';
szShapeInstanceName.GetText()[1] = 'H';
szShapeInstanceName.GetText()[2] = 'P';
// Add the -ORG to identify the shape
szShapeInstanceName.Concat("-ORG");
if (!pTemplate->Parent()->Children().Find(&szShapeTemplateName,&szShapeInstanceName,&pShpTemplate))
{
DXTRACE("ERROR: SI_TriangleList(%s) with no SI_Shape definition.. Ignoring\n",szInstanceName.GetText());
return SI_ERR_ELEM_NOTFOUND;
}
There’s some string manipulation trickery in the play, done with the aid of the
CSIBCString class.
Anyhow, the actual search for SI_Shape happens in the call to
CdotXSITemplates::Find().
Look up the FTK’s Reference Guide for more information on this function.
Now we have the SI_Shape template in hand (saved in pShpTemplate),
and we do already have the SI_TriangleList (passed to the function as pTemplate).
Now on to the next step, we need to allocate memory for our Vertex Buffer (VB)
and Index Buffer (IB). We have to know how many triangles are there in the
SI_TriangleList, so we can estimate the size of the buffers.
This code fragment does this:
CdotXSIParams *pTriParams,*pShpParams;
pTriParams = &pTemplate->Params();
pShpParams = &pShpTemplate->Params();
SI_TinyVariant vValue1,vValue2;
UINT uTrisCount=0;
bool bHasNormals=false;
// Get the number of triangles in the list
GET_XSI_PARAM_VAL(pTriParams,SI_TRIANGLELIST_NBTRIANGLES,vValue1);
uTrisCount = (UINT)vValue1.nVal;
if (uTrisCount == 0)
{
// An empty mesh.. Ignore it..
DXTRACE("WARNING: SI_TriangleList(%s) doesn't contain any triangles.. Ignoring\n",szInstanceName.GetText());
return SI_SUCCESS;
}
Now we have to know what information does our vertices should contain.
For the sake of simplicity, I just wrote the code that’s aware of the additional
presence of normals (they're almost always there anyways).
You might want to add code that handles per-vertex colors and UV coordinates sets too.
Once you know how to do it with normals, you should be able to implement the rest
alone, because the code is almost identical.
Let’s see what’s happening on the ship’s board:
// Get the components of the shape's vertices
GET_XSI_PARAM_VAL(pTriParams,SI_TRIANGLELIST_INFORMATION,vValue1);
// Check if we have normals...
if (strstr(vValue1.p_cVal,"NORMAL") != NULL)
bHasNormals = true;
// Compose D3D's FVF (vertex descriptor)...
UINT uVertexSize = 0;
DWORD dwMeshFVF = D3DFVF_XYZ; // Each vertex contains position information
dwMeshFVF |= bHasNormals?D3DFVF_NORMAL:0; // And maybe normals
uVertexSize = D3DXGetFVFVertexSize(dwMeshFVF);
// Allocate Vertex Buffer and Index Buffer
DWORD dwVerticesCount = uTrisCount*3;
BYTE *pVB = new BYTE[uVertexSize*dwVerticesCount];
WORD *pIB = new WORD[uTrisCount*3];
if ((pVB == NULL) || (pIB == NULL))
{
SAFE_DELETE_ARRAY(pVB);
SAFE_DELETE_ARRAY(pIB);
DXTRACE("ERROR: SI_TriangleList(%s) failed to allocate memory for vertex data.. Ignoring\n",szInstanceName.GetText());
return SI_ERR_ALLOC_PROBLEM;
}
Wasn’t that easy or what? Note that we allocated vertices that are of count
uTrisCount*3, which means that each triangle will be described by 3 unique
vertices. This isn’t truly the case, because that way we’re omitting the need for
an Index Buffer. But if you really want to implement true indexing, you’ll have
to do some additional operations. I won’t show you how to do it here, because it’s a
little bit too lengthy and might not be suitable for the young pirates among us.
Our next stop, filling the VB with actual data from SI_Shape. Just in case
there are non-D3D coders around here, a D3D Vertex Buffer contains vertex information
in an interleaved way. The figure below describes this:
If you remember how data is laid inside SI_Shape, you’ll notice that each vertex
component is listed in a contiguous array. That is, an array that holds all the
positions, then a different array that contains all the normals, and so on.
So we’ll have to do some VB composing to get things right for Direct3D.
We’ll do a full pass on the vertices just to copy their positions into the VB,
and do another full pass to fill in the normals, and so on. There’s a lot of array
and pointer manipulation code ahead, so keep your mind clear here:
// Fill in the VB with data..
// First, vertex position
// Get the indices from the SI_TriangleList template
GET_XSI_PARAM_VAL(pTriParams,SI_TRIANGLELIST_VERTICES_ARRAY,vValue1);
// And the actual values from the SI_Shape template
GET_XSI_PARAM_VAL(pShpParams,SI_SHAPE_ARRAYx(0),vValue2);
DWORD i;
float *pVBData;
float fRTL=-1.0f; // Right-Handed to Left-Handed coordinate system
// conversion variable. For OpenGL, set this value to 1.0f
for (i=0;i<dwVerticesCount;i++)
{
// Set the pointer to the next vertex position
pVBData = (float*)(pVB+(i*uVertexSize));
// Calculate the current vertex index from SI_TriangleList
DWORD dwIndex = vValue1.p_nVal[i]*3;
// 3 Floats: X,Y,Z
*pVBData = vValue2.p_fVal[dwIndex];
*(pVBData+1) = vValue2.p_fVal[dwIndex+1];
*(pVBData+2) = vValue2.p_fVal[dwIndex+2]*fRTL; // Coordinate-System dependence
}
vValue1 holds the indices, and vValue2 holds the actual data from SI_Shape.
We just index into SI_Shape using SI_TriangleList’s indices, and save the values
into our Vertex Buffer.
Pay attention to that sneaky fRTL variable. This is a quick dirty way to convert
right-handed coordinates to left-handed on the fly while copying the position data.
By now, you should’ve figured out how the code for copying normals works.
So, I’m just gonna list it and skim over it:
if (bHasNormals)
{
// Normals follow position information in the VB
DWORD dwNormalsOffset = 3*sizeof(float); // Position then Normals
// In dotXSI 3.6.1, normals follow positions in SI_TriangleList
GET_XSI_PARAM_VAL(pTriParams,SI_TRIANGLELIST_VERTICES_ARRAY+1,vValue1);
// Get to the shape's normals (right after the positions array)
GET_XSI_PARAM_VAL(pShpParams,SI_SHAPE_ARRAYx(1),vValue2);
// Do the copy
for (i=0;i<dwVerticesCount;i++)
{
// Set the pointer to the next vertex normal
pVBData = (float*)(pVB+(i*uVertexSize)+dwNormalsOffset);
// Calculate the current vertex index from SI_TriangleList
DWORD dwIndex = vValue1.p_nVal[i]*3;
// 3 Floats: nX,nY,nZ
*pVBData = vValue2.p_fVal[dwIndex];
*(pVBData+1) = vValue2.p_fVal[dwIndex+1];
*(pVBData+2) = vValue2.p_fVal[dwIndex+2]*fRTL; // Coordinate-System dependence
}
}
Whew, we’ve actually sailed 80% of the journey, and we’re about to get to the shore
(within about two days... Just kidding).
All what’s left is some easy steppies and we’re done.
Next, we’ll construct the
Index Buffer. As I said, because of the way we loaded data into the Vertex Buffer,
there won't be vertex reusage, which eliminates the main reason for using an Index Buffer.
Anyway, it’s still required by Direct3D, so let’s take a look at how it’s constructed:
// Now, it's time to build the Index Buffer
// For now, we'll build a linear IB (i.e. 0,1,2,3,4...)
// VB requires vertices swap for accurate culling in D3D
// v0,v1,v2 => v0,v2,v1 .. Do it through the IB
for (WORD j=0;j<uTrisCount*3;j+=3)
{
// Fill in the IB.. Sequential Vertices
pIB[j] = j;
pIB[j+1] = j+2; // D3D
pIB[j+2] = j+1; // D3D
//pIB[j+1] = j+1; // OpenGL
//pIB[j+2] = j+2; // OpenGL
}
Nothing fancy. Let’s get on to the next step.
We have to retrieve the XSIMESH
container that should hold our newly loaded mesh. This is done with this code:
// Find our model's container
XSIMESH *pXSIMesh = FindXSIMesh(ExcludePrefix(szParentInstanceName.GetText(),"MSH-"));
if (pXSIMesh == NULL)
{
SAFE_DELETE_ARRAY(pVB);
SAFE_DELETE_ARRAY(pIB);
DXTRACE("ERROR: Failed to find a model for SI_Mesh(%s).. Ignoring\n",szParentInstanceName.GetText());
return SI_ERR_ELEM_NOTFOUND;
}
The code left is plain Direct3D code, so don’t pay much attention to it if you’re an
OpenGL inhabitant. It just initializes an ID3DXMesh, and fills it with the data we just
extracted. For more info on ID3DXMesh, consult your DirectX documentation.
// Should we use Software Vertex Processing?
DWORD dwSWVP = m_pd3dDevice->GetSoftwareVertexProcessing()?D3DXMESH_SOFTWAREPROCESSING:0;
// Create the D3DX mesh
HRESULT Retval;
Retval = D3DXCreateMeshFVF(
uTrisCount, // NumFaces
dwVerticesCount, // NumVertices
D3DXMESH_MANAGED|D3DXMESH_WRITEONLY|dwSWVP, // Options
dwMeshFVF, // FVF
m_pd3dDevice, // pDevice
&pXSIMesh->pMesh); // ppMesh
if (FAILED(Retval))
{
SAFE_DELETE_ARRAY(pVB);
SAFE_DELETE_ARRAY(pIB);
DXTRACE("ERROR: Failed to create D3DX mesh for SI_Mesh(%s).. Ignoring\n",szParentInstanceName.GetText());
return Retval;
}
// Fill in the data
BYTE *pData = NULL;
pXSIMesh->pMesh->LockVertexBuffer(0,(void**)&pData);
memcpy(pData,pVB,uVertexSize*dwVerticesCount);
pXSIMesh->pMesh->UnlockVertexBuffer();
pXSIMesh->pMesh->LockIndexBuffer(0,(void**)&pData);
memcpy(pData,pIB,uTrisCount*3*sizeof(WORD)); // We use 16-bit indices (WORD)
pXSIMesh->pMesh->UnlockIndexBuffer();
// Done with the temp buffers
SAFE_DELETE_ARRAY(pVB);
SAFE_DELETE_ARRAY(pIB);
Yep! And that concludes our implementation of dotXSILoadTriangleList().
The Code: A Direct3D Chore
Let’s finish things off by adjusting some functionality in CMyD3DApplication.
Go to the implementation of CMyD3DApplication::InitDeviceObjects(),
and comment out the lines that create a teapot (you won’t see those if you
didn’t specify a teapot in the AppWizard):
//// Tutorial Code Start ////
// Delete (or comment) the following lines
// Create a teapot mesh using D3DX
//if( FAILED( hr = D3DXCreateTeapot( m_pd3dDevice, &m_pD3DXMesh, NULL ) ) )
//return DXTRACE_ERR( "D3DXCreateTeapot", hr );
//// Tutorial Code End ////
Also, it would be bright to add the following couple of lines to the end of the function:
//// Tutorial Code Start ////
// Attempt to open the last valid file
if (strlen(m_szFileName) > 0)
OpenDotXSIFile(m_szFileName,m_szFileTitle);
//// Tutorial Code End ////
This will instruct the framework to reload the dotXSI file when the Direct3D
device gets destroyed then reinitialized. Because when that happens, we have to
destroy everything that depends on Direct3D then recreate it again, which
–in our case- happens to be the ID3DXMesh object.
Let’s see the code that prepares our meshes for rendering. It’s in
CMyD3DApplication::FrameMove(). We have to add code that calculates the
transformation matrix for our mesh, plus the code that allows the user to “zoom”
in and out of the scene. Let’s see the new FrameMove() that contains all these settings:
HRESULT CMyD3DApplication::FrameMove()
{
// TODO: update world
//// Tutorial Code Start ////
// Build our models' transformation matrices
// using SRT order of multiplication
for (DWORD i=0;i<MAX_XSIMESHES_COUNT;i++)
{
if (!m_XSIMeshes[i].bInitialized)
continue; // Uninitialize model, skip it
XSIMESH *pXSIMesh = &m_XSIMeshes[i];
D3DXMATRIX matTemp;
// Scaling goes first
D3DXMatrixScaling(&pXSIMesh->matWorld,
pXSIMesh->vScaling.x,
pXSIMesh->vScaling.y,
pXSIMesh->vScaling.z);
// Rotation comes next (X,Y then Z)
D3DXMatrixRotationX(&matTemp,pXSIMesh->vRotation.x);
D3DXMatrixMultiply(&pXSIMesh->matWorld,&pXSIMesh->matWorld,&matTemp);
D3DXMatrixRotationY(&matTemp,pXSIMesh->vRotation.y);
D3DXMatrixMultiply(&pXSIMesh->matWorld,&pXSIMesh->matWorld,&matTemp);
D3DXMatrixRotationZ(&matTemp,pXSIMesh->vRotation.z);
D3DXMatrixMultiply(&pXSIMesh->matWorld,&pXSIMesh->matWorld,&matTemp);
// Finaly, translation
D3DXMatrixTranslation(&pXSIMesh->matWorld,
pXSIMesh->vTranslation.x,
pXSIMesh->vTranslation.y,
pXSIMesh->vTranslation.z);
D3DXMatrixMultiply(&pXSIMesh->matWorld,&pXSIMesh->matWorld,&matTemp);
}
//// Tutorial Code End ////
// Update user input state
UpdateInput( &m_UserInput );
// Update the world state according to user input
D3DXMATRIX matWorld;
D3DXMATRIX matRotY;
D3DXMATRIX matRotX;
if( m_UserInput.bRotateLeft && !m_UserInput.bRotateRight )
m_fWorldRotY += m_fElapsedTime;
else if( m_UserInput.bRotateRight && !m_UserInput.bRotateLeft )
m_fWorldRotY -= m_fElapsedTime;
if( m_UserInput.bRotateUp && !m_UserInput.bRotateDown )
m_fWorldRotX += m_fElapsedTime;
else if( m_UserInput.bRotateDown && !m_UserInput.bRotateUp )
m_fWorldRotX -= m_fElapsedTime;
D3DXMatrixRotationX( &matRotX, m_fWorldRotX );
D3DXMatrixRotationY( &matRotY, m_fWorldRotY );
D3DXMatrixMultiply( &matWorld, &matRotX, &matRotY );
//// Tutorial Code Start ////
// Add scaling control
if( m_UserInput.bZoomIn && !m_UserInput.bZoomOut )
m_fWorldScale += m_fElapsedTime;
else if( m_UserInput.bZoomOut && !m_UserInput.bZoomIn )
m_fWorldScale -= m_fElapsedTime;
D3DXMatrixScaling(&matRotX,m_fWorldScale,m_fWorldScale,m_fWorldScale);
D3DXMatrixMultiply(&matWorld,&matWorld,&matRotX);
// Transform all of our meshes with the "camera" matrix
for (DWORD i=0;i<MAX_XSIMESHES_COUNT;i++)
{
if (!m_XSIMeshes[i].bInitialized)
continue; // Uninitialize model, skip it
D3DXMatrixMultiply(&m_XSIMeshes[i].matWorld,&m_XSIMeshes[i].matWorld,&matWorld);
}
//// Tutorial Code End ////
//// Tutorial Code Start ////
// Delete (or comment) the following lines
//m_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld );
//// Tutorial Code End ////
return S_OK;
}
First, we iterate our meshes list, and for each mesh, we calculate a transformation
matrix, composed of Scaling, Rotation and then Translation (in that order).
By the end of the function, we re-iterate all the meshes list again, but this time,
we multiply their transformation matrices with a “camera” matrix. Now this is not
truly a camera matrix. It’s a matrix that the framework calculates based on the user’s
input. So it behaves like a camera. With it, you can rotate and scale the meshes you see.
Let’s get to CMyD3DApplication::UpdateInput() and add the zooming code.
The function should look like this:
void CMyD3DApplication::UpdateInput( UserInput* pUserInput )
{
pUserInput->bRotateUp = ( m_bActive && (GetAsyncKeyState( VK_UP ) & 0x8000) == 0x8000 );
pUserInput->bRotateDown = ( m_bActive && (GetAsyncKeyState( VK_DOWN ) & 0x8000) == 0x8000 );
pUserInput->bRotateLeft = ( m_bActive && (GetAsyncKeyState( VK_LEFT ) & 0x8000) == 0x8000 );
pUserInput->bRotateRight = ( m_bActive && (GetAsyncKeyState( VK_RIGHT ) & 0x8000) == 0x8000 );
//// Tutorial Code Start ////
pUserInput->bZoomIn = ( m_bActive && (GetAsyncKeyState( VK_ADD ) & 0x8000) == 0x8000 );
pUserInput->bZoomOut = ( m_bActive && (GetAsyncKeyState( VK_SUBTRACT ) & 0x8000) == 0x8000 );
//// Tutorial Code End ////
}
It’s very clear that we bind the add (+) key to the zoom-in functionality,
and the subtract (-) key to zoom-out.
Okaaaay, now let’s tweak the main rendering function CMyD3DApplication::Render()
to make it render our tiny list:
HRESULT CMyD3DApplication::Render()
{
// Clear the viewport
m_pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
0x000000ff, 1.0f, 0L );
// Begin the scene
if( SUCCEEDED( m_pd3dDevice->BeginScene() ) )
{
// TODO: render world
//// Tutorial Code Start ////
// Traverse our meshes list, drawing each one
for (DWORD i=0;i<MAX_XSIMESHES_COUNT;i++)
{
if (!m_XSIMeshes[i].bInitialized)
continue; // Uninitialize model, skip it
if (m_XSIMeshes[i].pMesh == NULL)
continue; // An empty model, skip it
m_pd3dDevice->SetTransform(D3DTS_WORLD,&m_XSIMeshes[i].matWorld);
m_XSIMeshes[i].pMesh->DrawSubset(0);
}
//// Tutorial Code End ////
//// Tutorial Code Start ////
// Delete (or comment) the following lines
// Render the teapot mesh
//m_pD3DXMesh->DrawSubset(0);
//// Tutorial Code End ////
// Render stats and help text
RenderText();
// End the scene.
m_pd3dDevice->EndScene();
}
return S_OK;
}
No comments!
Finally, head to CMyD3DApplication::DeleteDeviceObjects() and comment out the line
that releases the teapot mesh (m_pD3DXMesh).
Land Ho!
Yes, we’ve arrived to the end of this scurvy tutorial!
You’re free now to compile the app and watch Direct3D displaying your dotXSI files.
Use the arrow keys to rotate the meshes you see, and use (+) and (-) to zoom-in and out.
I’ve supplied a couple of dotXSI files that contain typical meshes.
You can load them and watch how they get parsed by the application.
Also, don’t forget your sail's navigation map XSIDump, with which you can have an overlook
of how the file’s templates are exported and organized.
And before I say good bye, I have a couple of comments on the code we just written.
First, don’t measure speed with this code. This code is not for performance.
It’s written to be clear and informative.
Second, I’ve deliberated to do some jobs in a not-so-good way just to keep clarity
and ease to maximum. This includes the case where we loaded the triangle list
in a sub-optimal way. That’s because –as you’ve noticed- we didn’t use shared vertices
at all. So if two triangles share two vertices, the code will actually split them
and redefine the same vertex twice instead of indexing it twice.
The code for optimized loading is more complex, so I don’t think it would fit in with
this tutorial.
And another thing is about your engine. In this tutorial, I used the Direct3D
Framework to host our dotXSI scene. In a real world application, you’ll be parsing
dotXSI files into some world-class engine (hopefully). And believe me, the more your
engine is flexible and advanced, the easier you’ll find it to load in your dotXSI files.
For example, one engine might be able to take separate vertex streams as inputs,
and it automatically prepares them for rendering, thus you’re free from the hassle
of doing it yourself. And as the templates you want to support grow, you’ll require
more features from your engine. And if it’s bad, you might end up failing to support
the whole thing.
And don’t go nuts trying to support each and every template in dotXSI!
Because you simply can’t (even XSI itself doesn't parse them all!).
Instead, only parse those templates you DO require in your application.
And remember, Softimage holds an active discussion list
(3dgames@softimage.com),
which you can subscribe to when you want to discuss dotXSI issues, follow
the instructions on this link to subscribe.
P.S. If you felt mad on the colors used in the application, then don’t blame me!
The Direct3D Framework defaults to that dumb blue background, and that eye-popping
bright red material. You can change them if you want (and I prefer you do so!) to
something more relieving.
OK, that’s it for today. I’m off.
Good bye, ye scurvy dogs!
See Also
Special Thanks
For all the ever patient readers of this article, I know I was a little late for releasing this article, but actually I had to steal seconds from my work hours
just to get this article done. And special thanks go to the Softimage folks (lead by Maggie Kathwaroon) who technically-proofed this article. Thanks Maggie,
you always give me the enthusiasm to write dotXSI stuff.