用D3D实现Skin Meshes

来源:互联网 发布:谁有淘宝刷手机单qq群 编辑:程序博客网 时间:2024/04/29 12:59
                                                                                                                                         作者:花不香
源文件地址
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的例子,它执行许多检查并且都实现索引与非索引顶点混合。 
原创粉丝点击