如何使用3dmax导出插件导出模型信息

最近研究了下3dmax导出插件的东西.游戏编程精粹2上给过部分代码,但不完整而且我觉得那上面的实现并不好.这里贴下我的实现.
    首先定义骨骼的结构:

struct Bone { INode* pNode;//该骨骼的节点 int parent;//父节点序号 typedef std::vector Child_Vec; Child_Vec childs;//子节点序号 Matrix3 matrix;//基本变换矩阵 }; typedef std::vector Bone_Vec; Bone_Vec m_bones;  
        我将骨骼结构放入一个顺序表中,Bone::parent和Bone::childs中存放的都是这个顺序表中的索引号.
    然后是权重信息.精粹2上是将权重信息放到骨骼的结构中去,我认为这样不太好.因为如果要使用硬件shader实现骨骼动画的话,是需要把权重信息放到顶点缓冲中去传送给显卡的,因此应该放到每个顶点的信息中去.
    我是这样定义:
  typedef std::map Weight_Vertx;//权重顶点
    其中键是骨骼的索引号,值是权重.
    typedef std::vector Weight_Vertx_List;//权重顶点列表
    Weight_Vertx_List m_weight_vertexs;//权重顶点
    最终希望的是读取骨架结构,放入m_bones中,然后读取每点的权重,放入m_weight_vertexs
中.
=============================传说中的分割线======================================
    下面先给出整个导出类的定义:

class Export_Mesh { public: Export_Mesh(INode* pRoot); ~Export_Mesh(); bool export_file(const TCHAR* name); private: void export_mesh(INode* pNode); int load_bone_struct(INode* pNode, int parentIdx);//读取骨架结构 bool load_bone_weights(INode* pNode); bool is_mesh(INode* pNode);//判断一个节点是否是模型 bool is_bone(INode* pNode);//判断一个节点是否是骨骼 int get_bone_index(INode* pNode);//取得一个骨骼节点的序号 Modifier* get_mod(INode* pNode, Class_ID id);//取得指定变形器 void get_skin_weights(INode* pNode, Modifier* pMod);//取得皮肤权重 void get_physique_weights(INode* pNode, Modifier* pMod);//取得体形权重 private: INode* m_root; typedef std::map Weight_Vertx;//权重顶点 typedef std::vector Weight_Vertx_List;//权重顶点列表 Weight_Vertx_List m_weight_vertexs;//权重顶点 //一块骨骼 struct Bone { INode* pNode;//该骨骼的节点 int parent;//父节点序号 typedef std::vector Child_Vec; Child_Vec childs;//子节点序号 Matrix3 matrix;//基本变换矩阵 }; typedef std::vector Bone_Vec; Bone_Vec m_bones; };
先给出几个基本的函数的实现,在精粹2上都有的:

bool Export_Mesh::is_mesh(INode* pNode) { if(NULL == pNode) return false; ObjectState os = pNode->EvalWorldState(0); if(!os.obj) return false; return os.obj->SuperClassID() == GEOMOBJECT_CLASS_ID; } //----------------------------------------------------------------------------- bool Export_Mesh::is_bone(INode* pNode) { if(NULL == pNode) return false; ObjectState os = pNode->EvalWorldState(0); if(!os.obj) return false; if( os.obj->ClassID() == Class_ID(BONE_CLASS_ID, 0) ) return true; if( os.obj->ClassID() == Class_ID(DUMMY_CLASS_ID, 0) ) return false; Control* cont = pNode->GetTMController(); //Biped部分 return cont->ClassID() == BIPSLAVE_CONTROL_CLASS_ID || //Biped root "Bip01" cont->ClassID() == BIPBODY_CONTROL_CLASS_ID; }  
    我改动的一点是取得骨骼的索引值(get_bone_index函数).按照精粹2的写法,取得的索引值不是从0开始连续的,因此不容易按照他给的索引值放到一个顺序表中.因此我换了一种方法.
    还记得在struct Bone结构中的INode* pNode;指针么,我是先读取整个骨架结构,然后将每个读取到的骨骼放入顺序表中,并记录下每个骨骼的节点指针.这样需要取得指定骨骼节点的索引只需要去查骨骼列表就好了.废话不多说了,实现如下:

int Export_Mesh::load_bone_struct(INode* pNode, int parentIdx) { //如果该节点为骨骼节点 if(this->is_bone(pNode)) { //向骨骼列表中添加该节点 Bone bone; bone.pNode = pNode;//记录该节点指针 bone.parent = parentIdx;//记录父节点索引 bone.matrix = pNode->GetNodeTM(0.0f);//记录基本变换矩阵 bone.matrix.NoScale(); bone.matrix.Invert(); m_bones.push_back(bone); const int currIdx = static_cast(m_bones.size() - 1); //递归遍历所有子节点 for(int i=0;iNumberOfChildren();i++) { INode* cNode = pNode->GetChildNode(i); int cIdx = this->load_bone_struct(pNode->GetChildNode(i), currIdx); if(cIdx >= 0) { m_bones.at(currIdx).childs.push_back(cIdx); } } return currIdx; } else//如果该节点不是骨骼节点,则递归遍历子节点 { for(int i = 0; i < pNode->NumberOfChildren(); ++i) { this->load_bone_struct(pNode->GetChildNode(i), -1); } return -1; } }
在这个函数完成工作后,就可以通过get_bone_index函数查找骨骼的索引了.

int Export_Mesh::get_bone_index(INode* pNode) { //测试该节点是否为骨骼节点 if(!this->is_bone(pNode)) { return -1; } //遍历骨骼数组查找该节点是否存在其中 for(int i=0; i < m_bones.size(); ++i) { if(m_bones.at(i).pNode == pNode) { return i; } } return -1; } 
    接下来是读取权重信息.先看看load_bone_weights函数的实现:

bool Export_Mesh::load_bone_weights(INode* pNode) { if((!pNode)||(!this->is_mesh(pNode))) return false; //尝试取得体形变形器 Modifier* pmf = this->get_mod(pNode, Class_ID(PHYSIQUE_CLASS_ID_A, PHYSIQUE_CLASS_ID_B)); if(pmf)//如果取得成功 { //设置权重顶点尺寸 this->get_physique_weights(pNode, pmf); } else//如果没有,尝试取得皮肤变形器 { pmf = this->get_mod(pNode, SKIN_CLASSID); if(pmf) { this->get_skin_weights(pNode, pmf); } } //递归处理子节点 const int childCnt = pNode->NumberOfChildren(); for(int i = 0; i < childCnt; ++i) { this->load_bone_weights(pNode->GetChildNode(i)); } return true; }
在3dmax中,骨骼动画可能由常规的皮肤构造,也可以由character studio构造(好吧,我承认我是猜的.实际上我基本没用过3dmax).因此一个带有骨骼动画的模型节点可能存在两种变形器:体形变形器和皮肤变形器.要导出权重就需要变形器对象.下面看看取得变形器使用的函数:
Modifier* Export_Mesh::get_mod(INode* pNode, Class_ID id) { Object* pObj = pNode->GetObjectRef(); if(!pObj) return NULL; //是否是个导出对象 while(pObj->SuperClassID() == GEN_DERIVOB_CLASS_ID) { //如果是导出对象 IDerivedObject* pDerivedObj = static_cast(pObj); //循环变形器堆栈上的所有实体 for(int i=0; iNumModifiers(); ++i) { //取得当前的变形器 Modifier* pMod = pDerivedObj->GetModifier(i); //是否是体形? if(pMod->ClassID() == id) { return pMod; } } pObj = pDerivedObj->GetObjRef(); } //没有找到 return NULL; } 
    这个函数没有什么好解释的,就是检查下ClassID的工作.
    下面是重点,从变形器中取出顶点在各个骨骼上的权重,先看下取得形体变形器的get_physique_weights:
void Export_Mesh::get_physique_weights(INode* pNode, Modifier* pMod) { //为变形器创建一个体形导出接口 IPhysiqueExport* phyInterface = (IPhysiqueExport*)pMod->GetInterface(I_PHYINTERFACE); if(!phyInterface) return; //为指定节点创建一个ModContext导出接口 IPhyContextExport* modContextInt = (IPhyContextExport*)phyInterface->GetContextInterface(pNode); if(modContextInt) { //需要顶点接口(现在只支持刚体) modContextInt->ConvertToRigid(TRUE); //允许每个节点超过一个骨骼 modContextInt->AllowBlending(TRUE); const int totalVtx = modContextInt->GetNumberVertices();//取得总顶点数 //调整权重顶点的尺寸 assert(m_weight_vertexs.size() == 0); m_weight_vertexs.resize(totalVtx); for(int i = 0; i < totalVtx; ++i)//遍历所有的顶点 { //取得顶点导出器的接口 IPhyVertexExport* vtxInterface = (IPhyVertexExport*)modContextInt->GetVertexInterface(i); if(!vtxInterface) continue; int vtxType = vtxInterface->GetVertexType(); if(RIGID_TYPE == vtxType)//只受一块骨骼影响,权重为1 { //取得骨骼节点 INode* boneNode = ((IPhyRigidVertex*)vtxInterface)->GetNode(); int boneIdx = this->get_bone_index(boneNode); //设置骨骼权重 m_weight_vertexs.at(i)[boneIdx] = 1.0f; } else if(RIGID_BLENDED_TYPE == vtxType)//受多块骨骼影响 { IPhyBlendedRigidVertex* vtxBlendedInt = (IPhyBlendedRigidVertex*)vtxInterface; assert(vtxBlendedInt); for(int j = 0; j < vtxBlendedInt->GetNumberNodes(); ++j) { INode* boneNode = vtxBlendedInt->GetNode(j); int boneIdx = this->get_bone_index(boneNode); //查找此权重顶点中是否存在此骨骼的权重 Weight_Vertx::iterator fpos = m_weight_vertexs.at(i).find(boneIdx); if(fpos == m_weight_vertexs.at(i).end())//如果不存在 { //则向当前骨骼的权重列表中添加新的一项 m_weight_vertexs.at(i)[boneIdx] = vtxBlendedInt->GetWeight(j); } else//如果存在 { //则累加上当前骨骼的权重 fpos->second += vtxBlendedInt->GetWeight(j); } } } } phyInterface->ReleaseContextInterface(modContextInt); } pMod->ReleaseInterface(I_PHYINTERFACE, phyInterface); } 
    我想代码的注释已经很清楚了,没有必要解释了吧?get_skin_weights函数基本和上面那个是一样的:

void Export_Mesh::get_skin_weights(INode* pNode, Modifier* pMod) { //为变形器创建一个皮肤导出接口 ISkin* skin = (ISkin*)pMod->GetInterface(I_SKIN); if(!skin) return; //为指定节点创建一个皮肤上下文 ISkinContextData* skincontext = skin->GetContextInterface(pNode); if(skincontext) { //遍历所有顶点,取得权重 const int numVert = skincontext->GetNumPoints(); //调整权重顶点的尺寸 assert(m_weight_vertexs.size() == 0); m_weight_vertexs.resize(numVert); for(int i = 0; i < numVert; ++i) { const int numBones = skincontext->GetNumAssignedBones(i); for(int j = 0; j < numBones; ++j) { INode* bone = skin->GetBone(skincontext->GetAssignedBone(i,j));//do not use j,but use GetAssignedBone(i,j) if(!bone) continue; int boneIdx = this->get_bone_index(bone); if(-1 == boneIdx) continue; //查找此权重顶点中是否存在此骨骼的权重 Weight_Vertx::iterator fpos = m_weight_vertexs.at(i).find(boneIdx); if(fpos == m_weight_vertexs.at(i).end())//如果不存在 { //则向当前骨骼的权重列表中添加新的一项 m_weight_vertexs.at(i)[boneIdx] = skincontext->GetBoneWeight(i,j); } else//如果存在 { //则累加上当前骨骼的权重 fpos->second += skincontext->GetBoneWeight(i,j); } } } } pMod->ReleaseInterface(I_SKIN, skin); }
最后要导出整个模型的全部信息,就冲根节点开始递归遍历所有节点进行导出工作:
void Export_Mesh::export_mesh(INode* pNode) { if(this->is_mesh(pNode)) { this->load_bone_weights(pNode); } //递归遍历所有的模型子节点 for(int i = 0; i < pNode->NumberOfChildren(); ++i) { this->export_mesh(pNode->GetChildNode(i)); } } 
    现在终于把骨骼动画的信息按照我需要的格式放进内存了.为了看下导出的效果,输出到文本文件:

bool Export_Mesh::export_file(const TCHAR* name) { std::ofstream outfile(name); //先读取骨架信息 this->load_bone_struct(pRoot, -1); //遍历所有的模型节点,读出权重信息 this->export_mesh(m_root); //输出骨架信息 outfile << "Bone Information: " << m_bones.size() << "/n"; for(int i = 0; i < m_bones.size(); ++i) { outfile << "Bone Index: " << i << "/n" << "Bone id: " << (void*)(m_bones.at(i).pNode) << "/n" << "Parent Index: " << m_bones.at(i).parent << "/n" << "Childs Index: "; for( Bone::Child_Vec::iterator j = m_bones.at(i).childs.begin(); j != m_bones.at(i).childs.end(); ++j ) { outfile << *j << " "; } outfile << "/n=======================================================/n"; } outfile << "Bone Weights: " << m_weight_vertexs.size() << "/n"; for(int i = 0; i < m_weight_vertexs.size(); ++i) { outfile << "Vertex Index:" << i << "/n"; for(Weight_Vertx::iterator j = m_weight_vertexs.at(i).begin(); j != m_weight_vertexs.at(i).end(); ++j) { outfile << "/tBone Index:" << j->first << "; Weight: " << j->second << "/n"; } outfile << "/n=======================================================/n"; } outfile.flush(); outfile.close(); return true; }  
    最后一步,在3dmax导出插件中让它起作用:

int pge_3dmax_exporter::DoExport(const TCHAR *name,ExpInterface *ei,Interface *i, BOOL suppressPrompts, DWORD options) { Export_Mesh export_mesh(i->GetRootNode()); export_mesh.export_file(name); return TRUE; }  
 最终输出的文本文件如下:
Bone Information: 32
Bone Index: 0
Bone id: 08CC6CB0
Parent Index: -1
Childs Index: 1
=======================================================
Bone Index: 1
Bone id: 08CC33A0
Parent Index: 0
Childs Index: 2
=======================================================
......

Bone Weights: 462
Vertex Index:0
Bone Index:26; Weight: 1
=======================================================
Vertex Index:1
Bone Index:7; Weight: 0.999999
Bone Index:18; Weight: 1.01289e-006
=======================================================
Vertex Index:2
Bone Index:5; Weight: 9.13872e-006
Bone Index:7; Weight: 0.999991
......

你可能感兴趣的:(图形学)