3DsMax导出插件编写(二)——常规SDK方法进行信息获取和保存文件

之前已经把配置vs和maxSdk的方法介绍过了, 下面来介绍一下导出插件的具体写法。不过这不是一个容易说的很详细的问题。因为我们要写导出插件,通常都是因为想根据自己想要的信息来导出,所以就算我把我整个工程都公开,意义也不大的,因为那是根据我自己需要的数据写的业务,估计不太可能和你想要的一样的。所以我也只能简单的说明一些几个关键获取数据的方法,和保存文件的方法,如果想看具体的导出范例工程,你可以去maxSdk文件夹下面找到maxsdk\samples\import_export\3dsexp.cpp,这是一个完整的导出3ds文件的例子。

然后maxSdk提供的操作数据的方法有两种,一种是常规方法,一种是IGame方法。下面我们先来介绍一下常规的方法,至于IGame的方法,我会在另外一遍文章里面说明。

我建议先看看常规方法,对maxSdk有一个了解,然后我在介绍IGame的时候,会将两种方法做一个对比。
一、数据获取
首先要知道的是,maxSdk是通过一个ITreeEnumProc类来遍历场景里面所有的节点的。
然后需要知道,在场景里面,每一个物体就是一个节点,包括网格模型、灯光、摄像机、骨骼、辅助物体等,都是节点。我们可以通过获取节点的ClassID来判断该节点的实际类型。
最后,我们需要知道导出整个场景和导出选择中的物体的区别。这个并不是自动功能来的,也是需要自己来写代码判断的。
我这里举的例子比较简单,大家可以自己扩展。
先讲讲判断导出整个场景和导出选择中物体的判断方法:
我们先定义一个布尔变量,用作判断导出的模型,比如
static BOOL exportSelected;
然后,在DoExport方法里面,我们判断:
exportSelected = (options & SCENE_EXPORT_SELECTED) ? TRUE : FALSE;
如果是true,那就是导出单独选择的物体,如果是false,就是导出全部物体了。
最后,我们可以在节点树回调方法里面,判断,如果
if(exportSelected && node->Selected() == FALSE)这说明了开启了导出选择物体的模式,并且当前的节点没有被选中,我们直接return掉就行了。

下面正式开始导出数据:
1、写一个管理树节点的类,并写回调的方法

class MyTreeEnum : public ITreeEnumProc
{
public:
    int callback(INode*node);
};

int MyTreeEnum::callback(INode*node)
{
//判断是否导出选中的物体
if(exportSelected && node->Selected() == FALSE)
return TREE_CONTINUE;
ObjectState os = node->EvalWorldState(0);
if(os.obj)
{
    if(os.obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID,0)))
    {
        //这个节点是网格模型,根据需要对数据进行操作         
        return TREE_CONTINUE;
    }
    else
    {
        switch (os.obj->SuperClassID()) 
         { 
             case CAMERA_CLASS_ID: 
             //这个节点是摄像机,根据需要对数据进行操作 
             break; 
             case LIGHT_CLASS_ID: 
            //这个节点是灯光,根据需要对数据进行操作   
             break; 
            default:
             break; 
         }  
    }
}

上面是一个简单判断当前节点是什么类型的方法,我们还可以根据自己的需要,写一些判断某种单独类型的方法,比如判断一个节点是否为骨骼,可以这样写:

bool isBone(INode*thisBone)
{
ObjectState pObs=thisBone->EvalWorldState(0);
Class_ID id = pObs.obj->ClassID();
SClass_ID sid = pObs.obj->SuperClassID();
if (pObs.obj->ClassID()==Class_ID(BONE_CLASS_ID,0))
{
return true; 
}
if (pObs.obj->ClassID()==BONE_OBJ_CLASSID)
{
return true; 
}
if (pObs.obj->ClassID()==Class_ID(37157,0))
{
    return true; 
}
return false;

}

最后在DoExport方法里面
MyTreeEnum tempProc;
ei->theScene->EnumTree(&tempProc);
这样程序就会遍历所有的节点,然后做回调时的处理。

值得注意的问题是:
maxSdk里面的BONE_CLASS_ID或者BONE_OBJ_CLASSID,指的都是3dsmax的经典骨骼,也就是bones,不包括biped的。所以如果只用这两个ClassId来判断骨骼,是不行的,会把biped漏掉。阿赵我自己通过断点找出biped的ClassId是37157,但找不到sdk里面对应的类ID,所以只能直接ClassID()==Class_ID(37157,0)来判断它就是biped骨骼了。
上面这个判断也不是绝对的,比如有些动画师喜欢拿辅助物体作为骨骼来控制动画,那么这个判断骨骼的方法里面,就需要加上DUMMY_CLASS_ID的判断了,诸如此类,各位可以根据自己的业务去扩展。

2、获取网格模型信息:
刚才我们已经能判断到某个节点是网格模型了,所以我们接下来就可以对其进行信息的获取。
获取节点的名称:
node->GetName();
获取网格模型的具体网格:
TriObject* tri = (TriObject*)os.obj->ConvertToType(0, Class_ID(TRIOBJ_CLASS_ID,0));
Mesh *pMesh = &tri->GetMesh();
获取网格的点的数量:
int VerticesNum = pMesh->getNumVerts();
获取网格面的数量:
int FaceNum = pMesh->getNumFaces();
遍历所有的面,然后获取每个面的顶点索引:

for(i = 0;i<FaceNum;i++)
 { 
     Face face = pMesh->faces[i]; 
    //自己写个数组把它们存起来
     indexList.push_back(face.v[0]); 
     indexList.push_back(face.v[1]); 
     indexList.push_back(face.v[2]); 
}

遍历所有顶点,获取顶点座标:

for(i = 0;i<VerticesNum;i++)
{ 
    Vertex_t vertInfo; 
    int FaceNumber;
    Point3 pos = pMesh->getVert(i);
    //自己写个数组把它们存起来
    vertList.push_back(pos); 
}
    //uv座标
        for(i = 0;i<FaceNum;i++)
        {
            TVFace tvFace = pMesh->tvFace[i];
            for (int k=0; k<3; k++)  
            {
                Point3 uv = pMesh->tVerts[tvFace.t[k]];
                //自己写个数组存起来
                uvList.push_back(uv); 
            }
        }
这样,网格模型需要的顶点座标、索引、uv都有了,如果还需要法线或者其他信息,自己可以扩展。

3、获取蒙皮修改器:
对于已经蒙皮的模型,我们需要查找它的蒙皮修改器(这里用的是skin修改器)

ISkin * FindSkinModifier(INode *pINode)
{ 
Object * pObject = pINode->GetObjectRef(); 
if(pObject == 0) return 0; 
// 循环检测所有的DerivedObject 
while(pObject->SuperClassID() == GEN_DERIVOB_CLASS_ID){ 
IDerivedObject * pDerivedObject = static_cast<IDerivedObject *>(pObject); 
for(int stackId = 0; stackId < pDerivedObject->NumModifiers(); stackId++){ 
Modifier * pModifier = pDerivedObject->GetModifier(stackId); 
//检测ClassID是不是Skin修改器
if(pModifier->ClassID() == SKIN_CLASSID) { return (ISkin*)pModifier->GetInterface(I_SKIN);} 
} 
pObject = pDerivedObject->GetObjRef();//下一个Derived Object 
} 
return 0; 

}

在得到了有蒙皮信息之后,就可以获取它上面的蒙皮信息数据:

ISkinContextData* pSkinCtx = skin->GetContextInterface(node);
int nBones = pSkinCtx->GetNumAssignedBones(i);  
for (j = 0;j<nBones;j++)
{
    INode *pBone = skin->GetBone(j);
    //骨骼名称
    char* bName = pBone->GetName();
    //权重
    float weight = pSkinCtx->GetBoneWeight(i,j);
    //其他信息可以自己去扩展
    //……自己想办法存起来
}

我建议在获取蒙皮信息之前,最好先遍历所以节点把骨骼全部找出来,并排好序。因为我们最后保存的蒙皮信息里面的骨骼名称,我们可以变成骨骼的索引,包括骨骼父物体也是保存成索引,那么文件的容量会减少很多。

以上我是简单的说明了几种常用的信息的获取方法,在完全理解了之后,其他的信息的获取方法是一样的,具体可以查找一下maxSdk的api文档

二、保存文件
获取好数据之后,最后我们就要来保存了。很多人以为这个保存也是maxSdk提供的方法,其实不是的,保存文件,就是直接用c++自己的方法。比如我们可以用ofstream来保存。我们可以把之前获得的所有数据,都集中的转换成字符串,比如json,或者自己觉得解析起来比较方便的格式的字符串,然后就可以用ofstream来保存了。
具体的保存路径,是DoExport的传入参数name。

发布了69 篇原创文章 · 获赞 79 · 访问量 9万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章