• <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>

            麒麟子

            ~~

            導航

            <2013年4月>
            31123456
            78910111213
            14151617181920
            21222324252627
            2829301234
            567891011

            統計

            常用鏈接

            留言簿(12)

            隨筆分類

            隨筆檔案

            Friends

            WebSites

            積分與排名

            最新隨筆

            最新評論

            閱讀排行榜

            評論排行榜

            關于骨骼動畫及微軟示例Skinned Mesh的解析

             

            原文鏈接:http://www.gameres.com/document.asp?TopicID=87707

            這是我自個寫的,第一次發. 沒想到這個貼子編輯器極差. 原文是有字體字色的.現在只能清一色了.
            版主,發貼的編輯器太難用! 你有必要向上反映一下. 下面的字體是我敲html標記加上的,大家湊和看.

             

            關于骨骼動畫及微軟示例Skinned Mesh的解析

            骨骼動畫是D3D的一個重要應用。盡管微軟DXSDK提供了示例Skinned Mesh,但由于涉及眾多概念和技術細節,示例相對于初學者非常復雜,難以看懂。在此,提供一些重要問題評論,以使初學者走出迷局,順利上手。文中所述都是參照各種資料加上自己的理解,也有可能出些偏差,有則回貼拍磚,無則權當一笑。


            一 骨骼動畫原理
            原理方面在網上資料比較多,大家都基本明白。在此說一下重點:
            總體上,絕大部分動畫實現原理一致,就是“提供一種機制,描述各頂點位置隨時間的變化”。有三種方法:
            1.1 關節動畫:由于大部分運動,都是皮膚隨骨骼在動,皮膚相對于它的骨骼本身并沒有發生運動,所以只要描述清楚骨骼的運動就行了。用矩陣描述各個骨骼的相對于父骨骼運動。(大多運動都是旋轉型) 易知,從子骨骼用矩陣乘法累積到最頂層根骨骼,就可以得到每個子骨骼相對于世界坐標系的轉換矩陣。
              這種動畫,只須用普通Mesh保存最初始的各頂點坐標,以及一系列后續時刻所對應的各骨骼的運動矩陣。不用保存每時刻的頂點數據,節省了大量存儲空間。而且比較靈活,可以利用關鍵幀插值運算,便于通過運算調節動作。缺點是在兩段骨骼交接處,容易產生裂縫,影響效果。

            1.2 漸變動畫:通過保存一系列時刻的頂點坐標來完成動畫。雖然比較逼真,但占用大量空間,靈活性也不高。

            1.3 骨骼蒙皮動畫(skinned Mesh)
              相當于上面兩方法的折中。現在比較流行。
              在關節動畫的基礎上,利用頂點混合(Vertex Blend)技術,對于關節附近的頂點,由影響這些頂點的兩段(或多段)骨骼運動,分別賦以權值,共同決定頂點位置。相當于在骨骼關節上動態蒙皮,有效解決了裂縫問題。

              這里,引入一個D3D技術概念:“Vertex Blending”---頂點混合技術。比如說,你肯定用過SetTransform(D3DTS_WORLD,....),但SetTransform(D3DTS_WORLDMATRIX(i),....)是不是很奇怪?這個問題后文會講到。 你也可以在微軟的DXSDK的幫助文件中搜索“Geometry Blending”主題,有裂縫及其解決辦法圖示。

             

            二 X文件如何保存骨骼動畫

            理解X文件格式,對用好相關的DX函數是非常重要的。

            不含動畫的普通X文件,有一個Mesh單元,保存了各頂點信息、各三角面的索引信息、材質種類及定義等。

            動畫X文件,則在這個單元中增加了“各骨骼蒙皮信息”、“骨骼層次及結構信息”、“各時刻骨骼矩陣信息”等。

            2.1 網格蒙皮信息:首先,在Mesh{}單元中,在原有的普通網格頂點數據基礎上,新增了XSkinMeshHeader{}結構,以及多個SkinWeights{}結構。用以描述各個骨骼的蒙皮信息。

            其中,XSkinMeshHeader是總括,舉一實例,如下:

            XSkinMeshHeader
            {
            2,//一個頂點可以受到骨骼影響的最大骨骼數,可用于計算共同作用時減少遍歷次數
            4,//一個三角面可以受到骨骼影響的最大骨骼數。這個數字對硬件頂點混合計算提出了基本要求。
            35 //當前Mesh的骨骼總數。
            }

            由于每個骨骼的蒙皮信息都需要用SkinWeights結構去描述,所以有多少塊骨骼,在Mesh中就有多少個SkinWeights對象。
            注意,一般把SkinWeights視作Mesh的一部分。這種Mesh又稱Skinned Mesh (蒙皮網格)

            SkinWeights 結構如下:
            {
              STRING      transformNodeName;      //骨骼名
              DWORD       nWeights;               //權重數組的元素個數,即該骨骼相關的頂點個數
              array DWORD vertexIndices[nWeights];//受該骨骼控制的頂點索引,實際上定義了該骨骼的蒙皮
              array float weights[nWeights];      //蒙皮各頂點的受本骨骼影響的權值
              Matrix4x4   matrixOffset;           //骨骼偏移矩陣,用來從初始Mesh坐標,反向計算頂點在子骨骼坐標系中的初始坐標。
            }
            在有的書中,把上面的matrixOffset叫骨骼權重矩陣,是不恰當的。應該稱為骨骼偏移矩陣比較合適。

            [問題] 在整個動畫過程中,子骨骼運動矩陣的數值是不斷變化的。上面的骨骼偏移矩陣變化嗎?有沒有必要重新計算?它在什么時候使用?
            答:各骨骼的偏移矩陣matrixOffset專門用來從原始Mesh數據計算出各頂點相對于骨骼坐標系的原始坐標。在繪制前,把它與當前變換矩陣相乘,就可以得到該骨骼的當前的最終變換矩陣。 總之,骨骼偏移矩陣是與原始Mesh頂點數值相關聯的,在整個動畫過程中是不變的,也不應該變。在動畫過程中變化是當前骨骼變換矩陣,可由.X中的AnimatonKey中的各時刻矩陣得到。這個矩陣乘法在示例中的對應代碼如下:
            D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );

            即,D3DXMatrixMultiply(輸出最終世界矩陣, 該骨骼的偏移矩陣, 該骨骼的變換矩陣)


            2.2 骨骼層次信息

            在X文件中,Frame是基本的組成單元。又稱框架Frame。 一個.x可以有多個Frame。(注意此處的Frame不是幀,與幀沒什么關系)

            框架Frame允許嵌套,這樣就存在父子框架了。而并列的框架,稱為兄弟框架。這兩種關系組合在一起,即可以縱深,又可以并列,形成一種層次結構。這種結構,可用二叉樹描述。

            每個框架結構的最前面,有一個FrameTransformMatrix矩陣數據,描述了該框架相對于父框架的變換矩陣。也就是說,該框架中的坐標,與該矩陣相乘,可轉換為父框架坐標系的坐標。
            這種層次結構,使得X文件能描述許多復雜的物體。如地形場景。

            在骨骼動畫文件中,框架結構可直接拿來描述人物骨骼的層次結構。框架的名字通常為對應的骨骼名。
            如“左上臂->左前臂->手掌->手指”就形成一個父子骨骼鏈。而左上臂與右上臂是并行關系。

            數據示例: D:\D9XSDK\Samples\Media\tiny.x

            Frame ...{
              .....

              Frame Bip01_R_Calf { //子骨骼
                  
                   FrameTransformMatrix {
                    1.000000,-0.000691,-0.000000,0.000000,0.000691,1.000000,-0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,119.231522,0.000021,-0.000011,1.000000;;
                   }

                    Frame Bip01_R_Foot {//--孫子骨骼
                  
                    FrameTransformMatrix {
                     0.988831,0.124156,0.082452,0.000000,-0.122246,0.992109,-0.027835,0.000000,-0.085257,0.017445,0.996206,0.000000,119.231476,-0.000039,0.000023,1.000000;;
                    }

                    ....縮進
                }
            }

            [問題]查看示例tiny.x文件,發現只有根框架下有一個Mesh,包含了所有頂點信息。其它各個Frame都沒有Mesh數據。怎么理解?
            答: 一般來說,每個動畫文件只有一個Mesh網格,包含物體所有頂點信息。
                 其它Frame,只是借用來描述各骨骼的層次信息,沒必要再定義骨骼網格。每塊骨骼對應的蒙皮頂點信息,由根Mesh中的相應骨骼的SkinWeights中蒙皮頂點索引描述的。在動畫過程中,各個頂點的新坐標,要借助SkinWeights中的頂點索引來進行重新計算。

            2.3 動畫信息:
            由一系列AnimatonKey組成,數據示例如下:

              AnimationKey {
               4;--動畫類型 4表示矩陣
               62; --動畫幀數,即下面矩陣個數
               0;16;1.000000,-0.000691,-0.000000,0.000000,0.000691,1.000000,0.000000,0.000000,0.000000,-0.000000,1.000000,0.000000,119.231514,-0.000005,0.000001,1.000000;;,
               80;16;0.992696,-0.120646,-0.000000,0.000000,0.120646,0.992696,0.000000,0.000000,-0.000000,-0.000000,1.000000,0.000000,119.231514,0.000002,-0.000002,1.000000;;,

               ..上面紅數字表示時刻tick,蘭數字表示數值的個數。
               ...其它各時刻矩陣...

               { Bip01_R_Calf }--對應的骨骼對象引用
              }


            注意:
            (1)每塊骨骼都有一個AnimationKey{}.
            (2)在上面數據結構中,主要保存了各典型時刻的該骨骼相對于父的變換矩陣.
            (3)在0時刻的矩陣,與該骨骼對應的前面的Frame所對應的矩陣是相同的。如Frame Bip01_R_Calf{}中的變換矩陣,與Bip01_R_Calf所對應的AnimationKey 的第0時刻矩陣是一樣的。這說明,在以后動畫運行時,DX會提供一種功能,用AnimatonKey中的對應數據刷新初始的變換矩陣(也可能啟用關鍵幀插值算法)。這個功能對應于示例中的m_pAnimController->SetTime(...)語句。

            三 怎樣從X文件加載骨骼動畫信息?
            3.1 負責加載的函數:
              可能有多種加載方式,在此以SDK中的示例為準,敘述一種標準加載方式,需要用到DX函數D3DXLoadMeshHierarchyFromX(),函數字面意思是讀取Mesh層次信息。
            HRESULT WINAPI
                D3DXLoadMeshHierarchyFromX(
                    LPCSTR Filename,                 //.x文件名
                    DWORD MeshOptions,               //Mesh選項,一般選D3DXMESH_MANAGED
                    LPDIRECT3DDEVICE9 pD3DDevice,    //指向D3D設備Device
                    LPD3DXALLOCATEHIERARCHY pAlloc,  //自定義數據容器
                    LPD3DXLOADUSERDATA pUserDataLoader,  //一般選NULL
                    LPD3DXFRAME *ppFrameHierarchy,       //返回根Frame指針,指向代表整個骨架的Frame層次結構
                    LPD3DXANIMATIONCONTROLLER *ppAnimController //返回相應的動畫控制器
            );

            這個函數后面的兩個輸出參數很重要,也很好理解,但輸入參數中的自定義數據容器是怎么回事呢?
            原來,鑒于動畫數據的復雜性,需要你配合完成加載過程。比如你是否用到自定義擴展結構,Mesh等數據保存在哪里,怎樣使用戶自己創建容器,自己決定卸載等等。
            DX提供了ID3DXALLOCATEHIERARCHY接口,提供了這個自定義的機會,你重載這個接口的虛函數,在加載過程中,它就像回調函數那樣運作。

            你需要像下面這樣建立一個自定義數據容器類:
            class CAllocateHierarchy: public ID3DXAllocateHierarchy
            {
            public:
                STDMETHOD(CreateFrame)(THIS_ LPCTSTR Name, LPD3DXFRAME *ppNewFrame);
                STDMETHOD(CreateMeshContainer)(THIS_ LPCTSTR Name, LPD3DXMESHDATA pMeshData,
                                        LPD3DXMATERIAL pMaterials, LPD3DXEFFECTINSTANCE pEffectInstances, DWORD NumMaterials,
                                        DWORD *pAdjacency, LPD3DXSKININFO pSkinInfo,
                                        LPD3DXMESHCONTAINER *ppNewMeshContainer);
                STDMETHOD(DestroyFrame)(THIS_ LPD3DXFRAME pFrameToFree);
                STDMETHOD(DestroyMeshContainer)(THIS_ LPD3DXMESHCONTAINER pMeshContainerBase);
                CAllocateHierarchy(CMyD3DApplication *pApp) :m_pApp(pApp) {}
            public:
                CMyD3DApplication* m_pApp;
            };

            [問題]上面的STDMETHOD是什么意思?
            答:相當于virtual   HRESULT   __stdcall 的宏。<評論> 因為這種類要與D3D的COM接口打交道,不僅僅在C++內部使用,所以,所有類方法必須做成stdcall的,可對外開放的。
            #define   STDMETHOD(method)               virtual   HRESULT   STDMETHODCALLTYPE   method  
            #define   STDMETHODCALLTYPE               __stdcall  
            這樣當寫一個函數STDMETHOD(op1(int   i))      
            展開后成為:     virtual   HRESULT   __stdcall   op1(int   i);  

            3.2 自定義數據容器以及具體的讀取過程:
            根據.X文件,在加載過程中,主要有兩方面數據需要保存,一個是骨架Frame信息,一個是網格蒙皮Mesh信息。這兩個信息保存在如下結構中。

            框架信息(對應于骨骼)
            typedef struct _D3DXFRAME
            {
                LPSTR                   Name;
                D3DXMATRIX              TransformationMatrix; //本骨骼的轉換矩陣

                LPD3DXMESHCONTAINER     pMeshContainer;       //本骨骼所對應Mesh數據

                struct _D3DXFRAME       *pFrameSibling;       //兄弟骨骼
                struct _D3DXFRAME       *pFrameFirstChild;    //子骨骼
            } D3DXFRAME, *LPD3DXFRAME;

            自定義數據容器,其數據來源由上面接口的CreateMeshContainer()函數提供
            typedef struct _D3DXMESHCONTAINER
            {
                LPSTR                   Name;       //容器名
                D3DXMESHDATA            MeshData;   //Mesh數據,可創建SkinMesh取代這個Mesh
                LPD3DXMATERIAL          pMaterials; //材質數組
                LPD3DXEFFECTINSTANCE    pEffects;  
                DWORD                   NumMaterials;//材質數
                DWORD*                  pAdjacency;  //鄰接三角形數組
                LPD3DXSKININFO          pSkinInfo;   //蒙皮信息,其中含.x中的各個skinweight蒙皮頂點索引及各骨骼偏移矩陣等。
                struct _D3DXMESHCONTAINER *pNextMeshContainer;
            } D3DXMESHCONTAINER, *LPD3DXMESHCONTAINER;


            [評論]
            .在動畫文件中,框架通常用來描述骨骼。可以把Frame視做骨骼,所以不細加區分。
            .在上面D3DXFRAME結構中,pFrameSibling, pFrameFirstChild兩個指針,常用于遞歸函數中,遍歷整個骨架。
            .在D3DXFRAME結構中有一個pMeshContainer指針,難道框架與Mesh是一一對應的嗎?
            有一個框架(骨骼)就有一個Mesh嗎?怎么.X文件中只有一個Mesh?難道加載時拆開存放?
            答:從D3DXFrame結構上看,每個Frame都有一個pMeshContainer指針。這就有三種解釋:
               第一種,加載到內存后所有的pMeshContainer都指向同一個全局Mesh
               第二種,加載到內存后,只有一個主框架的pMeshContainer不為空,其它Frame的pMeshContainer均為NULL,因為在.X中,它們沒有定義自己的Mesh
               第三種,加載到內存后,D3D將Mesh拆分,分開到各骨骼所對應的Frame,每個Frame都有自己的Mesh。
               這個問題我以前也不是很清楚,通過查看示例源碼及跟蹤發現,正確解釋應該是第2種。唯一的一個全局Mesh存放在Frame "body"下的無名Frame中。而其它Frame由于沒有自己專門的Mesh而指向NULL. 應該大致如此。這個問題之所以讓人困繞,是因為從后續代碼上看,在渲染DrawFrame時,是遍歷每一個frame分別繪制它們對應的Mesh. 如果對應于同一個mesh,就繪制多遍。如果對應各自mesh,那么變換矩陣怎么組織運算等等。所以,根據第二種解釋,由于只有一個pMeshContainer不為NULL,所以參與繪制及蒙皮的只有這一個MeshContainer,人體所有頂點數據及蒙皮信息都在這個mesh中。
            所以,讀取tiny.x文件后,會產生多個D3DXFRAME對象,但只有一個D3DXMESHCONTAINER對象。

            在示例代碼的CMyD3DApplication::InitDeviceObjects()中,有:
                hr = D3DXLoadMeshHierarchyFromX(strMeshPath, D3DXMESH_MANAGED, m_pd3dDevice, &Alloc, NULL, &m_pFrameRoot, &m_pAnimController);
                if (FAILED(hr))
                    return hr;
            其中的Alloc是就自定義的數據容器對象。m_pFrameRoot是根骨骼,對遍歷很重要。m_pAnimController是動畫控制器,對刷新矩陣很重要。

            你在運行完這句話后,下一個斷點,觀察m_pFrameRoot,會發現如下內容:

            m_pFrameRoot 0x00c59380 {Name=0x00c53630 "Scene_Root" .....} //根框架
            pMeshContainer 0x00000000
            pFrameSibling 0x00000000
            pFrameFirstChild 0x00c59428 {Name=0x00c53ca8 "body" pMeshContainer=0x00000000...}//子框架 骨骼body
               +---  pMeshContainer 0x00000000
                       +---  pFrameSibling 0x01419f00 {Name=0x00c5ffd8 "Box01" pMeshContainer=0x00000000 ...}//兄弟框架
                  +---  pFrameFirstChild 0x00c594d0 {Name=0x00000000 pMeshContainer=0x00c59828 //子框架---該框架就是.x中含有唯一全局Mesh的無名框架


            可見,在內存中的Frame布局是與.x中一一對應的。除了pFrameFirstChild 0x00c594d0這個地方的Frame中的pMeshContainer不為空,其它框架的這個mesh指針都是空值。
            另外一點可以看出,并不是每個Frame都對就一塊骨骼,有的是別的用途。也就是說Frame對象的個數可能多于骨骼數。

            3.3 分析CAllocateHierarchy類
            下面繼續研究自定義數據容器CAllocateHierarchy,顧名思義,該類是在加載過程中自行分配層次數據空間。它有4個成員,都是重載D3D的接口虛函數。
            它的成員CreateFrame()是用來創建D3DXFrame對象的,而CreateMeshContainer()是用來創建Mesh數據對象的。你可以在這兩個函數中下斷點,發現CreateFrame會運行多次,而CreateMeshContainer只運行一次,再次驗證了上面的說法。

            值得注意的是,示例對上面的D3DXFRAME,D3DXMESHCONTAINER兩個結構做了擴展,分別代之以D3DXFRAME_DERIVED結構和D3DXMESHCONTAINER_DERIVED結構,以集中存儲數據方便程序處理。

            CreateFrame()處理比較簡單,你只是new一個Frame對象空間,填入傳進來的Name,其它內容由DX負責維護填充。

            CreateMeshContainer()較為復雜。它的任務一是保存傳入的網格數據數據,二是根據這些數據及蒙皮信息調用GenerateSkinnedMesh()函數生成蒙皮網格。只有這個新的BlendMesh才能在Render()時支持頂點混合,完成蒙皮的顯示。在D3DXMESHCONTAINER_DERIVED結構中,用pOrigMesh保存舊的Mesh普通網格信息。而Meshdata.Mesh則指向新產生的BlendMesh

            在這個函數中,多次用到了AddRef(),對COM不熟悉的新手容易困惑。D3D是COM組件,它在服務進程中運行,而不在當前的客戶進程中。在DX組件運行過程中,要創建一系列接口對象,如CreateDevice()返回接口指針,這些接口及其占用內存什么時候釋放,要通過“引用計數”的技術來解決。AddRef()給這個接口指針的計數加1,而Release()會將之減1。一旦減到0,表示沒有客戶使用了,相關的接口就釋放了。 由此可知,每次調用Rlease()后,并不一定會釋放內存,而是當引用計數歸0時釋放內存。
            這樣,對接口指針的使用,就像維護堆棧的平衡一樣,要仔細,而且按照某種約定規則使用。

            但平時D3D編程中,怎么不用AddRef()呢?這是由于一個接口指針,如ID3DDevice,或VertexBuf指針,都是D3DXCreate出來的,在Create時候,在內部已經事先AddRef()了,你就不需要再做這工作了。只要你在不用時,調用 p指針->Relase()就釋放了。一般編程,特別是小型示例程序,都是初始化時建立一次,關閉時釋放,都遵守了這種約定,所以不存在這種問題。

            但在CreateMeshContainer()函數中,以多種方式使用了指針,在局部指針變量中來回傳遞,所以問題復雜化了。在COM編程中約定,任何時候地接口指針賦值(復制),都要AddRef(),在指針變量結束生命期前,再Release(). 但許多程序員都不是嚴格這么做。因為在局部變量用完就廢了,先AddRef()增加計數再Release()減少,和直接使用最后是等效的。幾乎是多此一舉。這與編程習慣有關系。一旦引用計數不對,如果沒有統一的習慣,不好排查。在CreateMeshContainer()中,對接口指針的使用有三種方式,例舉如下:

            方式一:不使用AddRef()。和普通指針一樣,臨時變量是左值,接口指針是右值,直接賦值使用。如:
                    pMesh = pMeshData->pMesh;
                    這是由于pMesh是局部變量,它只是臨時引用一下,沒必要為它先AddRef(),后Release()。

            方式二:隱式的使用AddRef()。 由于用到了一些內部有AddRef()動作的函數,就要按照COM約定,在子程序結束前Release()
                    pMesh->GetDevice(&pd3dDevice);//此處d3d設備引用計數已經加1
                    ....
                    SAFE_RELEASE(pd3dDevice);//--此處將引用計數減1,并不是真的釋放d3d設備
                    在本例中,pd3dDevice在GetDevice()中已經Addref()過了,所以,在退出CreateMeshContainer()前,必須pd3dDevice->Release()

            方式三:顯式的使用AddRef()。 如果一個指針值,不是由D3DXCreate出來的,而是通過賦值方式復制給一個全局變量或長期變量的。 所以,可以通過AddRef()的方式來延遲該對象的釋放。因為,如果不AddRef(),極有可能在函數返回該對象就可能釋放了。它就像一個加油站,使得傳入對象的壽命延長至自己控制范圍內。用了AddRef(),就要在相關的Destroy中添加Release()。

            在本函數,有三處這樣的語句:
                    pMeshContainer->MeshData.pMesh = pMesh;
                    pMeshContainer->MeshData.Type = D3DXMESHTYPE_MESH;
                    pMesh->AddRef();
                     ....
                    pMeshContainer->pSkinInfo = pSkinInfo;
                    pSkinInfo->AddRef();

                    pMeshContainer->pOrigMesh = pMesh;
                    pMesh->AddRef();
                     ....

                    將來在DestroyMeshContainer()中,要釋放這些指針:
                    ....
                    SAFE_RELEASE( pMeshContainer->MeshData.pMesh );
                    SAFE_RELEASE( pMeshContainer->pSkinInfo );
                    SAFE_RELEASE( pMeshContainer->pOrigMesh );

                    由于這些指針值的創建、更改等都是用戶自己經營的,所以務必要加前后吻合,在CreateMeshContainer()中AddRef(),在DestroyMeshContainer()中Release().


            再來看數據的保存部分。
            在CreateMeshContainer()的傳入參數中,有pMeshData,pMaterials,pEffectInstances,NumMaterials,pAdjacency,pSkinInfo
            你需要把這些數據保存到自己的D3DXMESHCONTAINER對象中。并且其中的所有數組所需的空間都要在全局堆中new出來。所以在該代碼中,有如下new:
            pMeshContainer = new D3DXMESHCONTAINER_DERIVED;//自定義的擴展數據容器對象
            memset(pMeshContainer, 0, sizeof(D3DXMESHCONTAINER_DERIVED));//初始化pMeshContainer,清0
                ...
            pMeshContainer->pMaterials = new D3DXMATERIAL[pMeshContainer->NumMaterials];//準備保存材質
            pMeshContainer->ppTextures = new LPDIRECT3DTEXTURE9[pMeshContainer->NumMaterials];//準備創建紋理對象。它聲明在擴展部分。
            pMeshContainer->pAdjacency = new DWORD[NumFaces*3];//準備保存鄰接三角形數組,NumFaces = pMesh->GetNumFaces();

            然后,對數據進行memcpy保存。pEffectInstances由于在繪制中不需要,并沒進行保存。對于沒有貼圖的賦以默認材質屬性。
            值得注意的是,所有這些new,必須在DestroyMeshContainer()時進行delete.

            接下來的處理中,如果發現Mesh的FVF中沒有法向量,要用CloneMeshFVF()重建Mesh,計算頂點平均法向量。以備光照處理。

            最后,我們看看蒙皮信息pSkinInfo的處理。這是重頭戲。
            如果發現pSkinInfo!=NULL,就準備著手從各個蒙皮骨骼信息創建SkinMesh.
            首先,用擴展容器結構D3DXMESHCONTAINER_DERIVED中的各屬性保存原Mesh指針值,pMeshContainer->pOrigMesh = pMesh, 因為接下來我們要創建SkinMesh替代原Mesh.然后,把SkinInfo中的各骨骼的偏移矩陣保存到pMeshContainer->pBoneOffsetMatrices中
                  cBones = pSkinInfo->GetNumBones();
                  pMeshContainer->pBoneOffsetMatrices = new D3DXMATRIX[cBones];
                  .....
                 每個“骨骼偏移矩陣”pBoneOffsetMatrices,在將來DrawMeshContainer()中是必須要用的。因為原始Mesh中的頂點數據乘以“骨骼偏移矩陣”,再乘以“變換矩陣”,才能求得各骨骼頂點在世界坐標系中的坐標。 即:
                骨骼上各點在世界坐標系中的新坐標=初始網格中的各點坐標*骨骼偏移矩陣*骨骼當前的變換矩陣
                其中,“初始網格中的各點坐標*骨骼偏移矩陣” = 骨骼上各點初始時刻在該骨骼坐標系中的局部坐標

            做了以上工作后,調用GenerateSkinnedMesh(pMeshContainer),創建SkinMesh. 接下來,我們看看GenerateSkinnedMesh()做了哪些工作。

            3.4 怎樣生成蒙皮網格SkinMesh? GenerateSkinnedMesh()分析

            由于要重定義pMeshContainer->MeshData.pMesh,所以先SAFE_RELEASE( pMeshContainer->MeshData.pMesh ); 釋放原pMesh

            在這個函數中,是根據當前繪圖方式設置進行加載數據的。因為頂點混合,有無索引的頂點混合,有含索引的頂點混合,所使用的函數和對應的SkinMesh數據內容也有所不同。
            在示例中,自定義了枚舉m_SkinningMethod,主要分為D3DNONINDEXED和D3DINDEXED,以有純軟件渲染等。運行示例后,你可以選擇菜單中的Options選擇不同的渲染方式。

            我們著重分析一下帶索引的蒙皮網格。在程序中,就是D3DINDEXED相關的部分。
            if (m_SkinningMethod == D3DINDEXED){ ....}

            注意! 示例默認工作在D3DNONINDEXED下,如果要跟蹤D3DINDEXED部分的代碼,必須選擇菜單中的Options選擇indexed!


            最主要的,要通過DX的ConvertToIndexedBlendedMesh()函數,生成支持“索引頂點混合”的SkinMesh.有關索引頂點混合的技術,你可以在DXSDK幫助文件中搜索“Indexed Vertex Blending”主題,對著英文和插圖將就看,確有收獲。

            要想用硬件對頂點進行混合,那么參與混合者不能太多。也就是說同時影響一個頂點的骨骼數不能多。我們假定一個頂點最多同時受4個骨骼的影響(也就是同時最多有4個骨骼矩陣參與加權求和),那么同時影響一個三角形面的骨骼數最多就是3*4=12個。
            我們用NumMaxFaceInfl表示影響一個三角面的最多骨骼矩陣數,那么,通過調用pSkinInfo->GetMaxFaceInfluences()獲取這個數值,一般也就3-4。如果這個數值太大,我們強制使用NumMaxFaceInfl = min(NumMaxFaceInfl, 12);來最多取值12。

            用NumMaxFaceInfl 這個數值干什么呢? 我們用來它分析當前的顯卡倒底行不行。

            if (m_d3dCaps.MaxVertexBlendMatrixIndex + 1 < NumMaxFaceInfl)//如果顯卡達不到該要求
            {
                  //很奇怪。2005年底買的GeForce 6600GT顯卡,竟然m_d3dCaps.MaxVertexBlendMatrixIndex=0, 不支持索引頂點混合!是驅動問題還是怎么了?
                  //但它支持非索引混合。或者,也許要用HLSL支持混合。看起來,3D編程要多考慮。
                   ..
                  pMeshContainer->UseSoftwareVP = true;//用軟件渲染頂點。顯然不實用。
            }
            else
            {
                  pMeshContainer->NumPaletteEntries = min( ( m_d3dCaps.MaxVertexBlendMatrixIndex + 1 ) / 2,
                                                                 pMeshContainer->pSkinInfo->GetNumBones() );//--什么意思?
                  pMeshContainer->UseSoftwareVP = false;//采用硬件頂點混合。
                  Flags |= D3DXMESH_MANAGED;
            }

            [評論]在上面有一行代碼:
                 pMeshContainer->NumPaletteEntries = min( ( m_d3dCaps.MaxVertexBlendMatrixIndex + 1 ) / 2,pMeshContainer->pSkinInfo->GetNumBones() );
            盡管作者加了大段注釋,還是讓人一頭霧水。其實,我們做一個實驗,反爾更能理解它的用途。
            第一步,你在這句話后面下一個斷點,看一下在你機器上這個數值。我的ATI 9550顯卡機器上是19。比tiny.x中的骨骼數35少很多。
            第二步,你將上面=右邊瞎填一個大于4的數字,比如6。編譯后照樣運行。而且效果上幾乎看不出任何差別。
            為什么會這樣呢? 我們在繪制代碼部分,看看這個數值起什么作用。
            在DrawMeshContainer()代碼中,我們查找D3DINDEXED相關的部分。在mesh各子集的DrawSubset()之前,有如下代碼:
                  for (iAttrib = 0; iAttrib < pMeshContainer->NumAttributeGroups; iAttrib++)
                  {
                            // first calculate all the world matrices
                            for (iPaletteEntry = 0; iPaletteEntry < pMeshContainer->NumPaletteEntries; ++iPaletteEntry)
                            {
                                iMatrixIndex = pBoneComb[iAttrib].BoneId[iPaletteEntry];
                                if (iMatrixIndex != UINT_MAX)
                                {
                                    D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );
                                    m_pd3dDevice->SetTransform( D3DTS_WORLDMATRIX( iPaletteEntry ), &matTemp );
                                }
                            }
                   ...
                  }
            下面仔細評估一下這些代碼.
            先注意看其中奇怪的D3DTS_WORLDMATRIX()宏,我們以前還沒這樣用過。它是做什么用的呢?通過查DXSDK幫助,我們在Geometry Blending主題中找到相關說明,并在"Indexed Vertex Blending"主題中給出了內部實現原理。原來,當你用m_pd3dDevice->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE);開啟了索引頂點混合后,在硬件上就啟用了“palette of matrices”,即矩陣寄存器組,它最多支持同時256個索引。就像過去用256色調色板來表現彩色一樣。D3DTS_WORLDMATRIX()宏就是有256-511這256個數表示矩陣索引號。

            這些矩陣參與如下計算:

            V最終頂點位置=V*M[索引值1]*權重1 + V*M[索引值2]*權重2 + ....+V*M[索引n]*(1-其它權重和)

            這個公式的來源,相信大家在眾多資料上見過,不贅述。 當然,我們也可以用程序完成這個蒙皮計算過程,但逐個讀頂點卻很麻煩。現在是由硬件代勞了。我們只設矩陣就行了。
            我們用m_pd3dDevice->SetTransform( D3DTS_WORLDMATRIX( iPaletteEntry ), &matTemp );這種方式設定各索引對應的矩陣。

            那么權重呢?我們怎么設?原來在上面所說的DX提供的ConvertToIndexedBlendedMesh()函數中,生成SkinMesh時,各網格頂點格式FVF已經有變化了,增加了新格式,D3DFVF_XYZB2,D3DFVF_LASTBETA_UBYTE4,用以記錄頂點對應的權重值以及矩陣索引。如下
            struct VERTEX
            {
                float x,y,z;
                float weight;
                DWORD matrixIndices;
                float normal[3];
            };
            #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZB2 | D3DFVF_LASTBETA_UBYTE4 |D3DFVF_NORMAL);

            D3DFVF_LASTBETA_UBYTE4對應于DWORD數值,用于矩陣索引時,每個字節表示一個索引,最多可以允許4個索引,同時有4個矩陣參于該點的混合。如果一次繪制中涉及了9塊骨骼矩陣,你可以把這9個矩陣全部用SetTransform設置到矩陣寄存器中,但每個頂點在渲染時,最多使用其中的4個。由此可知,pMeshContainer->NumPaletteEntries這個數值,確定了一趟DrawSubset繪制所用到的矩陣個數,個數越多,在一趟繪制中就可以納入的更多頂點。所以,當我們減少pMeshContainer->NumPaletteEntries這個數值時,pMeshContainer->NumAttributeGroups數值就會增加。也就是說,一趟繪制中所允許涉及的骨骼數越少,那么子集的數量NumAttributeGroups就會增加,需要多繪幾趟。
            你可以在此下斷點觀察,當NumPaletteEntries=19時,NumAttributeGroups=3 當NumPaletteEntries=6時,NumAttributeGroups=12 當NumPaletteEntries=4時,NumAttributeGroups=31,幾乎和無索引時的分組一樣多了。

            頂點中的權重weight存放了它當前骨骼的權重。(一個頂點對應的多個骨骼權重怎么存放?是不是在當前子集中有多個同樣的頂點,權重不同,對應的矩陣索引不同,然后混合)


            由上所述,ConvertToIndexedBlendedMesh()是一個很重要函數,由DX自動將Mesh頂點分組成多個子集,以便DrawSubset. 你必須把它的返回參數都記錄下來,在繪制時使用。

             

            四. 怎樣繪制顯示動畫?

            DrawFrame()用來繪制整個X框架。它遍歷各個框架,找到Mesh不為空的進行繪制。(其實整個.x中通常只有一個不為空,見上文所述)
            DrawMeshContainer()是繪制函數。

            4.1 怎樣開啟頂點混合?
            注意應用有關的Vertex Blending技術。如在索引方式的繪制中,
            m_pd3dDevice->SetRenderState(D3DRS_VERTEXBLEND, pMeshContainer->NumInfl - 1);
            其實是設定了D3DVBF_2WEIGHTS或D3DVBF_3WEIGHTS
            注意要m_pd3dDevice->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE);

            4.2 矩陣的刷新:
            首先,在FrameMove()調用m_pAnimController->SetTime()設置當前時間(或在DX9.0c中用AdvanceTime()設置時間差),從而刷新各個pFrame->TransformationMatrix,即骨骼轉換矩陣
            其次,調用UpdateFrameMatrices()做乘法累積,計算出各骨骼坐標系到根世界轉換矩陣。
            最后,在繪制前,將該轉換矩陣左乘偏移矩陣,得到最終的轉換矩陣。
                  D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );

            由此可見,你如果注釋掉了m_pAnimController->SetTime,畫面肯定停了。

            4.3 繪制輸出 是在DrawMeshContainer()中,調用SkinMesh的DrawSubset進行繪制。一些細節內容如D3DTS_WORLDMATRIX(),在上面已經有說明,不再羅嗦。

             

            4.4 關于示例中多種繪制方式分析
            在示例中,用到了多種渲染方式,包括傳統的非索引頂點混合,還有新興的HLSL方式。而且我發現,ATI RADEON 9550 顯卡MaxVertexBlendMatrixIndex=37,而價格更高的Gefoce 6600GT MaxVertexBlendMatrixIndex竟然為0,不支持index vertex blending!
            所以,還是有必要分析一下該示例中各種vertex blending方式的處理,以便掌握多種繪制方式適應不同顯卡。
            經測試,示例中所涉及的多種方式,由慢到快,依次是以下幾種:
                SOFTWARE,
                D3DNONINDEXED,
                D3DINDEXED,
                D3DINDEXEDVS,
                D3DINDEXEDHLSLVS,

            從最慢的SW到最快的HLSL,大約相差20%,有時會大到40%。 差別不是特別懸殊的原因,主要是頂點混合并不是瓶頸。

            關于頂點處理方式,是在創建D3D設備時指定的。共有三種方式:
               D3DCREATE_SOFTWARE_VERTEXPROCESSING 軟件頂點運算  (簡記 sw vp)
               D3DCREATE_HARDWARE_VERTEXPROCESSING 硬件頂點運算。必須有這項才支持有HAL (簡記 hw vp)
               D3DCREATE_MIXED_VERTEXPROCESSING 混合頂點運算,即硬件+軟件 (簡記 mixed vp)

            一旦用D3DCREATE_HARDWARE_VERTEXPROCESSING方式創建設備,就只能在硬件方式下進行頂點處理。如果調用m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)來切換到軟件頂點處理,HRESULT會返回失敗。
              所以,如果你對客戶的顯卡沒有足夠的信息,就用D3DCREATE_MIXED_VERTEXPROCESSING方式創建設備。它默認工作方式是HAL。一旦發現進行某種繪制時硬件能力不夠,就可以調用調用m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)切換到軟件模式。在示例中就是這么做的,啟動示例后,運行在mixed模式下。

            在Gefoce6600GT顯卡中,由于D3DINDEXED方式不支持,采用了軟件混合方式,在這種方式下速度甚至比SOFTWARE慢。HLSL還好,還是最快。

            要確定設備的硬件頂點處理能力,可以參考D3DCAPS9結構的VertexProcessingCaps成員。可以獲取下列屬性
            MaxActiveLights,MaxUserClipPlanes,MaxVertexBlendMatrices,MaxStreams,MaxVertexIndex

            (1)D3DNONINDEXED方式:

            首先看GenerateSkinnedMesh()中怎樣創建蒙皮網格的。
            這種方式下,用ConvertToBlendedMesh()建立蒙皮網格,而不是ConvertToIndexBlendedMesh()

            為了繪制蒙皮,在這個函數中對Mesh各子集的頂點再次進行的分組。分組的標準是各頂點(或三角面)所涉及的骨骼矩陣個數不超過pMeshContainer->NumInfl個。(這個數字是由在ConvertToBlendedMesh()時,由參數pMaxFaceInfl返回的)。一個Mesh子集可能被拆開成多個分組。 最后,分組的屬性保存在pBoneCombinationBuf中,如子集ID,該子集的各骨骼ID,起始三角面,三角面個數等供繪制時使用,分組的個數保存在pMeshContainer->NumAttributeGroups中。

            接下來檢查每個分組所涉及的骨骼數,是不是超過硬件允許的最大混合矩陣數---MaxVertexBlendMatrices。如果超過了就把所有分組截為兩大部分,前一部分用硬件混合,后一部分采用軟件混合。而且,一旦發現有需要軟件混合,要采用CloneMeshFVF(D3DXMESH_SOFTWAREPROCESSING|...)的方式重新生成網格。

            再來看繪制部分DrawMeshContainer()

            用pBoneComb指向骨骼分組屬性,掃描各分組。找出其中骨骼數滿足硬件性能的用進行繪制。
            然后開啟軟件頂點渲染m_pd3dDevice->SetSoftwareVertexProcessing(TRUE),對那些骨骼數超出硬件性能的進行繪制。
            SetSoftwareVertexProcessing()需要當前d3d設備以D3DCREATE_MIXED_VERTEXPROCESSING方式創建。

            (2)D3DINDEXED,這種方式上面分析過了,從略。用pMeshContainer->UseSoftwareVP表示是否采用軟件繪制。
            值得注意的是在這種方式下,一旦硬件性能不足,會徹底使用軟件頂點渲染,而不是像上面一樣拆為兩部分。

            (3)D3DINDEXEDVS,D3DINDEXEDHLSLVS
            這種情況下使用了著色器和高級著色語言。超出本文主旨,討論從略。

            (4)SOFTWARE--軟件方式? 讓人有些迷惑,與上面的m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)有何區別?

            從代碼看,這種方式下反而比較簡單。GenerateSkinnedMesh()中,
            先直接從原始Mesh克隆一個Mesh,然后讀取它的材質屬性數組。開辟一個空間m_pBoneMatrices,用以存放各塊骨骼的轉換矩陣。

            在繪制時,從pMeshContainer中的變換矩陣乘以偏移矩陣,放在pBoneMatrices中。把這個矩陣數組,以原Mesh的頂點作為源頂點,以新克隆的MeshData.pMesh做為目標頂點,調用pSkinInfo->UpdateSkinnedMesh(),用軟件方式計算各骨骼頂點的新位置(相當于軟件計算方式蒙皮)。

            然后調用MeshData.pMesh->DrawSubset()繪制。

            可見,在SOFTWARE方式下,最終頂點的渲染還是HAL方式的,只不過蒙皮計算是由軟件完成的。它和上面的m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)直接設置軟件頂點渲染還是有區別的。

            posted on 2009-11-16 00:47 麒麟子 閱讀(2034) 評論(2)  編輯 收藏 引用 所屬分類: DirectX

            評論

            # re: 關于骨骼動畫及微軟示例Skinned Mesh的解析 2012-05-16 08:19 文西

            正在學習 感謝樓主精彩分享  回復  更多評論   

            # re: 關于骨骼動畫及微軟示例Skinned Mesh的解析 2013-03-26 18:35 永遇樂

            作者花了大氣力分析,很受用,謝謝分享。  回復  更多評論   

            99久久免费国产精精品| 国产精品久久久久aaaa| 无码人妻久久一区二区三区蜜桃 | 一本一本久久a久久综合精品蜜桃 一本一道久久综合狠狠老 | 久久青青草视频| 99久久婷婷免费国产综合精品| 狠狠精品干练久久久无码中文字幕| 国产精品一区二区久久精品涩爱 | 久久精品国产亚洲AV麻豆网站| 97超级碰碰碰碰久久久久| 久久精品国产2020| 久久九九免费高清视频| www.久久99| 人妻少妇久久中文字幕| 色欲综合久久躁天天躁| 精品99久久aaa一级毛片| 91精品国产91久久综合| 亚洲精品乱码久久久久久蜜桃图片 | 欧美喷潮久久久XXXXx| 亚洲&#228;v永久无码精品天堂久久 | 亚洲AV无一区二区三区久久| 一本久久精品一区二区| 久久久久无码国产精品不卡| 精品国产热久久久福利| 亚洲国产成人久久综合一 | 久久男人AV资源网站| 中文字幕亚洲综合久久2| 99久久人妻无码精品系列蜜桃| 久久精品日日躁夜夜躁欧美| 亚洲人AV永久一区二区三区久久| 久久播电影网| 久久国产一片免费观看| 久久综合狠狠综合久久97色| 久久性精品| 国内精品久久久久影院亚洲| 久久强奷乱码老熟女网站| 久久99热这里只有精品国产| 狠狠综合久久AV一区二区三区 | 久久久受www免费人成| 亚洲国产成人精品91久久久 | 成人午夜精品无码区久久|