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对象的个数可能多于骨骼数。
|