關(guān)于骨骼動畫及微軟示例Skinned Mesh的解析骨骼動畫是D3D的一個重要應(yīng)用。盡管微軟DXSDK提供了示例Skinned Mesh,但由于涉及眾多概念和技術(shù)細(xì)節(jié),示例相對于初學(xué)者非常復(fù)雜,難以看懂。在此,提供一些重要問題評論,以使初學(xué)者走出迷局,順利上手。文中所述都是參照各種資料加上自己的理解,也有可能出些偏差,有則回貼拍磚,無則權(quán)當(dāng)一笑。
一 骨骼動畫原理原理方面在網(wǎng)上資料比較多,大家都基本明白。在此說一下重點:總體上,絕大部分動畫實現(xiàn)原理一致,就是“提供一種機(jī)制,描述各頂點位置隨時間的變化”。有三種方法:1.1 關(guān)節(jié)動畫:由于大部分運動,都是皮膚隨骨骼在動,皮膚相對于它的骨骼本身并沒有發(fā)生運動,所以只要描述清楚骨骼的運動就行了。用矩陣描述各個骨骼的相對于父骨骼運動。(大多運動都是旋轉(zhuǎn)型) 易知,從子骨骼用矩陣乘法累積到最頂層根骨骼,就可以得到每個子骨骼相對于世界坐標(biāo)系的轉(zhuǎn)換矩陣。
這種動畫,只須用普通Mesh保存最初始的各頂點坐標(biāo),以及一系列后續(xù)時刻所對應(yīng)的各骨骼的運動矩陣。不用保存每時刻的頂點數(shù)據(jù),節(jié)省了大量存儲空間。而且比較靈活,可以利用關(guān)鍵幀插值運算,便于通過運算調(diào)節(jié)動作。缺點是在兩段骨骼交接處,容易產(chǎn)生裂縫,影響效果。
1.2 漸變動畫:通過保存一系列時刻的頂點坐標(biāo)來完成動畫。雖然比較逼真,但占用大量空間,靈活性也不高。
1.3 骨骼蒙皮動畫(skinned Mesh)
相當(dāng)于上面兩方法的折中。現(xiàn)在比較流行。
在關(guān)節(jié)動畫的基礎(chǔ)上,利用頂點混合(Vertex Blend)技術(shù),對于關(guān)節(jié)附近的頂點,由影響這些頂點的兩段(或多段)骨骼運動,分別賦以權(quán)值,共同決定頂點位置。相當(dāng)于在骨骼關(guān)節(jié)上動態(tài)蒙皮,有效解決了裂縫問題。
這里,引入一個D3D技術(shù)概念:“Vertex Blending”——頂點混合技術(shù)。比如說,你肯定用過SetTransform(D3DTS_WORLD,……),但SetTransform(D3DTS_WORLDMATRIX(i),……)是不是很奇怪?這個問題后文會講到。 你也可以在微軟的DXSDK的幫助文件中搜索“Geometry Blending”主題,有裂縫及其解決辦法圖示。
二 X文件如何保存骨骼動畫理解X文件格式,對用好相關(guān)的DX函數(shù)是非常重要的。
不含動畫的普通X文件,有一個Mesh單元,保存了各頂點信息、各三角面的索引信息、材質(zhì)種類及定義等。
動畫X文件,則在這個單元中增加了“各骨骼蒙皮信息”、“骨骼層次及結(jié)構(gòu)信息”、“各時刻骨骼矩陣信息”等。
2.1 網(wǎng)格蒙皮信息:首先,在Mesh{}單元中,在原有的普通網(wǎng)格頂點數(shù)據(jù)基礎(chǔ)上,新增了XSkinMeshHeader{}結(jié)構(gòu),以及多個SkinWeights{}結(jié)構(gòu)。用以描述各個骨骼的蒙皮信息。
其中,XSkinMeshHeader是總括,舉一實例,如下:
XSkinMeshHeader { 2,//一個頂點可以受到骨骼影響的最大骨骼數(shù),可用于計 算共同作用時減少遍歷次數(shù)
4,//一個三角面可以受到骨骼影響的最大骨骼數(shù)。這個數(shù)字對硬件頂點混合計算提出了基本要求。
35 //當(dāng)前Mesh的骨骼總數(shù)。
}由于每個骨骼的蒙皮信息都需要用SkinWeights結(jié)構(gòu)去描述,所以有多少塊骨骼,在Mesh中就有多少個SkinWeights對象。
注意,一般把SkinWeights視作Mesh的一部分。這種Mesh又稱Skinned Mesh (蒙皮網(wǎng)格)
SkinWeights 結(jié)構(gòu)如下:{ STRING transformNodeName; //骨骼名
DWORD nWeights; //權(quán)重數(shù)組的元素個數(shù),即該骨骼相關(guān)的頂點個數(shù)
array DWORD vertexIndices[nWeights];//受該骨骼控制的頂點索引,實際上定義了該骨骼的蒙皮
array float weights[nWeights]; //蒙皮各頂點的受本骨骼影響的權(quán)值Matrix4x4 matrixOffset; //骨骼偏移矩陣,用來從初始Mesh坐標(biāo),反向計算頂點在子骨骼坐標(biāo)系中的初始坐標(biāo)。
}在有的書中,把上面的matrixOffset叫骨骼權(quán)重矩陣,是不恰當(dāng)?shù)摹?yīng)該稱為骨骼偏移矩陣比較合適。
[問題] 在整個動畫過程中,子骨骼運動矩陣的數(shù)值是不斷變化的。上面的骨骼偏移矩陣變化嗎?有沒有必要重新計算?它在什么時候使用?
答:各骨骼的偏移矩陣matrixOffset專門用來從原始Mesh數(shù)據(jù)計算出各頂點相對于骨骼坐標(biāo)系的原始坐標(biāo)。在繪制前,把它與當(dāng)前變換矩陣相乘,就可以得到該骨骼的當(dāng)前的最終變換矩陣。 總之,骨骼偏移矩陣是與原始Mesh頂點數(shù)值相關(guān)聯(lián)的,在整個動畫過程中是不變的,也不應(yīng)該變。在動畫過程中變化是當(dāng)前骨骼變換矩陣,可由。X中的AnimatonKey中的各時刻矩陣得到。這個矩陣乘法在示例中的對應(yīng)代碼如下:D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );即,D3DXMatrixMultiply(輸出最終世界矩陣, 該骨骼的偏移矩陣, 該骨骼的變換矩陣)
2.2 骨骼層次信息在X文件中,F(xiàn)rame是基本的組成單元。又稱框架Frame. 一個。x可以有多個Frame.(注意此處的Frame不是幀,與幀沒什么關(guān)系)
框架Frame允許嵌套,這樣就存在父子框架了。而并列的框架,稱為兄弟框架。這兩種關(guān)系組合在一起,即可以縱深,又可以并列,形成一種層次結(jié)構(gòu)。這種結(jié)構(gòu),可用二叉樹描述。
每個框架結(jié)構(gòu)的最前面,有一個FrameTransformMatrix矩陣數(shù)據(jù),描述了該框架相對于父框架的變換矩陣。也就是說,該框架中的坐標(biāo),與該矩陣相乘,可轉(zhuǎn)換為父框架坐標(biāo)系的坐標(biāo)。
這種層次結(jié)構(gòu),使得X文件能描述許多復(fù)雜的物體。如地形場景。
在骨骼動畫文件中,框架結(jié)構(gòu)可直接拿來描述人物骨骼的層次結(jié)構(gòu)。框架的名字通常為對應(yīng)的骨骼名。
如“左上臂->左前臂->手掌->手指”就形成一個父子骨骼鏈。而左上臂與右上臂是并行關(guān)系。
數(shù)據(jù)示例: 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;;}……縮進(jìn)}
[問題]查看示例tiny.x文件,發(fā)現(xiàn)只有根框架下有一個Mesh,包含了所有頂點信息。其它各個Frame都沒有Mesh數(shù)據(jù)。怎么理解?
答: 一般來說,每個動畫文件只有一個Mesh網(wǎng)格,包含物體所有頂點信息。
其它Frame,只是借用來描述各骨骼的層次信息,沒必要再定義骨骼網(wǎng)格。每塊骨骼對應(yīng)的蒙皮頂點信息,由根Mesh中的相應(yīng)骨骼的SkinWeights中蒙皮頂點索引描述的。在動畫過程中,各個頂點的新坐標(biāo),要借助SkinWeights中的頂點索引來進(jìn)行重新計算。
2.3 動畫信息:由一系列AnimatonKey組成,數(shù)據(jù)示例如下:AnimationKey { 4;——動畫類型 4表示矩陣62; ——動畫幀數(shù),即下面矩陣個數(shù)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;;,……上面紅數(shù)字表示時刻tick,蘭數(shù)字表示數(shù)值的個數(shù)。
……其它各時刻矩陣……
{ Bip01_R_Calf }——對應(yīng)的骨骼對象引用}注意:(1)每塊骨骼都有一個AnimationKey{}.(2)在上面數(shù)據(jù)結(jié)構(gòu)中,主要保存了各典型時刻的該骨骼相對于父的變換矩陣。
(3)在0時刻的矩陣,與該骨骼對應(yīng)的前面的Frame所對應(yīng)的矩陣是相同的。如Frame Bip01_R_Calf{}中的變換矩陣,與Bip01_R_Calf所對應(yīng)的AnimationKey 的第0時刻矩陣是一樣的。這說明,在以后動畫運行時,DX會提供一種功能,用AnimatonKey中的對應(yīng)數(shù)據(jù)刷新初始的變換矩陣(也可能啟用關(guān)鍵幀插值算法)。這個功能對應(yīng)于示例中的m_pAnimController->SetTime(……)語句。
三 怎樣從X文件加載骨骼動畫信息?
3.1 負(fù)責(zé)加載的函數(shù):可能有多種加載方式,在此以SDK中的示例為準(zhǔn),敘述一種標(biāo)準(zhǔn)加載方式,需要用到DX函數(shù)D3DXLoadMeshHierarchyFromX(),函數(shù)字面意思是讀取Mesh層次信息。
HRESULT WINAPI D3DXLoadMeshHierarchyFromX(
LPCSTR Filename,//.x文件名
DWORD MeshOptions, //Mesh選項,一般選D3DXMESH_MANAGED LPDIRECT3DDEVICE9 pD3DDevice, //指向D3D設(shè)備Device LPD3DXALLOCATEHIERARCHY pAlloc, //自定義數(shù)據(jù)容器LPD3DXLOADUSERDATA pUserDataLoader, //一般選NULL
LPD3DXFRAME *ppFrameHierarchy, //返回根Frame指針,指向代表整個骨架的Frame層次結(jié)構(gòu)
LPD3DXANIMATIONCONTROLLER *ppAnimController //返回相應(yīng)的動畫控制器);
這個函數(shù)后面的兩個輸出參數(shù)很重要,也很好理解,但輸入?yún)?shù)中的自定義數(shù)據(jù)容器是怎么回事呢?
原來,鑒于動畫數(shù)據(jù)的復(fù)雜性,需要你配合完成加載過程。比如你是否用到自定義擴(kuò)展結(jié)構(gòu),Mesh等數(shù)據(jù)保存在哪里,怎樣使用戶自己創(chuàng)建容器,自己決定卸載等等。
DX提供了ID3DXALLOCATEHIERARCHY接口,提供了這個自定義的機(jī)會,你重載這個接口的虛函數(shù),在加載過程中,它就像回調(diào)函數(shù)那樣運作。
你需要像下面這樣建立一個自定義數(shù)據(jù)容器類:
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是什么意思?
答:相當(dāng)于virtual HRESULT __stdcall 的宏。<評論> 因為這種類要與D3D的COM接口打交道,不僅僅在C++內(nèi)部使用,所以,所有類方法必須做成stdcall的,可對外開放的。
#define STDMETHOD(method) virtual HRESULT STDMETHODCALLTYPE method
#define STDMETHODCALLTYPE __stdcall
這樣當(dāng)寫一個函數(shù)STDMETHOD(op1(int i))
展開后成為: virtual HRESULT __stdcall op1(int i);
3.2 自定義數(shù)據(jù)容器以及具體的讀取過程:根據(jù)。X文件,在加載過程中,主要有兩方面數(shù)據(jù)需要保存,一個是骨架Frame信息,一個是網(wǎng)格蒙皮Mesh信息。這兩個信息保存在如下結(jié)構(gòu)中。
框架信息(對應(yīng)于骨骼)
typedef struct _D3DXFRAME { LPSTR Name;
D3DXMATRIX TransformationMatrix; //本骨骼的轉(zhuǎn)換矩陣LPD3DXMESHCONTAINER pMeshContainer; //本骨骼所對應(yīng)Mesh數(shù)據(jù)struct _D3DXFRAME *pFrameSibling; //兄弟骨骼struct _D3DXFRAME *pFrameFirstChild; //子骨骼}
D3DXFRAME, *LPD3DXFRAME;自定義數(shù)據(jù)容器,其數(shù)據(jù)來源由上面接口的
CreateMeshContainer()函數(shù)提供
typedef struct _D3DXMESHCONTAINER { LPSTR Name; //容器名D3DXMESHDATA MeshData; //Mesh數(shù)據(jù),可創(chuàng)建SkinMesh取代這個Mesh
LPD3DXMATERIAL pMaterials; //材質(zhì)數(shù)組LPD3DXEFFECTINSTANCE pEffects;
DWORD NumMaterials;//材質(zhì)數(shù)
DWORD* pAdjacency; //鄰接三角形數(shù)組
LPD3DXSKININFO pSkinInfo; //蒙皮信息,其中含.x中的各個skinweight蒙皮頂點索引及各骨骼偏移矩陣等.
struct _D3DXMESHCONTAINER *pNextMeshContainer;
}D3DXMESHCONTAINER, *LPD3DXMESHCONTAINER;
[評論]。在動畫文件中,框架通常用來描述骨骼。可以把Frame視做骨骼,所以不細(xì)加區(qū)分。。在上面D3DXFRAME結(jié)構(gòu)中,pFrameSibling, pFrameFirstChild兩個指針,常用于遞歸函數(shù)中,遍歷整個骨架。在D3DXFRAME結(jié)構(gòu)中有一個pMeshContainer指針,難道框架與Mesh是一一對應(yīng)的嗎?
有一個框架(骨骼)就有一個Mesh嗎?怎么.X文件中只有一個Mesh?難道加載時拆開存放?
答:從D3DXFrame結(jié)構(gòu)上看,每個Frame都有一個pMeshContainer指針。這就有三種解釋:第一種,加載到內(nèi)存后所有的pMeshContainer都指向同一個全局Mesh第二種,加載到內(nèi)存后,只有一個主框架的pMeshContainer不為空,其它Frame的pMeshContainer均為NULL,因為在。X中,它們沒有定義自己的Mesh第三種,加載到內(nèi)存后,D3D將Mesh拆分,分開到各骨骼所對應(yīng)的Frame,每個Frame都有自己的Mesh.這個問題我以前也不是很清楚,通過查看示例源碼及跟蹤發(fā)現(xiàn),正確解釋應(yīng)該是第2種。唯一的一個全局Mesh存放在Frame "body"下的無名Frame中。而其它Frame由于沒有自己專門的Mesh而指向NULL. 應(yīng)該大致如此。這個問題之所以讓人困繞,是因為從后續(xù)代碼上看,在渲染DrawFrame時,是遍歷每一個frame分別繪制它們對應(yīng)的Mesh. 如果對應(yīng)于同一個mesh,就繪制多遍。如果對應(yīng)各自mesh,那么變換矩陣怎么組織運算等等。所以,根據(jù)第二種解釋,由于只有一個pMeshContainer不為NULL,所以參與繪制及蒙皮的只有這一個MeshContainer,人體所有頂點數(shù)據(jù)及蒙皮信息都在這個mesh中。
所以,讀取tiny.x文件后,會產(chǎn)生多個D3DXFRAME對象,但只有一個D3DXMESHCONTAINER對象。
在示例代碼的CMyD3DApplication::InitDeviceObjects()中,有:
hr = D3DXLoadMeshHierarchyFromX(strMeshPath, D3DXMESH_MANAGED, m_pd3dDevice, &Alloc, NULL, &m_pFrameRoot, &m_pAnimController);
if (FAILED(hr))
return hr;
其中的Alloc是就自定義的數(shù)據(jù)容器對象。m_pFrameRoot是根骨骼,對遍歷很重要。m_pAnimController是動畫控制器,對刷新矩陣很重要。
你在運行完這句話后,下一個斷點,觀察m_pFrameRoot,會發(fā)現(xiàn)如下內(nèi)容: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的無名框架可見,在內(nèi)存中的Frame布局是與。x中一一對應(yīng)的。除了pFrameFirstChild 0x00c594d0這個地方的Frame中的pMeshContainer不為空,其它框架的這個mesh指針都是空值。
另外一點可以看出,并不是每個Frame都對就一塊骨骼,有的是別的用途。也就是說Frame對象的個數(shù)可能多于骨骼數(shù)。
3.3 分析CAllocateHierarchy類下面繼續(xù)研究自定義數(shù)據(jù)容器CAllocateHierarchy,顧名思義,該類是在加載過程中自行分配層次數(shù)據(jù)空間。它有4個成員,都是重載D3D的接口虛函數(shù)。
它的成員CreateFrame()是用來創(chuàng)建D3DXFrame對象的,而CreateMeshContainer()是用來創(chuàng)建Mesh數(shù)據(jù)對象的。你可以在這兩個函數(shù)中下斷點,發(fā)現(xiàn)CreateFrame會運行多次,而CreateMeshContainer只運行一次,再次驗證了上面的說法。
值得注意的是,示例對上面的D3DXFRAME,D3DXMESHCONTAINER兩個結(jié)構(gòu)做了擴(kuò)展,分別代之以D3DXFRAME_DERIVED結(jié)構(gòu)和D3DXMESHCONTAINER_DERIVED結(jié)構(gòu),以集中存儲數(shù)據(jù)方便程序處理。
CreateFrame()處理比較簡單,你只是new一個Frame對象空間,填入傳進(jìn)來的Name,其它內(nèi)容由DX負(fù)責(zé)維護(hù)填充。
CreateMeshContainer()較為復(fù)雜。它的任務(wù)一是保存?zhèn)魅氲木W(wǎng)格數(shù)據(jù)數(shù)據(jù),二是根據(jù)這些數(shù)據(jù)及蒙皮信息調(diào)用GenerateSkinnedMesh()函數(shù)生成蒙皮網(wǎng)格。只有這個新的BlendMesh才能在Render()時支持頂點混合,完成蒙皮的顯示。在D3DXMESHCONTAINER_DERIVED結(jié)構(gòu)中,用pOrigMesh保存舊的Mesh普通網(wǎng)格信息。而Meshdata.Mesh則指向新產(chǎn)生的BlendMesh在這個函數(shù)中,多次用到了AddRef(),對COM不熟悉的新手容易困惑。D3D是COM組件,它在服務(wù)進(jìn)程中運行,而不在當(dāng)前的客戶進(jìn)程中。在DX組件運行過程中,要創(chuàng)建一系列接口對象,如CreateDevice()返回接口指針,這些接口及其占用內(nèi)存什么時候釋放,要通過“引用計數(shù)”的技術(shù)來解決。AddRef()給這個接口指針的計數(shù)加1,而Release()會將之減1.一旦減到0,表示沒有客戶使用了,相關(guān)的接口就釋放了。 由此可知,每次調(diào)用Rlease()后,并不一定會釋放內(nèi)存,而是當(dāng)引用計數(shù)歸0時釋放內(nèi)存。
這樣,對接口指針的使用,就像維護(hù)堆棧的平衡一樣,要仔細(xì),而且按照某種約定規(guī)則使用。
但平時D3D編程中,怎么不用AddRef()呢?這是由于一個接口指針,如ID3DDevice,或VertexBuf指針,都是D3DXCreate出來的,在Create時候,在內(nèi)部已經(jīng)事先AddRef()了,你就不需要再做這工作了。只要你在不用時,調(diào)用 p指針->Relase()就釋放了。一般編程,特別是小型示例程序,都是初始化時建立一次,關(guān)閉時釋放,都遵守了這種約定,所以不存在這種問題。
但在CreateMeshContainer()函數(shù)中,以多種方式使用了指針,在局部指針變量中來回傳遞,所以問題復(fù)雜化了。在COM編程中約定,任何時候地接口指針賦值(復(fù)制),都要AddRef(),在指針變量結(jié)束生命期前,再Release()。 但許多程序員都不是嚴(yán)格這么做。因為在局部變量用完就廢了,先AddRef()增加計數(shù)再Release()減少,和直接使用最后是等效的。幾乎是多此一舉。這與編程習(xí)慣有關(guān)系。一旦引用計數(shù)不對,如果沒有統(tǒng)一的習(xí)慣,不好排查。在CreateMeshContainer()中,對接口指針的使用有三種方式,例舉如下:方式一:不使用AddRef()。和普通指針一樣,臨時變量是左值,接口指針是右值,直接賦值使用。如:pMesh = pMeshData->pMesh;這是由于pMesh是局部變量,它只是臨時引用一下,沒必要為它先AddRef(),后Release()。
方式二:隱式的使用AddRef()。 由于用到了一些內(nèi)部有AddRef()動作的函數(shù),就要按照COM約定,在子程序結(jié)束前Release()
pMesh->GetDevice(&pd3dDevice);//此處d3d設(shè)備引用計數(shù)已經(jīng)加1……
SAFE_RELEASE(pd3dDevice);//——此處將引用計數(shù)減1,并不是真的釋放d3d設(shè)備在本例中,pd3dDevice在GetDevice()中已經(jīng)Addref()過了,所以,在退出CreateMeshContainer()前,必須pd3dDevice->Release()
方式三:顯式的使用AddRef()。 如果一個指針值,不是由D3DXCreate出來的,而是通過賦值方式復(fù)制給一個全局變量或長期變量的。 所以,可以通過AddRef()的方式來延遲該對象的釋放。因為,如果不AddRef(),極有可能在函數(shù)返回該對象就可能釋放了。它就像一個加油站,使得傳入對象的壽命延長至自己控制范圍內(nèi)。用了AddRef(),就要在相關(guān)的Destroy中添加Release()。
在本函數(shù),有三處這樣的語句:
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 );由于這些指針值的創(chuàng)建、更改等都是用戶自己經(jīng)營的,所以務(wù)必要加前后吻合,在CreateMeshContainer()中AddRef(),在DestroyMeshContainer()中Release()
再來看數(shù)據(jù)的保存部分。
在CreateMeshContainer()的傳入?yún)?shù)中,有pMeshData,pMaterials,pEffectInstances,NumMaterials,pAdjacency,pSkinInfo你需要把這些數(shù)據(jù)保存到自己的D3DXMESHCONTAINER對象中。并且其中的所有數(shù)組所需的空間都要在全局堆中new出來。所以在該代碼中,有如下new:
pMeshContainer = new D3DXMESHCONTAINER_DERIVED;//自定義的擴(kuò)展數(shù)據(jù)容器對象
memset(pMeshContainer, 0, sizeof(D3DXMESHCONTAINER_DERIVED));//初始化pMeshContainer,清0……
pMeshContainer->pMaterials = new D3DXMATERIAL[pMeshContainer->NumMaterials];//準(zhǔn)備保存材質(zhì)
pMeshContainer->ppTextures=new LPDIRECT3DTEXTURE9[pMeshContainer->NumMaterials];//準(zhǔn)備創(chuàng)建紋理對象。它聲明在擴(kuò)展部分。
pMeshContainer->pAdjacency = new DWORD[NumFaces*3];//準(zhǔn)備保存鄰接三角形數(shù)組,NumFaces = pMesh->GetNumFaces();然后,對數(shù)據(jù)進(jìn)行memcpy保存。pEffectInstances由于在繪制中不需要,并沒進(jìn)行保存。對于沒有貼圖的賦以默認(rèn)材質(zhì)屬性。
值得注意的是,所有這些new,必須在DestroyMeshContainer()時進(jìn)行delete.接下來的處理中,如果發(fā)現(xiàn)Mesh的FVF中沒有法向量,要用CloneMeshFVF()重建Mesh,計算頂點平均法向量。以備光照處理。
最后,我們看看蒙皮信息pSkinInfo的處理。這是重頭戲。
如果發(fā)現(xiàn)pSkinInfo!=NULL,就準(zhǔn)備著手從各個蒙皮骨骼信息創(chuàng)建SkinMesh.首先,用擴(kuò)展容器結(jié)構(gòu)D3DXMESHCONTAINER_DERIVED中的各屬性保存原Mesh指針值,pMeshContainer->pOrigMesh = pMesh, 因為接下來我們要創(chuàng)建SkinMesh替代原Mesh.然后,把SkinInfo中的各骨骼的偏移矩陣保存到
pMeshContainer->pBoneOffsetMatrices中cBones = pSkinInfo->GetNumBones();pMeshContainer->pBoneOffsetMatrices = new D3DXMATRIX[cBones];……
每個“骨骼偏移矩陣”pBoneOffsetMatrices,在將來DrawMeshContainer()中是必須要用的。因為原始Mesh中的頂點數(shù)據(jù)乘以“骨骼偏移矩陣”,再乘以“變換矩陣”,才能求得各骨骼頂點在世界坐標(biāo)系中的坐標(biāo)。 即:骨骼上各點在世界坐標(biāo)系中的新坐標(biāo)=初始網(wǎng)格中的各點坐標(biāo)*骨骼偏移矩陣*骨骼當(dāng)前的變換矩陣其中,“初始網(wǎng)格中的各點坐標(biāo)*骨骼偏移矩陣” = 骨骼上各點初始時刻在該骨骼坐標(biāo)系中的局部坐標(biāo)做了以上工作后,調(diào)用GenerateSkinnedMesh(pMeshContainer),創(chuàng)建SkinMesh. 接下來,我們看看GenerateSkinnedMesh()做了哪些工作。
3.4 怎樣生成蒙皮網(wǎng)格SkinMesh?
GenerateSkinnedMesh()分析由于要重定義pMeshContainer->MeshData.pMesh,所以先SAFE_RELEASE( pMeshContainer->MeshData.pMesh ); 釋放原pMesh在這個函數(shù)中,是根據(jù)當(dāng)前繪圖方式設(shè)置進(jìn)行加載數(shù)據(jù)的。因為頂點混合,有無索引的頂點混合,有含索引的頂點混合,所使用的函數(shù)和對應(yīng)的SkinMesh數(shù)據(jù)內(nèi)容也有所不同。
在示例中,自定義了枚舉m_SkinningMethod,主要分為D3DNONINDEXED和D3DINDEXED,以有純軟件渲染等。運行示例后,你可以選擇菜單中的Options選擇不同的渲染方式。
我們著重分析一下帶索引的蒙皮網(wǎng)格。在程序中,就是D3DINDEXED相關(guān)的部分。
if (m_SkinningMethod == D3DINDEXED){ ……}注意! 示例默認(rèn)工作在D3DNONINDEXED下,如果要跟蹤D3DINDEXED部分的代碼,必須選擇菜單中的Options選擇indexed!
最主要的,要通過DX的ConvertToIndexedBlendedMesh()函數(shù),生成支持“索引頂點混合”的SkinMesh.有關(guān)索引頂點混合的技術(shù),你可以在DXSDK幫助文件中搜索“Indexed Vertex Blending”主題,對著英文和插圖將就看,確有收獲。
要想用硬件對頂點進(jìn)行混合,那么參與混合者不能太多。也就是說同時影響一個頂點的骨骼數(shù)不能多。我們假定一個頂點最多同時受4個骨骼的影響(也就是同時最多有4個骨骼矩陣參與加權(quán)求和),那么同時影響一個三角形面的骨骼數(shù)最多就是3*4=12個。
我們用NumMaxFaceInfl表示影響一個三角面的最多骨骼矩陣數(shù),那么,通過調(diào)用pSkinInfo->GetMaxFaceInfluences()獲取這個數(shù)值,一般也就3-4.如果這個數(shù)值太大,我們強(qiáng)制使用NumMaxFaceInfl = min(NumMaxFaceInfl, 12);來最多取值12.用NumMaxFaceInfl 這個數(shù)值干什么呢? 我們用來它分析當(dāng)前的顯卡倒底行不行。
if (m_d3dCaps.MaxVertexBlendMatrixIndex + 1 < NumMaxFaceInfl)
//如果顯卡達(dá)不到該要求
{ //很奇怪。2005年底買的GeForce 6600GT顯卡,竟然
m_d3dCaps.MaxVertexBlendMatrixIndex=0,
不支持索引頂點混合!是驅(qū)動問題還是怎么了?
//但它支持非索引混合。或者,也許要用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() );盡管作者加了大段注釋,還是讓人一頭霧水。其實,我們做一個實驗,反爾更能理解它的用途。
第一步,你在這句話后面下一個斷點,看一下在你機(jī)器上這個數(shù)值。我的ATI 9550顯卡機(jī)器上是19.比tiny.x中的骨骼數(shù)35少很多。
第二步,你將上面=右邊瞎填一個大于4的數(shù)字,比如6.編譯后照樣運行。而且效果上幾乎看不出任何差別。
為什么會這樣呢? 我們在繪制代碼部分,看看這個數(shù)值起什么作用。
在DrawMeshContainer()代碼中,我們查找D3DINDEXED相關(guān)的部分。在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主題中找到相關(guān)說明,并在"Indexed Vertex Blending"主題中給出了內(nèi)部實現(xiàn)原理。原來,當(dāng)你用m_pd3dDevice->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE);開啟了索引頂點混合后,在硬件上就啟用了“palette of matrices”,即矩陣寄存器組,它最多支持同時256個索引。就像過去用256色調(diào)色板來表現(xiàn)彩色一樣。D3DTS_WORLDMATRIX()宏就是有256-511這256個數(shù)表示矩陣索引號。
這些矩陣參與如下計算:
V最終頂點位置=V*M[索引值1]*權(quán)重1 + V*M[索引值2]*權(quán)重2 + ……+V*M[索引n]*(1-其它權(quán)重和)
這個公式的來源,相信大家在眾多資料上見過,不贅述。 當(dāng)然,我們也可以用程序完成這個蒙皮計算過程,但逐個讀頂點卻很麻煩。現(xiàn)在是由硬件代勞了。我們只設(shè)矩陣就行了。
我們用
m_pd3dDevice->SetTransform(D3DTS_WORLDMATRIX(iPaletteEntry),&matTemp;
這種方式設(shè)定各索引對應(yīng)的矩陣。
那么權(quán)重呢?我們怎么設(shè)?原來在上面所說的DX提供的ConvertToIndexedBlendedMesh()函數(shù)中,生成SkinMesh時,各網(wǎng)格頂點格式FVF已經(jīng)有變化了,增加了新格式,D3DFVF_XYZB2,D3DFVF_LASTBETA_UBYTE4,用以記錄頂點對應(yīng)的權(quán)重值以及矩陣索引。如下
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對應(yīng)于DWORD數(shù)值,用于矩陣索引時,每個字節(jié)表示一個索引,最多可以允許4個索引,同時有4個矩陣參于該點的混合。如果一次繪制中涉及了9塊骨骼矩陣,你可以把這9個矩陣全部用SetTransform設(shè)置到矩陣寄存器中,但每個頂點在渲染時,最多使用其中的4個。由此可知,pMeshContainer->NumPaletteEntries這個數(shù)值,確定了一趟DrawSubset繪制所用到的矩陣個數(shù),個數(shù)越多,在一趟繪制中就可以納入的更多頂點。所以,當(dāng)我們減少pMeshContainer->NumPaletteEntries這個數(shù)值時,pMeshContainer->NumAttributeGroups數(shù)值就會增加。也就是說,一趟繪制中所允許涉及的骨骼數(shù)越少,那么子集的數(shù)量NumAttributeGroups就會增加,需要多繪幾趟。
你可以在此下斷點觀察,
當(dāng)NumPaletteEntries=19時,NumAttributeGroups=3
當(dāng)NumPaletteEntries=6時,NumAttributeGroups=12
當(dāng)NumPaletteEntries=4時,NumAttributeGroups=31,
幾乎和無索引時的分組一樣多了。
頂點中的權(quán)重weight存放了它當(dāng)前骨骼的權(quán)重。(一個頂點對應(yīng)的多個骨骼權(quán)重怎么存放?是不是在當(dāng)前子集中有多個同樣的頂點,權(quán)重不同,對應(yīng)的矩陣索引不同,然后混合)
由上所述,ConvertToIndexedBlendedMesh()是一個很重要函數(shù),由DX自動將Mesh頂點分組成多個子集,以便DrawSubset. 你必須把它的返回參數(shù)都記錄下來,在繪制時使用。
四。 怎樣繪制顯示動畫?
DrawFrame()用來繪制整個X框架。它遍歷各個框架,找到Mesh不為空的進(jìn)行繪制。(其實整個。x中通常只有一個不為空,見上文所述)
DrawMeshContainer()是繪制函數(shù)。
4.1 怎樣開啟頂點混合?
注意應(yīng)用有關(guān)的Vertex Blending技術(shù)。如在索引方式的繪制中,
m_pd3dDevice->SetRenderState(D3DRS_VERTEXBLEND, pMeshContainer->NumInfl - 1);
其實是設(shè)定了 D3DVBF_2WEIGHTS或 D3DVBF_3WEIGHTS 注意要
m_pd3dDevice->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE,TRUE);
4.2 矩陣的刷新:
首先,在FrameMove()調(diào)用m_pAnimController->SetTime()設(shè)置當(dāng)前時間(或在DX9.0c中用AdvanceTime()設(shè)置時間差),從而刷新各個pFrame->TransformationMatrix,即骨骼轉(zhuǎn)換矩陣其次,調(diào)用UpdateFrameMatrices()做乘法累積,計算出各骨骼坐標(biāo)系到根世界轉(zhuǎn)換矩陣。
最后,在繪制前,將該轉(zhuǎn)換矩陣左乘偏移矩陣,得到最終的轉(zhuǎn)換矩陣。
D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );
由此可見,你如果注釋掉了m_pAnimController->SetTime,畫面肯定停了。
4.3 繪制輸出 是在DrawMeshContainer()中,調(diào)用SkinMesh的DrawSubset進(jìn)行繪制。一些細(xì)節(jié)內(nèi)容如D3DTS_WORLDMATRIX(),在上面已經(jīng)有說明,不再羅嗦。
4.4 關(guān)于示例中多種繪制方式分析在示例中,用到了多種渲染方式,包括傳統(tǒng)的非索引頂點混合,還有新興的HLSL方式。而且我發(fā)現(xiàn),ATI RADEON 9550 顯卡MaxVertexBlendMatrixIndex=37,而價格更高的Gefoce 6600GT MaxVertexBlendMatrixIndex竟然為0,不支持index vertex blending!
所以,還是有必要分析一下該示例中各種vertex blending方式的處理,以便掌握多種繪制方式適應(yīng)不同顯卡。
經(jīng)測試,示例中所涉及的多種方式,由慢到快,依次是以下幾種:SOFTWARE,D3DNONINDEXED,D3DINDEXED,D3DINDEXEDVS,D3DINDEXEDHLSLVS,從最慢的SW到最快的HLSL,大約相差20%,有時會大到40%. 差別不是特別懸殊的原因,主要是頂點混合并不是瓶頸。
關(guān)于頂點處理方式,是在創(chuàng)建D3D設(shè)備時指定的。共有三種方式:
D3DCREATE_SOFTWARE_VERTEXPROCESSING 軟件頂點運算 (簡記 sw vp)
D3DCREATE_HARDWARE_VERTEXPROCESSING 硬件頂點運算。必須有這項才支持有HAL (簡記 hw vp)
D3DCREATE_MIXED_VERTEXPROCESSING 混合頂點運算,即硬件+軟件 (簡記 mixed vp)
一旦用D3DCREATE_HARDWARE_VERTEXPROCESSING方式創(chuàng)建設(shè)備,就只能在硬件方式下進(jìn)行頂點處理。如果調(diào)用m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)來切換到軟件頂點處理,HRESULT會返回失敗。
所以,如果你對客戶的顯卡沒有足夠的信息,就用D3DCREATE_MIXED_VERTEXPROCESSING方式創(chuàng)建設(shè)備。它默認(rèn)工作方式是HAL.一旦發(fā)現(xiàn)進(jìn)行某種繪制時硬件能力不夠,就可以調(diào)用調(diào)用m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)切換到軟件模式。在示例中就是這么做的,啟動示例后,運行在mixed模式下。
在Gefoce6600GT顯卡中,由于D3DINDEXED方式不支持,采用了軟件混合方式,在這種方式下速度甚至比SOFTWARE慢。HLSL還好,還是最快。
要確定設(shè)備的硬件頂點處理能力,可以參考D3DCAPS9結(jié)構(gòu)的VertexProcessingCaps成員。可以獲取下列屬性MaxActiveLights,MaxUserClipPlanes,MaxVertexBlendMatrices,MaxStreams,MaxVertexIndex(1)D3DNONINDEXED方式:首先看GenerateSkinnedMesh()中怎樣創(chuàng)建蒙皮網(wǎng)格的。
這種方式下,用ConvertToBlendedMesh()建立蒙皮網(wǎng)格,而不是ConvertToIndexBlendedMesh()
為了繪制蒙皮,在這個函數(shù)中對Mesh各子集的頂點再次進(jìn)行的分組。分組的標(biāo)準(zhǔn)是各頂點(或三角面)所涉及的骨骼矩陣個數(shù)不超過pMeshContainer->NumInfl個。(這個數(shù)字是由在ConvertToBlendedMesh()時,由參數(shù)pMaxFaceInfl返回的)。一個Mesh子集可能被拆開成多個分組。 最后,分組的屬性保存在pBoneCombinationBuf中,如子集ID,該子集的各骨骼ID,起始三角面,三角面?zhèn)€數(shù)等供繪制時使用,分組的個數(shù)保存在pMeshContainer->NumAttributeGroups中。
接下來檢查每個分組所涉及的骨骼數(shù),是不是超過硬件允許的最大混合矩陣數(shù)——MaxVertexBlendMatrices.如果超過了就把所有分組截為兩大部分,前一部分用硬件混合,后一部分采用軟件混合。而且,一旦發(fā)現(xiàn)有需要軟件混合,要采用CloneMeshFVF(D3DXMESH_SOFTWAREPROCESSING|……)的方式重新生成網(wǎng)格。
再來看繪制部分DrawMeshContainer()
用pBoneComb指向骨骼分組屬性,掃描各分組。找出其中骨骼數(shù)滿足硬件性能的用進(jìn)行繪制。
然后開啟軟件頂點渲染m_pd3dDevice->SetSoftwareVertexProcessing(TRUE),對那些骨骼數(shù)超出硬件性能的進(jìn)行繪制。
SetSoftwareVertexProcessing()需要當(dāng)前d3d設(shè)備以D3DCREATE_MIXED_VERTEXPROCESSING方式創(chuàng)建。
(2)D3DINDEXED,這種方式上面分析過了,從略。用pMeshContainer->UseSoftwareVP表示是否采用軟件繪制。
值得注意的是在這種方式下,一旦硬件性能不足,會徹底使用軟件頂點渲染,而不是像上面一樣拆為兩部分。
(3)D3DINDEXEDVS,D3DINDEXEDHLSLVS這種情況下使用了著色器和高級著色語言。超出本文主旨,討論從略。
(4)SOFTWARE——軟件方式? 讓人有些迷惑,與上面的m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)有何區(qū)別?
從代碼看,這種方式下反而比較簡單。GenerateSkinnedMesh()中,先直接從原始Mesh克隆一個Mesh,然后讀取它的材質(zhì)屬性數(shù)組。開辟一個空間m_pBoneMatrices,用以存放各塊骨骼的轉(zhuǎn)換矩陣。
在繪制時,從pMeshContainer中的變換矩陣乘以偏移矩陣,放在pBoneMatrices中。把這個矩陣數(shù)組,以原Mesh的頂點作為源頂點,以新克隆的MeshData.pMesh做為目標(biāo)頂點,調(diào)用pSkinInfo->UpdateSkinnedMesh(),用軟件方式計算各骨骼頂點的新位置(相當(dāng)于軟件計算方式蒙皮)。
然后調(diào)用MeshData.pMesh->DrawSubset()繪制。
可見,在SOFTWARE方式下,最終頂點的渲染還是HAL方式的,只不過蒙皮計算是由軟件完成的。它和上面的m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)直接設(shè)置軟件頂點渲染還是有區(qū)別的。