In|Framez® Papers

dotXSI Programming – Animated!

By Wessam Bahnassi


This tutorial is divided into the following sections:


Welcome Back...

Ah yes...
Indeed this article took a long time to come out, but it is very important if you ask me. By the end of this series of dotXSI tutorials, I intend to leave you with the ability to fully read and animate characters in your game via dotXSI files exported from Softimage|XSI (and other packages that can export dotXSI too).

This article covers an important requirement of operating animated characters, which is the ability to play animations in the first place, plus, the fact that these animations will be applied to transformation nodes that compose the character’s skeleton hierarchy. So, we need a database that can hold and manage these things for us. Enter the scene graph!

To sum up, in this article we will:
Also, there are always those nifty small tips and side quests that we will be facing during our journey (e.g. reading mentalRay shader information into D3D materials and reading texture nodes from the render tree).

On to the core!

dotXSI Animated! Time for some action!



Requirements




Words On The Sample Code

You will notice that in this time the project is built with the Microsoft DirectX 9.0 February 2005 SDK. The sample framework for this SDK version has changed a lot since its appearance in the first DirectX 9 SDKs. Now that the SDK is updating every couple of months, you might find the sample framework different in your SDK, so you might not be able to walk with the tutorial step-by-step. However, it should build correctly as we are not making use of the continuously evolving parts of the SDK (D3DX to be specific).
If you’re not interested in the whole D3D thing, and just want to learn how to load dotXSI into that "other" API then you should be running fine too. The code is isolated and well-commented so you can easily substitute API-specific parts of the code with your own and get things working quickly (I even added notes for users of "that" other API in the code for API-specific adjustments).

Download the sample code here (1,637,037 bytes) (including a working binary).


Code Layout

The project is a simple Direct3D application taken directly from the DirectX SDK Sample Browser 'Empty Project'. In addition to the project’s initial files, we have added two files for one module, and these are: SceneGraph.h and SceneGraph.cpp. We can think of the project as being divided to these sections:
  1. Internal DirectX Framework code.
  2. Application-specific code directly communicating with the DirectX framework.
  3. Application-specific code directly communicating with the dotXSI FTK.

In the sample code, the files with names looking like dx*.* are files pertaining to section 1 above. Section 2 is resembled by the file dotXSIGuys.cpp. So, we’re left with two files representing section 3. These are SceneGraph.h and its implementor SceneGraph.cpp, and these are the files we will be working with the most time in this article.

The sample framework has a file named dxstdafx.h (the DirectX-version of the infamous MFC primary #include). In this header, we add a couple of #includes for ourselves to be able to make use of the dotXSI FTK within our project. Also, we borrow the <vector> template class from C++’s STL. We need this in our scene graph as we will see shortly. Below is an excerpt of the code we added to dxstdafx.h:


#include <vector>

#define __XSI_APPLICATION_
#define XSI_STATIC_LINK		// This flag is now necessary
#pragma warning(disable: 4100 4244 4511 4505)	// Disable some warnings for the FTK, oh shame Softimage!
#include <SemanticLayer.h>
#include <SIILPICFileDriver.h>
#pragma warning(default: 4100 4244 4511)

#include "..\SceneGraph.h"

using namespace std;



Of Scene And Graph


So what is a scene graph anyway?

It is an important module that keeps track of all the various things you’re using in your scene. For example, a scene graph holds all the meshes that compose your scene, and not just meshes. The scene graph manages all entities, like textures, transformation nodes, characters, dogs, shoes, you name it! Actually it can hold anything you like. It is a central repository that you refer to whenever you will be asking for a certain object in your scene. So it is totally up to you to define what and what not to track in the scene graph.
Scene graphs have many different ways of implementation, all of which are beyond the scope of this article. The scene graph we will be using here is very simple that it can hardly be name one. All it does is to hold an array of references to all the meshes and transformation nodes in the scene. Why hold them? Let me introduce the outline of the sample code’s operation:
  1. Initialize the 3D device.
  2. Load a dotXSI scene into the scene graph.
  3. Render all meshes in the scene.
  4. Have fun!

Step 3 shows that we need to render all meshes in the scene. This implies that we need to have a list of the meshes in the scene stored somewhere, and somewhere in our case is the scene graph.

Let’s take a look at the class declaration of our scene graph:

class CSceneGraph
{
public:
	CSceneGraph();
	~CSceneGraph();
	void Clear(void);

	bool LoadSceneFromdotXSI(const char *pszFileName,IDirect3DDevice9 *pD3DDevice);

	void StepTime(float fFrames);
	void RenderScene(void);

protected:
	bool dotXSILoadMeshes(CSLModel *pRootModel);
	bool dotXSILoadModel(CSLModel *pModel);
	bool dotXSILoadMesh(CSLMesh *pMesh);

	std::vector<CSRTNode*> m_aNodes;
	std::vector<CXSIMesh*> m_aMeshes;

	struct CSLHolder
	{
		CSLScene Scene;
	} *m_pDotXSI;

	float m_fCurrentTime;
	float m_fSceneTime;
};


This is a very simple class.
The constructor initializes all members to their default values.
The destructor clears everything allocated by the scene (via calling Clear()).
LoadSceneFromdotXSI()’s functionality is self-explanatory. Still, we will delve into its details soon.
StepTime() is the function that advances the time in the scene, and updates animations in it. You give this function how many frames do you want to advance time and voila!
RenderScene() loops on all geometry in the scene, transforming and rendering each object to the screen.

In addition to these public functions, there are a couple of protected functions that are used only for loading meshes and models from dotXSI.

Note the two member array variables m_aNodes and m_aMeshes. These are the arrays that hold the meshes contained in the scene, and the transformation nodes used to place these meshes at their correct places in the world.
The other member fields are m_fSceneTime, which is the full length of the scene’s animation (e.g. 100 frames), and m_fCurrentTime which tells where the animation is currently standing (for example, at frame 30).

We are left with a strange member of the scene graph, and that is the m_pDotXSI field, which is a pointer to a CSLHolder struct object. We will discuss this struct shortly in the loading code.


Scene Inhabitants

Meshes in the scene are instances of the CXSIMesh class. This is yet another simple class whose sole purpose is to express a mesh made of a triangle list (indexed). The mesh has a material and can have a texture too.
class CXSIMesh
{
public:
	CXSIMesh();
	~CXSIMesh();

	void Clear(void);

	bool LoadFromdotXSI(CSLMesh *pMesh);

	void Render(IDirect3DDevice9 *pDevice);

protected:
	void LoadMaterialFromdotXSI(CSLTriangleList *pTriList);

	CSRTNode *m_pParentNode;
	ID3DXMesh *m_pMesh;
	IDirect3DTexture9 *m_pTexture;
	D3DMATERIAL9 m_Material;
};

The class exposes a function to load the mesh from dotXSI, and another function to render it. Amongst the member variables in this class is the m_pParentNode field. This is a pointer to the node that places this mesh in the world. It is of type CSRTNode*.

The CSRTNode class is what we use to position our meshes in the world. We can build a full hierarchy of CSRTNodes expressing some character’s skeleton. Later in the next article, I will show how to make this skeleton deform the skin of some character, but let’s stick to rigid disconnected body parts this time.
class CSRTNode
{
public:
	CSRTNode();
	~CSRTNode();

	bool LoadFromdotXSI(CSLModel *pModel);

	void UpdateAnimations(float fTime);
	void UpdateLocal(void);
	void UpdateWorld(void);
	const D3DXMATRIXA16& GetLocalMatrix(void) const;
	const D3DXMATRIXA16& GetWorldMatrix(void) const;
	int GetDepth(void) const;

protected:
	CSRTNode *m_pParent;	// Reference to parent node

	// References to animated parameters
	CSLFCurve *m_aScalingAnims[3];
	CSLFCurve *m_aRotationAnims[3];
	CSLFCurve *m_aTranslationAnims[3];

	// SRT output values
	D3DXVECTOR3 m_vec3Scaling;
	D3DXVECTOR3 m_vec3Rotation;
	D3DXVECTOR3 m_vec3Translation;

	// Transformation matrices
	D3DXMATRIXA16 m_matLocal;
	D3DXMATRIXA16 m_matWorld;
};

As the name of the class implies, it holds transformation values in shape of three 3D vectors: Scaling, Euler XYZ Rotation, and Translation.
The two D3DXMATRIX members are the output of the node’s calculations, and are used to transform meshes connected to this node.
I assume that you do know how we use a local matrix and a world matrix to transform nodes and objects in the world. In short, the local matrix transforms this node to its parent node’s space, while the world matrix ultimately transforms this node to world space. These matrices are recalculated by calling the UpdateLocal() and UpdateWorld() functions. A reference to the parent of this node is held in the m_pParent member field. If this field is NULL, then this is a top-most node (e.g. scene root).

The other important fields left in CSRTNode are the animation fields: m_aScalingAnims, m_aRotationAnims, and m_aTranslationAnims.
Each one of these is an array of three pointers to CSLFCurve objects. The CSLFCurve object represents an FCurve in XSI (animation curve) that tells the value of its associated member at each moment of the animation. There is an FCurve for each transformation’s dimension (e.g. Translation-X, Rotation-Y, and Scaling-Z). These FCurves are created by the Semantic Layer, and contain the internal raw key data (which you can access if you want).

As we all know, animation requires interpolation, and each engine I have seen seems to have its own animation system, thus, I won’t be showing code for how to transfer keyframe data to a specific engine. Rather, we will directly make use of the CSLFCurve objects to perform the interpolation and get the resulting data. This is why CSRTNode has references to CSLFCurves. The UpdateAnimations() function makes direct calls to these FCurves to get interpolated values at the specific scene time. However, in your case, you can get the actual keyframe data from the FCurve and feed that to your own animation system.

Note: Softimage|XSI has several curve types to interpolate between keyframes (cubic, hermite, linear, and constant). CSLFCurve can do the interpolation for you without having to worry about the actual FCurve data. However, if you plan to do the interpolation yourself, you must have the correct interpolator math functions so you get the same results given by CSLFCurve.

Ok. We have covered the general structure of the scene graph and the modules it manages. Now, let’s delve into the actual loading code.


The Actual Work

Once you click that slick blue 'Open dotXSI File...' button in the application, the CSceneGraph::LoadSceneFromdotXSI() function gets called, and the process starts.
bool CSceneGraph::LoadSceneFromdotXSI(const char *pszFileName,IDirect3DDevice9 *pD3DDevice)
{
	// Extract file title
	char szDrive[200];
	char szDir[200];
	char szFileTitle[500];
	char szFileExt[50];
	_splitpath(pszFileName,szDrive,szDir,szFileTitle,szFileExt);
	strcat(szDrive,szDir);
	strcat(szFileTitle,szFileExt);

	g_pD3DDevice = pD3DDevice;
	g_pSceneGraph = this;

	m_pDotXSI = new CSLHolder;

	// Open the xsi file
	eSI_Error siError = (eSI_Error)m_pDotXSI->Scene.Open((char*)pszFileName);
	if (siError != SI_SUCCESS)
	{
		TraceMsg("ERROR: Failed to open dotXSI file: %s\n",szFileTitle);
		return false;
	}

	// Attempt to read the file... Save the result in our error code variable...
	siError = (eSI_Error)m_pDotXSI->Scene.Read();
	if (siError != SI_SUCCESS)
	{
		TraceMsg("ERROR: Failed to read dotXSI file: %s\n",szFileTitle);
		return false;
	}

	// Dump file info
	TraceMsg("INFO: %s header information:\n",szFileTitle);
	TraceMsg("      File version : %d.%d\n",m_pDotXSI->Scene.Parser()->GetdotXSIFileVersionMajor(),m_pDotXSI->Scene.Parser()->GetdotXSIFileVersionMinor());

	// Does this file have SI_FileInfo template instance?
	CSLFileInfo *pFileInfo = m_pDotXSI->Scene.FileInfo();
	if (pFileInfo)
	{
		// Yep, dump the info
		TraceMsg("      Project Name: %s\n",pFileInfo->GetProjectName());
		TraceMsg("      User Name: %s\n",pFileInfo->GetUsername());
		TraceMsg("      Last Time Modified: %s\n",pFileInfo->GetSaveDateTime());
		TraceMsg("      Built in: %s\n",pFileInfo->GetOriginator());
	}

	if (m_pDotXSI->Scene.SceneInfo())
		m_fSceneTime = m_pDotXSI->Scene.SceneInfo()->GetEnd() - m_pDotXSI->Scene.SceneInfo()->GetStart();

	// We assume that textures are at the same place as the dotXSI file
	SetCurrentDirectoryA(szDrive);

	// Start the recursion from the scene's root model
	if (!dotXSILoadModel(m_pDotXSI->Scene.Root()))
		return false;

	// Sort the nodes based on their depth
	qsort(&m_aNodes[0],m_aNodes.size(),sizeof(CSRTNode*),NodesDepthSort);
	return true;
}

The operation starts by allocating a CSLHolder object which wraps a single CSLScene object.
Why wrap CSLScene with CSLHolder?
Because allocating CSLScene objects on the heap doesn’t seem to be supported in this version of the FTK. As soon as you delete the object, the program will crash because of corrupted heap memory. However, we need to control CSLScene’s lifetime ourselves since we use CSLFCurve objects from it. The solution is to avoid allocating CSLScene directly via 'new', and this is what CSLHolder helps us do.

I won’t talk about each line of code in there, as much of the code is already explained in the previous articles.
After we open and read the dotXSI file, we start iterating the model hierarchy in the file by calling dotXSILoadModel() and passing the scene’s root model.
bool CSceneGraph::dotXSILoadModel(CSLModel *pModel)
{
	if (!pModel)
		return true;	// Done recursion

	// Allocate structure to save model data in it
	CSRTNode *pNode = new CSRTNode();
	m_aNodes.push_back(pNode);

	if (!pNode->LoadFromdotXSI(pModel))
		return false;

	// Only interested in models containing meshes
	if (pModel->GetPrimitiveType() == CSLModel::SI_MESH)
	{
		// Load the mesh
		if (!dotXSILoadMesh((CSLMesh*)pModel->Primitive()))	// Load the mesh
			return false;
	}

	// Recurse on children
	CSLModel **ppModels = pModel->GetChildrenList();
	int iChildrenCount = pModel->GetChildrenCount();
	for (int i=0;i<iChildrenCount;i++)
	{
		CSLModel *pThisModel = ppModels[i];
		if (!dotXSILoadModel(pThisModel))
			return false;
	}
	return true;
}

dotXSILoadModel() allocates a new CSRTNode object to represent the model’s transformation, then asks it to load its values from the file. After that, it makes a check if this model has a mesh associated with it. If so, it calls dotXSILoadMesh() on it. Finally, it recurses on all of its direct children.

As in dotXSILoadModel(), dotXSILoadMesh() allocates a new CXSIMesh object and asks it to load itself from the file.
bool CSceneGraph::dotXSILoadMesh(CSLMesh *pMesh)
{
	// Create it
	CXSIMesh *pXSIMesh = new CXSIMesh();
	m_aMeshes.push_back(pXSIMesh);

	return pXSIMesh->LoadFromdotXSI(pMesh);
}

So, how CSRTNode loads its values from a dotXSI SI_Model template?
bool CSRTNode::LoadFromdotXSI(CSLModel *pModel)
{
	// Save our object along with the template
	pModel->AttachUserData("NodeObject",this);

	CSLModel *pParent = pModel->ParentModel();
	if (pParent)
		m_pParent = (CSRTNode*)GetTemplateUserData(pParent);
	else m_pParent = NULL;

	// Get the model's SI_Transform parameters
	CSLTransform *pTransform = pModel->Transform();
	if (!pTransform)
		return true;	// No SI_Transform?

	m_aScalingAnims[0] = pTransform->GetSpecificFCurve(CSLTemplate::EFCurveType::SI_SCALING_X);
	m_aScalingAnims[1] = pTransform->GetSpecificFCurve(CSLTemplate::EFCurveType::SI_SCALING_Y);
	m_aScalingAnims[2] = pTransform->GetSpecificFCurve(CSLTemplate::EFCurveType::SI_SCALING_Z);

	m_aRotationAnims[0] = pTransform->GetSpecificFCurve(CSLTemplate::EFCurveType::SI_ROTATION_X);
	m_aRotationAnims[1] = pTransform->GetSpecificFCurve(CSLTemplate::EFCurveType::SI_ROTATION_Y);
	m_aRotationAnims[2] = pTransform->GetSpecificFCurve(CSLTemplate::EFCurveType::SI_ROTATION_Z);

	m_aTranslationAnims[0] = pTransform->GetSpecificFCurve(CSLTemplate::EFCurveType::SI_TRANSLATION_X);
	m_aTranslationAnims[1] = pTransform->GetSpecificFCurve(CSLTemplate::EFCurveType::SI_TRANSLATION_Y);
	m_aTranslationAnims[2] = pTransform->GetSpecificFCurve(CSLTemplate::EFCurveType::SI_TRANSLATION_Z);


	CSIBCVector3D vec3Value;
	vec3Value = pTransform->GetTranslation();
	m_vec3Translation.x  = vec3Value.m_fX;
	m_vec3Translation.y = vec3Value.m_fY;
	m_vec3Translation.z = vec3Value.m_fZ;

	vec3Value = pTransform->GetEulerRotation();
	m_vec3Rotation.x = vec3Value.m_fX;
	m_vec3Rotation.y = vec3Value.m_fY;
	m_vec3Rotation.z = vec3Value.m_fZ;

	vec3Value = pTransform->GetScale();
	m_vec3Scaling.x = vec3Value.m_fX;
	m_vec3Scaling.y = vec3Value.m_fY;
	m_vec3Scaling.z = vec3Value.m_fZ;

	return true;
}

Looking at the code from CSRTNode::LoadFromdotXSI(), we can clearly see how it first attaches itself as user-data to the template, then binds itself to its parent (if it exists). Next, it explicitly asks for the transformation FCurves. The call to CSLTransform:: GetSpecificFCurve() takes the type of the FCurve you’re asking for, and returns a reference to it if it exists, or NULL if that member is not animated, in which case we use the static value placed in the SI_Transform template associated with the node..

Now we can understand how CSRTNode::UpdateAnimations() loops on all of the loaded FCurves, asking them to calculate the new interpolated value, before placing it in the relevant transformation member.
void CSRTNode::UpdateAnimations(float fTime)
{
	// Scaling
	for (int i=0;i<3;i++)
	{
		if (!m_aScalingAnims[i])
			continue;
		m_aScalingAnims[i]->Evaluate(fTime);
		m_vec3Scaling[i] = m_aScalingAnims[i]->GetLastEvaluation();
	}

	// Rotation
	for (int i=0;i<3;i++)
	{
		if (!m_aRotationAnims[i])
			continue;
		m_aRotationAnims[i]->Evaluate(fTime);
		m_vec3Rotation[i] = m_aRotationAnims[i]->GetLastEvaluation();
	}

	// Translation
	for (int i=0;i<3;i++)
	{
		if (!m_aTranslationAnims[i])
			continue;
		m_aTranslationAnims[i]->Evaluate(fTime);
		m_vec3Translation[i] = m_aTranslationAnims[i]->GetLastEvaluation();
	}
}

Note: Rotations in XSI are usually expressed using degrees rather than radians, so you might need to convert them if your math library takes angles as radians.

The code for CXSIMesh::LoadFromdotXSI() is the same code used in the previous article (it only loads meshes exported as triangle lists). However, by the end of the function, there is a call to LoadMaterialFromdotXSI(), plus it retrieves the parent node of this mesh by asking for the user-data of the parent model.

CXSIMesh::LoadMaterialFromdotXSI() takes the material associated with the triangle list, runs through all the shader nodes connected to the material, searching for certain value names (e.g. diffuse, ambient, and specular). While it is looping, it makes special consideration for texture nodes and loads the associated image into the m_pTexture field of CXSIMesh.

Back to the scene graph, the final operation that is done before returning from the loading function is to sort the CSRTNodes based on their depth. This is very important because -as we know- nodes express a skeleton. The nature of hierarchical calculations requires that parent nodes be calculated prior to calculating children nodes, which rely on up-to-date calculations of their parent nodes. So we sort our array of nodes by their hierarchy depth. That is, the scene root is the first node, along with nodes at the same level, followed by children nodes. After sorting, we can make a single run on all the nodes in the scene to transform them, and we are guarenteed that a child node won't be updated before its parent gets updated for the current scene time.

The scene rendering code is simple. It just sets up a couple of lights, then loops on all the meshes to render them.

The scene graph time and rendering functions are called from the application’s OnFrameMove() and OnFrameRender() functions.


Until Then...

Yes! That’s all there’s to it. Looking at the code, we can see that we managed to implement all this functionality with just less than 1000 lines of easy code. Isn’t life beautiful?

One final unnecessary note, loading dotXSI files can be very easy if you have all the required facilities ready in your host engine. In the case of the sample code, the scene graph helped us a lot load and manage the dotXSI scene. Without it, we would clutter the program with unorganized objects floating everywhere with no one keeping track of them.

Yep... See ya soon at the next dotXSI article.
And th.. th.. th.. that’s all folks! (Loony Tunes theme)


See Also