用DX8實現Skin Meshes(翻譯版)

源文件地址
http://www.gamedev.net/reference/articles/article1835.asp

以下爲翻譯:
Implementing Skin Meshes with DirectX 8
by Sarmad Kh Abdulla

  Skin meshes或者骨骼meshes在3D世界裏是大多數重要課題之一。骨骼動畫總是受注視爲重要的角色讓他們有組成織的活動。骨骼動畫是建立在對象的外形是由多個骨架並且能由改變骨架的位置和方向作簡單的活動思想基礎上的。你能找到許多討論這個課題的理論資源,但這個文章討論用D3D8與D3DX來實現骨骼meshes。但在我開始討論實現之前,我在後面的skin meshes在數學模塊上來一個快速的回顧。

Skin Meshes的概述
  Skin meshes是分層場景(hierarchical scene)的一種類型。分層場景(hierarchical scene)被用於對象的相互連接。例如,手指屬於手掌,手掌屬於前臂等等。每個對象座標的給予是相對於屬於對象的局部空間,因此,旋轉前臂將也引起手掌和手指的移動和旋轉。下列插圖顯示如何人體使用一個分層場景如何構造。

  

  用數學公式能非常簡單的表示這個場景;矩陣代數是關鍵。給予場景,如果我們考慮前臂矩陣到前臂的變換矩陣,手掌矩陣到手掌的變換矩陣與手指矩陣到手指的變換矩陣,然後我們能簡單的計算手指相對於世界的變換矩陣,由下列公式:

  FingerWorldMat = FingerMat * PalmMat * ForearmMat * ... * BodyMat

  在上面的公式裏,我們考慮了分層場景的基礎是身體,因此,它的矩陣是相對於世界空間。有我們的mesh拆分進入局部在那裏每個部分是被一個特定骨架的世界矩陣變換了的,將使我們能夠容易的活動mesh。

  然而,有的人物是由分開的固定子對象組成現在不接受這種做法,因爲在關節處它會引起裂縫。今後上升到skin meshes。Skin meshes克服了這個問題由人物根據位置和骨架的方位改變較小的部分的已有的外形。Skin meshes同樣有骨架體系,但一個skin mesh的單個部分能被不止一個骨架變換。下列一個線性插值公式,頂點能有它們的位置被兩個(或更多)骨架代替一個骨架影響。下列方法,我們能克服裂縫的問題。這個技術我們叫作頂點混合(Vertex Blending)。頂點混合要求每個頂點有一個混合權以便頂點描影能確定骨架如何影響頂點。例如,如果我們有一個頂點被兩個骨架影響,下列公式將給予頂點的世界座標作爲被兩個骨架影響:

  Vw = Vm * M1 * w + Vm * M2 * (1-w)

  在上面Vw是頂點在世界座標裏的位置。Vm是頂點相對於模型的局部空間位置。M1和M2是兩個骨架的變換矩陣。w是混合權。注意混合權要求骨架數字小於1。

  你能參考DX8文檔看關於頂點混合更詳細的信息。

Skin Meshes and DirectX 8
  重要的是在開始實現我們的skin mesh代碼以前應該知道DX如何處理skin mesh。當然,有許多文件格式能存儲skin mesh但在此時最容易的一個是X文件。X文件能存儲標準的靜態meshes以外還能存儲skin meshes。在我們討論skin meshes以前,讓我們對X文件有一個一般概念。我將討論X文件的通用設計並且你能參考DX文檔獲得更詳細的信息。X文件存儲數據作爲一套模版。象C語音的結構一樣,模版有定義決定數據將如何存儲在模版的實例裏。模版的每個類型,你能有一個或更多的實例。有模版的許多類型,每一個想象成存儲一個專用的數據類型。模版能有子類模版,使我們能夠構造分層場景。雖然它不是強制性的,模版的實例能有名字。我們不需要用模版的所有類型直接處理,DX將爲我們處理大多數工作,但在我們能開始工作以前我們需要有一些關於下列模版的概念。

l Frame
模版用於存儲一個框架。框架是建造分層場景的成份。框架有它們的自己的變換矩陣並且它們能包含子類對象。框架也能包含子類框架。在skin mesh裏,一個骨架參照一個框架。

l FrameTransformationMatrix
與名字暗示一樣,這個是爲框架的變換矩陣。它在框架模版內被用於實例的說明。

l Mesh
模版存儲單個靜態mesh和它的材質一起。在skin mesh裏,整個人物將用一個mesh並且s皮膚信息規定mesh的每個部分如何被骨架影響。Mesh將在內部的分裂進入子對象;每個子對象將被一個骨架的特定設置影響。

l XSkinMeshHeader
這個模版存儲關於皮膚的自然信息,信息用mesh輸出。這個模版包含在Mesh模版內。

l SkinWeights
真實的皮膚信息存儲在這裏。模版定義一個特定的骨架如何能影響mesh。也就是,這個模版爲影響mesh的每個骨架具體說明一次。如果有十二個骨架影響mesh,mesh模版將有十二個SkinWeights模版在內部說明。

  在skin mesh與靜態mesh之間的差別只是XSkinMeshHeader和SkinWeights模版的存在。從任何skin mesh的mesh模版移去這兩個模版就把它變成了一個靜態mesh。

  其它的模版我們將也處理活動數據。我將在文章後面提涉它們。

  有好消息也有壞消息。好消息是DX將處理所有的工作讀取mesh需要它的材質和皮膚信息。壞消息是我們必須做餘下的。我們將需要讀取框架和構造分層場景。我們將也需要連接skin mesh到骨架。通常,X文件將包含一個框架分層結構並且包含一個(或更多)與皮膚信息一起的mesh模版。我們將獨立的讀取每個框架分層結構與mesh模版,然後手動連接mesh到它的骨架(框架)。用一個skin mesh建立一個X文件是製作模型者的任務。在用任何商業軟件製作人物後,製作模型者能容易使用爲這個目的設計特殊的插件導出它的模型到一個X文件。在Microsoft的網址上,你能找到爲3DMAX和MAYA導出爲X文件的插件。你甚至能找到支持建立X文件的應用程序。

  現在我們知道數據在X文件裏是如何組織的。爲了讀取數據,我們需要使用X文件庫。IDirectXFile是庫的主要接口。IDirectXFile有一個方法爲創建IDirectXFileEnumObject。IDirectXFileEnumObject方法是被用於從一個指定的X文件裏檢索數據。IDirectXFileEnumObject::GetNextDataObject將循環通過在X文件裏所有的頂層模版並且返回一個IDirectXFileData接口。稍後在X文件內部被用於檢索單個模版的數據。相似於IDirectXFileEnumObject::GetNextDataObject,方法IDirectXFileData::GetNextObject循環通過所有的子類模版並且返回一個IDirectXFileData接口。方法IDirectXFileData::GetData被用於從模版裏檢索數據,但在我們能檢索數據以前,我們需要知道模版的類型。方法IDirectXFileData::GetID返回模版的GUID。例如,如果模版是一個框架模版,GetID將返回TID_D3DRMFrame,它被預先定義在DX的頭文件裏。假如你需要模版的實例的名字(象是用框架的情況一樣),方法GetName把名字將給你。

  在我們進行通過X文件模版期間,我們將尋找一個模版與一個GUID等於TID_D3DRMMesh,它意味着它包含一個mesh。現在是DX給我們一些幫助的時候了。函數D3DXLoadSkinMeshFromXof將讀取skin mesh與所有的餘下的數據。僅給它一個指向IDirectXFileData接口的指針並且它將做餘下的工作。

  函數D3DXLoadSkinMeshFromXof將給予我們一個指向ID3DXSkinMesh對象的指針。這個對象包含skin mesh。在內部,這個對象包含mesh數據作爲組(group)。每個組(group)被一個不同骨架的設置變換。函數將也返回一個材質數據由skin mesh使用。與我先前提到的一樣,我們的工作是連接skin mesh到骨架。D3DXLoadSkinMeshFromXof給予一個緩衝區包含影響mesh的所有骨架的名字。它也給予其它緩衝區包含它們的變換。我們將使用名字爲指定的骨架查遍我們的框架分層結構。骨架變換有點讓人混亂。假定變換不在這裏被包含在框架裏。事實上,這個變換是骨架偏移(bone offset)。那麼骨架偏移又是什麼呢?重要的知道skin mesh的所有頂點是被存儲在相對於一個原點,它是mesh的原點並不是骨架的局部原點。這意味着在mesh上爲了有骨架的影響,我們應該使mesh變形由位於骨架的當前變換與骨架的原點變換之間的差別。或者換句話說,我們應該變換頂點到骨架的局部空間,然後變換它們回到mesh的空間使用新的骨架的變換。爲了我們有一個更清楚的概念,讓我們示範舉例。讓我們說說我們有一個骨架被放置在(0,50,0)與一個頂點被放置在(0,51,0)並且讓我們假定這個頂點是隻由一個骨架影響。如果我們移動骨架從它的原始位置到這個新的位置(0,51,0),頂點應該被移動到位置(0,52,0),但如果我們簡單的由骨架的變換乘頂點,頂點將有新的位置等於(0,102,0)它是錯誤的座標。因此,我們使用骨架的偏移矩陣到變換頂點從它的原始位置到一個相對於骨架的位置。新的位置將是(0,1,0)它將是被骨架的當前矩陣到新的位置變換,它是(0,52,0)。簡單步驟如下:當你使用一個骨架,由偏移矩陣乘以它的當前變換矩陣並且使用結果作爲世界矩陣。

  讓我們回到我們的ID3DXSkinMesh對象。這個對象在它的原型裏包含skin mesh。這個對象爲渲染skin mesh沒有任何機能。因此,我們需要首先轉換mesh進入一個ID3DXMesh對象。函數ConvertToBlendedMesh將做這工作。雖然它是相同的對象被用於渲染靜態mesh,ID3DXMesh獲得從ConvertToBlendedMesh裏有區別它的頂點包含混合權,因此我們所需要做的是啓用頂點混合並且在調用ID3DXMesh的DrawSubset方法以前設置我們的骨架矩陣。象前面提到的一樣,mesh將被分裂進入組或子對象。每個子對象應該與一個指定的材質和一個指定的骨架設置一起被渲染。結構D3DXBONECOMBINATION指定材質和骨架被用於一個mesh的單個子對象。結構的數組也從ConvertToBlendedMesh函數裏獲得。我們所需要做的是循環通過這個數組,設置材質與骨架然後調用ID3DXMesh的DrawSubset方法給予它在數據裏的下標。

實現
  現在我們準備開始寫實現我們的skin mesh代碼了。多數重要的部分設計如下,下列插圖顯示我們代碼的設計:

  

  插圖不顯示所有類的成員,它只顯示重要的東西。與在插圖裏顯示的一樣,類CMeshNode和CFrameNode兩者從CObject裏劃分出來的。CObject的目的是提供連接三個機構;任何對象是從CObject裏劃分出來的將有能力連接進入一個連接樹。CFrameNode是建立我們的場景分層結構的元素並且CmeshNode包含它自己的mesh。CMeshNode是包含在CFrameNode內部,CFrameNode包含在CSkinMesh內部。整個場景開始在CSkinMesh裏因爲它包含基礎的框架。所有有關操作skin mesh將在CSkinMesh類裏被初始化它依次將傳遞控制到分層結構作爲必要的,因此,主程序將只處理CSkinMesh;CFrameNode和CMeshNode將只由CSkinMesh聯繫。

下列算法顯示場景如何從X文件裏建立:
CSkinMesh::Create()
Begin
    Initialize X file API
    Register D3DRM templates
    Open the X file
    For every top level template in the X file
    Begin
        Retrieve the X file data object
        Pass the data object to RootFrame.Load
    End
    Link the bones to the skin mesh(es)
End

CFrameNode::Load()
Begin
    Check the type of the data object
    If the type is Mesh
    Begin
        Create new CMeshNode object
        Attach the new object to the frame
        Pass the data object to CMeshNode::Create of the new mesh
    End
    Else if type is FrameTransformationMatrix
        Load the transformation matrix
    Else if type is Frame
    Begin
        Create new CFrameNode object
        Attach the new object to this frame
        Set the name of the child frame to the name of the template
        For every child template of the current
        Begin
            Retrieve the X file data object
            Pass it to newframe.Load
        End
    End
End

CMeshNode::Create()
Begin
    Set the name of the object to the name of the template
    Load the skin mesh
    Generate blended mesh from this skin mesh object
    Load materials
End

在建立skin mesh後,我們能開始渲染它了。渲染操作將包含兩個階段。在第一個階段期間,所有骨架的世界被計算(通過矩陣增值)並且被存儲在CMeshNode對象裏。在第二個階段裏期間,skin mesh將被渲染。下列算法顯示這個操作:
CSkinMesh::Render()
Begin
    Calculate the world matrix of all the frames
    Call CMeshNode::Render of all mesh nodes in the hierarchy
End

CMeshNode::Render
Begin
Enable vertex blending
For every subset in the skin mesh
Begin
Set the bones' transformation matrices to the device
Set the material
Render
End
Set vertex blending back to disabled
End


在X文件裏的動畫
  動畫在X文件裏不是爲skin mesh特定使用的;它們能被應用於在X文件裏的任何框架。X文件存儲關鍵幀框架並且應用程序應該產生在框架使用線性插值中間。動畫關鍵幀有四種類型,旋轉,縮放,位置,和矩陣關鍵幀。旋轉是被存儲作爲四元法(quaternions)並且能被球狀線性插值(spherical linear interpolation)進行插值運算。函數D3DXQuaternionSlerp由D3DX提供實現球狀線性插值。下面的X文件模版是被用於存儲動畫:

l AnimationKey
這個模版被用於存儲動畫關鍵幀。模版的每個實例包含關鍵幀(位置,縮放,旋轉,或矩陣)的類型與關鍵幀的數組。每個元素在數組裏包含關鍵幀的值和DWORD值指定的時間。

l Animation
這個模版存儲指定框架的動畫關鍵幀。它應該至少包含一個AnimationKey模版。它也應該包含一個標準的目標框架。

l AnimationSet
行動爲動畫模版作爲一個容器。動畫模版包含在集合裏有相同的時間值。


實現動畫
  爲了在我們的skin mesh裏實現動畫,我們需要添加一個新的類。我們把這個類命名爲CAnimationNode。這個類將包含動畫關鍵幀和一個指向目標框架的指針一起。這個類也包含一個用從動畫關鍵幀裏在新的時間值上獲得的矩陣來更新目標框架的變換矩陣的SetTime函數。CAnimationNode的每個實例將包含動畫模版的單個實例的數據。下列插圖顯示爲我們的代碼新的設計方向:

  

考慮到要與動畫一起,讀取代碼將稍微有些修改。下載是先前的讀取代碼在要求下做的相應的修改:
CSkinMesh::Create()
Begin
    Initialize X file API
    Register D3DRM templates
    Open the X file
    For every top level template in the X file
    Begin
        Retrieve the X file data object
        Pass the data object to RootFrame.Load
    End
    Link the bones to the skin mesh(es)
    Link the bones to the animations
End

CFrameNode::Load()
Begin
    Check the type of the data object
    If the type is Mesh
    Begin
        Create new CMeshNode object
        Attach the new object to the frame
        Pass the data object to CMeshNode::Create of the new mesh
    End
    Else if type is FrameTransformationMatrix
        Load the transformation matrix
    Else if type is Frame
    Else if type is Animation
        Instruct CSkinMesh to load the new animation
        Begin
            Create new CFrameNode object
            Attach the new object to this frame
            Set the name of the child frame to the name of the template
            For every child template of the current
            Begin
                Retrieve the X file data object
                Pass it to newframe.Load
            End
        End
End

CSkinMesh::LoadAnimation()
Begin
    Create new CAnimationNode object
    Attach the new object to the link list
    For every child template
    Call CAnimationNode::Load for the new animation object
End

CAnimationNode::Load()
Begin
    Check the type of the data object
    If the type is a reference
    Begin
        Get the referenced template, which is a frame template
        Get the name of it
        Store the name
    End
    Else if type is data
    Begin
        Check the type of the animation key
        Load the key accordingly
    End
End

SetTime函數在那裏執行所有的動畫機能。CSkinMesh::SetTime簡單調用所有的動畫對象的SetTime函數。
CAnimationNode::SetTime()
Begin
    If a matrix key is available
    Begin
        Get the nearest matrix to the given time
        Set it to the target frame
    End
    Else
    Begin
        Prepare an identity matrix called TransMat
        If a scale key is available
        Begin
            Calculate the accurate scale value
            Prepare a scale matrix for this scale value
            Append the matrix to TransMat
        End
        If a rotation key is available
        Begin
            Calculate the accurate rotation quaternion
            Prepare a rotation matrix from this value
            Append the matrix to TransMat
        End
        If a position key is available
        Begin
            Calculate the accurate position value
            Prepare a matrix for it
            Append the matrix to TransMat
        End
    Set TransMat to the target frame
    End
End

  現在你瞭解有關skin mesh所有的東西了,是時候讓你嘗試了(
源代碼下載)。注意源代碼因需要清晰的緣故做了一些簡化。代碼假定你有一個3D加速器並且它假定你的系統支持必要的混合權。代碼使用非索引頂點緩衝區。爲一個更復雜的例子,你能參照DX8 SDK的例子,它執行許多檢查並且都實現索引與非索引頂點混合。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章