范统 发表于 2007-7-10 00:01:00

关于骨骼动画及微软示例Skinned Mesh的解析

这是<font color="#9c0000">zzzworm</font>写的.<p></p><p><font size="3"><b>关于骨骼动画及微软示例Skinned&nbsp;Mesh的解析</b></font></p><p>骨骼动画是D3D的一个重要应用。尽管微软DXSDK提供了示例Skinned&nbsp;Mesh,但由于涉及众多概念和技术细节,示例相对于初学者非常复杂,难以看懂。在此,提供一些重要问题评论,以使初学者走出迷局,顺利上手。文中所述都是参照各种资料加上自己的理解,也有可能出些偏差,有则回贴拍砖,无则权当一笑。</p><p><br/><b>一&nbsp;骨骼动画原理</b><br/>原理方面在网上资料比较多,大家都基本明白。在此说一下重点:<br/>总体上,绝大部分动画实现原理一致,就是“提供一种机制,描述各顶点位置随时间的变化”。有三种方法:<br/><font color="#0000ff">1.1&nbsp;关节动画:</font>由于大部分运动,都是皮肤随骨骼在动,皮肤相对于它的骨骼本身并没有发生运动,所以只要描述清楚骨骼的运动就行了。用矩阵描述各个骨骼的相对于父骨骼运动。(大多运动都是旋转型)&nbsp;易知,从子骨骼用矩阵乘法累积到最顶层根骨骼,就可以得到每个子骨骼相对于世界坐标系的转换矩阵。<br/>&nbsp;&nbsp;这种动画,只须用普通Mesh保存最初始的各顶点坐标,以及一系列后续时刻所对应的各骨骼的运动矩阵。不用保存每时刻的顶点数据,节省了大量存储空间。而且比较灵活,可以利用关键帧插值运算,便于通过运算调节动作。缺点是在两段骨骼交接处,容易产生裂缝,影响效果。</p><p><font color="#0000ff">1.2&nbsp;渐变动画:</font>通过保存一系列时刻的顶点坐标来完成动画。虽然比较逼真,但占用大量空间,灵活性也不高。</p><p><font color="#0000ff">1.3&nbsp;骨骼蒙皮动画(skinned&nbsp;Mesh)</font><br/>&nbsp;&nbsp;相当于上面两方法的折中。现在比较流行。<br/>&nbsp;&nbsp;在关节动画的基础上,利用顶点混合(Vertex&nbsp;Blend)技术,对于关节附近的顶点,由影响这些顶点的两段(或多段)骨骼运动,分别赋以权值,共同决定顶点位置。相当于在骨骼关节上动态蒙皮,有效解决了裂缝问题。</p><p>&nbsp;&nbsp;这里,引入一个D3D技术概念:<font color="#ff0000">“Vertex&nbsp;Blending”</font>---顶点混合技术。比如说,你肯定用过SetTransform(D3DTS_WORLD,....),但SetTransform(<font color="#800080">D3DTS_WORLDMATRIX(i)</font>,....)是不是很奇怪?这个问题后文会讲到。&nbsp;你也可以在微软的DXSDK的帮助文件中搜索“Geometry&nbsp;Blending”主题,有裂缝及其解决办法图示。</p><p></p><p><b>二&nbsp;X文件如何保存骨骼动画</b></p><p>理解X文件格式,对用好相关的DX函数是非常重要的。</p><p>不含动画的普通X文件,有一个Mesh单元,保存了各顶点信息、各三角面的索引信息、材质种类及定义等。</p><p>动画X文件,则在这个单元中增加了“各骨骼蒙皮信息”、“骨骼层次及结构信息”、“各时刻骨骼矩阵信息”等。</p><p><font color="#0000ff">2.1&nbsp;网格蒙皮信息:</font>首先,在Mesh{}单元中,在原有的普通网格顶点数据基础上,新增了XSkinMeshHeader{}结构,以及多个SkinWeights{}结构。用以描述各个骨骼的蒙皮信息。</p><p>其中,XSkinMeshHeader是总括,举一实例,如下:</p><p>XSkinMeshHeader<br/>{<br/>&nbsp;2,//一个顶点可以受到骨骼影响的最大骨骼数,可用于计算共同作用时减少遍历次数<br/>&nbsp;4,//一个三角面可以受到骨骼影响的最大骨骼数。这个数字对硬件顶点混合计算提出了基本要求。<br/>&nbsp;35&nbsp;//当前Mesh的骨骼总数。<br/>}</p><p>由于每个骨骼的蒙皮信息都需要用SkinWeights结构去描述,所以有多少块骨骼,在Mesh中就有多少个SkinWeights对象。<br/>注意,一般把SkinWeights视作Mesh的一部分。这种Mesh又称Skinned&nbsp;Mesh&nbsp;(蒙皮网格)</p><p>SkinWeights&nbsp;结构如下:<br/>{<br/>&nbsp;&nbsp;STRING&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;transformNodeName;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//骨骼名<br/>&nbsp;&nbsp;DWORD&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;nWeights;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//权重数组的元素个数,即该骨骼相关的顶点个数<br/>&nbsp;&nbsp;array&nbsp;DWORD&nbsp;vertexIndices;//受该骨骼控制的顶点索引,实际上定义了该骨骼的蒙皮<br/>&nbsp;&nbsp;array&nbsp;float&nbsp;weights;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//蒙皮各顶点的受本骨骼影响的权值<br/>&nbsp;&nbsp;Matrix4x4&nbsp;&nbsp;&nbsp;matrixOffset;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//骨骼偏移矩阵,用来从初始Mesh坐标,反向计算顶点在子骨骼坐标系中的初始坐标。<br/>}<br/>在有的书中,把上面的matrixOffset叫骨骼权重矩阵,是不恰当的。应该称为骨骼偏移矩阵比较合适。</p><p><font color="#ff0000">[问题]</font>&nbsp;在整个动画过程中,子骨骼运动矩阵的数值是不断变化的。上面的骨骼偏移矩阵变化吗?有没有必要重新计算?它在什么时候使用?<br/>答:各骨骼的偏移矩阵matrixOffset专门用来从原始Mesh数据计算出各顶点相对于骨骼坐标系的原始坐标。在绘制前,把它与当前变换矩阵相乘,就可以得到该骨骼的当前的最终变换矩阵。&nbsp;总之,骨骼偏移矩阵是与原始Mesh顶点数值相关联的,在整个动画过程中是不变的,也不应该变。在动画过程中变化是当前骨骼变换矩阵,可由.X中的AnimatonKey中的各时刻矩阵得到。这个矩阵乘法在示例中的对应代码如下:<br/>D3DXMatrixMultiply(&nbsp;&amp;matTemp,&nbsp;&amp;pMeshContainer-&gt;pBoneOffsetMatrices,&nbsp;pMeshContainer-&gt;ppBoneMatrixPtrs&nbsp;);</p><p>即,D3DXMatrixMultiply(输出最终世界矩阵,&nbsp;该骨骼的偏移矩阵,&nbsp;该骨骼的变换矩阵)</p><p><br/><font color="#0000ff">2.2&nbsp;骨骼层次信息</font></p><p>在X文件中,Frame是基本的组成单元。又称框架Frame。&nbsp;一个.x可以有多个Frame。(注意此处的Frame不是帧,与帧没什么关系)</p><p>框架Frame允许嵌套,这样就存在父子框架了。而并列的框架,称为兄弟框架。这两种关系组合在一起,即可以纵深,又可以并列,形成一种层次结构。这种结构,可用二叉树描述。</p><p>每个框架结构的最前面,有一个FrameTransformMatrix矩阵数据,描述了该框架相对于父框架的变换矩阵。也就是说,该框架中的坐标,与该矩阵相乘,可转换为父框架坐标系的坐标。<br/>这种层次结构,使得X文件能描述许多复杂的物体。如地形场景。</p><p>在骨骼动画文件中,框架结构可直接拿来描述人物骨骼的层次结构。框架的名字通常为对应的骨骼名。<br/>如“左上臂-&gt;左前臂-&gt;手掌-&gt;手指”就形成一个父子骨骼链。而左上臂与右上臂是并行关系。</p><p>数据示例:&nbsp;D:\D9XSDK\Samples\Media\tiny.x</p><p>Frame&nbsp;...{<br/>&nbsp;&nbsp;.....</p><p>&nbsp;&nbsp;Frame&nbsp;Bip01_R_Calf&nbsp;{&nbsp;//子骨骼<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FrameTransformMatrix&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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;;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}</p><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Frame&nbsp;Bip01_R_Foot&nbsp;{//--孙子骨骼<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FrameTransformMatrix&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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;;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}</p><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;....缩进<br/>&nbsp;&nbsp;&nbsp;&nbsp;}<br/>}<br/>&nbsp;</p><p><font color="#ff0000">[问题]</font>查看示例tiny.x文件,发现只有根框架下有一个Mesh,包含了所有顶点信息。其它各个Frame都没有Mesh数据。怎么理解?<br/>答:&nbsp;一般来说,每个动画文件只有一个Mesh网格,包含物体所有顶点信息。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;其它Frame,只是借用来描述各骨骼的层次信息,没必要再定义骨骼网格。每块骨骼对应的蒙皮顶点信息,由根Mesh中的相应骨骼的SkinWeights中蒙皮顶点索引描述的。在动画过程中,各个顶点的新坐标,要借助SkinWeights中的顶点索引来进行重新计算。</p><p><font color="#0000ff">2.3&nbsp;动画信息:</font><br/>由一系列AnimatonKey组成,数据示例如下:</p><p>&nbsp;&nbsp;AnimationKey&nbsp;{<br/>&nbsp;&nbsp;&nbsp;4;--动画类型&nbsp;4表示矩阵<br/>&nbsp;&nbsp;&nbsp;62;&nbsp;--动画帧数,即下面矩阵个数<br/>&nbsp;&nbsp;&nbsp;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;;,<br/>&nbsp;&nbsp;&nbsp;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;;,</p><p>&nbsp;&nbsp;&nbsp;..上面红数字表示时刻tick,兰数字表示数值的个数。<br/>&nbsp;&nbsp;&nbsp;...其它各时刻矩阵...</p><p>&nbsp;&nbsp;&nbsp;{&nbsp;Bip01_R_Calf&nbsp;}--对应的骨骼对象引用<br/>&nbsp;&nbsp;}</p><p><br/>注意:<br/>(1)每块骨骼都有一个AnimationKey{}.<br/>(2)在上面数据结构中,主要保存了各典型时刻的该骨骼相对于父的变换矩阵.<br/>(3)在0时刻的矩阵,与该骨骼对应的前面的Frame所对应的矩阵是相同的。如Frame&nbsp;Bip01_R_Calf{}中的变换矩阵,与Bip01_R_Calf所对应的AnimationKey&nbsp;的第0时刻矩阵是一样的。这说明,在以后动画运行时,DX会提供一种功能,用AnimatonKey中的对应数据刷新初始的变换矩阵(也可能启用关键帧插值算法)。这个功能对应于示例中的m_pAnimController-&gt;SetTime(...)语句。</p><p><b>三&nbsp;怎样从X文件加载骨骼动画信息?</b><br/><font color="#0000ff">3.1&nbsp;负责加载的函数:</font><br/>&nbsp;&nbsp;可能有多种加载方式,在此以SDK中的示例为准,叙述一种标准加载方式,需要用到DX函数D3DXLoadMeshHierarchyFromX(),函数字面意思是读取Mesh层次信息。<br/>HRESULT&nbsp;WINAPI&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;D3DXLoadMeshHierarchyFromX(<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPCSTR&nbsp;Filename,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//.x文件名<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DWORD&nbsp;MeshOptions,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//Mesh选项,一般选D3DXMESH_MANAGED<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPDIRECT3DDEVICE9&nbsp;pD3DDevice,&nbsp;&nbsp;&nbsp;&nbsp;//指向D3D设备Device<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXALLOCATEHIERARCHY&nbsp;pAlloc,&nbsp;&nbsp;//<b>自定义数据容器</b><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXLOADUSERDATA&nbsp;pUserDataLoader,&nbsp;&nbsp;//一般选NULL<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXFRAME&nbsp;*ppFrameHierarchy,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//返回根Frame指针,指向代表整个骨架的Frame层次结构<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXANIMATIONCONTROLLER&nbsp;*ppAnimController&nbsp;//返回相应的动画控制器<br/>);</p><p>这个函数后面的两个输出参数很重要,也很好理解,但输入参数中的自定义数据容器是怎么回事呢?<br/>原来,鉴于动画数据的复杂性,需要你配合完成加载过程。比如你是否用到自定义扩展结构,Mesh等数据保存在哪里,怎样使用户自己创建容器,自己决定卸载等等。&nbsp;<br/>DX提供了ID3DXALLOCATEHIERARCHY接口,提供了这个自定义的机会,你重载这个接口的虚函数,在加载过程中,它就像回调函数那样运作。</p><p>你需要像下面这样建立一个自定义数据容器类:<br/>class&nbsp;CAllocateHierarchy:&nbsp;public&nbsp;ID3DXAllocateHierarchy<br/>{<br/>public:<br/>&nbsp;&nbsp;&nbsp;&nbsp;STDMETHOD(CreateFrame)(THIS_&nbsp;LPCTSTR&nbsp;Name,&nbsp;LPD3DXFRAME&nbsp;*ppNewFrame);<br/>&nbsp;&nbsp;&nbsp;&nbsp;STDMETHOD(CreateMeshContainer)(THIS_&nbsp;LPCTSTR&nbsp;Name,&nbsp;LPD3DXMESHDATA&nbsp;pMeshData,<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXMATERIAL&nbsp;pMaterials,&nbsp;LPD3DXEFFECTINSTANCE&nbsp;pEffectInstances,&nbsp;DWORD&nbsp;NumMaterials,&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DWORD&nbsp;*pAdjacency,&nbsp;LPD3DXSKININFO&nbsp;pSkinInfo,&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXMESHCONTAINER&nbsp;*ppNewMeshContainer);<br/>&nbsp;&nbsp;&nbsp;&nbsp;STDMETHOD(DestroyFrame)(THIS_&nbsp;LPD3DXFRAME&nbsp;pFrameToFree);<br/>&nbsp;&nbsp;&nbsp;&nbsp;STDMETHOD(DestroyMeshContainer)(THIS_&nbsp;LPD3DXMESHCONTAINER&nbsp;pMeshContainerBase);<br/>&nbsp;&nbsp;&nbsp;&nbsp;CAllocateHierarchy(CMyD3DApplication&nbsp;*pApp)&nbsp;:m_pApp(pApp)&nbsp;{}<br/>public:<br/>&nbsp;&nbsp;&nbsp;&nbsp;CMyD3DApplication*&nbsp;m_pApp;<br/>};</p><p><font color="#ff0000">[问题]</font>上面的STDMETHOD是什么意思?<br/>答:相当于virtual&nbsp;&nbsp;&nbsp;HRESULT&nbsp;&nbsp;&nbsp;__stdcall&nbsp;的宏。&lt;评论&gt;&nbsp;因为这种类要与D3D的COM接口打交道,不仅仅在C++内部使用,所以,所有类方法必须做成stdcall的,可对外开放的。<br/>#define&nbsp;&nbsp;&nbsp;STDMETHOD(method)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;virtual&nbsp;&nbsp;&nbsp;HRESULT&nbsp;&nbsp;&nbsp;STDMETHODCALLTYPE&nbsp;&nbsp;&nbsp;method&nbsp;&nbsp;&nbsp;<br/>#define&nbsp;&nbsp;&nbsp;STDMETHODCALLTYPE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;__stdcall&nbsp;&nbsp;&nbsp;<br/>这样当写一个函数STDMETHOD(op1(int&nbsp;&nbsp;&nbsp;i))&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>展开后成为:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;virtual&nbsp;&nbsp;&nbsp;HRESULT&nbsp;&nbsp;&nbsp;__stdcall&nbsp;&nbsp;&nbsp;op1(int&nbsp;&nbsp;&nbsp;i);&nbsp;&nbsp;&nbsp;</p><p><font color="#0000ff">3.2&nbsp;自定义数据容器以及具体的读取过程:</font><br/>根据.X文件,在加载过程中,主要有两方面数据需要保存,一个是骨架Frame信息,一个是网格蒙皮Mesh信息。这两个信息保存在如下结构中。</p><p>框架信息(对应于骨骼)<br/>typedef&nbsp;struct&nbsp;_D3DXFRAME<br/>{<br/>&nbsp;&nbsp;&nbsp;&nbsp;LPSTR&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Name;<br/>&nbsp;&nbsp;&nbsp;&nbsp;D3DXMATRIX&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TransformationMatrix;&nbsp;//本骨骼的转换矩阵</p><p>&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXMESHCONTAINER&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//本骨骼所对应Mesh数据</p><p>&nbsp;&nbsp;&nbsp;&nbsp;struct&nbsp;_D3DXFRAME&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*pFrameSibling;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//兄弟骨骼<br/>&nbsp;&nbsp;&nbsp;&nbsp;struct&nbsp;_D3DXFRAME&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*pFrameFirstChild;&nbsp;&nbsp;&nbsp;&nbsp;//子骨骼<br/>}&nbsp;D3DXFRAME,&nbsp;*LPD3DXFRAME;</p><p>自定义数据容器,其数据来源由上面接口的CreateMeshContainer()函数提供<br/>typedef&nbsp;struct&nbsp;_D3DXMESHCONTAINER<br/>{<br/>&nbsp;&nbsp;&nbsp;&nbsp;LPSTR&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Name;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//容器名<br/>&nbsp;&nbsp;&nbsp;&nbsp;D3DXMESHDATA&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MeshData;&nbsp;&nbsp;&nbsp;//Mesh数据,可创建SkinMesh取代这个Mesh<br/>&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXMATERIAL&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMaterials;&nbsp;//材质数组<br/>&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXEFFECTINSTANCE&nbsp;&nbsp;&nbsp;&nbsp;pEffects;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;DWORD&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;NumMaterials;//材质数<br/>&nbsp;&nbsp;&nbsp;&nbsp;DWORD*&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pAdjacency;&nbsp;&nbsp;//邻接三角形数组<br/>&nbsp;&nbsp;&nbsp;&nbsp;LPD3DXSKININFO&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pSkinInfo;&nbsp;&nbsp;&nbsp;//蒙皮信息,其中含.x中的各个skinweight蒙皮顶点索引及各骨骼偏移矩阵等。<br/>&nbsp;&nbsp;&nbsp;&nbsp;struct&nbsp;_D3DXMESHCONTAINER&nbsp;*pNextMeshContainer;<br/>}&nbsp;D3DXMESHCONTAINER,&nbsp;*LPD3DXMESHCONTAINER;</p><p><br/><font color="#ff0000">[评论]</font><br/>.在动画文件中,框架通常用来描述骨骼。可以把Frame视做骨骼,所以不细加区分。<br/>.在上面D3DXFRAME结构中,pFrameSibling,&nbsp;pFrameFirstChild两个指针,常用于递归函数中,遍历整个骨架。<br/>.在D3DXFRAME结构中有一个pMeshContainer指针,难道框架与Mesh是一一对应的吗?<br/>&nbsp;有一个框架(骨骼)就有一个Mesh吗?怎么.X文件中只有一个Mesh?难道加载时拆开存放?<br/>答:从D3DXFrame结构上看,每个Frame都有一个pMeshContainer指针。这就有三种解释:<br/>&nbsp;&nbsp;&nbsp;第一种,加载到内存后所有的pMeshContainer都指向同一个全局Mesh<br/>&nbsp;&nbsp;&nbsp;第二种,加载到内存后,只有一个主框架的pMeshContainer不为空,其它Frame的pMeshContainer均为NULL,因为在.X中,它们没有定义自己的Mesh<br/>&nbsp;&nbsp;&nbsp;第三种,加载到内存后,D3D将Mesh拆分,分开到各骨骼所对应的Frame,每个Frame都有自己的Mesh。<br/>&nbsp;&nbsp;&nbsp;这个问题我以前也不是很清楚,通过查看示例源码及跟踪发现,正确解释应该是第2种。唯一的一个全局Mesh存放在Frame&nbsp;"body"下的无名Frame中。而其它Frame由于没有自己专门的Mesh而指向NULL.&nbsp;应该大致如此。这个问题之所以让人困绕,是因为从后续代码上看,在渲染DrawFrame时,是遍历每一个frame分别绘制它们对应的Mesh.&nbsp;如果对应于同一个mesh,就绘制多遍。如果对应各自mesh,那么变换矩阵怎么组织运算等等。所以,根据第二种解释,由于只有一个pMeshContainer不为NULL,所以参与绘制及蒙皮的只有这一个MeshContainer,人体所有顶点数据及蒙皮信息都在这个mesh中。<br/>所以,读取tiny.x文件后,会产生多个D3DXFRAME对象,但只有一个D3DXMESHCONTAINER对象。</p><p>在示例代码的CMyD3DApplication::InitDeviceObjects()中,有:<br/>&nbsp;&nbsp;&nbsp;&nbsp;hr&nbsp;=&nbsp;D3DXLoadMeshHierarchyFromX(strMeshPath,&nbsp;D3DXMESH_MANAGED,&nbsp;m_pd3dDevice,&nbsp;&amp;Alloc,&nbsp;NULL,&nbsp;&amp;m_pFrameRoot,&nbsp;&amp;m_pAnimController);<br/>&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(FAILED(hr))<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;hr;<br/>其中的Alloc是就自定义的数据容器对象。m_pFrameRoot是根骨骼,对遍历很重要。m_pAnimController是动画控制器,对刷新矩阵很重要。</p><p>你在运行完这句话后,下一个断点,观察m_pFrameRoot,会发现如下内容:</p><p>m_pFrameRoot 0x00c59380&nbsp;{Name=0x00c53630&nbsp;"Scene_Root"&nbsp;.....}&nbsp;//根框架<br/>&nbsp; pMeshContainer 0x00000000&nbsp;<br/>pFrameSibling 0x00000000&nbsp;<br/>pFrameFirstChild 0x00c59428&nbsp;{Name=0x00c53ca8&nbsp;"body"&nbsp;pMeshContainer=0x00000000...}//子框架&nbsp;骨骼body<br/>&nbsp;&nbsp;&nbsp;+---&nbsp;&nbsp;pMeshContainer&nbsp;0x00000000&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+---&nbsp;&nbsp;pFrameSibling &nbsp;0x01419f00&nbsp;{Name=0x00c5ffd8&nbsp;"Box01"&nbsp;pMeshContainer=0x00000000&nbsp;...}//兄弟框架<br/>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;+---&nbsp;&nbsp;pFrameFirstChild&nbsp;0x00c594d0&nbsp;{Name=0x00000000&nbsp;<font color="#804000">pMeshContainer=0x00c59828</font>&nbsp;//子框架---该框架就是.x中含有唯一全局Mesh的无名框架</p><p><br/>可见,在内存中的Frame布局是与.x中一一对应的。除了pFrameFirstChild&nbsp;0x00c594d0这个地方的Frame中的pMeshContainer不为空,其它框架的这个mesh指针都是空值。<br/>另外一点可以看出,并不是每个Frame都对就一块骨骼,有的是别的用途。也就是说Frame对象的个数可能多于骨骼数。</p><p><font color="#0000ff">3.3&nbsp;分析CAllocateHierarchy类</font><br/>下面继续研究自定义数据容器CAllocateHierarchy,顾名思义,该类是在加载过程中自行分配层次数据空间。它有4个成员,都是重载D3D的接口虚函数。<br/>它的成员CreateFrame()是用来创建D3DXFrame对象的,而CreateMeshContainer()是用来创建Mesh数据对象的。你可以在这两个函数中下断点,发现CreateFrame会运行多次,而CreateMeshContainer只运行一次,再次验证了上面的说法。</p><p>值得注意的是,示例对上面的D3DXFRAME,D3DXMESHCONTAINER两个结构做了扩展,分别代之以D3DXFRAME_DERIVED结构和D3DXMESHCONTAINER_DERIVED结构,以集中存储数据方便程序处理。</p><p>CreateFrame()处理比较简单,你只是new一个Frame对象空间,填入传进来的Name,其它内容由DX负责维护填充。</p><p>CreateMeshContainer()较为复杂。它的任务一是保存传入的网格数据数据,二是根据这些数据及蒙皮信息调用GenerateSkinnedMesh()函数生成蒙皮网格。只有这个新的BlendMesh才能在Render()时支持顶点混合,完成蒙皮的显示。在D3DXMESHCONTAINER_DERIVED结构中,用pOrigMesh保存旧的Mesh普通网格信息。而Meshdata.Mesh则指向新产生的BlendMesh</p><p>在这个函数中,多次用到了<font color="#008000">AddRef()</font>,对COM不熟悉的新手容易困惑。D3D是COM组件,它在服务进程中运行,而不在当前的客户进程中。在DX组件运行过程中,要创建一系列接口对象,如CreateDevice()返回接口指针,这些接口及其占用内存什么时候释放,要通过“引用计数”的技术来解决。AddRef()给这个接口指针的计数加1,而Release()会将之减1。一旦减到0,表示没有客户使用了,相关的接口就释放了。&nbsp;由此可知,每次调用Rlease()后,并不一定会释放内存,而是当引用计数归0时释放内存。<br/>这样,对接口指针的使用,就像维护堆栈的平衡一样,要仔细,而且按照某种约定规则使用。</p><p>但平时D3D编程中,怎么不用AddRef()呢?这是由于一个接口指针,如ID3DDevice,或VertexBuf指针,都是D3DXCreate出来的,在Create时候,在内部已经事先AddRef()了,你就不需要再做这工作了。只要你在不用时,调用&nbsp;p指针-&gt;Relase()就释放了。一般编程,特别是小型示例程序,都是初始化时建立一次,关闭时释放,都遵守了这种约定,所以不存在这种问题。</p><p>但在CreateMeshContainer()函数中,以多种方式使用了指针,在局部指针变量中来回传递,所以问题复杂化了。在COM编程中约定,任何时候地接口指针赋值(复制),都要AddRef(),在指针变量结束生命期前,再Release().&nbsp;但许多程序员都不是严格这么做。因为在局部变量用完就废了,先AddRef()增加计数再Release()减少,和直接使用最后是等效的。几乎是多此一举。这与编程习惯有关系。一旦引用计数不对,如果没有统一的习惯,不好排查。在CreateMeshContainer()中,对接口指针的使用有三种方式,例举如下:</p><p>方式一:不使用AddRef()。和普通指针一样,临时变量是左值,接口指针是右值,直接赋值使用。如:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMesh&nbsp;=&nbsp;pMeshData-&gt;pMesh;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;这是由于pMesh是局部变量,它只是临时引用一下,没必要为它先AddRef(),后Release()。</p><p>方式二:隐式的使用AddRef()。&nbsp;由于用到了一些内部有AddRef()动作的函数,就要按照COM约定,在子程序结束前Release()<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMesh-&gt;GetDevice(&amp;pd3dDevice);//此处d3d设备引用计数已经加1<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;....<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SAFE_RELEASE(pd3dDevice);//--此处将引用计数减1,并不是真的释放d3d设备<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在本例中,pd3dDevice在GetDevice()中已经Addref()过了,所以,在退出CreateMeshContainer()前,必须pd3dDevice-&gt;Release()</p><p>方式三:显式的使用AddRef()。&nbsp;如果一个指针值,不是由D3DXCreate出来的,而是通过赋值方式复制给一个全局变量或长期变量的。&nbsp;所以,<b>可以通过AddRef()的方式来延迟该对象的释放</b>。因为,如果不AddRef(),极有可能在函数返回该对象就可能释放了。它就像一个加油站,使得传入对象的寿命延长至自己控制范围内。用了AddRef(),就要在相关的Destroy中添加Release()。</p><p>&nbsp;在本函数,有三处这样的语句:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;MeshData.pMesh&nbsp;=&nbsp;pMesh;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;MeshData.Type&nbsp;=&nbsp;D3DXMESHTYPE_MESH;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMesh-&gt;AddRef();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;....<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;pSkinInfo&nbsp;=&nbsp;pSkinInfo;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pSkinInfo-&gt;AddRef();</p><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;pOrigMesh&nbsp;=&nbsp;pMesh;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMesh-&gt;AddRef();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;....</p><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;将来在DestroyMeshContainer()中,要释放这些指针:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;....<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SAFE_RELEASE(&nbsp;pMeshContainer-&gt;MeshData.pMesh&nbsp;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SAFE_RELEASE(&nbsp;pMeshContainer-&gt;pSkinInfo&nbsp;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SAFE_RELEASE(&nbsp;pMeshContainer-&gt;pOrigMesh&nbsp;);</p><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;由于这些指针值的创建、更改等都是用户自己经营的,所以务必要加前后吻合,在CreateMeshContainer()中AddRef(),在DestroyMeshContainer()中Release().</p><p><br/>再来看数据的保存部分。<br/>在CreateMeshContainer()的传入参数中,有pMeshData,pMaterials,pEffectInstances,NumMaterials,pAdjacency,pSkinInfo<br/>你需要把这些数据保存到自己的D3DXMESHCONTAINER对象中。并且其中的所有数组所需的空间都要在全局堆中new出来。所以在该代码中,有如下new:<br/>pMeshContainer&nbsp;=&nbsp;new&nbsp;D3DXMESHCONTAINER_DERIVED;//自定义的扩展数据容器对象<br/>memset(pMeshContainer,&nbsp;0,&nbsp;sizeof(D3DXMESHCONTAINER_DERIVED));//初始化pMeshContainer,清0<br/>&nbsp;&nbsp;&nbsp;&nbsp;...<br/>pMeshContainer-&gt;pMaterials&nbsp;=&nbsp;new&nbsp;D3DXMATERIAL;//准备保存材质<br/>pMeshContainer-&gt;ppTextures&nbsp;=&nbsp;new&nbsp;LPDIRECT3DTEXTURE9;//准备创建纹理对象。它声明在扩展部分。<br/>pMeshContainer-&gt;pAdjacency&nbsp;=&nbsp;new&nbsp;DWORD;//准备保存邻接三角形数组,NumFaces&nbsp;=&nbsp;pMesh-&gt;GetNumFaces();</p><p>然后,对数据进行memcpy保存。pEffectInstances由于在绘制中不需要,并没进行保存。对于没有贴图的赋以默认材质属性。<br/>值得注意的是,所有这些new,必须在DestroyMeshContainer()时进行delete.</p><p>接下来的处理中,如果发现Mesh的FVF中没有法向量,要用CloneMeshFVF()重建Mesh,计算顶点平均法向量。以备光照处理。</p><p>最后,我们看看蒙皮信息pSkinInfo的处理。这是重头戏。<br/>如果发现pSkinInfo!=NULL,就准备着手从各个蒙皮骨骼信息创建SkinMesh.<br/>首先,用扩展容器结构D3DXMESHCONTAINER_DERIVED中的各属性保存原Mesh指针值,pMeshContainer-&gt;pOrigMesh&nbsp;=&nbsp;pMesh,&nbsp;因为接下来我们要创建SkinMesh替代原Mesh.然后,把SkinInfo中的各骨骼的偏移矩阵保存到pMeshContainer-&gt;pBoneOffsetMatrices中<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cBones&nbsp;=&nbsp;pSkinInfo-&gt;GetNumBones();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;pBoneOffsetMatrices&nbsp;=&nbsp;new&nbsp;D3DXMATRIX;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.....<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;每个“骨骼偏移矩阵”pBoneOffsetMatrices,在将来DrawMeshContainer()中是必须要用的。因为原始Mesh中的顶点数据乘以“骨骼偏移矩阵”,再乘以“变换矩阵”,才能求得各骨骼顶点在世界坐标系中的坐标。&nbsp;即:&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;骨骼上各点在世界坐标系中的新坐标=初始网格中的各点坐标*骨骼偏移矩阵*骨骼当前的变换矩阵<br/>&nbsp;&nbsp;&nbsp;&nbsp;其中,“初始网格中的各点坐标*骨骼偏移矩阵”&nbsp;=&nbsp;骨骼上各点初始时刻在该骨骼坐标系中的局部坐标</p><p>做了以上工作后,调用GenerateSkinnedMesh(pMeshContainer),创建SkinMesh.&nbsp;接下来,我们看看GenerateSkinnedMesh()做了哪些工作。</p><p><font color="#0000ff">3.4&nbsp;怎样生成蒙皮网格SkinMesh?&nbsp;GenerateSkinnedMesh()分析</font></p><p>由于要重定义pMeshContainer-&gt;MeshData.pMesh,所以先SAFE_RELEASE(&nbsp;pMeshContainer-&gt;MeshData.pMesh&nbsp;);&nbsp;释放原pMesh</p><p>在这个函数中,是根据当前绘图方式设置进行加载数据的。因为顶点混合,有无索引的顶点混合,有含索引的顶点混合,所使用的函数和对应的SkinMesh数据内容也有所不同。<br/>在示例中,自定义了枚举m_SkinningMethod,主要分为D3DNONINDEXED和D3DINDEXED,以有纯软件渲染等。运行示例后,你可以选择菜单中的Options选择不同的渲染方式。</p><p>我们着重分析一下带索引的蒙皮网格。在程序中,就是D3DINDEXED相关的部分。<br/>if&nbsp;(m_SkinningMethod&nbsp;==&nbsp;D3DINDEXED){&nbsp;....}</p><p>注意!&nbsp;示例默认工作在D3DNONINDEXED下,<font color="#a00000">如果要跟踪D3DINDEXED部分的代码,必须选择菜单中的Options选择indexed!</font></p><p><br/>最主要的,要通过DX的ConvertToIndexedBlendedMesh()函数,生成支持“索引顶点混合”的SkinMesh.有关索引顶点混合的技术,你可以在DXSDK帮助文件中搜索“Indexed&nbsp;Vertex&nbsp;Blending”主题,对着英文和插图将就看,确有收获。</p><p>要想用硬件对顶点进行混合,那么参与混合者不能太多。也就是说同时影响一个顶点的骨骼数不能多。我们假定一个顶点最多同时受4个骨骼的影响(也就是同时最多有4个骨骼矩阵参与加权求和),那么同时影响一个三角形面的骨骼数最多就是3*4=12个。<br/>我们用NumMaxFaceInfl表示影响一个三角面的最多骨骼矩阵数,那么,通过调用pSkinInfo-&gt;GetMaxFaceInfluences()获取这个数值,一般也就3-4。如果这个数值太大,我们强制使用NumMaxFaceInfl&nbsp;=&nbsp;min(NumMaxFaceInfl,&nbsp;12);来最多取值12。</p><p>用NumMaxFaceInfl&nbsp;这个数值干什么呢?&nbsp;我们用来它分析当前的显卡倒底行不行。</p><p>if&nbsp;(m_d3dCaps.MaxVertexBlendMatrixIndex&nbsp;+&nbsp;1&nbsp;&lt;&nbsp;NumMaxFaceInfl)//如果显卡达不到该要求<br/>{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//很奇怪。2005年底买的GeForce&nbsp;6600GT显卡,竟然m_d3dCaps.MaxVertexBlendMatrixIndex=0,&nbsp;不支持索引顶点混合!是驱动问题还是怎么了?<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//但它支持非索引混合。或者,也许要用HLSL支持混合。看起来,3D编程要多考虑。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;..<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;UseSoftwareVP&nbsp;=&nbsp;true;//用软件渲染顶点。显然不实用。<br/>}<br/>else<br/>{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;NumPaletteEntries&nbsp;=&nbsp;min(&nbsp;(&nbsp;m_d3dCaps.MaxVertexBlendMatrixIndex&nbsp;+&nbsp;1&nbsp;)&nbsp;/&nbsp;2,&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;pSkinInfo-&gt;GetNumBones()&nbsp;);//--什么意思?<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;UseSoftwareVP&nbsp;=&nbsp;false;//采用硬件顶点混合。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Flags&nbsp;|=&nbsp;D3DXMESH_MANAGED;<br/>}</p><p><font color="#0000ff">[评论]</font>在上面有一行代码:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pMeshContainer-&gt;NumPaletteEntries&nbsp;=&nbsp;min(&nbsp;(&nbsp;m_d3dCaps.MaxVertexBlendMatrixIndex&nbsp;+&nbsp;1&nbsp;)&nbsp;/&nbsp;2,pMeshContainer-&gt;pSkinInfo-&gt;GetNumBones()&nbsp;);<br/>尽管作者加了大段注释,还是让人一头雾水。其实,我们做一个实验,反尔更能理解它的用途。<br/>第一步,你在这句话后面下一个断点,看一下在你机器上这个数值。我的ATI&nbsp;9550显卡机器上是19。比tiny.x中的骨骼数35少很多。<br/>第二步,你将上面=右边瞎填一个大于4的数字,比如6。编译后照样运行。而且效果上几乎看不出任何差别。<br/>为什么会这样呢?&nbsp;我们在绘制代码部分,看看这个数值起什么作用。<br/>在DrawMeshContainer()代码中,我们查找D3DINDEXED相关的部分。在mesh各子集的DrawSubset()之前,有如下代码:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;(iAttrib&nbsp;=&nbsp;0;&nbsp;iAttrib&nbsp;&lt;&nbsp;pMeshContainer-&gt;NumAttributeGroups;&nbsp;iAttrib++)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;first&nbsp;calculate&nbsp;all&nbsp;the&nbsp;world&nbsp;matrices<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;(iPaletteEntry&nbsp;=&nbsp;0;&nbsp;iPaletteEntry&nbsp;&lt;&nbsp;pMeshContainer-&gt;NumPaletteEntries;&nbsp;++iPaletteEntry)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iMatrixIndex&nbsp;=&nbsp;pBoneComb.BoneId;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(iMatrixIndex&nbsp;!=&nbsp;UINT_MAX)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;D3DXMatrixMultiply(&nbsp;&amp;matTemp,&nbsp;&amp;pMeshContainer-&gt;pBoneOffsetMatrices,&nbsp;pMeshContainer-&gt;ppBoneMatrixPtrs&nbsp;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_pd3dDevice-&gt;SetTransform(&nbsp;D3DTS_WORLDMATRIX(&nbsp;iPaletteEntry&nbsp;),&nbsp;&amp;matTemp&nbsp;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;...<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>下面仔细评估一下这些代码.<br/>先注意看其中奇怪的D3DTS_WORLDMATRIX()宏,我们以前还没这样用过。它是做什么用的呢?通过查DXSDK帮助,我们在Geometry&nbsp;Blending主题中找到相关说明,并在"Indexed&nbsp;Vertex&nbsp;Blending"主题中给出了内部实现原理。原来,当你用m_pd3dDevice-&gt;SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE,&nbsp;TRUE);开启了索引顶点混合后,在硬件上就启用了“palette&nbsp;of&nbsp;matrices”,即矩阵寄存器组,它最多支持同时256个索引。就像过去用256色调色板来表现彩色一样。D3DTS_WORLDMATRIX()宏就是有256-511这256个数表示矩阵索引号。</p><p>这些矩阵参与如下计算:</p><p>V最终顶点位置=V*M[索引值1]*权重1&nbsp;+&nbsp;V*M[索引值2]*权重2&nbsp;+&nbsp;....+V*M[索引n]*(1-其它权重和)</p><p>这个公式的来源,相信大家在众多资料上见过,不赘述。&nbsp;当然,我们也可以用程序完成这个蒙皮计算过程,但逐个读顶点却很麻烦。现在是由硬件代劳了。我们只设矩阵就行了。<br/>我们用m_pd3dDevice-&gt;SetTransform(&nbsp;D3DTS_WORLDMATRIX(&nbsp;iPaletteEntry&nbsp;),&nbsp;&amp;matTemp&nbsp;);这种方式设定各索引对应的矩阵。</p><p>那么权重呢?我们怎么设?原来在上面所说的DX提供的ConvertToIndexedBlendedMesh()函数中,生成SkinMesh时,各网格顶点格式FVF已经有变化了,增加了新格式,D3DFVF_XYZB2,D3DFVF_LASTBETA_UBYTE4,用以记录顶点对应的权重值以及矩阵索引。如下<br/>struct&nbsp;VERTEX<br/>{<br/>&nbsp;&nbsp;&nbsp;&nbsp;float&nbsp;x,y,z;<br/>&nbsp;&nbsp;&nbsp;&nbsp;float&nbsp;weight;<br/>&nbsp;&nbsp;&nbsp;&nbsp;DWORD&nbsp;matrixIndices;<br/>&nbsp;&nbsp;&nbsp;&nbsp;float&nbsp;normal;<br/>};<br/>#define&nbsp;D3DFVF_CUSTOMVERTEX&nbsp;(D3DFVF_XYZB2&nbsp;|&nbsp;D3DFVF_LASTBETA_UBYTE4&nbsp;|D3DFVF_NORMAL);</p><p>D3DFVF_LASTBETA_UBYTE4对应于DWORD数值,用于矩阵索引时,每个字节表示一个索引,最多可以允许4个索引,同时有4个矩阵参于该点的混合。如果一次绘制中涉及了9块骨骼矩阵,你可以把这9个矩阵全部用SetTransform设置到矩阵寄存器中,但每个顶点在渲染时,最多使用其中的4个。由此可知,pMeshContainer-&gt;NumPaletteEntries这个数值,确定了一趟DrawSubset绘制所用到的矩阵个数,个数越多,在一趟绘制中就可以纳入的更多顶点。所以,当我们减少pMeshContainer-&gt;NumPaletteEntries这个数值时,pMeshContainer-&gt;NumAttributeGroups数值就会增加。也就是说,一趟绘制中所允许涉及的骨骼数越少,那么子集的数量NumAttributeGroups就会增加,需要多绘几趟。<br/>你可以在此下断点观察,当NumPaletteEntries=19时,NumAttributeGroups=3&nbsp;当NumPaletteEntries=6时,NumAttributeGroups=12&nbsp;当NumPaletteEntries=4时,NumAttributeGroups=31,几乎和无索引时的分组一样多了。</p><p>顶点中的权重weight存放了它当前骨骼的权重。(一个顶点对应的多个骨骼权重怎么存放?是不是在当前子集中有多个同样的顶点,权重不同,对应的矩阵索引不同,然后混合)</p><p><br/>由上所述,ConvertToIndexedBlendedMesh()是一个很重要函数,由DX自动将Mesh顶点分组成多个子集,以便DrawSubset.&nbsp;你必须把它的返回参数都记录下来,在绘制时使用。</p><p></p><p><b>四.&nbsp;怎样绘制显示动画?</b></p><p>DrawFrame()用来绘制整个X框架。它遍历各个框架,找到Mesh不为空的进行绘制。(其实整个.x中通常只有一个不为空,见上文所述)<br/>DrawMeshContainer()是绘制函数。</p><p><font color="#0000ff">4.1&nbsp;怎样开启顶点混合?</font><br/>注意应用有关的Vertex&nbsp;Blending技术。如在索引方式的绘制中,<br/>m_pd3dDevice-&gt;SetRenderState(D3DRS_VERTEXBLEND,&nbsp;pMeshContainer-&gt;NumInfl&nbsp;-&nbsp;1);<br/>其实是设定了D3DVBF_2WEIGHTS或D3DVBF_3WEIGHTS<br/>注意要m_pd3dDevice-&gt;SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE,&nbsp;TRUE);</p><p><font color="#0000ff">4.2&nbsp;矩阵的刷新:</font><br/>首先,在FrameMove()调用m_pAnimController-&gt;SetTime()设置当前时间(或在DX9.0c中用AdvanceTime()设置时间差),从而刷新各个pFrame-&gt;TransformationMatrix,即骨骼转换矩阵<br/>其次,调用UpdateFrameMatrices()做乘法累积,计算出各骨骼坐标系到根世界转换矩阵。<br/>最后,在绘制前,将该转换矩阵左乘偏移矩阵,得到最终的转换矩阵。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;D3DXMatrixMultiply(&nbsp;&amp;matTemp,&nbsp;&amp;pMeshContainer-&gt;pBoneOffsetMatrices,&nbsp;pMeshContainer-&gt;ppBoneMatrixPtrs&nbsp;);</p><p>由此可见,你如果注释掉了m_pAnimController-&gt;SetTime,画面肯定停了。</p><p><font color="#0000ff">4.3&nbsp;绘制输出</font>&nbsp;是在DrawMeshContainer()中,调用SkinMesh的DrawSubset进行绘制。一些细节内容如D3DTS_WORLDMATRIX(),在上面已经有说明,不再罗嗦。</p><p></p><p><font color="#0000ff">4.4&nbsp;关于示例中多种绘制方式分析</font><br/>在示例中,用到了多种渲染方式,包括传统的非索引顶点混合,还有新兴的HLSL方式。而且我发现,ATI&nbsp;RADEON&nbsp;9550&nbsp;显卡MaxVertexBlendMatrixIndex=37,而价格更高的Gefoce&nbsp;6600GT&nbsp;MaxVertexBlendMatrixIndex竟然为0,不支持index&nbsp;vertex&nbsp;blending!<br/>所以,还是有必要分析一下该示例中各种vertex&nbsp;blending方式的处理,以便掌握多种绘制方式适应不同显卡。<br/>经测试,示例中所涉及的多种方式,由慢到快,依次是以下几种:<br/>&nbsp;&nbsp;&nbsp;&nbsp;SOFTWARE,<br/>&nbsp;&nbsp;&nbsp;&nbsp;D3DNONINDEXED,<br/>&nbsp;&nbsp;&nbsp;&nbsp;D3DINDEXED,<br/>&nbsp;&nbsp;&nbsp;&nbsp;D3DINDEXEDVS,<br/>&nbsp;&nbsp;&nbsp;&nbsp;D3DINDEXEDHLSLVS,</p><p>从最慢的SW到最快的HLSL,大约相差20%,有时会大到40%。&nbsp;差别不是特别悬殊的原因,主要是顶点混合并不是瓶颈。</p><p>关于顶点处理方式,是在创建D3D设备时指定的。共有三种方式:<br/>&nbsp;&nbsp;&nbsp;D3DCREATE_SOFTWARE_VERTEXPROCESSING&nbsp;软件顶点运算&nbsp;&nbsp;(简记&nbsp;sw&nbsp;vp)<br/>&nbsp;&nbsp;&nbsp;D3DCREATE_HARDWARE_VERTEXPROCESSING&nbsp;硬件顶点运算。必须有这项才支持有HAL&nbsp;(简记&nbsp;hw&nbsp;vp)<br/>&nbsp;&nbsp;&nbsp;D3DCREATE_MIXED_VERTEXPROCESSING&nbsp;混合顶点运算,即硬件+软件&nbsp;(简记&nbsp;mixed&nbsp;vp)</p><p>一旦用D3DCREATE_HARDWARE_VERTEXPROCESSING方式创建设备,就只能在硬件方式下进行顶点处理。如果调用m_pd3dDevice-&gt;SetSoftwareVertexProcessing(TRUE)来切换到软件顶点处理,HRESULT会返回失败。<br/>&nbsp;&nbsp;所以,如果你对客户的显卡没有足够的信息,就用D3DCREATE_MIXED_VERTEXPROCESSING方式创建设备。它默认工作方式是HAL。一旦发现进行某种绘制时硬件能力不够,就可以调用调用m_pd3dDevice-&gt;SetSoftwareVertexProcessing(TRUE)切换到软件模式。在示例中就是这么做的,启动示例后,运行在mixed模式下。</p><p>在Gefoce6600GT显卡中,由于D3DINDEXED方式不支持,采用了软件混合方式,在这种方式下速度甚至比SOFTWARE慢。HLSL还好,还是最快。</p><p>要确定设备的硬件顶点处理能力,可以参考D3DCAPS9结构的VertexProcessingCaps成员。可以获取下列属性<br/>MaxActiveLights,MaxUserClipPlanes,MaxVertexBlendMatrices,MaxStreams,MaxVertexIndex<br/>&nbsp;</p><p>(1)D3DNONINDEXED方式:</p><p>首先看GenerateSkinnedMesh()中怎样创建蒙皮网格的。<br/>这种方式下,用ConvertToBlendedMesh()建立蒙皮网格,而不是ConvertToIndexBlendedMesh()</p><p>为了绘制蒙皮,在这个函数中对Mesh各子集的顶点再次进行的分组。分组的标准是各顶点(或三角面)所涉及的骨骼矩阵个数不超过pMeshContainer-&gt;NumInfl个。(这个数字是由在ConvertToBlendedMesh()时,由参数pMaxFaceInfl返回的)。一个Mesh子集可能被拆开成多个分组。&nbsp;最后,分组的属性保存在pBoneCombinationBuf中,如子集ID,该子集的各骨骼ID,起始三角面,三角面个数等供绘制时使用,分组的个数保存在pMeshContainer-&gt;NumAttributeGroups中。</p><p>接下来检查每个分组所涉及的骨骼数,是不是超过硬件允许的最大混合矩阵数---MaxVertexBlendMatrices。如果超过了就把所有分组截为两大部分,前一部分用硬件混合,后一部分采用软件混合。而且,一旦发现有需要软件混合,要采用CloneMeshFVF(D3DXMESH_SOFTWAREPROCESSING|...)的方式重新生成网格。</p><p>再来看绘制部分DrawMeshContainer()</p><p>用pBoneComb指向骨骼分组属性,扫描各分组。找出其中骨骼数满足硬件性能的用进行绘制。<br/>然后开启软件顶点渲染m_pd3dDevice-&gt;SetSoftwareVertexProcessing(TRUE),对那些骨骼数超出硬件性能的进行绘制。<br/>SetSoftwareVertexProcessing()需要当前d3d设备以D3DCREATE_MIXED_VERTEXPROCESSING方式创建。</p><p>(2)D3DINDEXED,这种方式上面分析过了,从略。用pMeshContainer-&gt;UseSoftwareVP表示是否采用软件绘制。<br/>值得注意的是在这种方式下,一旦硬件性能不足,会彻底使用软件顶点渲染,而不是像上面一样拆为两部分。</p><p>(3)D3DINDEXEDVS,D3DINDEXEDHLSLVS<br/>这种情况下使用了着色器和高级着色语言。超出本文主旨,讨论从略。</p><p>(4)SOFTWARE--软件方式?&nbsp;让人有些迷惑,与上面的m_pd3dDevice-&gt;SetSoftwareVertexProcessing(TRUE)有何区别?</p><p>从代码看,这种方式下反而比较简单。GenerateSkinnedMesh()中,<br/>先直接从原始Mesh克隆一个Mesh,然后读取它的材质属性数组。开辟一个空间m_pBoneMatrices,用以存放各块骨骼的转换矩阵。</p><p>在绘制时,从pMeshContainer中的变换矩阵乘以偏移矩阵,放在pBoneMatrices中。把这个矩阵数组,以原Mesh的顶点作为源顶点,以新克隆的MeshData.pMesh做为目标顶点,调用pSkinInfo-&gt;UpdateSkinnedMesh(),用软件方式计算各骨骼顶点的新位置(相当于软件计算方式蒙皮)。</p><p>然后调用MeshData.pMesh-&gt;DrawSubset()绘制。</p><p>可见,在SOFTWARE方式下,最终顶点的渲染还是HAL方式的,只不过蒙皮计算是由软件完成的。它和上面的m_pd3dDevice-&gt;SetSoftwareVertexProcessing(TRUE)直接设置软件顶点渲染还是有区别的。</p>
[此贴子已经被作者于2007-7-10 0:02:54编辑过]

Newer 发表于 2008-2-25 16:53:00

回复:关于骨骼动画及微软示例Skinned Mesh的解析

这个俺倒是看过,写的非常好。
很容易明白,然后我自己设计了一个格式,加载显示也很顺利
但是我是以四元数为基础 磁盘存储和使用的软件实现的

hing 发表于 2009-6-10 00:28:00

回复: 关于骨骼动画及微软示例Skinned Mesh的解析

sdferytuufdgsdfg

fly19840303man 发表于 2009-7-4 20:47:00

回复:关于骨骼动画及微软示例Skinned Mesh的解析

有代码吗?教程配代码,才是最好的,带带新手啊。

ylumei 发表于 2012-4-15 20:37:32

学习!

ddfpks 发表于 2012-4-26 02:09:27

感觉关节那一块不是问题 其他的比较麻烦
页: [1]
查看完整版本: 关于骨骼动画及微软示例Skinned Mesh的解析