原文鏈接:
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)直接設置軟件頂點渲染還是有區別的。