DirectX里的Skin Mesh角色动画

DirectX里的Skin Mesh角色动画

    在写我们的skin mesh代码之前了解DX怎样处理skin mesh是很重要的.当然,现在有很多可以存skin meshes的文件格式,但是现在最容易使用的是在DirecdtX中有支持的X文件.X文件可以存储正常的静态的meshes,也同样支持skin meshes.在开始讲skin meshes 之前,让我们对X文件有个大概的了解.我将大体上讲述一下X文件,要想知道更多X文件的东西,你可以参阅dx文档.X文件是把数据当作一系列的模板来存储 的.正C语言里面的结构,模板有事先的定义,这个定义决定数据是如何存储在这个模板的一个实例中的.对每一个模板,你可以有一个或多个实例.有很多类型的 模板,每一种都应该只存储一种特定类型的数据.模板可以有子模板,这让我们可以使我们得以实现层次场景.模板的实例要有名字, 尽管这不是必需的.我们不需要直接去处理各种各样的模板,因为DX会为我们处理这些事情,但我们将首先对这些模板作些了解,然后才开始实现代码.
  
  Frame
  这个模板用来存储一个帧.帧是层次场景的构造元素.帧有它们自己的转换矩阵,还可以有自己的子对象(child objects).帧也可以有子帧.在skin meshes里面,骨块(bone)指的是一个帧.
  
  FrameTransformMatrix
  正所谓人如其名,这个变换矩阵是帧的变换.它在Frame模板里有实例.
  
  Mesh
   这个模板存储一个表态的mesh和mesh的材质.在skin meshes里,整个角色将只是一个mesh,由皮肤信息(skinning information)确定mesh中的每一个部分是如何受到骨骼(bone)的影响的.mesh在内部会分成几个子集;每一个子集将受到一个特定的骨 骼的集合的影响.
  
  XskinMeshHeader
  这个模板存储随mesh一样导出的(exported)关于皮肤信息(skinning information)的属性(nature).这个模板包含在mesh模板里面.
  
  SkinWeights
   真正的皮肤信息(skinning information)就存储在这里.这个模板定义了一个特定的骨骼能够去影响一个mesh.这个模板在每一个影响到mesh的骨骼(bone)里面都 有实例,例如,有12个骨骼(bone)影响到mesh,Mesh模板里将有12个SkinWeights模板的实例.
  
  Skin mesh和 static mesh(静态)的唯一不同点就是XskinMeshHeader和SkinWeights模板是否存在.如果把这两个模板从任何一个skin mesh里面移走的话,就可以得到一个静态的mesh(static mesh).
  
  我们稍候将处理的模板会包含动画信息(animation data).文章的后面将会提及这些模板.
  
   现在,有好消息也有坏消息.好消息就是DX将会处理加载mesh时需要做的所有工作(包括材质和皮肤信息).我们需要将skin mesh连到骨骼上.一般地,X文件会有帧的层次和一个或多个的Mesh包含皮肤信息(skinning information)的模板.我们需要一个个地加载这些模板然后手动把mesh同它的骨骼(即帧,如前所述,每 一个骨骼就是一个帧—译者注)连接起来.建造带有skin mesh的X文件是建模师的工作.用任何一个商业软件做好角色后,建模师用特定的插件可以很容易地把模型导出为X文件.在微软站点上面,你可以找到3D Max和Maya的插件,用鼠标几个点击就可以把模型导出为X文件.你甚至可以找到支持X文件的建模型软件.
  
  现在我们已经知道在 X文件里面数据是怎样组织的了.为了加载数据,我们需要用DX自带的X文件的代码库.IDirectXFile是这个库的主接口. IDirectXFile有一个创建IDirectXFileEnumObject的方法.我们用IDirectXFileEnumObject接口的方 法从一个特定的X文件里面获取数据.IDirectXFileEnumObject::GetNextDataObject将会历遍X文件中的所有的高层 模板(top-level template),然后返回一个IDirectXFileData接口.IDirectXFileData被用来在X文件中获取单个模型中的数据.类似 IDirectXFileEnumObject::GetNextDataObect,IDirectXFileData::GetNextObject 历遍所以的子模板然后返回一个IDirectXFileData接口.方法IDirectXFileData::GetData被用于获取来自模板的数 据,但在我们可以获取数据之前,我们需要知道模板的类型.方法IDirectXFileData::GetID返回模板的GUID(global unique identifier).例如,如果模板是Frame模板,GetID将会返回TID_D3DRMFrame,这在DX的头文件里有定义.一旦你需要模板 实例的名字,GetName可以做到这一点.
  
  在我们了解X文件过程中,我们将会发现一个GUID为TID_D3DRMMesh的 模板,这意味着模板里面存有一个mesh.是时候DX给我们些协助了.函数D3DXLoadSkinMeshFromXof将会加载skin mesh和所有其它的补充性数据.只需要向它传递一个IDirectXFileData的指针,然后它将为做剩下的事情.
  
   D3DXLoadSkinMeshFromXof返回给我们一个指向ID3DXSkinMesh对象(object)的指针.这个对象含有skin mesh.在内部这个对象把mesh数据分组存储.每一组都由不同的骨骼进行转换(transform).这个函数也返还一个被skin mesh使用的材质数据.正如我前面所提到的,需要我们自己把skin mesh和骨骼连接起来.D3DXLoadSkinMeshFromXof返回一个缓冲区.这个缓冲区包含所有影响到这个mesh的骨骼的名字.这个方法 还返回另一个包含它们的变换的缓冲区.我们用名字来为特定的骨骼历遍所以的帧层次.骨骼转换有点令人混淆.转换应该被包含在帧里面而不是这里.实际上,这 个转换是骨骼的偏移量.但是,什么是骨骼偏移量?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的同一个对象,从ConvertToBlendedMesh函数转换得来的ID3DXMesh对象和前者有不同点,这就是它 的顶点包含blending weights,所以我们需要做的所有的事情就是打开vertex blending然后在调用DrawSubset之后设置骨骼矩阵.正如前面所讲到的,mesh将会被分成几组子集(subset).每一个子集应该用特 定的材质和特定的骨骼集合去渲染.结构D3DXBONECOMBINATION为每一个mesh子集指定材质.这个结构的一个队列(数组)也由 ConvertToBlendedMesh函数中得到.我们需要做的是历遍这个数组,设置材质和骨骼,然后把材质的索引传给ID3DXMesh的 DrawSubset进行渲染.

 实现

  现在我们已经准备好去实现我们的skin mesh了.实现代码的最为重要的部分就是设计.下面这个图展示了我们的编码的设计:

  

  这个图没有显示出所有的类成员.它只显示出那些重要的成员.正如图表里所示的,类CMeshNode和CFrameNode都派生自CObject.

  

  CObject的目的在于提供一个树的机制;任一个派生自CObject的对象都有链成 一棵树的能力.CFrameNode是场景层次的构造元素而CMeshNode本身则拥有mesh.CMeshNode被包含在CFrameNode里 面,而CFrameNode又被包含在CSkinMesh里面.整个场景由CSkinMesh开始,因为它拥有根帧(root frame).所有与skin mesh相关的操作都将在CSkinMesh里面初始化.相应地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 FrameTransformMatrix

    Load the transform matrix

  Else if type is Frame

  Begin

    Create the 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 disabled

  End

X文件里的动画不是skin mesh仅有的;它们对X文件里的任何的帧都是适用的.X文件存储了关键帧而程序则应用线性插值的方法产生中间帧.有4种动画关键帧:转动,缩放,位置, 矩阵.转动帧被作为一个四元组存储,使用spherical linear interpolation(这应该怎么译?译者也不清楚).函数D3DXQuaternionSlerp可以实现这种插值.下面的X文件模板被用于存储 动画.

  

  AnimationKey

  这个模板用于存储动画关键帧码.这个模板的每一个实例都包含帧码的类型(位置,缩放,转动,矩阵)和帧码数组.数组中的每一个元素包含帧码的值和一个DWORD型的值指出帧的持续时间.

  

  Animation

  这个模板存储特定的动画帧码,它应该至少包含一个AnimationKey模板,也应该有一个指向目标帧的指针.

  

  AnimationSet

  用于作为Animation模板的一个容器.在这个集合里面的Animation模板有同样的持续时间值.

  

  实现动画

  为了实现skin mesh,我们将需要增加一个新的类.我们把这个类命名为CAnimationNode.这个类拥有帧码和指向目标帧的指针.这个类也同样包含一个 SetTime函数.这个函数会用从AnimationKey得到的的新的时间值更新目标帧的变换矩阵.CAnimationNode的每 一个实例将拥有Animation模板的一个实例.下面这个图显示出代码的新的设计:

  

  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加速卡,以及你的系统支持blending weights.对于一个更为复杂的示例,你应该参考DX8 SDK.SDK上面有很多indexed和non-indexed的顶点混合的检测和实现.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值