You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1096 lines
30 KiB
1096 lines
30 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
// |
|
//=============================================================================// |
|
|
|
#include "MAX.H" |
|
#include "DECOMP.H" |
|
#include "STDMAT.H" |
|
#include "ANIMTBL.H" |
|
#include "istdplug.h" |
|
#include "phyexp.h" |
|
#include "BonesPro.h" |
|
|
|
#include "smexprc.h" |
|
#include "smedefs.h" |
|
|
|
//=================================================================== |
|
// Prototype declarations |
|
// |
|
int GetIndexOfINode(INode *pnode,BOOL fAssertPropExists = TRUE); |
|
void SetIndexOfINode(INode *pnode, int inode); |
|
BOOL FUndesirableNode(INode *pnode); |
|
BOOL FNodeMarkedToSkip(INode *pnode); |
|
float FlReduceRotation(float fl); |
|
|
|
|
|
//=================================================================== |
|
// Global variable definitions |
|
// |
|
|
|
// Save for use with dialogs |
|
static HINSTANCE hInstance; |
|
|
|
// We just need one of these to hand off to 3DSMAX. |
|
static SmdExportClassDesc SmdExportCD; |
|
|
|
// For OutputDebugString and misc sprintf's |
|
static char st_szDBG[300]; |
|
|
|
// INode mapping table |
|
static int g_inmMac = 0; |
|
|
|
//=================================================================== |
|
// Utility functions |
|
// |
|
|
|
static int AssertFailedFunc(char *sz) |
|
{ |
|
MessageBox(GetActiveWindow(), sz, "Assert failure", MB_OK); |
|
int Set_Your_Breakpoint_Here = 1; |
|
return 1; |
|
} |
|
#define ASSERT_MBOX(f, sz) ((f) ? 1 : AssertFailedFunc(sz)) |
|
|
|
|
|
//=================================================================== |
|
// Required plug-in export functions |
|
// |
|
BOOL WINAPI DllMain( HINSTANCE hinstDLL, ULONG fdwReason, LPVOID lpvReserved) |
|
{ |
|
static int fFirstTimeHere = TRUE; |
|
if (fFirstTimeHere) |
|
{ |
|
fFirstTimeHere = FALSE; |
|
hInstance = hinstDLL; |
|
} |
|
return TRUE; |
|
} |
|
|
|
|
|
EXPORT_THIS int LibNumberClasses(void) |
|
{ |
|
return 1; |
|
} |
|
|
|
|
|
EXPORT_THIS ClassDesc *LibClassDesc(int iWhichClass) |
|
{ |
|
switch(iWhichClass) |
|
{ |
|
case 0: return &SmdExportCD; |
|
default: return 0; |
|
} |
|
} |
|
|
|
|
|
EXPORT_THIS const TCHAR *LibDescription() |
|
{ |
|
return _T("Valve SMD Plug-in."); |
|
} |
|
|
|
|
|
EXPORT_THIS ULONG LibVersion() |
|
{ |
|
return VERSION_3DSMAX; |
|
} |
|
|
|
|
|
//===================================================================== |
|
// Methods for SmdExportClass |
|
// |
|
|
|
CONSTRUCTOR SmdExportClass::SmdExportClass(void) |
|
{ |
|
m_rgmaxnode = NULL; |
|
} |
|
|
|
|
|
DESTRUCTOR SmdExportClass::~SmdExportClass(void) |
|
{ |
|
if (m_rgmaxnode) |
|
delete[] m_rgmaxnode; |
|
} |
|
|
|
|
|
int SmdExportClass::DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options) |
|
{ |
|
ExpInterface *pexpiface = ei; // Hungarian |
|
Interface *piface = i; // Hungarian |
|
|
|
// Reset the name-map property manager |
|
g_inmMac = 0; |
|
|
|
if (!suppressPrompts) |
|
{ |
|
// Present the user with the Export Options dialog |
|
if (DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_EXPORTOPTIONS), GetActiveWindow(), |
|
ExportOptionsDlgProc, (LPARAM)this) <= 0) |
|
return 0; // error or cancel |
|
} |
|
else |
|
{ |
|
m_fReferenceFrame = 0; |
|
} |
|
|
|
// Break up filename, re-assemble longer versions |
|
TSTR strPath, strFile, strExt; |
|
TCHAR szFile[MAX_PATH]; |
|
SplitFilename(TSTR(name), &strPath, &strFile, &strExt); |
|
sprintf(szFile, "%s\\%s.%s", (char*)strPath, (char*)strFile, DEFAULT_EXT); |
|
|
|
/* |
|
if (m_fReferenceFrame) |
|
sprintf(szFile, "%s\\%s_model.%s", (char*)strPath, (char*)strFile, DEFAULT_EXT); |
|
*/ |
|
|
|
FILE *pFile; |
|
if ((pFile = fopen(szFile, "w")) == NULL) |
|
return FALSE/*failure*/; |
|
|
|
fprintf( pFile, "version %d\n", 1 ); |
|
|
|
// Get animation metrics |
|
m_intervalOfAnimation = piface->GetAnimRange(); |
|
m_tvStart = m_intervalOfAnimation.Start(); |
|
m_tvEnd = m_intervalOfAnimation.End(); |
|
m_tpf = ::GetTicksPerFrame(); |
|
|
|
// Count nodes, label them, collect into array |
|
if (!CollectNodes(pexpiface)) |
|
return 0; /*fail*/ |
|
|
|
// Output nodes |
|
if (!DumpBones(pFile, pexpiface)) |
|
{ |
|
fclose( pFile ); |
|
return 0; /*fail*/ |
|
} |
|
|
|
// Output bone rotations, for each frame. Do only first frame if this is the reference frame MAX file |
|
DumpRotations(pFile, pexpiface); |
|
|
|
// Output triangle meshes (first frame/all frames), if this is the reference frame MAX file |
|
if (m_fReferenceFrame) |
|
{ |
|
DumpModel(pFile, pexpiface); |
|
} |
|
|
|
if (!suppressPrompts) |
|
{ |
|
// Tell user that exporting is finished (it can take a while with no feedback) |
|
char szExportComplete[300]; |
|
sprintf(szExportComplete, "Exported %s.", szFile); |
|
MessageBox(GetActiveWindow(), szExportComplete, "Status", MB_OK); |
|
} |
|
|
|
fclose( pFile ); |
|
|
|
return 1/*success*/; |
|
} |
|
|
|
|
|
BOOL SmdExportClass::CollectNodes( ExpInterface *pexpiface) |
|
{ |
|
// Count total nodes in the model, so I can alloc array |
|
// Also "brands" each node with node index, or with "skip me" marker. |
|
CountNodesTEP procCountNodes; |
|
procCountNodes.m_cNodes = 0; |
|
(void) pexpiface->theScene->EnumTree(&procCountNodes); |
|
ASSERT_MBOX(procCountNodes.m_cNodes > 0, "No nodes!"); |
|
|
|
// Alloc and fill array |
|
m_imaxnodeMac = procCountNodes.m_cNodes; |
|
m_rgmaxnode = new MaxNode[m_imaxnodeMac]; |
|
ASSERT_MBOX(m_rgmaxnode != NULL, "new failed"); |
|
|
|
|
|
CollectNodesTEP procCollectNodes; |
|
procCollectNodes.m_phec = this; |
|
(void) pexpiface->theScene->EnumTree(&procCollectNodes); |
|
|
|
return TRUE; |
|
} |
|
|
|
|
|
BOOL SmdExportClass::DumpBones(FILE *pFile, ExpInterface *pexpiface) |
|
{ |
|
// Dump bone names |
|
DumpNodesTEP procDumpNodes; |
|
procDumpNodes.m_pfile = pFile; |
|
procDumpNodes.m_phec = this; |
|
fprintf(pFile, "nodes\n" ); |
|
(void) pexpiface->theScene->EnumTree(&procDumpNodes); |
|
fprintf(pFile, "end\n" ); |
|
|
|
return TRUE; |
|
} |
|
|
|
|
|
BOOL SmdExportClass::DumpRotations(FILE *pFile, ExpInterface *pexpiface) |
|
{ |
|
// Dump bone-rotation info, for each frame |
|
// Also dumps root-node translation info (the model's world-position at each frame) |
|
DumpFrameRotationsTEP procDumpFrameRotations; |
|
procDumpFrameRotations.m_pfile = pFile; |
|
procDumpFrameRotations.m_phec = this; |
|
|
|
TimeValue m_tvTill = (m_fReferenceFrame) ? m_tvStart : m_tvEnd; |
|
|
|
fprintf(pFile, "skeleton\n" ); |
|
for (TimeValue tv = m_tvStart; tv <= m_tvTill; tv += m_tpf) |
|
{ |
|
fprintf(pFile, "time %d\n", tv / GetTicksPerFrame() ); |
|
procDumpFrameRotations.m_tvToDump = tv; |
|
(void) pexpiface->theScene->EnumTree(&procDumpFrameRotations); |
|
} |
|
fprintf(pFile, "end\n" ); |
|
|
|
return TRUE; |
|
} |
|
|
|
|
|
BOOL SmdExportClass::DumpModel( FILE *pFile, ExpInterface *pexpiface) |
|
{ |
|
// Dump mesh info: vertices, normals, UV texture map coords, bone assignments |
|
DumpModelTEP procDumpModel; |
|
procDumpModel.m_pfile = pFile; |
|
procDumpModel.m_phec = this; |
|
fprintf(pFile, "triangles\n" ); |
|
procDumpModel.m_tvToDump = m_tvStart; |
|
(void) pexpiface->theScene->EnumTree(&procDumpModel); |
|
fprintf(pFile, "end\n" ); |
|
return TRUE; |
|
} |
|
|
|
|
|
|
|
|
|
//============================================================================= |
|
// TREE-ENUMERATION PROCEDURES |
|
//============================================================================= |
|
|
|
#define ASSERT_AND_ABORT(f, sz) \ |
|
if (!(f)) \ |
|
{ \ |
|
ASSERT_MBOX(FALSE, sz); \ |
|
cleanup( ); \ |
|
return TREE_ABORT; \ |
|
} |
|
|
|
|
|
//================================================================= |
|
// Methods for CountNodesTEP |
|
// |
|
int CountNodesTEP::callback( INode *node) |
|
{ |
|
INode *pnode = node; // Hungarian |
|
|
|
ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!"); |
|
|
|
if (::FUndesirableNode(pnode)) |
|
{ |
|
// Mark as skippable |
|
::SetIndexOfINode(pnode, SmdExportClass::UNDESIRABLE_NODE_MARKER); |
|
return TREE_CONTINUE; |
|
} |
|
|
|
// Establish "node index"--just ascending ints |
|
::SetIndexOfINode(pnode, m_cNodes); |
|
|
|
m_cNodes++; |
|
|
|
return TREE_CONTINUE; |
|
} |
|
|
|
|
|
//================================================================= |
|
// Methods for CollectNodesTEP |
|
// |
|
int CollectNodesTEP::callback(INode *node) |
|
{ |
|
INode *pnode = node; // Hungarian |
|
|
|
ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!"); |
|
|
|
if (::FNodeMarkedToSkip(pnode)) |
|
return TREE_CONTINUE; |
|
|
|
// Get pre-stored "index" |
|
int iNode = ::GetIndexOfINode(pnode); |
|
ASSERT_MBOX(iNode >= 0 && iNode <= m_phec->m_imaxnodeMac-1, "Bogus iNode"); |
|
|
|
// Get name, store name in array |
|
TSTR strNodeName(pnode->GetName()); |
|
strcpy(m_phec->m_rgmaxnode[iNode].szNodeName, (char*)strNodeName); |
|
|
|
// Get Node's time-zero Transformation Matrices |
|
m_phec->m_rgmaxnode[iNode].mat3NodeTM = pnode->GetNodeTM(0/*TimeValue*/); |
|
m_phec->m_rgmaxnode[iNode].mat3ObjectTM = pnode->GetObjectTM(0/*TimeValue*/); |
|
|
|
// I'll calculate this later |
|
m_phec->m_rgmaxnode[iNode].imaxnodeParent = SmdExportClass::UNDESIRABLE_NODE_MARKER; |
|
|
|
return TREE_CONTINUE; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
//================================================================= |
|
// Methods for DumpNodesTEP |
|
// |
|
int DumpNodesTEP::callback(INode *pnode) |
|
{ |
|
ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!"); |
|
|
|
if (::FNodeMarkedToSkip(pnode)) |
|
return TREE_CONTINUE; |
|
|
|
// Get node's parent |
|
INode *pnodeParent; |
|
pnodeParent = pnode->GetParentNode(); |
|
|
|
// The model's root is a child of the real "scene root" |
|
TSTR strNodeName(pnode->GetName()); |
|
BOOL fNodeIsRoot = pnodeParent->IsRootNode( ); |
|
|
|
int iNode = ::GetIndexOfINode(pnode); |
|
int iNodeParent = ::GetIndexOfINode(pnodeParent, !fNodeIsRoot/*fAssertPropExists*/); |
|
|
|
// Convenient time to cache this |
|
m_phec->m_rgmaxnode[iNode].imaxnodeParent = fNodeIsRoot ? SmdExportClass::UNDESIRABLE_NODE_MARKER : iNodeParent; |
|
|
|
// Root node has no parent, thus no translation |
|
if (fNodeIsRoot) |
|
iNodeParent = -1; |
|
|
|
// check to see if the matrix isn't right handed |
|
m_phec->m_rgmaxnode[iNode].isMirrored = DotProd( CrossProd( m_phec->m_rgmaxnode[iNode].mat3ObjectTM.GetRow(0).Normalize(), m_phec->m_rgmaxnode[iNode].mat3ObjectTM.GetRow(1).Normalize() ).Normalize(), m_phec->m_rgmaxnode[iNode].mat3ObjectTM.GetRow(2).Normalize() ) < 0; |
|
|
|
// Dump node description |
|
fprintf(m_pfile, "%3d \"%s\" %3d\n", |
|
iNode, |
|
strNodeName, |
|
iNodeParent ); |
|
|
|
return TREE_CONTINUE; |
|
} |
|
|
|
|
|
|
|
//================================================================= |
|
// Methods for DumpFrameRotationsTEP |
|
// |
|
int DumpFrameRotationsTEP::callback(INode *pnode) |
|
{ |
|
ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!"); |
|
|
|
if (::FNodeMarkedToSkip(pnode)) |
|
return TREE_CONTINUE; |
|
|
|
int iNode = ::GetIndexOfINode(pnode); |
|
|
|
TSTR strNodeName(pnode->GetName()); |
|
|
|
// The model's root is a child of the real "scene root" |
|
INode *pnodeParent = pnode->GetParentNode(); |
|
BOOL fNodeIsRoot = pnodeParent->IsRootNode( ); |
|
|
|
// Get Node's "Local" Transformation Matrix |
|
Matrix3 mat3NodeTM = pnode->GetNodeTM(m_tvToDump); |
|
Matrix3 mat3ParentTM = pnodeParent->GetNodeTM(m_tvToDump); |
|
mat3NodeTM.NoScale(); // Clear these out because they apparently |
|
mat3ParentTM.NoScale(); // screw up the following calculation. |
|
Matrix3 mat3NodeLocalTM = mat3NodeTM * Inverse(mat3ParentTM); |
|
Point3 rowTrans = mat3NodeLocalTM.GetTrans(); |
|
|
|
// check to see if the parent bone was mirrored. If so, mirror invert this bones position |
|
if (m_phec->m_rgmaxnode[iNode].imaxnodeParent >= 0 && m_phec->m_rgmaxnode[m_phec->m_rgmaxnode[iNode].imaxnodeParent].isMirrored) |
|
{ |
|
rowTrans = rowTrans * -1.0f; |
|
} |
|
|
|
// Get the rotation (via decomposition into "affine parts", then quaternion-to-Euler) |
|
// Apparently the order of rotations returned by QuatToEuler() is X, then Y, then Z. |
|
AffineParts affparts; |
|
float rgflXYZRotations[3]; |
|
|
|
decomp_affine(mat3NodeLocalTM, &affparts); |
|
QuatToEuler(affparts.q, rgflXYZRotations); |
|
|
|
float xRot = rgflXYZRotations[0]; // in radians |
|
float yRot = rgflXYZRotations[1]; // in radians |
|
float zRot = rgflXYZRotations[2]; // in radians |
|
|
|
// Get rotations in the -2pi...2pi range |
|
xRot = ::FlReduceRotation(xRot); |
|
yRot = ::FlReduceRotation(yRot); |
|
zRot = ::FlReduceRotation(zRot); |
|
|
|
// Print rotations |
|
fprintf(m_pfile, "%3d %f %f %f %f %f %f\n", |
|
// Node:%-15s Rotation (x,y,z)\n", |
|
iNode, rowTrans.x, rowTrans.y, rowTrans.z, xRot, yRot, zRot); |
|
|
|
return TREE_CONTINUE; |
|
} |
|
|
|
|
|
|
|
//================================================================= |
|
// Methods for DumpModelTEP |
|
// |
|
Modifier *FindPhysiqueModifier (INode *nodePtr) |
|
{ |
|
// Get object from node. Abort if no object. |
|
Object *ObjectPtr = nodePtr->GetObjectRef(); |
|
if (!ObjectPtr) return NULL; |
|
|
|
// Is derived object ? |
|
if (ObjectPtr->SuperClassID() == GEN_DERIVOB_CLASS_ID) |
|
{ |
|
// Yes -> Cast. |
|
IDerivedObject *DerivedObjectPtr = static_cast<IDerivedObject*>(ObjectPtr); |
|
|
|
// Iterate over all entries of the modifier stack. |
|
int ModStackIndex = 0; |
|
while (ModStackIndex < DerivedObjectPtr->NumModifiers()) |
|
{ |
|
// Get current modifier. |
|
Modifier *ModifierPtr = DerivedObjectPtr->GetModifier(ModStackIndex); |
|
|
|
// Is this Physique ? |
|
if (ModifierPtr->ClassID() == Class_ID( PHYSIQUE_CLASS_ID_A, PHYSIQUE_CLASS_ID_B) ) |
|
{ |
|
// Yes -> Exit. |
|
return ModifierPtr; |
|
} |
|
// Next modifier stack entry. |
|
ModStackIndex++; |
|
} |
|
} |
|
// Not found. |
|
return NULL; |
|
} |
|
|
|
Modifier *FindBonesProModifier (INode *nodePtr) |
|
{ |
|
// Get object from node. Abort if no object. |
|
Object *ObjectPtr = nodePtr->GetObjectRef(); |
|
if (!ObjectPtr) return NULL; |
|
|
|
// Is derived object ? |
|
if (ObjectPtr->SuperClassID() == GEN_DERIVOB_CLASS_ID) |
|
{ |
|
// Yes -> Cast. |
|
IDerivedObject *DerivedObjectPtr = static_cast<IDerivedObject*>(ObjectPtr); |
|
|
|
// Iterate over all entries of the modifier stack. |
|
int ModStackIndex = 0; |
|
while (ModStackIndex < DerivedObjectPtr->NumModifiers()) |
|
{ |
|
// Get current modifier. |
|
Modifier *ModifierPtr = DerivedObjectPtr->GetModifier(ModStackIndex); |
|
|
|
// Is this Bones Pro OSM? |
|
if (ModifierPtr->ClassID() == BP_CLASS_ID_OSM ) |
|
{ |
|
// Yes -> Exit. |
|
return ModifierPtr; |
|
} |
|
// Is this Bones Pro WSM? |
|
if (ModifierPtr->ClassID() == BP_CLASS_ID_WSM ) |
|
{ |
|
// Yes -> Exit. |
|
return ModifierPtr; |
|
} |
|
// Next modifier stack entry. |
|
ModStackIndex++; |
|
} |
|
} |
|
// Not found. |
|
return NULL; |
|
} |
|
|
|
// #define DEBUG_MESH_DUMP |
|
|
|
//================================================================= |
|
// Methods for DumpModelTEP |
|
// |
|
int DumpModelTEP::callback(INode *pnode) |
|
{ |
|
Object* pobj; |
|
int fHasMat = TRUE; |
|
|
|
// clear physique export parameters |
|
m_mcExport = NULL; |
|
m_phyExport = NULL; |
|
m_phyMod = NULL; |
|
m_bonesProMod = NULL; |
|
|
|
ASSERT_MBOX(!(pnode)->IsRootNode(), "Encountered a root node!"); |
|
|
|
if (::FNodeMarkedToSkip(pnode)) |
|
return TREE_CONTINUE; |
|
|
|
// Actually, if it's not selected, skip it! |
|
//if (!pnode->Selected()) |
|
// return TRUE; |
|
|
|
int iNode = ::GetIndexOfINode(pnode); |
|
TSTR strNodeName(pnode->GetName()); |
|
|
|
// The Footsteps node apparently MUST have a dummy mesh attached! Ignore it explicitly. |
|
if (FStrEq((char*)strNodeName, "Bip01 Footsteps")) |
|
return TREE_CONTINUE; |
|
|
|
// Helper nodes don't have meshes |
|
pobj = pnode->GetObjectRef(); |
|
if (pobj->SuperClassID() == HELPER_CLASS_ID) |
|
return TREE_CONTINUE; |
|
|
|
// The model's root is a child of the real "scene root" |
|
INode *pnodeParent = pnode->GetParentNode(); |
|
BOOL fNodeIsRoot = pnodeParent->IsRootNode( ); |
|
|
|
// Get node's material: should be a multi/sub (if it has a material at all) |
|
Mtl *pmtlNode = pnode->GetMtl(); |
|
if (pmtlNode == NULL) |
|
{ |
|
return TREE_CONTINUE; |
|
fHasMat = FALSE; |
|
} |
|
else if (!(pmtlNode->ClassID() == Class_ID(MULTI_CLASS_ID, 0) && pmtlNode->IsMultiMtl())) |
|
{ |
|
// sprintf(st_szDBG, "ERROR--Material on node %s isn't a Multi/Sub-Object", (char*)strNodeName); |
|
// ASSERT_AND_ABORT(FALSE, st_szDBG); |
|
// fHasMat = FALSE; |
|
} |
|
|
|
// Get Node's object, convert to a triangle-mesh object, so I can access the Faces |
|
ObjectState os = pnode->EvalWorldState(m_tvToDump); |
|
pobj = os.obj; |
|
TriObject *ptriobj; |
|
BOOL fConvertedToTriObject = |
|
pobj->CanConvertToType(triObjectClassID) && |
|
(ptriobj = (TriObject*)pobj->ConvertToType(m_tvToDump, triObjectClassID)) != NULL; |
|
if (!fConvertedToTriObject) |
|
return TREE_CONTINUE; |
|
Mesh *pmesh = &ptriobj->mesh; |
|
|
|
// Shouldn't have gotten this far if it's a helper object |
|
if (pobj->SuperClassID() == HELPER_CLASS_ID) |
|
{ |
|
sprintf(st_szDBG, "ERROR--Helper node %s has an attached mesh, and it shouldn't.", (char*)strNodeName); |
|
ASSERT_AND_ABORT(FALSE, st_szDBG); |
|
} |
|
|
|
// Ensure that the vertex normals are up-to-date |
|
pmesh->buildNormals(); |
|
|
|
// We want the vertex coordinates in World-space, not object-space |
|
Matrix3 mat3ObjectTM = pnode->GetObjectTM(m_tvToDump); |
|
|
|
|
|
// initialize physique export parameters |
|
m_phyMod = FindPhysiqueModifier(pnode); |
|
if (m_phyMod) |
|
{ |
|
// Physique Modifier exists for given Node |
|
m_phyExport = (IPhysiqueExport *)m_phyMod->GetInterface(I_PHYINTERFACE); |
|
|
|
if (m_phyExport) |
|
{ |
|
// create a ModContext Export Interface for the specific node of the Physique Modifier |
|
m_mcExport = (IPhyContextExport *)m_phyExport->GetContextInterface(pnode); |
|
|
|
if (m_mcExport) |
|
{ |
|
// convert all vertices to Rigid |
|
m_mcExport->ConvertToRigid(TRUE); |
|
} |
|
} |
|
} |
|
|
|
// initialize bones pro export parameters |
|
m_wa = NULL; |
|
m_bonesProMod = FindBonesProModifier(pnode); |
|
if (m_bonesProMod) |
|
{ |
|
m_bonesProMod->SetProperty( BP_PROPID_GET_WEIGHTS, &m_wa ); |
|
} |
|
|
|
// Dump the triangle face info |
|
int cFaces = pmesh->getNumFaces(); |
|
for (int iFace = 0; iFace < cFaces; iFace++) |
|
{ |
|
Face* pface = &pmesh->faces[iFace]; |
|
TVFace* ptvface = (pmesh->tvFace) ? &pmesh->tvFace[iFace] : NULL; |
|
DWORD smGroupFace = pface->getSmGroup(); |
|
|
|
// Get face's 3 indexes into the Mesh's vertex array(s). |
|
DWORD iVertex0 = pface->getVert(0); |
|
DWORD iVertex1 = pface->getVert(1); |
|
DWORD iVertex2 = pface->getVert(2); |
|
ASSERT_AND_ABORT((int)iVertex0 < pmesh->getNumVerts(), "Bogus Vertex 0 index"); |
|
ASSERT_AND_ABORT((int)iVertex1 < pmesh->getNumVerts(), "Bogus Vertex 1 index"); |
|
ASSERT_AND_ABORT((int)iVertex2 < pmesh->getNumVerts(), "Bogus Vertex 2 index"); |
|
|
|
// Get the 3 Vertex's for this face |
|
Point3 pt3Vertex0 = pmesh->getVert(iVertex0); |
|
Point3 pt3Vertex1 = pmesh->getVert(iVertex1); |
|
Point3 pt3Vertex2 = pmesh->getVert(iVertex2); |
|
|
|
// Get the 3 RVertex's for this face |
|
// NOTE: I'm using getRVertPtr instead of getRVert to work around a 3DSMax bug |
|
RVertex *prvertex0 = pmesh->getRVertPtr(iVertex0); |
|
RVertex *prvertex1 = pmesh->getRVertPtr(iVertex1); |
|
RVertex *prvertex2 = pmesh->getRVertPtr(iVertex2); |
|
|
|
// Find appropriate normals for each RVertex |
|
// A vertex can be part of multiple faces, so the "smoothing group" |
|
// is used to locate the normal for this face's use of the vertex. |
|
Point3 pt3Vertex0Normal; |
|
Point3 pt3Vertex1Normal; |
|
Point3 pt3Vertex2Normal; |
|
if (smGroupFace) |
|
{ |
|
pt3Vertex0Normal = Pt3GetRVertexNormal(prvertex0, smGroupFace); |
|
pt3Vertex1Normal = Pt3GetRVertexNormal(prvertex1, smGroupFace); |
|
pt3Vertex2Normal = Pt3GetRVertexNormal(prvertex2, smGroupFace); |
|
} |
|
else |
|
{ |
|
pt3Vertex0Normal = pmesh->getFaceNormal( iFace ); |
|
pt3Vertex1Normal = pmesh->getFaceNormal( iFace ); |
|
pt3Vertex2Normal = pmesh->getFaceNormal( iFace ); |
|
} |
|
ASSERT_AND_ABORT( Length( pt3Vertex0Normal ) <= 1.1, "bogus orig normal 0" ); |
|
ASSERT_AND_ABORT( Length( pt3Vertex1Normal ) <= 1.1, "bogus orig normal 1" ); |
|
ASSERT_AND_ABORT( Length( pt3Vertex2Normal ) <= 1.1, "bogus orig normal 2" ); |
|
|
|
// Get Face's sub-material from node's material, to get the bitmap name. |
|
// And no, there isn't a simpler way to get the bitmap name, you have to |
|
// dig down through all these levels. |
|
TCHAR szBitmapName[256] = "null.bmp"; |
|
if (fHasMat) |
|
{ |
|
Texmap *ptexmap = NULL; |
|
MtlID mtlidFace = pface->getMatID(); |
|
if (pmtlNode->IsMultiMtl()) |
|
{ |
|
if (mtlidFace >= pmtlNode->NumSubMtls()) |
|
{ |
|
sprintf(st_szDBG, "ERROR--Bogus sub-material index %d in node %s; highest valid index is %d", |
|
mtlidFace, (char*)strNodeName, pmtlNode->NumSubMtls()-1); |
|
// ASSERT_AND_ABORT(FALSE, st_szDBG); |
|
mtlidFace = 0; |
|
} |
|
Mtl *pmtlFace = pmtlNode->GetSubMtl(mtlidFace); |
|
ASSERT_AND_ABORT(pmtlFace != NULL, "NULL Sub-material returned"); |
|
|
|
/* |
|
if ((pmtlFace->ClassID() == Class_ID(MULTI_CLASS_ID, 0) && pmtlFace->IsMultiMtl())) |
|
{ |
|
// it's a sub-sub material. Gads. |
|
pmtlFace = pmtlFace->GetSubMtl(mtlidFace); |
|
ASSERT_AND_ABORT(pmtlFace != NULL, "NULL Sub-material returned"); |
|
} |
|
*/ |
|
|
|
if (!(pmtlFace->ClassID() == Class_ID(DMTL_CLASS_ID, 0))) |
|
{ |
|
|
|
sprintf(st_szDBG, |
|
"ERROR--Sub-material with index %d (used in node %s) isn't a 'default/standard' material [%x].", |
|
mtlidFace, (char*)strNodeName, pmtlFace->ClassID()); |
|
ASSERT_AND_ABORT(FALSE, st_szDBG); |
|
} |
|
StdMat *pstdmtlFace = (StdMat*)pmtlFace; |
|
ptexmap = pstdmtlFace->GetSubTexmap(ID_DI); |
|
} |
|
else |
|
{ |
|
ptexmap = pmtlNode->GetActiveTexmap(); |
|
} |
|
|
|
// ASSERT_AND_ABORT(ptexmap != NULL, "NULL diffuse texture") |
|
if (ptexmap != NULL) |
|
{ |
|
if (!(ptexmap->ClassID() == Class_ID(BMTEX_CLASS_ID, 0))) |
|
{ |
|
sprintf(st_szDBG, |
|
"ERROR--Sub-material with index %d (used in node %s) doesn't have a bitmap as its diffuse texture.", |
|
mtlidFace, (char*)strNodeName); |
|
ASSERT_AND_ABORT(FALSE, st_szDBG); |
|
} |
|
BitmapTex *pbmptex = (BitmapTex*)ptexmap; |
|
strcpy(szBitmapName, pbmptex->GetMapName()); |
|
TSTR strPath, strFile; |
|
SplitPathFile(TSTR(szBitmapName), &strPath, &strFile); |
|
strcpy(szBitmapName,strFile); |
|
} |
|
} |
|
|
|
UVVert UVvertex0( 0, 0, 0 ); |
|
UVVert UVvertex1( 1, 0, 0 ); |
|
UVVert UVvertex2( 0, 1, 0 ); |
|
|
|
// All faces must have textures assigned to them |
|
if (ptvface && (pface->flags & HAS_TVERTS)) |
|
{ |
|
// Get TVface's 3 indexes into the Mesh's TVertex array(s). |
|
DWORD iTVertex0 = ptvface->getTVert(0); |
|
DWORD iTVertex1 = ptvface->getTVert(1); |
|
DWORD iTVertex2 = ptvface->getTVert(2); |
|
ASSERT_AND_ABORT((int)iTVertex0 < pmesh->getNumTVerts(), "Bogus TVertex 0 index"); |
|
ASSERT_AND_ABORT((int)iTVertex1 < pmesh->getNumTVerts(), "Bogus TVertex 1 index"); |
|
ASSERT_AND_ABORT((int)iTVertex2 < pmesh->getNumTVerts(), "Bogus TVertex 2 index"); |
|
|
|
// Get the 3 TVertex's for this TVFace |
|
// NOTE: I'm using getRVertPtr instead of getRVert to work around a 3DSMax bug |
|
UVvertex0 = pmesh->getTVert(iTVertex0); |
|
UVvertex1 = pmesh->getTVert(iTVertex1); |
|
UVvertex2 = pmesh->getTVert(iTVertex2); |
|
} |
|
else |
|
{ |
|
//sprintf(st_szDBG, "ERROR--Node %s has a textureless face. All faces must have an applied texture.", (char*)strNodeName); |
|
//ASSERT_AND_ABORT(FALSE, st_szDBG); |
|
} |
|
|
|
/* |
|
const char *szExpectedExtension = ".bmp"; |
|
if (stricmp(szBitmapName+strlen(szBitmapName)-strlen(szExpectedExtension), szExpectedExtension) != 0) |
|
{ |
|
sprintf(st_szDBG, "Node %s uses %s, which is not a %s file", (char*)strNodeName, szBitmapName, szExpectedExtension); |
|
ASSERT_AND_ABORT(FALSE, st_szDBG); |
|
} |
|
*/ |
|
|
|
// Determine owning bones for the vertices. |
|
int iNodeV0, iNodeV1, iNodeV2; |
|
// Simple 3dsMax model: the vertices are owned by the object, and hence the node |
|
iNodeV0 = iNode; |
|
iNodeV1 = iNode; |
|
iNodeV2 = iNode; |
|
|
|
// Rotate the face vertices out of object-space, and into world-space space |
|
Point3 v0 = pt3Vertex0 * mat3ObjectTM; |
|
Point3 v1 = pt3Vertex1 * mat3ObjectTM; |
|
Point3 v2 = pt3Vertex2 * mat3ObjectTM; |
|
|
|
|
|
Matrix3 mat3ObjectNTM = mat3ObjectTM; |
|
mat3ObjectNTM.NoScale( ); |
|
ASSERT_AND_ABORT( Length( pt3Vertex0Normal ) <= 1.1, "bogus pre normal 0" ); |
|
pt3Vertex0Normal = VectorTransform(mat3ObjectNTM, pt3Vertex0Normal); |
|
ASSERT_AND_ABORT( Length( pt3Vertex0Normal ) <= 1.1, "bogus post normal 0" ); |
|
ASSERT_AND_ABORT( Length( pt3Vertex1Normal ) <= 1.1, "bogus pre normal 1" ); |
|
pt3Vertex1Normal = VectorTransform(mat3ObjectNTM, pt3Vertex1Normal); |
|
ASSERT_AND_ABORT( Length( pt3Vertex1Normal ) <= 1.1, "bogus post normal 1" ); |
|
ASSERT_AND_ABORT( Length( pt3Vertex2Normal ) <= 1.1, "bogus pre normal 2" ); |
|
pt3Vertex2Normal = VectorTransform(mat3ObjectNTM, pt3Vertex2Normal); |
|
ASSERT_AND_ABORT( Length( pt3Vertex2Normal ) <= 1.1, "bogus post normal 2" ); |
|
|
|
// Finally dump the bitmap name and 3 lines of face info |
|
fprintf(m_pfile, "%s\n", szBitmapName); |
|
fprintf(m_pfile, "%3d %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f", |
|
iNodeV0, v0.x, v0.y, v0.z, |
|
pt3Vertex0Normal.x, pt3Vertex0Normal.y, pt3Vertex0Normal.z, |
|
UVvertex0.x, UVvertex0.y); |
|
DumpWeights( iVertex0 ); |
|
fprintf(m_pfile, "%3d %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f", |
|
iNodeV1, v1.x, v1.y, v1.z, |
|
pt3Vertex1Normal.x, pt3Vertex1Normal.y, pt3Vertex1Normal.z, |
|
UVvertex1.x, UVvertex1.y); |
|
DumpWeights( iVertex1 ); |
|
fprintf(m_pfile, "%3d %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f", |
|
iNodeV2, v2.x, v2.y, v2.z, |
|
pt3Vertex2Normal.x, pt3Vertex2Normal.y, pt3Vertex2Normal.z, |
|
UVvertex2.x, UVvertex2.y); |
|
DumpWeights( iVertex2 ); |
|
} |
|
|
|
cleanup( ); |
|
return TREE_CONTINUE; |
|
} |
|
|
|
|
|
#define MAX_BLEND_WEIGHTS 8 |
|
|
|
static struct { |
|
int iNode; |
|
float flWeight; |
|
} aWeights[MAX_BLEND_WEIGHTS+1]; |
|
|
|
int AddWeight( int iCount, int iNode, float flWeight ) |
|
{ |
|
if (flWeight < 0.001) |
|
return iCount; |
|
|
|
for (int i = 0; i < iCount; i++) |
|
{ |
|
if (aWeights[i].flWeight < flWeight) |
|
{ |
|
for (int j = iCount; j > i; j--) |
|
{ |
|
aWeights[j] = aWeights[j-1]; |
|
} |
|
break; |
|
} |
|
} |
|
aWeights[i].iNode = iNode; |
|
aWeights[i].flWeight = flWeight; |
|
|
|
iCount++; |
|
if (iCount > MAX_BLEND_WEIGHTS) |
|
iCount = MAX_BLEND_WEIGHTS; |
|
|
|
return iCount; |
|
} |
|
|
|
|
|
void DumpModelTEP::DumpWeights(int iVertex) |
|
{ |
|
if (m_mcExport) |
|
{ |
|
IPhyVertexExport *vtxExport = m_mcExport->GetVertexInterface(iVertex); |
|
|
|
if (vtxExport) |
|
{ |
|
if (vtxExport->GetVertexType() & BLENDED_TYPE) |
|
{ |
|
IPhyBlendedRigidVertex *pBlend = ((IPhyBlendedRigidVertex *)vtxExport); |
|
int iCount = 0; |
|
|
|
for (int i = 0; i < pBlend->GetNumberNodes(); i++) |
|
{ |
|
iCount = AddWeight( iCount, GetIndexOfINode( pBlend->GetNode( i ) ), pBlend->GetWeight( i ) ); |
|
} |
|
|
|
fprintf(m_pfile, " %2d ", iCount ); |
|
for (i = 0; i < iCount; i++) |
|
{ |
|
fprintf(m_pfile, " %2d %5.3f ", aWeights[i].iNode, aWeights[i].flWeight ); |
|
} |
|
} |
|
else |
|
{ |
|
INode *Bone = ((IPhyRigidVertex *)vtxExport)->GetNode(); |
|
|
|
fprintf(m_pfile, " 1 %2d 1.000", GetIndexOfINode(Bone) ); |
|
} |
|
m_mcExport->ReleaseVertexInterface(vtxExport); |
|
} |
|
} |
|
else if (m_wa != NULL) |
|
{ |
|
int iCount = 0; |
|
|
|
for ( int iBone = 0; iBone < m_wa->nb; iBone++) |
|
{ |
|
if (m_wa->w[iVertex * m_wa->nb + iBone] > 0.0) |
|
{ |
|
BonesPro_Bone bone; |
|
bone.t = BP_TIME_ATTACHED; |
|
bone.index = iBone; |
|
m_bonesProMod->SetProperty( BP_PROPID_GET_BONE_STAT, &bone ); |
|
if (bone.node != NULL) |
|
{ |
|
iCount = AddWeight( iCount, GetIndexOfINode( bone.node ), m_wa->w[iVertex * m_wa->nb + iBone] ); |
|
} |
|
} |
|
} |
|
|
|
fprintf(m_pfile, " %2d ", iCount ); |
|
for (int i = 0; i < iCount; i++) |
|
{ |
|
fprintf(m_pfile, " %2d %5.3f ", aWeights[i].iNode, aWeights[i].flWeight ); |
|
} |
|
} |
|
|
|
fprintf(m_pfile, "\n" ); |
|
fflush( m_pfile ); |
|
} |
|
|
|
void DumpModelTEP::cleanup(void) |
|
{ |
|
if (m_phyMod && m_phyExport) |
|
{ |
|
if (m_mcExport) |
|
{ |
|
m_phyExport->ReleaseContextInterface(m_mcExport); |
|
m_mcExport = NULL; |
|
} |
|
m_phyMod->ReleaseInterface(I_PHYINTERFACE, m_phyExport); |
|
m_phyExport = NULL; |
|
m_phyMod = NULL; |
|
} |
|
} |
|
|
|
|
|
Point3 DumpModelTEP::Pt3GetRVertexNormal(RVertex *prvertex, DWORD smGroupFace) |
|
{ |
|
// Lookup the appropriate vertex normal, based on smoothing group. |
|
int cNormals = prvertex->rFlags & NORCT_MASK; |
|
|
|
ASSERT_MBOX((cNormals == 1 && prvertex->ern == NULL) || |
|
(cNormals > 1 && prvertex->ern != NULL), "BOGUS RVERTEX"); |
|
|
|
if (cNormals == 1) |
|
return prvertex->rn.getNormal(); |
|
else |
|
{ |
|
for (int irn = 0; irn < cNormals; irn++) |
|
if (prvertex->ern[irn].getSmGroup() & smGroupFace) |
|
break; |
|
|
|
if (irn >= cNormals) |
|
{ |
|
irn = 0; |
|
// ASSERT_MBOX(irn < cNormals, "unknown smoothing group\n"); |
|
} |
|
return prvertex->ern[irn].getNormal(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
//=========================================================== |
|
// Dialog proc for export options |
|
// |
|
static BOOL CALLBACK ExportOptionsDlgProc( |
|
HWND hDlg, |
|
UINT message, |
|
WPARAM wParam, |
|
LPARAM lParam) |
|
{ |
|
static SmdExportClass *pexp; |
|
switch (message) |
|
{ |
|
case WM_INITDIALOG: |
|
pexp = (SmdExportClass*) lParam; |
|
CheckRadioButton(hDlg, IDC_CHECK_SKELETAL, IDC_CHECK_REFFRAME, IDC_CHECK_SKELETAL); |
|
SetFocus(GetDlgItem(hDlg,IDOK)); |
|
return FALSE; |
|
case WM_DESTROY: |
|
return FALSE; |
|
case WM_COMMAND: |
|
switch (LOWORD(wParam)) |
|
{ |
|
case IDOK: |
|
pexp->m_fReferenceFrame = IsDlgButtonChecked(hDlg, IDC_CHECK_REFFRAME); |
|
EndDialog(hDlg, 1); // 1 indicates "ok to export" |
|
return TRUE; |
|
case IDCANCEL: // 0 indicates "cancel export" |
|
EndDialog(hDlg, 0); |
|
return TRUE; |
|
case IDC_CHECK_SKELETAL: |
|
case IDC_CHECK_REFFRAME: |
|
CheckRadioButton(hDlg, IDC_CHECK_SKELETAL, IDC_CHECK_REFFRAME, LOWORD(wParam)); |
|
break; |
|
} |
|
} |
|
return FALSE; |
|
} |
|
|
|
|
|
|
|
//======================================================================== |
|
// Utility functions for getting/setting the personal "node index" property. |
|
// NOTE: I'm storing a string-property because I hit a 3DSMax bug in v1.2 when I |
|
// NOTE: tried using an integer property. |
|
// FURTHER NOTE: those properties seem to change randomly sometimes, so I'm |
|
// implementing my own. |
|
|
|
typedef struct |
|
{ |
|
char szNodeName[SmdExportClass::MAX_NAME_CHARS]; |
|
int iNode; |
|
} NAMEMAP; |
|
const int MAX_NAMEMAP = 512; |
|
static NAMEMAP g_rgnm[MAX_NAMEMAP]; |
|
|
|
int GetIndexOfINode(INode *pnode, BOOL fAssertPropExists) |
|
{ |
|
TSTR strNodeName(pnode->GetName()); |
|
for (int inm = 0; inm < g_inmMac; inm++) |
|
{ |
|
if (FStrEq(g_rgnm[inm].szNodeName, (char*)strNodeName)) |
|
{ |
|
return g_rgnm[inm].iNode; |
|
} |
|
} |
|
|
|
if (fAssertPropExists) |
|
ASSERT_MBOX(FALSE, "No NODEINDEXSTR property"); |
|
return -7777; |
|
} |
|
|
|
|
|
void SetIndexOfINode(INode *pnode, int inode) |
|
{ |
|
TSTR strNodeName(pnode->GetName()); |
|
NAMEMAP *pnm; |
|
for (int inm = 0; inm < g_inmMac; inm++) |
|
if (FStrEq(g_rgnm[inm].szNodeName, (char*)strNodeName)) |
|
break; |
|
if (inm < g_inmMac) |
|
pnm = &g_rgnm[inm]; |
|
else |
|
{ |
|
ASSERT_MBOX(g_inmMac < MAX_NAMEMAP, "NAMEMAP is full"); |
|
pnm = &g_rgnm[g_inmMac++]; |
|
strcpy(pnm->szNodeName, (char*)strNodeName); |
|
} |
|
pnm->iNode = inode; |
|
} |
|
|
|
|
|
//============================================================= |
|
// Returns TRUE if a node should be ignored during tree traversal. |
|
// |
|
BOOL FUndesirableNode(INode *pnode) |
|
{ |
|
// Get Node's underlying object, and object class name |
|
Object *pobj = pnode->GetObjectRef(); |
|
|
|
// Don't care about lights, dummies, and cameras |
|
if (pobj->SuperClassID() == CAMERA_CLASS_ID) |
|
return TRUE; |
|
if (pobj->SuperClassID() == LIGHT_CLASS_ID) |
|
return TRUE; |
|
|
|
return FALSE; |
|
} |
|
|
|
|
|
//============================================================= |
|
// Returns TRUE if a node has been marked as skippable |
|
// |
|
BOOL FNodeMarkedToSkip(INode *pnode) |
|
{ |
|
return (::GetIndexOfINode(pnode) == SmdExportClass::UNDESIRABLE_NODE_MARKER); |
|
} |
|
|
|
|
|
//============================================================= |
|
// Reduces a rotation to within the -2PI..2PI range. |
|
// |
|
static float FlReduceRotation(float fl) |
|
{ |
|
while (fl >= TWOPI) |
|
fl -= TWOPI; |
|
while (fl <= -TWOPI) |
|
fl += TWOPI; |
|
return fl; |
|
}
|
|
|