• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            永遠(yuǎn)也不完美的程序

            不斷學(xué)習(xí),不斷實(shí)踐,不斷的重構(gòu)……

            常用鏈接

            統(tǒng)計(jì)

            積分與排名

            好友鏈接

            最新評(píng)論

            游戲程序中的骨骼插件(上)

            文/潘李亮

            引言

            在3D引擎中,骨骼動(dòng)畫系統(tǒng)是非常重要的一個(gè)組成部分,雖然在一個(gè)游戲的真正開發(fā)過程中,一個(gè)優(yōu)秀的游戲引擎也許不需要用戶去關(guān)心它的骨骼動(dòng)畫系統(tǒng)是如何實(shí)現(xiàn)的,但是還是有很多人希望了解這樣的一個(gè)技術(shù)。

            本文將會(huì)介紹骨骼動(dòng)畫系統(tǒng)里的一個(gè)基礎(chǔ)部件:3Ds MAX 的骨骼動(dòng)畫導(dǎo)出插件。

            3Ds Max SDK和插件系統(tǒng)

            最新版本MAX9的MAXSDK包含在安裝光盤里,在安裝完MAX后直接安裝SDK,并在工程里添加maxsdk的包含路徑和庫的路徑就可以開始編譯max插件了。MAX SDK還提供了3Ds Max Help for Visual Studio,這個(gè)幫助系統(tǒng)可以集成到Visual Studio .NET的幫助系統(tǒng)中,非常方便。建議在安裝的時(shí)候一起裝上。

            MaxSDK主要目的就是用來開發(fā)MAX插件,雖然Max也提供了MaxScript,也可以用來做插件,但是對(duì)C++程序原來說,MaxSDK則更順手一些。

            Max插件根據(jù)用途分為好幾種,每種對(duì)應(yīng)不同的擴(kuò)展名,在游戲開發(fā)中,我們通常比較關(guān)心三種類型的插件,他們分別是: 導(dǎo)入/導(dǎo)出插件,對(duì)應(yīng)擴(kuò)展名為dli/dle, utility 插件,對(duì)應(yīng)擴(kuò)展名為dlu,以及擴(kuò)展名為dlm的modifier。導(dǎo)入導(dǎo)出插件基本上說是MAX與其它工具交互的接口。Utility插件則可以為 MAX增加很多操作功能面板。Modifier則是3DsMAX

            3DsMax自帶的插件放在X:\3DsMax\maxsdk\stdplugs目錄下,而我們自己編寫的插件通常會(huì)放到X:\3DsMax \maxsdk\ plugins目錄下。只要把插件放到這兩個(gè)目錄下,Max在啟動(dòng)的時(shí)候就會(huì)自動(dòng)加載你的插件。很多初學(xué)者可能會(huì)問dlm/dle這些插件是怎么生成的呢?其實(shí)這些都是一些標(biāo)準(zhǔn)的dll程序,只是擴(kuò)展名不同而已。跟編譯一個(gè)普通的Windows DLL沒有區(qū)別。



            初學(xué)MaxSDK最好的例子應(yīng)該就是MAXSDK自帶的sample。在maxsdk的安裝目錄下可以找到,一般是X:\3DsMax \maxsdk\samples 下。這個(gè)目錄下已經(jīng)對(duì)插件的種類進(jìn)行了分類。一般在做骨骼動(dòng)畫導(dǎo)出插件的時(shí)候,我們不會(huì)選擇導(dǎo)出插件而是選擇utility插件,這樣做的目的是 ultility插件在max啟動(dòng)的時(shí)候就處于激活狀態(tài),而導(dǎo)出插件則只會(huì)在用戶選擇export命令的那一刻,并且這些插件都可以訪問到MAX的整個(gè)環(huán)境,因此,使用utility插件會(huì)讓用戶更加的方便,本文的例子就是采用utility插件。

            構(gòu)造第一個(gè)3Ds Max 插件

            本節(jié)我將講述如何快速的建立一個(gè)utility插件的框架,因?yàn)殛P(guān)心的是導(dǎo)出插件本身的功能,而不是插件框架本身,因此我給大家提供一個(gè)個(gè)比較簡(jiǎn)潔的方法:使用3dsmaxPluginWizard. 這是MaxSDK提供的一個(gè)組件,位于X:\3dsmax\maxsdk\howto\3dsmaxPluginWizard下,仔細(xì)閱讀一遍這個(gè)目錄下的ReadMe.txt文件的Installing一小節(jié),就可安裝好3DsMaxPluginWizard. 這時(shí)候打開Visual Studio 2005.在新建工程中就可以看到3Ds Max Plugin Wizard一項(xiàng),選擇后,看到標(biāo)簽頁一共有三頁,在第一頁P(yáng)lugin-Type里,選擇Utility項(xiàng),在接下來的Plugin Detail里填入詳細(xì)信息如圖2所示。



            最后在Project Detials 選項(xiàng)卡里填入maxsdk的路徑,插件輸出路徑和3dsmax.exe所在的路徑就可以生成一個(gè)utility工程了。

            生成的工程僅僅是一個(gè)架子,它包含了兩個(gè)類和一個(gè)IDD_PANEL的對(duì)話框。第一個(gè)類MyMaxSkinExporter是從 UtilityObj派生下來的,代表了插件本身。另外一個(gè)類從ClassDesc2派生,用來描述這個(gè)插件的一些信息。IDD_PANEL則是我們插件的主界面,我們可以簡(jiǎn)單的理解它就是我們插件的主窗口。

            MyMaxSkinExporter有兩個(gè)重要的函數(shù): BeginEditParams(Interface *ip,IUtil *iu)和EndEditParams(Interface *ip,IUtil *iu)這兩個(gè)函數(shù)。BeginEditParams可以簡(jiǎn)單的理解成插件的初始化函數(shù),EndEditParams則在插件退出時(shí)候被調(diào)用。參數(shù) Interface *ip 則代表整個(gè)Max對(duì)象,用它可以訪問到MAX程序的所有功能。

            編譯這個(gè)工程,一個(gè)簡(jiǎn)單的utility插件就已經(jīng)生成了,你可以在剛才Project Detials選項(xiàng)卡里填入的插件輸出路徑里找到生成的插件。

            3Ds MAX的場(chǎng)景組織和幾何管道

            要編寫一個(gè)導(dǎo)出骨骼動(dòng)畫的插件,必須先了解MAX是如何組織場(chǎng)景的,并了解MAX中一個(gè)mesh對(duì)象從建立到最終輸出都經(jīng)歷那些階段。下面首先介紹一下MAX的場(chǎng)景組織。

            MAX的整個(gè)場(chǎng)景是一個(gè)樹狀結(jié)構(gòu),樹的節(jié)點(diǎn)用INode來表示,整個(gè)樹的根節(jié)點(diǎn)可以通過Interface::GetRootNode來獲得,場(chǎng)景中的所有物體都是INode。INode中的NumberOfChildren函數(shù)和GetChildNode則用來訪問INode的子節(jié)點(diǎn)。要遍歷整個(gè)場(chǎng)景中對(duì)象,只需要通過Interface::GetRootNode和GetChildNode做一個(gè)遞歸Ѭ環(huán)就可以了。如果僅僅是想獲得在視口中選定的物體,則可以使用Interface::GetSelNodes函數(shù)。

            INode僅僅是一個(gè)虛擬的節(jié)點(diǎn),它本身僅僅包含一些標(biāo)記和變換信息,并不表示實(shí)際的Object。實(shí)際的Object需要附著在INode上,并以INode的坐標(biāo)系為Object的本地坐標(biāo)系。Max中常見的Object有形狀(各種參數(shù)曲線),Camera,Mesh等。Object有自己的變換矩陣(TM), 在很多情況下這個(gè)矩陣都是單位矩陣。



            INode的變換矩陣可以通過INode::GetNodeTM來獲得,而附著在INode上的Object的變換矩陣則通過 INode::GetObjectTM來獲得,因?yàn)镺bject相對(duì)于Node的變換矩陣通常是單位矩陣,GetNodeTM和GetObjectTM獲得的矩陣通常也是一樣的,但是在必要的時(shí)候一定要加以區(qū)別。關(guān)于INode和Object的變換矩陣問題的詳細(xì)討論可以參考我blog上的一篇文章:http://blog.csdn.net/Nhsoft/archive/2005/01/06/241629.aspx

            接下來我來看一下3DsMax一個(gè)幾何物體的Pipeline。前面說過Object是附著在INode上的,在MAX里,Node有一個(gè) Object Reference的指針,指向一個(gè)物體對(duì)象。熟悉MAX的操作方式的讀者都知道,我們?cè)贛AX里建立一個(gè)對(duì)象后,可以在上面添加各種修改器 -Modifier。在Max的幾何管道中,我們建立的對(duì)象通常稱為Base Object。所有施加在這個(gè)物體上的修改器形成一個(gè)修改器堆棧-ModStack。BaseObject經(jīng)過這個(gè)ModStack后形成一個(gè)新的 Object Reference。ModStack中的每個(gè)Modifier都是輸入一個(gè)Object Reference,輸出一個(gè)Object Reference, 并且在應(yīng)用第一個(gè)Modifier的時(shí)候會(huì)自動(dòng)在幾何管道里插入一個(gè)Derived Object。最終INode的Object Reference將指向這個(gè)Derived Object。

            Modifier在管道中的應(yīng)用實(shí)例是ModApp對(duì)象,一個(gè)ModApp代表一個(gè)應(yīng)用在Object上Modifier修改,ModApp包含一個(gè)ModContext的數(shù)據(jù)對(duì)象,Modifier用ModContext中的數(shù)據(jù)來對(duì)Object進(jìn)行修改,以生成最終數(shù)據(jù)。

            修改器按照應(yīng)用的坐標(biāo)系不同分成局部空間修改器和世界空間修改器(World Space Modifier)。局部空間修改器僅僅在Object的局部空間中修改Object,不會(huì)對(duì)坐標(biāo)系造成影響。世界空間的修改器比如水波紋修改器則要求先將物體變換到世界空間后再進(jìn)行修改,修改完成后的坐標(biāo)也是世界空間的坐標(biāo)。相對(duì)來說處理世界空間修改器會(huì)麻煩的多。(如果一個(gè)物體應(yīng)用了世界空間修改器,則通過Mesh對(duì)象取得的坐標(biāo)已經(jīng)是世界坐標(biāo)系中的了。不需要再乘以INode::GetObjectTM了)。

            導(dǎo)出骨骼動(dòng)畫數(shù)據(jù)

            了解了MAX的場(chǎng)景管理和幾何管道以后,我們就可以很方便的建立一個(gè)如何取得MAX場(chǎng)景中定點(diǎn)數(shù)據(jù)的流程了。

            骨骼動(dòng)畫系統(tǒng),首先應(yīng)該包括物體的蒙皮數(shù)據(jù)和頂點(diǎn)與骨骼的綁定信息。我們分兩部分介紹皮膚數(shù)據(jù)的導(dǎo)出。第一步,我們要導(dǎo)出蒙皮數(shù)據(jù),為了簡(jiǎn)單起見,在這里只導(dǎo)出蒙皮的位置,法向量與切向量紋理坐標(biāo)等信息留給讀者自己去研究。在3DsMax里。要建立骨骼動(dòng)畫模型,可以使用兩種修改器Skin 和,Physique。其中Physique是屬于CharacterStudio的,他的API相對(duì)比較復(fù)雜,本文只介紹使用Skin修改器制作的骨骼動(dòng)畫模型文件。

            在界面上增加一些按鈕

            在了解了那么多理論后,我們可以開始做一些實(shí)質(zhì)上的事情了,首先我們要給我們的插件增加一些按鈕,通過這些按鈕,使用可以下達(dá)保存/加載骨架,導(dǎo)出動(dòng)作,導(dǎo)出皮膚等任務(wù)。

            我們?cè)诘谌?jié)中生成的IDD_PANEL的對(duì)話框中加入幾個(gè)按鈕,分別用于保存骨架,加載骨架,導(dǎo)出動(dòng)作,導(dǎo)出皮膚。并在對(duì)話框的WM_COMMAND消息中加入按鈕響應(yīng)代碼。對(duì)話框的窗口過程為MyMaxSkinExporterDlgProc。

            增加完的IDD_PANEL對(duì)話框看上去如圖4。



            定義頂點(diǎn)數(shù)據(jù)類型

            骨骼動(dòng)畫的頂點(diǎn)數(shù)據(jù)應(yīng)該包含頂點(diǎn)位置,紋理坐標(biāo),法向量,影響的骨骼編號(hào)和權(quán)重,一般影響到某個(gè)頂點(diǎn)的骨骼數(shù)目不會(huì)超過4個(gè)。同時(shí),頂點(diǎn)位置也有兩種記錄方法:相對(duì)于世界空間的和相對(duì)骨骼空間的,這里我們采用相對(duì)于世界空間的記錄方法,因?yàn)檫@種方案比較直觀,只需要記錄一個(gè)頂點(diǎn)位置就可以。麻煩的地方在于,因?yàn)楣趋赖淖儞Q矩陣要求頂點(diǎn)是相對(duì)于該骨骼的局部空間的,因此頂點(diǎn)在參與骨骼蒙皮計(jì)算的時(shí)候,需要先乘上骨骼的初始位置的矩陣的逆,以變換到骨骼空間。

               struct Vertex_t

               {

                   Point pos , normal , texCoord;

                  int    matID;

                   int   nEffBone;

                   struct{

                      int    boneIdx;

                       float  weight;

                   }Bone[4];

               };

            導(dǎo)出骨架

            骨骼動(dòng)畫系統(tǒng)中骨架為動(dòng)畫的載體,所有的蒙皮都附著在骨架之上。同時(shí)要保證屬于一個(gè)角色的所有的蒙皮都使用同一個(gè)骨架來建立和導(dǎo)出,這是一套換裝系統(tǒng)的基本需求。因此骨架的導(dǎo)出和保存通常是一次性的,后續(xù)導(dǎo)出皮膚的時(shí)候都應(yīng)該以這個(gè)骨架為基準(zhǔn)。這也要求我們?cè)趯?dǎo)出骨架的時(shí)候就需要導(dǎo)出所有的骨骼。

            骨架上的骨骼其實(shí)也是一個(gè)INode,骨骼僅僅是一些變換矩陣的信息而已。目前沒有特別好的辦法鑒定哪些INode是骨骼,比較可行的辦法是把所有Skin修改器使用到的INode都列為骨骼,同時(shí)美工還可以手動(dòng)指定哪些Node為骨骼,并把這些標(biāo)記用INode:: SetUserPropBool("IsBone",bIsBone);記錄到MAX文件中。

            保存骨架的時(shí)候,需要保存骨骼的父子關(guān)系。并需要保存這個(gè)骨骼的第一幀數(shù)據(jù)。這要求如果美工在兩個(gè)不同的MAX文件里制作不同的動(dòng)作的時(shí)候,除了保證骨架相同以外,第一幀也需要完全相同.

            骨架的保存和加載代碼如下:

               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函數(shù)的目的是在把從文件中加載的骨骼和MAX中的Node對(duì)應(yīng)起來.因?yàn)槭歉鶕?jù)名字來進(jìn)行查找比較,因此要求所有的 Node都必須要有唯一的名字.同時(shí),骨骼之間的父子關(guān)系也是通過名字來標(biāo)記的.每個(gè)Bone都記錄了它的父節(jié)點(diǎn)的名字.Save Skeleton骨架的按鈕響應(yīng)代碼如下:

               void OnSaveSkeleton()

               {

                      CSkeleton* pSkeleton = GetGlobalSkeleton();

                          Assert(ip->GetSelNodeCount() == 1); //導(dǎo)出骨架的時(shí)候只能選擇一個(gè)節(jié)點(diǎn)



                          const char* filename = GetSaveFileName() ;



                          if(filename){

                          pSkeleton-> saveSkeleton(filename , ip->GetSelNode(0) );

            }

               }

            導(dǎo)出骨架動(dòng)作

            骨架導(dǎo)出后,我們需要進(jìn)一步導(dǎo)出這個(gè)骨架的動(dòng)作。在導(dǎo)出動(dòng)作的時(shí)候,需要加載一個(gè)事先已經(jīng)導(dǎo)出的骨架。然后遍歷這個(gè)骨架中所有的骨骼,找到這個(gè)骨骼對(duì)應(yīng)的INode對(duì)象。然后確定動(dòng)畫的長(zhǎng)度和幀數(shù),為每一個(gè)骨頭的保存一個(gè)變換矩陣。

            void OnExportAnimation()

               {

                       const char* fileName = GetSaveFileName();

            ofstream out(fileName , ios::binary);

            Interval ARange =  ip->GetAnimRange(); //獲得動(dòng)畫的長(zhǎng)度



                        TimeValue   tAniTime =  ARange.End() - ARange.Start();

                    TimeValue   tTime = ARange.Start();

                    int nFrame = tAniTime/GetTicksPerFrame();

            //計(jì)算動(dòng)畫幀數(shù)

            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();

               }

            這里演示里我們記錄的是骨骼的絕對(duì)變換矩陣,而不是相對(duì)父骨骼的變換矩陣,這省去了我們從根骨骼開始計(jì)算骨架的麻煩,但是也多了很多限制,比如不能進(jìn)行動(dòng)作混合,不能做動(dòng)作的插值等,使用相對(duì)父骨骼的局部矩陣的算法留給讀者自己去實(shí)現(xiàn),也可以參考Cal3D和我開源的XReal3D的導(dǎo)出插件。此外,因?yàn)槲覀冊(cè)陧旤c(diǎn)數(shù)據(jù)中只保存了相對(duì)世界空間的位置,所以骨骼中的NodeInitTM將用來把相對(duì)世界空間的頂點(diǎn)位置變換到骨骼的局部空間中,皮膚混合的時(shí)候計(jì)算公式將如下:



            其中M(t,i)為第i塊骨頭在t時(shí)刻的變換矩陣。

            同樣的,我們只是簡(jiǎn)單的導(dǎo)出每一幀的變換矩陣,而沒有處理關(guān)鍵幀,使用關(guān)鍵幀加上相對(duì)父節(jié)點(diǎn)的局部變換矩陣的四元數(shù)插值,在保準(zhǔn)動(dòng)作的準(zhǔn)確性前提下能大大的降低動(dòng)作文件磁盤占用。

             

            文/潘李亮

            查找Skin修改器

            要找到一個(gè)Mesh上是不是有Skin修改器,根據(jù)MAX的幾何管道的結(jié)構(gòu),需要遍歷整個(gè)ModStack中的Derived Object。判斷應(yīng)用在這些Derived Object上的修改器的類型。MAX中所有的對(duì)象都有一個(gè)類似COM的GUID的唯一標(biāo)記ClassID。Skin修改器的ClassID為 SKIN_CLASSID,在獲得Derived Object的修改器后只需要檢查修改器的ClassID是不是SKIN_CLASSID即可。示例代碼如下:

            ISkin * FindSkinModifier(INode *pINode){

              Object * pObject = pINode->GetObjectRef();

              if(pObject == 0) return 0;

              // 循環(huán)檢測(cè)所有的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);

                  //檢測(cè)ClassID是不是Skin修改器

                  if(pModifier->ClassID() == SKIN_CLASSID) {

               return (ISkin*) pModifier->GetInterface(I_SKIN);

                  }

                }

                //下一//個(gè)Derived Object   

                pObject = pDerivedObject->GetObjRef();

              }

              return 0;

            }

            獲取Mesh對(duì)象

            根據(jù)第四節(jié)中描述的,要從一個(gè)INode中獲得Mesh對(duì)象,首先應(yīng)該從INode中獲得Object對(duì)象,然后再轉(zhuǎn)成Mesh對(duì)象。具體代碼如下:

            Mesh* GetMesh(INode* pNode , int iMaxTime){

              NullView view;

              //NullView是自定義的View類。詳細(xì)參見完整的插件代碼

              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;

            }

            獲取皮膚數(shù)據(jù)與頂點(diǎn)的骨骼綁定信息

            在成功獲取到一個(gè)節(jié)點(diǎn)的ISkin對(duì)象和Mesh以后,就可以使用這兩個(gè)對(duì)象來提取物體的頂點(diǎn)數(shù)據(jù)和骨骼的綁定信息了。

            Mesh中的數(shù)據(jù)保存在不同的數(shù)組中,常用的包含以下幾種:頂點(diǎn)位置信息,顏色信息,法向量,UV坐標(biāo),MapChannel信息等。其中法向量的信息不是特別的準(zhǔn)確,需要考慮平滑組,面法向量與頂點(diǎn)法向量的差異等。MapChannel用于僅僅有多層紋理貼圖坐標(biāo)的情況,在只有一層紋理坐標(biāo)的情況下則不需要考慮,使用UV坐標(biāo)就足夠了。本文僅僅演示如何導(dǎo)出頂點(diǎn)位置,單層紋理和骨骼綁定信息,如何準(zhǔn)確的計(jì)算法向量以及處理多層紋理坐標(biāo)的內(nèi)容不在本文的討論范圍。

            Mesh中的numVerts變量標(biāo)記了Mesh有幾個(gè)頂點(diǎn)位置,numTexCoords標(biāo)記了有幾個(gè)紋理坐標(biāo)。而通常這兩個(gè)值是不一樣的。

            要提取骨骼綁定信息,首先需要從ISkin對(duì)象中查詢到ISkinContextData接口: ISkinContextData* pSkinContext

            = pSkin->GetContextInterface(pNode); 通過ISkinContextData的GetNumAssignedBones函數(shù)可以得到影響到這個(gè)頂點(diǎn)的骨骼的個(gè)數(shù),進(jìn)而通過 GetAssignedBone和GetBoneWeight接口可以到這個(gè)Bone的index已經(jīng)這個(gè)Bone對(duì)這個(gè)頂點(diǎn)的權(quán)重。

            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);//第一幀的數(shù)據(jù)

              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);

              }

            }

            上述示例代碼中并沒有考慮到有個(gè)別頂點(diǎn)的骨骼數(shù)量超過4種情況,在正式的代碼中應(yīng)該對(duì)所有影響到這個(gè)頂點(diǎn)的骨骼權(quán)重進(jìn)行排序,取前4個(gè)權(quán)重最大的,丟棄其余的骨骼。并要考慮有有些重復(fù)骨骼的問題。

            導(dǎo)出面的和材質(zhì)信息

            在成功解決了頂點(diǎn)和骨骼的數(shù)據(jù)提取后,還需要獲得面的信息,即頂點(diǎn)的拓?fù)潢P(guān)系。3D渲染器能處理的面一般都是三角形面,因此一般要求美工在制作模型的時(shí)候首先將模型轉(zhuǎn)換成Editable Mesh。這樣能確保在Mesh取到的面都是三角形。

            MAX中的面有兩種,用來表示形狀的Face類和用來表示紋理的TVFace類。Face和TVFace是一一對(duì)應(yīng)的。就是說要獲得一個(gè)三角形的完整數(shù)據(jù),必須同時(shí)獲取Face類和TVFace類。

            Face類中的信息比較重要的有三個(gè)頂點(diǎn)的位置索引,可以通過Face::v[i]來獲得,得到這個(gè)索引后,在Mesh::verts數(shù)組里就可以獲得位置的數(shù)據(jù)。同理TVFace也一樣。

            Face類里除了頂點(diǎn)信息外,還保存了和材質(zhì)相關(guān)的信息:MaterialID。一個(gè)Mesh上通常只能應(yīng)用一個(gè)Material,那為什么會(huì)有 MaterialID這個(gè)概念呢?因?yàn)樵贛ax里除了標(biāo)準(zhǔn)材質(zhì)以外,還有一種美工非常有用的材質(zhì)叫多材質(zhì)(Multi-Material),這種材質(zhì)可以可以包含很多個(gè)標(biāo)準(zhǔn)材質(zhì),我們可以通過判斷材質(zhì)的ClassID來判斷它是不是一個(gè)多材質(zhì),如果是,就遍歷它的所有子材質(zhì)(Sub-Mateiral)。 MaterialID就是對(duì)應(yīng)的子材質(zhì)的序號(hào)。在繪制的時(shí)候,MaterialID相同的三角形表示它們有相同的紋理和材質(zhì),在導(dǎo)出的時(shí)候應(yīng)該按照 MaterialID進(jìn)行排序。

            Max中的Material使用Mtl類表示,可以通過INode::GetMtl()來獲得。Mtl:: NumSubMtls加GetSubMtl則可訪問到這個(gè)Material的所有Material。對(duì)于一個(gè)標(biāo)準(zhǔn)材質(zhì),我們可以獲得這個(gè)材質(zhì)的各種屬性,包括紋理貼圖,紋理貼圖使用的貼圖坐標(biāo)通道(MapChannel),以及環(huán)境光,高光等屬性。處理材質(zhì)的示例代碼如下:

            void SaveMaterial(const char* fileMat , INode* pNode){

              ofstream out(fileMat,ios::binary);

              Mtl* pMtl = pNode->GetMtl();

              int nSubMat = pMtl->NumSubMtls();

              if (nSubMat == 0) {

                //處理和保存一個(gè)標(biāo)準(zhǔn)材質(zhì)的代碼,詳見參考資料

                saveStdMaterial(out , (StdMat*)pMtl);

              } else {

                for(int i = 0 ; i < pMtl->NumSubMtls(); i++ )

                  saveStdMaterial(out ,

                   (StdMat*)pMtl->GetSubMtl(i));     

              }

            }

               以下為簡(jiǎn)單的提取三角形數(shù)據(jù)的代碼。

            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++) {

                  //一個(gè)三角形三個(gè)頂點(diǎn)

                   Vertex_t vert ;

                   vert.matID = face.getMatID();

                   GetVertexBoneInfo(pNode, pSkin, pMesh,

                     face.v[j], tvface.getTVert(j), vert);

                   //保存到文件

                   out.write( (char*)&vert,sizeof(vert) );

                }

              }

            }

               上述代碼僅僅是簡(jiǎn)單的保存了每一個(gè)面的信息,真正應(yīng)用的時(shí)候,至少還應(yīng)該把所有的面按照MateiralID來進(jìn)行排序,并且 GetVertexBoneInfo函數(shù)在生成頂點(diǎn)數(shù)據(jù)的時(shí)候必然有很多頂點(diǎn)是完全相同的,應(yīng)該把這些相同的頂點(diǎn)都去掉。面的信息使用Index Buffer來保存。鑒于處理這些代碼過長(zhǎng),就不在文中一一舉例。

            除了頂點(diǎn)材質(zhì)相關(guān)的信息之外,F(xiàn)ace類中比較重要的是法向量相關(guān)的信息,如RVertex和平滑組數(shù)據(jù)(smooth group)等。讀者可以參閱參考資料。

            進(jìn)一步的工作

            現(xiàn)在我們已經(jīng)基本可以獲得一個(gè)簡(jiǎn)單的骨骼動(dòng)畫系統(tǒng)所需要的大部分?jǐn)?shù)據(jù)了。但是這僅僅局限于一個(gè)演示性的骨骼動(dòng)畫系統(tǒng),離一個(gè)完整魯棒的系統(tǒng)還有很多事情要做。

            上述例子中,我們僅僅導(dǎo)出了皮膚的頂點(diǎn)和第一層紋理坐標(biāo)。讀者還需要進(jìn)一步處理多層貼圖,頂點(diǎn)顏色,法向量等數(shù)據(jù)。在導(dǎo)出的骨架中,還應(yīng)該能更方便的處理骨架的層次關(guān)系,以及能區(qū)分角色的上身和下身。因?yàn)橥ǔR粋€(gè)骨骼動(dòng)畫系統(tǒng)是需要進(jìn)行上下身的動(dòng)作融合的。其次骨骼的變換矩陣應(yīng)該是保存相對(duì)于父骨骼的局部變換矩陣,局部矩陣可以分解成四元數(shù),平移和縮放,使用四元數(shù)能進(jìn)行更平滑的插值和動(dòng)作間的過渡,而且為了減少動(dòng)作文件的大小,關(guān)鍵幀動(dòng)畫也是值得考慮的一個(gè)技術(shù)點(diǎn)。

            在數(shù)據(jù)組織方面,我認(rèn)為一個(gè)完善的骨骼動(dòng)畫系統(tǒng)應(yīng)該能合理而自然的管理所有數(shù)據(jù),XReal3D的數(shù)據(jù)組織采用的是流式存儲(chǔ),所有的動(dòng)作、骨架、頂點(diǎn)位置、紋理坐標(biāo)都擁有自己的流。這樣的存儲(chǔ)方式,在保持彈性的前提下大大的減少了一個(gè)骨骼動(dòng)畫角色的文件數(shù)量,方便管理和維護(hù)。

            此外,現(xiàn)在的角色動(dòng)畫系統(tǒng)還應(yīng)該包括表情動(dòng)畫和柔體系統(tǒng)。表情動(dòng)畫,一般稱為在3DsMax的例子里有一個(gè)修改器就是表情動(dòng)畫,編譯這個(gè)插件和使用它的頭文件,我們可以訪問Max里的表情動(dòng)畫。這個(gè)插件的位置在X:\3dsmax\maxsdk\samples\modifiers \morpher中。柔體系統(tǒng)也叫布料系統(tǒng),可以用它來制作衣服飄動(dòng)的效果。

            最后一個(gè)需要說的IGame, IGame是3DsMaxSDK中附帶一個(gè)用來為導(dǎo)出3D游戲相關(guān)數(shù)據(jù)的開發(fā)包。基本上,IGame是3DsMAXSDK的一個(gè)包裝,使用起來更加方便而已。要深入的了解和掌握3DsMax插件的開發(fā),還是需要對(duì)3DsMaxSDK有一定的熟悉程度。

            總而言之,一個(gè)完整的骨骼動(dòng)畫系統(tǒng)插件是非常復(fù)雜的工程,鑒于文章篇幅,不可能介紹的面面俱到,希望本文對(duì)那些初次接觸MAX插件開發(fā)的讀者能有一定的幫助。

            參考資料

            1.3DsMAX Help

            2.  Cal3D :  https://gna.org/projects/cal3d/

            3.  XReal3D MAX Exporter : http://gforge.osdn.net.cn/projects/xreal3d/

            作者簡(jiǎn)介:

            潘李亮,2003年畢業(yè)于西北工業(yè)大學(xué),愛好計(jì)算機(jī)圖形學(xué),曾在游戲公司負(fù)責(zé)3D引擎開發(fā)工作,目前在Corel公司從事多媒體軟件開發(fā)。

            posted on 2009-05-26 18:06 狂爛球 閱讀(2597) 評(píng)論(0)  編輯 收藏 引用 所屬分類: 圖形編程

            久久成人影院精品777| 精品国产热久久久福利| 久久人人爽人人爽人人片AV麻烦| 亚洲国产成人久久综合野外| 日日狠狠久久偷偷色综合96蜜桃| 久久久久久久综合狠狠综合| 亚洲精品乱码久久久久久中文字幕| avtt天堂网久久精品| 亚洲国产精品人久久| 一级做a爰片久久毛片毛片| 久久亚洲中文字幕精品有坂深雪| 久久精品免费观看| 久久久久婷婷| 国内精品久久久久影院免费| 青春久久| 久久99精品国产99久久6男男| 天天综合久久一二三区| 国产99久久精品一区二区| 欧美精品福利视频一区二区三区久久久精品| 久久综合给合综合久久| 久久不见久久见免费视频7| 三级韩国一区久久二区综合| 91精品国产高清91久久久久久| 亚洲精品乱码久久久久久蜜桃| 久久成人国产精品二三区| 亚洲色欲久久久综合网东京热| 91精品国产综合久久香蕉 | 午夜精品久久久内射近拍高清 | 国产一区二区精品久久岳| 亚洲午夜久久久久久久久电影网| 欧美精品福利视频一区二区三区久久久精品| 日韩人妻无码一区二区三区久久 | 亚洲狠狠婷婷综合久久蜜芽| 欧美激情精品久久久久久| 久久国产精品免费| 久久99精品国产麻豆蜜芽| 国产免费福利体检区久久| 7国产欧美日韩综合天堂中文久久久久 | 伊人久久大香线蕉综合影院首页| 色8激情欧美成人久久综合电| 久久亚洲色一区二区三区|