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。