1. 程式人生 > >3DsMax匯出外掛編寫(二)——常規SDK方法進行資訊獲取和儲存檔案

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。