在写我们的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进行渲染.