青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

永遠也不完美的程序

不斷學習,不斷實踐,不斷的重構……

常用鏈接

統(tǒng)計

積分與排名

好友鏈接

最新評論

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

文/潘李亮

引言

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

本文將會介紹骨骼動畫系統(tǒng)里的一個基礎部件:3Ds MAX 的骨骼動畫導出插件。

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

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

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

Max插件根據(jù)用途分為好幾種,每種對應不同的擴展名,在游戲開發(fā)中,我們通常比較關心三種類型的插件,他們分別是: 導入/導出插件,對應擴展名為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沒有區(qū)別。



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

構造第一個3Ds Max 插件

本節(jié)我將講述如何快速的建立一個utility插件的框架,因為關心的是導出插件本身的功能,而不是插件框架本身,因此我給大家提供一個個比較簡潔的方法:使用3dsmaxPluginWizard. 這是MaxSDK提供的一個組件,位于X:\3dsmax\maxsdk\howto\3dsmaxPluginWizard下,仔細閱讀一遍這個目錄下的ReadMe.txt文件的Installing一小節(jié),就可安裝好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有兩個重要的函數(shù): BeginEditParams(Interface *ip,IUtil *iu)和EndEditParams(Interface *ip,IUtil *iu)這兩個函數(shù)。BeginEditParams可以簡單的理解成插件的初始化函數(shù),EndEditParams則在插件退出時候被調用。參數(shù) Interface *ip 則代表整個Max對象,用它可以訪問到MAX程序的所有功能。

編譯這個工程,一個簡單的utility插件就已經生成了,你可以在剛才Project Detials選項卡里填入的插件輸出路徑里找到生成的插件。

3Ds MAX的場景組織和幾何管道

要編寫一個導出骨骼動畫的插件,必須先了解MAX是如何組織場景的,并了解MAX中一個mesh對象從建立到最終輸出都經歷那些階段。下面首先介紹一下MAX的場景組織。

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

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



INode的變換矩陣可以通過INode::GetNodeTM來獲得,而附著在INode上的Object的變換矩陣則通過 INode::GetObjectTM來獲得,因為Object相對于Node的變換矩陣通常是單位矩陣,GetNodeTM和GetObjectTM獲得的矩陣通常也是一樣的,但是在必要的時候一定要加以區(qū)別。關于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的數(shù)據(jù)對象,Modifier用ModContext中的數(shù)據(jù)來對Object進行修改,以生成最終數(shù)據(jù)。

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

導出骨骼動畫數(shù)據(jù)

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

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

在界面上增加一些按鈕

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

我們在第三節(jié)中生成的IDD_PANEL的對話框中加入幾個按鈕,分別用于保存骨架,加載骨架,導出動作,導出皮膚。并在對話框的WM_COMMAND消息中加入按鈕響應代碼。對話框的窗口過程為MyMaxSkinExporterDlgProc。

增加完的IDD_PANEL對話框看上去如圖4。



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

骨骼動畫的頂點數(shù)據(jù)應該包含頂點位置,紋理坐標,法向量,影響的骨骼編號和權重,一般影響到某個頂點的骨骼數(shù)目不會超過4個。同時,頂點位置也有兩種記錄方法:相對于世界空間的和相對骨骼空間的,這里我們采用相對于世界空間的記錄方法,因為這種方案比較直觀,只需要記錄一個頂點位置就可以。麻煩的地方在于,因為骨骼的變換矩陣要求頂點是相對于該骨骼的局部空間的,因此頂點在參與骨骼蒙皮計算的時候,需要先乘上骨骼的初始位置的矩陣的逆,以變換到骨骼空間。

   struct Vertex_t

   {

       Point pos , normal , texCoord;

      int    matID;

       int   nEffBone;

       struct{

          int    boneIdx;

           float  weight;

       }Bone[4];

   };

導出骨架

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

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

保存骨架的時候,需要保存骨骼的父子關系。并需要保存這個骨骼的第一幀數(shù)據(jù)。這要求如果美工在兩個不同的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函數(shù)的目的是在把從文件中加載的骨骼和MAX中的Node對應起來.因為是根據(jù)名字來進行查找比較,因此要求所有的 Node都必須要有唯一的名字.同時,骨骼之間的父子關系也是通過名字來標記的.每個Bone都記錄了它的父節(jié)點的名字.Save Skeleton骨架的按鈕響應代碼如下:

   void OnSaveSkeleton()

   {

          CSkeleton* pSkeleton = GetGlobalSkeleton();

              Assert(ip->GetSelNodeCount() == 1); //導出骨架的時候只能選擇一個節(jié)點



              const char* filename = GetSaveFileName() ;



              if(filename){

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

}

   }

導出骨架動作

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

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

//計算動畫幀數(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();

   }

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



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

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

 

文/潘李亮

查找Skin修改器

要找到一個Mesh上是不是有Skin修改器,根據(jù)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;

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

根據(jù)第四節(jié)中描述的,要從一個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;

}

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

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

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

Mesh中的numVerts變量標記了Mesh有幾個頂點位置,numTexCoords標記了有幾個紋理坐標。而通常這兩個值是不一樣的。

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

= pSkin->GetContextInterface(pNode); 通過ISkinContextData的GetNumAssignedBones函數(shù)可以得到影響到這個頂點的骨骼的個數(shù),進而通過 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);//第一幀的數(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);

  }

}

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

導出面的和材質信息

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

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

Face類中的信息比較重要的有三個頂點的位置索引,可以通過Face::v[i]來獲得,得到這個索引后,在Mesh::verts數(shù)組里就可以獲得位置的數(shù)據(jù)。同理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),以及環(huán)境光,高光等屬性。處理材質的示例代碼如下:

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

  }

}

   以下為簡單的提取三角形數(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++) {

      //一個三角形三個頂點

       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函數(shù)在生成頂點數(shù)據(jù)的時候必然有很多頂點是完全相同的,應該把這些相同的頂點都去掉。面的信息使用Index Buffer來保存。鑒于處理這些代碼過長,就不在文中一一舉例。

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

進一步的工作

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

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

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

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

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

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

參考資料

1.3DsMAX Help

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

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

作者簡介:

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

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

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <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>
            亚洲综合欧美日韩| 小黄鸭精品密入口导航| av不卡在线| 亚洲国产日韩欧美在线99 | 国产精品一区免费在线观看| 欧美日韩中文| 欧美日韩国产区| 欧美精品亚洲一区二区在线播放| 欧美成人亚洲成人| 欧美精品日本| 国产精品视频99| 国产一区二区三区四区老人| 国产综合欧美在线看| 一区免费在线| 亚洲乱亚洲高清| 亚洲一区二区毛片| 久久日韩粉嫩一区二区三区| 男女激情久久| 一区二区三区四区蜜桃| 午夜欧美理论片| 久久久亚洲欧洲日产国码αv| 久久久久久欧美| 欧美日韩在线大尺度| 国产精品免费一区豆花| 国产午夜亚洲精品不卡| 亚洲人成毛片在线播放| 亚洲一区二区在线看| 久久一日本道色综合久久| 欧美精品一区二区三区蜜桃| 欧美在线视频不卡| 老司机免费视频久久| 欧美日韩激情小视频| 国产精品一级久久久| 亚洲欧洲一区二区天堂久久 | 亚洲风情在线资源站| 一区二区三区久久精品| 久久亚洲综合色一区二区三区| 亚洲国产精品va在线观看黑人 | 亚洲裸体视频| 久久精品道一区二区三区| 亚洲人成网站777色婷婷| 亚洲欧美日韩中文播放| 欧美激情国产日韩精品一区18| 国产精品日韩一区| 99精品热视频| 欧美激情aaaa| 久久久久一本一区二区青青蜜月| 欧美美女bb生活片| 曰韩精品一区二区| 久久久久一区二区三区| 99天天综合性| 欧美精品在线一区二区三区| 国产综合精品一区| 欧美一区二区视频免费观看| 日韩视频在线播放| 欧美精品一区二区高清在线观看| 在线观看欧美一区| 免费在线成人av| 久久久五月天| 在线成人激情| 欧美肥婆在线| 欧美成人资源网| 日韩亚洲视频在线| 亚洲乱码精品一二三四区日韩在线| 免费国产一区二区| 亚洲日韩第九十九页| 免费高清在线一区| 久久久噜噜噜久噜久久| 激情亚洲一区二区三区四区| 久久精品免费观看| 欧美在线视频日韩| 一区二区三区自拍| 免费在线观看日韩欧美| 亚洲一区二区综合| 国产欧美日韩精品丝袜高跟鞋| 亚洲综合第一页| 亚洲午夜av在线| 国产精品视频yy9099| 欧美一区二区在线| 欧美专区亚洲专区| 亚洲福利视频网| 亚洲福利专区| 欧美色图五月天| 午夜一区在线| 亚洲女女女同性video| 亚洲国产一区二区视频| 欧美日韩日日夜夜| 午夜精品久久久久久| 亚洲国产精品99久久久久久久久| 免费视频一区二区三区在线观看| 依依成人综合视频| 免费亚洲一区| 免费观看不卡av| 亚洲免费大片| 亚洲图片欧美一区| 一区在线电影| 亚洲免费观看视频| 国产日产欧产精品推荐色| 免费观看不卡av| 国产精品久久精品日日| 久久久久**毛片大全| 欧美紧缚bdsm在线视频| 香蕉免费一区二区三区在线观看 | 日韩网站在线看片你懂的| 国产精品午夜在线观看| 欧美国产一区视频在线观看| 欧美日韩一视频区二区| 久久久久久婷| 欧美深夜影院| 欧美激情1区| 国产一区二区视频在线观看| 亚洲美女精品久久| 韩国v欧美v日本v亚洲v| 99国产精品久久久| 亚洲福利国产精品| 欧美伊人久久久久久久久影院| 亚洲视频在线二区| 欧美大片在线观看一区| 久久久噜噜噜久久狠狠50岁| 欧美日韩黄色一区二区| 欧美国产精品日韩| 黄色在线一区| 亚洲欧美在线播放| 亚洲在线一区二区三区| 欧美激情第9页| 欧美高清视频| 亚洲国产精品123| 久热综合在线亚洲精品| 看欧美日韩国产| 精品电影在线观看| 久久久久久久精| 久久久噜噜噜久久人人看| 国产一区二区三区久久| 亚洲欧美成人网| 欧美在线观看一区| 国产精品视频成人| 亚洲影视在线| 欧美一区二区视频97| 国产精品一区2区| 亚洲欧美日韩精品久久| 亚洲欧美中日韩| 久久久久久久一区二区| 国产九色精品成人porny| 亚洲美女黄网| 亚洲图片欧洲图片av| 欧美欧美午夜aⅴ在线观看| 亚洲福利免费| 亚洲视频一区| 国产精品专区h在线观看| 亚洲欧美资源在线| 久久精品水蜜桃av综合天堂| 国产婷婷成人久久av免费高清 | 中日韩在线视频| 欧美日韩免费观看一区三区| 亚洲美女视频在线观看| 亚洲综合不卡| 激情久久婷婷| 欧美a级一区二区| 亚洲美女黄色片| 久久激情五月激情| 亚洲第一精品在线| 欧美精品 国产精品| 亚洲一区www| 久久免费黄色| 一区二区动漫| 国产视频一区二区在线观看| 久久精品30| 日韩一二在线观看| 久久久亚洲成人| 99视频一区二区| 国产视频亚洲精品| 欧美精品日本| 久久福利毛片| 一本久道久久久| 老司机午夜免费精品视频| 亚洲伦理在线观看| 国产欧美一区二区三区久久 | 欧美在线欧美在线| 亚洲人成久久| 久久人体大胆视频| 在线视频精品一区| 韩日午夜在线资源一区二区| 欧美成人视屏| 午夜精品亚洲| 亚洲青涩在线| 蜜臀av一级做a爰片久久| 亚洲图片你懂的| 最新中文字幕亚洲| 国产精品久久久一本精品| 久久精品国产第一区二区三区| 亚洲精品久久久久久久久久久| 欧美在线视频a| 夜夜嗨一区二区三区| 国内成人精品2018免费看| 欧美精品在线一区二区| 久久全国免费视频| 亚洲欧美日韩综合| 亚洲人久久久| 女仆av观看一区| 久久一区二区三区国产精品|