在3D引擎中,骨骼動畫系統是非常重要的一個組成部分,雖然在一個游戲的真正開發過程中,一個優秀的游戲引擎也許不需要用戶去關心它的骨骼動畫系統是如何實現的,但是還是有很多人希望了解這樣的一個技術。
本文將會介紹骨骼動畫系統里的一個基礎部件:3Ds MAX 的骨骼動畫導出插件。
3Ds Max SDK和插件系統
最新版本MAX9的MAXSDK包含在安裝光盤里,在安裝完MAX后直接安裝SDK,并在工程里添加maxsdk的包含路徑和庫的路徑就可以開始編譯max插件了。MAX SDK還提供了3Ds Max Help for Visual Studio,這個幫助系統可以集成到Visual Studio .NET的幫助系統中,非常方便。建議在安裝的時候一起裝上。
MaxSDK主要目的就是用來開發MAX插件,雖然Max也提供了MaxScript,也可以用來做插件,但是對C++程序原來說,MaxSDK則更順手一些。
Max插件根據用途分為好幾種,每種對應不同的擴展名,在游戲開發中,我們通常比較關心三種類型的插件,他們分別是: 導入/導出插件,對應擴展名為dli/dle, utility 插件,對應擴展名為dlu,以及擴展名為dlm的modifier。導入導出插件基本上說是MAX與其它工具交互的接口。Utility插件則可以為MAX增加很多操作功能面板。Modifier則是3DsMAX
3DsMax自帶的插件放在X:/3DsMax/maxsdk/stdplugs目錄下,而我們自己編寫的插件通常會放到X:/3DsMax/maxsdk/ plugins目錄下。只要把插件放到這兩個目錄下,Max在啟動的時候就會自動加載你的插件。很多初學者可能會問dlm/dle這些插件是怎么生成的呢?其實這些都是一些標準的dll程序,只是擴展名不同而已。跟編譯一個普通的Windows DLL沒有區別。
初學MaxSDK最好的例子應該就是MAXSDK自帶的sample。在maxsdk的安裝目錄下可以找到,一般是X:/3DsMax/maxsdk/samples 下。這個目錄下已經對插件的種類進行了分類。一般在做骨骼動畫導出插件的時候,我們不會選擇導出插件而是選擇utility插件,這樣做的目的是ultility插件在max啟動的時候就處于激活狀態, 而導出插件則只會在用戶選擇export命令的那一刻,并且這些插件都可以訪問到MAX的整個環境,因此,使用utility插件會讓用戶更加的方便,本文的例子就是采用utility插件。
構造第一個3Ds Max 插件
本節我將講述如何快速的建立一個utility插件的框架, 因為關心的是導出插件本身的功能,而不是插件框架本身,因此我給大家提供一個個比較簡潔的方法:使用3dsmaxPluginWizard. 這是MaxSDK提供的一個組件,位于X:/3dsmax/maxsdk/howto/3dsmaxPluginWizard下, 仔細閱讀一遍這個目錄下的ReadMe.txt文件的Installing一小節,就可安裝好3DsMaxPluginWizard. 這時候打開Visual Studio 2005.在新建工程中就可以看到3Ds Max Plugin Wizard一項,選擇后,看到標簽頁一共有三頁,在第一頁Plugin-Type里,選擇Utility項,在接下來的Plugin Detail里填入詳細信息如圖2所示。
最后在Project Detials 選項卡里填入maxsdk的路徑,插件輸出路徑和3dsmax.exe所在的路徑就可以生成一個utility工程了。
生成的工程僅僅是一個架子,它包含了兩個類和一個IDD_PANEL的對話框。第一個類MyMaxSkinExporter是從UtilityObj派生下來的,代表了插件本身。另外一個類從ClassDesc2派生,用來描述這個插件的一些信息。IDD_PANEL則是我們插件的主界面,我們可以簡單的理解它就是我們插件的主窗口。
MyMaxSkinExporter有兩個重要的函數: BeginEditParams(Interface *ip,IUtil *iu)和EndEditParams(Interface *ip,IUtil *iu)這兩個函數。BeginEditParams可以簡單的理解成插件的初始化函數,EndEditParams則在插件退出時候被調用。參數Interface *ip 則代表整個Max對象,用它可以訪問到MAX程序的所有功能。
編譯這個工程,一個簡單的utility插件就已經生成了,你可以在剛才Project Detials選項卡里填入的插件輸出路徑里找到生成的插件。
3Ds MAX的場景組織和幾何管道
要編寫一個導出骨骼動畫的插件,必須先了解MAX是如何組織場景的,并了解MAX中一個mesh對象從建立到最終輸出都經歷那些階段。下面首先介紹一下MAX的場景組織。
MAX的整個場景是一個樹狀結構,樹的節點用INode來表示,整個樹的根節點可以通過Interface::GetRootNode來獲得,場景中的所有物體都是INode。INode中的NumberOfChildren函數和GetChildNode則用來訪問INode的子節點。要遍歷整個場景中對象,只需要通過Interface::GetRootNode和GetChildNode做一個遞歸Ѭ環就可以了。如果僅僅是想獲得在視口中選定的物體,則可以使用Interface::GetSelNodes函數。
INode僅僅是一個虛擬的節點,它本身僅僅包含一些標記和變換信息,并不表示實際的Object。實際的Object需要附著在INode上,并以INode的坐標系為Object的本地坐標系。Max中常見的Object有形狀(各種參數曲線),Camera,Mesh等。Object有自己的變換矩陣(TM), 在很多情況下這個矩陣都是單位矩陣。
INode的變換矩陣可以通過INode::GetNodeTM來獲得,而附著在INode上的Object的變換矩陣則通過INode::GetObjectTM來獲得,因為Object相對于Node的變換矩陣通常是單位矩陣,GetNodeTM和GetObjectTM獲得的矩陣通常也是一樣的,但是在必要的時候一定要加以區別。關于INode和Object的變換矩陣問題的詳細討論可以參考我blog上的一篇文章:http://blog.csdn.net/Nhsoft/archive/2005/01/06/241629.aspx
接下來我來看一下3DsMax一個幾何物體的Pipeline。前面說過Object是附著在INode上的,在MAX里,Node有一個Object Reference的指針,指向一個物體對象。熟悉MAX的操作方式的讀者都知道,我們在MAX里建立一個對象后,可以在上面添加各種修改器-Modifier。在Max的幾何管道中,我們建立的對象通常稱為Base Object。所有施加在這個物體上的修改器形成一個修改器堆棧-ModStack。BaseObject經過這個ModStack后形成一個新的Object Reference。ModStack中的每個Modifier都是輸入一個Object Reference,輸出一個Object Reference, 并且在應用第一個Modifier的時候會自動在幾何管道里插入一個Derived Object。最終INode的Object Reference將指向這個Derived Object。
Modifier在管道中的應用實例是ModApp對象,一個ModApp代表一個應用在Object上Modifier修改,ModApp包含一個ModContext的數據對象,Modifier用ModContext中的數據來對Object進行修改,以生成最終數據。
修改器按照應用的坐標系不同分成局部空間修改器和世界空間修改器(World Space Modifier)。局部空間修改器僅僅在Object的局部空間中修改Object,不會對坐標系造成影響。世界空間的修改器比如水波紋修改器則要求先將物體變換到世界空間后再進行修改,修改完成后的坐標也是世界空間的坐標。相對來說處理世界空間修改器會麻煩的多。(如果一個物體應用了世界空間修改器,則通過Mesh對象取得的坐標已經是世界坐標系中的了。不需要再乘以INode::GetObjectTM了)。
導出骨骼動畫數據
了解了MAX的場景管理和幾何管道以后,我們就可以很方便的建立一個如何取得MAX場景中定點數據的流程了。
骨骼動畫系統,首先應該包括物體的蒙皮數據和頂點與骨骼的綁定信息。我們分兩部分介紹皮膚數據的導出。第一步,我們要導出蒙皮數據,為了簡單起見,在這里只導出蒙皮的位置,法向量與切向量紋理坐標等信息留給讀者自己去研究。在3DsMax里。要建立骨骼動畫模型,可以使用兩種修改器Skin和,Physique。其中Physique是屬于CharacterStudio的,他的API相對比較復雜,本文只介紹使用Skin修改器制作的骨骼動畫模型文件。
在界面上增加一些按鈕
在了解了那么多理論后,我們可以開始做一些實質上的事情了,首先我們要給我們的插件增加一些按鈕,通過這些按鈕,使用可以下達保存/加載骨架,導出動作,導出皮膚等任務。
我們在第三節中生成的IDD_PANEL的對話框中加入幾個按鈕,分別用于保存骨架,加載骨架,導出動作,導出皮膚。并在對話框的WM_COMMAND消息中加入按鈕響應代碼。對話框的窗口過程為MyMaxSkinExporterDlgProc。
增加完的IDD_PANEL對話框看上去如圖4。
定義頂點數據類型
骨骼動畫的頂點數據應該包含頂點位置,紋理坐標,法向量,影響的骨骼編號和權重,一般影響到某個頂點的骨骼數目不會超過4個。同時,頂點位置也有兩種記錄方法:相對于世界空間的和相對骨骼空間的,這里我們采用相對于世界空間的記錄方法,因為這種方案比較直觀,只需要記錄一個頂點位置就可以。麻煩的地方在于,因為骨骼的變換矩陣要求頂點是相對于該骨骼的局部空間的,因此頂點在參與骨骼蒙皮計算的時候,需要先乘上骨骼的初始位置的矩陣的逆,以變換到骨骼空間。
struct Vertex_t
{
Point pos , normal , texCoord;
int matID;
int nEffBone;
struct{
int boneIdx;
float weight;
}Bone[4];
};
導出骨架
骨骼動畫系統中骨架為動畫的載體,所有的蒙皮都附著在骨架之上。同時要保證屬于一個角色的所有的蒙皮都使用同一個骨架來建立和導出,這是一套換裝系統的基本需求。因此骨架的導出和保存通常是一次性的,后續導出皮膚的時候都應該以這個骨架為基準。這也要求我們在導出骨架的時候就需要導出所有的骨骼。
骨架上的骨骼其實也是一個INode,骨骼僅僅是一些變換矩陣的信息而已。目前沒有特別好的辦法鑒定哪些INode是骨骼,比較可行的辦法是把所有Skin修改器使用到的INode都列為骨骼,同時美工還可以手動指定哪些Node為骨骼,并把這些標記用INode:: SetUserPropBool("IsBone",bIsBone);記錄到MAX文件中。
保存骨架的時候,需要保存骨骼的父子關系。并需要保存這個骨骼的第一幀數據。這要求如果美工在兩個不同的MAX文件里制作不同的動作的時候,除了保證骨架相同以外,第一幀也需要完全相同.
骨架的保存和加載代碼如下:
struct Bone_t{
Matrix NodeInitTM;
char Name[32],ParantName[32];
};
class CSkeleton {
public: vector<Bone_t> m_Bones;
void loadSkeleton(const char* skeFileName){
ifstream in(skeFileName , ios::binary);
while(!in.eof()){
Bone_t _bone;
in.read((char*)&_bone , sizeof(Bone_t));
m_Bones.push_back(_bone);
}
in.close();
}
int findBoneIndex(INode* pNode) {//
for(int i = 0 ; i < m_Bones.size() ; i ++)
if(string(m_Bones[i].Name) == pNode->GetName() ) return i;
return -1;
}
void saveBone(ostream& out , INode* pNode , bool bRoot){
Bone_t _bone;
_bone.NodeInitTM = pNode->GetNodeTM(0) ;
strncpy(_node.Name, pNode->GetName() , 32);
if(bRoot) _bone.ParantName[0] = 0;
else{
INode* pPNode =pNode->GetParantNode();
strncpy(_bone.ParantName, pPNode->GetName() , 32);
}
out.write( (char*)&_bone , sizeof(Bone_t) );
for(int i = 0 ; i < pNode->NumberOfChildren() ; i ++)
saveBone(out,pNode->GetChildNode(i), false);
}
void saveSkeleton(const char* skeFileName , INode* pRootNode){
ofstream out(skeFileName , ios::binary);
saveBone(out , pRootNode , true);
out.close();
}
};
findBoneIndex函數的目的是在把從文件中加載的骨骼和MAX中的Node對應起來.因為是根據名字來進行查找比較,因此要求所有的Node都必須要有唯一的名字.同時,骨骼之間的父子關系也是通過名字來標記的.每個Bone都記錄了它的父節點的名字.Save Skeleton骨架的按鈕響應代碼如下:
void OnSaveSkeleton()
{
CSkeleton* pSkeleton = GetGlobalSkeleton();
Assert(ip->GetSelNodeCount() == 1); //導出骨架的時候只能選擇一個節點
const char* filename = GetSaveFileName() ;
if(filename){
pSkeleton-> saveSkeleton(filename , ip->GetSelNode(0) );
}
}
導出骨架動作
骨架導出后,我們需要進一步導出這個骨架的動作。在導出動作的時候,需要加載一個事先已經導出的骨架。然后遍歷這個骨架中所有的骨骼,找到這個骨骼對應的INode對象。然后確定動畫的長度和幀數,為每一個骨頭的保存一個變換矩陣。
void OnExportAnimation()
{
const char* fileName = GetSaveFileName();
ofstream out(fileName , ios::binary);
Interval ARange = ip->GetAnimRange(); //獲得動畫的長度
TimeValue tAniTime = ARange.End() - ARange.Start();
TimeValue tTime = ARange.Start();
int nFrame = tAniTime/GetTicksPerFrame();
//計算動畫幀數
out.write((char*)&nFrame , sizeof(int));
//記錄有多少frame;
for(int i = 0 , ; i < nFrame ; i ++ ,tTime += GetTicksPerFrame()){
CSkeleton* pSkeleton = GetGlobalSkeleton();
for(int iBone = 0 ; iBone < pSkeleton->m_Bones.size() ; iBone ++){
Bone_t& bone = pSkeleton->m_Bones[iBone];
INode* pBoneNode = GetNodeByName(bone.Name);
//通過名字獲得INode指針
Matrix mat = pBoneNode->GetNodeTM(tTime,NULL);
out((char*)&mat , sizeof(Matrix));
}
}
out.close();
}
這里演示里我們記錄的是骨骼的絕對變換矩陣,而不是相對父骨骼的變換矩陣,這省去了我們從根骨骼開始計算骨架的麻煩,但是也多了很多限制,比如不能進行動作混合,不能做動作的插值等,使用相對父骨骼的局部矩陣的算法留給讀者自己去實現,也可以參考Cal3D和我開源的XReal3D的導出插件。 此外,因為我們在頂點數據中只保存了相對世界空間的位置,所以骨骼中的NodeInitTM將用來把相對世界空間的頂點位置變換到骨骼的局部空間中,皮膚混合的時候計算公式將如下:

其中M(t,i)為第i塊骨頭在t時刻的變換矩陣。
同樣的,我們只是簡單的導出每一幀的變換矩陣,而沒有處理關鍵幀,使用關鍵幀加上相對父節點的局部變換矩陣的四元數插值,在保準動作的準確性前提下能大大的降低動作文件磁盤占用。
游戲程序中的骨骼插件(下)
查找Skin修改器
要找到一個Mesh上是不是有Skin修改器,根據MAX的幾何管道的結構,需要遍歷整個ModStack中的Derived Object。判斷應用在這些Derived Object上的修改器的類型。MAX中所有的對象都有一個類似COM的GUID的唯一標記ClassID。Skin修改器的ClassID為SKIN_CLASSID,在獲得Derived Object的修改器后只需要檢查修改器的ClassID是不是SKIN_CLASSID即可。示例代碼如下:
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);
}
}
//下一//個Derived Object
pObject = pDerivedObject->GetObjRef();
}
return 0;
}
獲取Mesh對象
根據第四節中描述的,要從一個INode中獲得Mesh對象,首先應該從INode中獲得Object對象,然后再轉成Mesh對象。具體代碼如下:
Mesh* GetMesh(INode* pNode , int iMaxTime){
NullView view;
//NullView是自定義的View類。詳細參見完整的插件代碼
BOOL bNeedDelete = false;
ObjectState os = pNode->EvalWorldState(iMaxTime);
Object* pObj = os.obj;
TriObject * triObject = (TriObject *)pObj->
ConvertToType(iMaxTime, triObjectClassID);
GeomObject* pGeoObj = (GeomObject*)pObj;
Mesh * pMesh = pGeoObj->GetRenderMesh(
iMaxTime , m_pNode , view , bNeedDelete );
return pMesh;
}
獲取皮膚數據與頂點的骨骼綁定信息
在成功獲取到一個節點的ISkin對象和Mesh以后,就可以使用這兩個對象來提取物體的頂點數據和骨骼的綁定信息了。
Mesh中的數據保存在不同的數組中,常用的包含以下幾種:頂點位置信息,顏色信息,法向量,UV坐標,MapChannel信息等。其中法向量的信息不是特別的準確,需要考慮平滑組,面法向量與頂點法向量的差異等。MapChannel用于僅僅有多層紋理貼圖坐標的情況,在只有一層紋理坐標的情況下則不需要考慮,使用UV坐標就足夠了。本文僅僅演示如何導出頂點位置,單層紋理和骨骼綁定信息,如何準確的計算法向量以及處理多層紋理坐標的內容不在本文的討論范圍。
Mesh中的numVerts變量標記了Mesh有幾個頂點位置,numTexCoords標記了有幾個紋理坐標。而通常這兩個值是不一樣的。
要提取骨骼綁定信息,首先需要從ISkin對象中查詢到ISkinContextData接口: ISkinContextData* pSkinContext
= pSkin->GetContextInterface(pNode); 通過ISkinContextData的GetNumAssignedBones函數可以得到影響到這個頂點的骨骼的個數,進而通過GetAssignedBone和GetBoneWeight接口可以到這個Bone的index已經這個Bone對這個頂點的權重。
void GetVertexBoneInfo(INode* pNode , ISkin* pSkin , Mesh* mesh , int vertexIdx , int uvIdx , Vertex_t& vOut) {
CSkeleton* pSkeleton = GetGlobalSkeleton();
ISkinContextData* pSkinCtx =
pSkin->GetContextInterface(pNode);
int nBones =
pSkinCtx->GetNumAssignedBones(vertexIdx);
vOut.pos = mesh->verts[vertexIdx] *
pNode->GetObjectTM(0,NULL);//第一幀的數據
vOut.texCoord = mesh->texCoords[uvIdx];
vOut.nEffBone = nBones;
for(int jBone = 0; jBone < nBones; jBone ++) {
INode* pBone = pSkin->GetBone(
pSkinCtx->GetAssignedBone(vertexIdx, Bone));
vOut.Bone[jBone].weight =
pSkinCtx->GetBoneWeight(vertexIdx,jBone);
vOut.Bone[jBone].boneIdx =
pSkeleton->findBoneIndex(pBone);
}
}
上述示例代碼中并沒有考慮到有個別頂點的骨骼數量超過4種情況,在正式的代碼中應該對所有影響到這個頂點的骨骼權重進行排序,取前4個權重最大的,丟棄其余的骨骼。并要考慮有有些重復骨骼的問題。
導出面的和材質信息
在成功解決了頂點和骨骼的數據提取后,還需要獲得面的信息,即頂點的拓撲關系。3D渲染器能處理的面一般都是三角形面,因此一般要求美工在制作模型的時候首先將模型轉換成Editable Mesh。這樣能確保在Mesh取到的面都是三角形。
MAX中的面有兩種,用來表示形狀的Face類和用來表示紋理的TVFace類。Face和TVFace是一一對應的。就是說要獲得一個三角形的完整數據,必須同時獲取Face類和TVFace類。
Face類中的信息比較重要的有三個頂點的位置索引,可以通過Face::v[i]來獲得,得到這個索引后,在Mesh::verts數組里就可以獲得位置的數據。同理TVFace也一樣。
Face類里除了頂點信息外,還保存了和材質相關的信息:MaterialID。一個Mesh上通常只能應用一個Material,那為什么會有MaterialID這個概念呢?因為在Max里除了標準材質以外,還有一種美工非常有用的材質叫多材質(Multi-Material),這種材質可以可以包含很多個標準材質,我們可以通過判斷材質的ClassID來判斷它是不是一個多材質,如果是,就遍歷它的所有子材質(Sub-Mateiral)。MaterialID就是對應的子材質的序號。在繪制的時候,MaterialID相同的三角形表示它們有相同的紋理和材質,在導出的時候應該按照MaterialID進行排序。
Max中的Material使用Mtl類表示,可以通過INode::GetMtl()來獲得。Mtl:: NumSubMtls加GetSubMtl則可訪問到這個Material的所有Material。對于一個標準材質,我們可以獲得這個材質的各種屬性,包括紋理貼圖,紋理貼圖使用的貼圖坐標通道(MapChannel),以及環境光,高光等屬性。處理材質的示例代碼如下:
void SaveMaterial(const char* fileMat , INode* pNode){
ofstream out(fileMat,ios::binary);
Mtl* pMtl = pNode->GetMtl();
int nSubMat = pMtl->NumSubMtls();
if (nSubMat == 0) {
//處理和保存一個標準材質的代碼,詳見參考資料
saveStdMaterial(out , (StdMat*)pMtl);
} else {
for(int i = 0 ; i < pMtl->NumSubMtls(); i++ )
saveStdMaterial(out ,
(StdMat*)pMtl->GetSubMtl(i));
}
}
以下為簡單的提取三角形數據的代碼。
void SaveMesh(const char* fileMesh , INode* pNode , Mesh* pMesh) {
ISkin* pSkin = GetSkinModifier(pNode);
ofstream out(fileMesh , ios::binary);
int nFace = pMesh->numFaces ;
out.write( (char*)&nFace,sizeof(int) );
for(int i = 0 ;i < pMesh->numFaces ; i ++) {
TVFace& tvface = pMesh->tvFace[i];
Face& face = pMesh->faces[i];
for(int j = 0 ; j < 3 ; j++) {
//一個三角形三個頂點
Vertex_t vert ;
vert.matID = face.getMatID();
GetVertexBoneInfo(pNode, pSkin, pMesh,
face.v[j], tvface.getTVert(j), vert);
//保存到文件
out.write( (char*)&vert,sizeof(vert) );
}
}
}
上述代碼僅僅是簡單的保存了每一個面的信息,真正應用的時候,至少還應該把所有的面按照MateiralID來進行排序,并且GetVertexBoneInfo函數在生成頂點數據的時候必然有很多頂點是完全相同的,應該把這些相同的頂點都去掉。面的信息使用Index Buffer來保存。鑒于處理這些代碼過長,就不在文中一一舉例。
除了頂點材質相關的信息之外,Face類中比較重要的是法向量相關的信息,如RVertex和平滑組數據(smooth group)等。讀者可以參閱參考資料。
進一步的工作
現在我們已經基本可以獲得一個簡單的骨骼動畫系統所需要的大部分數據了。但是這僅僅局限于一個演示性的骨骼動畫系統,離一個完整魯棒的系統還有很多事情要做。
上述例子中,我們僅僅導出了皮膚的頂點和第一層紋理坐標。讀者還需要進一步處理多層貼圖,頂點顏色,法向量等數據。在導出的骨架中,還應該能更方便的處理骨架的層次關系,以及能區分角色的上身和下身。因為通常一個骨骼動畫系統是需要進行上下身的動作融合的。其次骨骼的變換矩陣應該是保存相對于父骨骼的局部變換矩陣,局部矩陣可以分解成四元數,平移和縮放,使用四元數能進行更平滑的插值和動作間的過渡,而且為了減少動作文件的大小,關鍵幀動畫也是值得考慮的一個技術點。
在數據組織方面,我認為一個完善的骨骼動畫系統應該能合理而自然的管理所有數據,XReal3D的數據組織采用的是流式存儲,所有的動作、骨架、頂點位置、紋理坐標都擁有自己的流。這樣的存儲方式,在保持彈性的前提下大大的減少了一個骨骼動畫角色的文件數量,方便管理和維護。
此外,現在的角色動畫系統還應該包括表情動畫和柔體系統。表情動畫,一般稱為在3DsMax的例子里有一個修改器就是表情動畫,編譯這個插件和使用它的頭文件,我們可以訪問Max里的表情動畫。這個插件的位置在X:/3dsmax/maxsdk/samples/modifiers/morpher中。柔體系統也叫布料系統,可以用它來制作衣服飄動的效果。
最后一個需要說的IGame, IGame是3DsMaxSDK中附帶一個用來為導出3D游戲相關數據的開發包。基本上,IGame是3DsMAXSDK的一個包裝,使用起來更加方便而已。要深入的了解和掌握3DsMax插件的開發,還是需要對3DsMaxSDK有一定的熟悉程度。
總而言之,一個完整的骨骼動畫系統插件是非常復雜的工程,鑒于文章篇幅,不可能介紹的面面俱到,希望本文對那些初次接觸MAX插件開發的讀者能有一定的幫助。
參考資料
1.3DsMAX Help
2. Cal3D : https://gna.org/projects/cal3d/
3. XReal3D MAX Exporter : http://gforge.osdn.net.cn/projects/xreal3d/
作者簡介:
潘李亮,2003年畢業于西北工業大學,愛好計算機圖形學,曾在游戲公司負責3D引擎開發工作,目前在Corel公司從事多媒體軟件開發。
文章列表