- 浏览: 16493800 次
- 性别:
- 来自: 济南
最新评论
-
wu1236:
ef0793cd94337324b6fefc4c9474af5 ...
Android ApiDemos示例解析(87):Media->MediaPlayer -
77219634:
0127bf2236bee4dd1f632ce430f1af1 ...
本博客文章都为转载,没有任何版权! -
77219634:
0127bf2236bee4dd1f632ce430f1af1 ...
VPLEX - EMC的RAC -
77219634:
0127bf2236bee4dd1f632ce430f1af1 ...
qTip2 Show -
77219634:
0127bf2236bee4dd1f632ce430f1af1 ...
SecureCRT中文乱码、复制粘贴乱码解决办法(修改版)
关于骨骼动画及微软示例Skinned Mesh的解析
这是我自个写的,第一次发. 没想到这个贴子编辑器极差. 原文是有字体字色的.现在只能清一色了.
版主,发贴的编辑器太难用! 你有必要向上反映一下. 下面的字体是我敲html标记加上的,大家凑和看.
关于骨骼动画及微软示例Skinned Mesh的解析
骨骼动画是D3D的一个重要应用。尽管微软DXSDK提供了示例Skinned Mesh,但由于涉及众多概念和技术细节,示例相对于初学者非常复杂,难以看懂。在此,提供一些重要问题评论,以使初学者走出迷局,顺利上手。文中所述都是参照各种资料加上自己的理解,也有可能出些偏差,有则回贴拍砖,无则权当一笑。
一 骨骼动画原理
原理方面在网上资料比较多,大家都基本明白。在此说一下重点:
总体上,绝大部分动画实现原理一致,就是“提供一种机制,描述各顶点位置随时间的变化”。有三种方法:
1.1 关节动画:由于大部分运动,都是皮肤随骨骼在动,皮肤相对于它的骨骼本身并没有发生运动,所以只要描述清楚骨骼的运动就行了。用矩阵描述各个骨骼的相对于父骨骼运动。(大多运动都是旋转型) 易知,从子骨骼用矩阵乘法累积到最顶层根骨骼,就可以得到每个子骨骼相对于世界坐标系的转换矩阵。
这种动画,只须用普通Mesh保存最初始的各顶点坐标,以及一系列后续时刻所对应的各骨骼的运动矩阵。不用保存每时刻的顶点数据,节省了大量存储空间。而且比较灵活,可以利用关键帧插值运算,便于通过运算调节动作。缺点是在两段骨骼交接处,容易产生裂缝,影响效果。
1.2 渐变动画:通过保存一系列时刻的顶点坐标来完成动画。虽然比较逼真,但占用大量空间,灵活性也不高。
1.3 骨骼蒙皮动画(skinned Mesh)
相当于上面两方法的折中。现在比较流行。
在关节动画的基础上,利用顶点混合(Vertex Blend)技术,对于关节附近的顶点,由影响这些顶点的两段(或多段)骨骼运动,分别赋以权值,共同决定顶点位置。相当于在骨骼关节上动态蒙皮,有效解决了裂缝问题。
这里,引入一个D3D技术概念:“Vertex Blending”---顶点混合技术。比如说,你肯定用过SetTransform(D3DTS_WORLD,....),但SetTransform(D3DTS_WORLDMATRIX(i),....)是不是很奇怪?这个问题后文会讲到。 你也可以在微软的DXSDK的帮助文件中搜索“Geometry Blending”主题,有裂缝及其解决办法图示。
二 X文件如何保存骨骼动画
理解X文件格式,对用好相关的DX函数是非常重要的。
不含动画的普通X文件,有一个Mesh单元,保存了各顶点信息、各三角面的索引信息、材质种类及定义等。
动画X文件,则在这个单元中增加了“各骨骼蒙皮信息”、“骨骼层次及结构信息”、“各时刻骨骼矩阵信息”等。
2.1 网格蒙皮信息:首先,在Mesh{}单元中,在原有的普通网格顶点数据基础上,新增了XSkinMeshHeader{}结构,以及多个SkinWeights{}结构。用以描述各个骨骼的蒙皮信息。
其中,XSkinMeshHeader是总括,举一实例,如下:
XSkinMeshHeader
{
2,//一个顶点可以受到骨骼影响的最大骨骼数,可用于计算共同作用时减少遍历次数
4,//一个三角面可以受到骨骼影响的最大骨骼数。这个数字对硬件顶点混合计算提出了基本要求。
35 //当前Mesh的骨骼总数。
}
由于每个骨骼的蒙皮信息都需要用SkinWeights结构去描述,所以有多少块骨骼,在Mesh中就有多少个SkinWeights对象。
注意,一般把SkinWeights视作Mesh的一部分。这种Mesh又称Skinned Mesh (蒙皮网格)
SkinWeights 结构如下:
{
STRINGtransformNodeName;//骨骼名
DWORD nWeights; //权重数组的元素个数,即该骨骼相关的顶点个数
array DWORD vertexIndices[nWeights];//受该骨骼控制的顶点索引,实际上定义了该骨骼的蒙皮
array float weights[nWeights];//蒙皮各顶点的受本骨骼影响的权值
Matrix4x4 matrixOffset; //骨骼偏移矩阵,用来从初始Mesh坐标,反向计算顶点在子骨骼坐标系中的初始坐标。
}
在有的书中,把上面的matrixOffset叫骨骼权重矩阵,是不恰当的。应该称为骨骼偏移矩阵比较合适。
[问题] 在整个动画过程中,子骨骼运动矩阵的数值是不断变化的。上面的骨骼偏移矩阵变化吗?有没有必要重新计算?它在什么时候使用?
答:各骨骼的偏移矩阵matrixOffset专门用来从原始Mesh数据计算出各顶点相对于骨骼坐标系的原始坐标。在绘制前,把它与当前变换矩阵相乘,就可以得到该骨骼的当前的最终变换矩阵。 总之,骨骼偏移矩阵是与原始Mesh顶点数值相关联的,在整个动画过程中是不变的,也不应该变。在动画过程中变化是当前骨骼变换矩阵,可由.X中的AnimatonKey中的各时刻矩阵得到。这个矩阵乘法在示例中的对应代码如下:
D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );
即,D3DXMatrixMultiply(输出最终世界矩阵, 该骨骼的偏移矩阵, 该骨骼的变换矩阵)
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;
D3DXMATRIXTransformationMatrix; //本骨骼的转换矩阵
LPD3DXMESHCONTAINER pMeshContainer; //本骨骼所对应Mesh数据
struct _D3DXFRAME *pFrameSibling; //兄弟骨骼
struct _D3DXFRAME *pFrameFirstChild;//子骨骼
} D3DXFRAME, *LPD3DXFRAME;
自定义数据容器,其数据来源由上面接口的CreateMeshContainer()函数提供
typedef struct _D3DXMESHCONTAINER
{
LPSTR Name; //容器名
D3DXMESHDATAMeshData; //Mesh数据,可创建SkinMesh取代这个Mesh
LPD3DXMATERIALpMaterials; //材质数组
LPD3DXEFFECTINSTANCEpEffects;
DWORD NumMaterials;//材质数
DWORD*pAdjacency;//邻接三角形数组
LPD3DXSKININFOpSkinInfo; //蒙皮信息,其中含.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对象的个数可能多于骨骼数。
3.3 分析CAllocateHierarchy类
下面继续研究自定义数据容器CAllocateHierarchy,顾名思义,该类是在加载过程中自行分配层次数据空间。它有4个成员,都是重载D3D的接口虚函数。
它的成员CreateFrame()是用来创建D3DXFrame对象的,而CreateMeshContainer()是用来创建Mesh数据对象的。你可以在这两个函数中下断点,发现CreateFrame会运行多次,而CreateMeshContainer只运行一次,再次验证了上面的说法。
值得注意的是,示例对上面的D3DXFRAME,D3DXMESHCONTAINER两个结构做了扩展,分别代之以D3DXFRAME_DERIVED结构和D3DXMESHCONTAINER_DERIVED结构,以集中存储数据方便程序处理。
CreateFrame()处理比较简单,你只是new一个Frame对象空间,填入传进来的Name,其它内容由DX负责维护填充。
CreateMeshContainer()较为复杂。它的任务一是保存传入的网格数据数据,二是根据这些数据及蒙皮信息调用GenerateSkinnedMesh()函数生成蒙皮网格。只有这个新的BlendMesh才能在Render()时支持顶点混合,完成蒙皮的显示。在D3DXMESHCONTAINER_DERIVED结构中,用pOrigMesh保存旧的Mesh普通网格信息。而Meshdata.Mesh则指向新产生的BlendMesh
在这个函数中,多次用到了AddRef(),对COM不熟悉的新手容易困惑。D3D是COM组件,它在服务进程中运行,而不在当前的客户进程中。在DX组件运行过程中,要创建一系列接口对象,如CreateDevice()返回接口指针,这些接口及其占用内存什么时候释放,要通过“引用计数”的技术来解决。AddRef()给这个接口指针的计数加1,而Release()会将之减1。一旦减到0,表示没有客户使用了,相关的接口就释放了。 由此可知,每次调用Rlease()后,并不一定会释放内存,而是当引用计数归0时释放内存。
这样,对接口指针的使用,就像维护堆栈的平衡一样,要仔细,而且按照某种约定规则使用。
但平时D3D编程中,怎么不用AddRef()呢?这是由于一个接口指针,如ID3DDevice,或VertexBuf指针,都是D3DXCreate出来的,在Create时候,在内部已经事先AddRef()了,你就不需要再做这工作了。只要你在不用时,调用 p指针->Relase()就释放了。一般编程,特别是小型示例程序,都是初始化时建立一次,关闭时释放,都遵守了这种约定,所以不存在这种问题。
但在CreateMeshContainer()函数中,以多种方式使用了指针,在局部指针变量中来回传递,所以问题复杂化了。在COM编程中约定,任何时候地接口指针赋值(复制),都要AddRef(),在指针变量结束生命期前,再Release(). 但许多程序员都不是严格这么做。因为在局部变量用完就废了,先AddRef()增加计数再Release()减少,和直接使用最后是等效的。几乎是多此一举。这与编程习惯有关系。一旦引用计数不对,如果没有统一的习惯,不好排查。在CreateMeshContainer()中,对接口指针的使用有三种方式,例举如下:
方式一:不使用AddRef()。和普通指针一样,临时变量是左值,接口指针是右值,直接赋值使用。如:
pMesh = pMeshData->pMesh;
这是由于pMesh是局部变量,它只是临时引用一下,没必要为它先AddRef(),后Release()。
方式二:隐式的使用AddRef()。 由于用到了一些内部有AddRef()动作的函数,就要按照COM约定,在子程序结束前Release()
pMesh->GetDevice(&pd3dDevice);//此处d3d设备引用计数已经加1
....
SAFE_RELEASE(pd3dDevice);//--此处将引用计数减1,并不是真的释放d3d设备
在本例中,pd3dDevice在GetDevice()中已经Addref()过了,所以,在退出CreateMeshContainer()前,必须pd3dDevice->Release()
方式三:显式的使用AddRef()。 如果一个指针值,不是由D3DXCreate出来的,而是通过赋值方式复制给一个全局变量或长期变量的。 所以,可以通过AddRef()的方式来延迟该对象的释放。因为,如果不AddRef(),极有可能在函数返回该对象就可能释放了。它就像一个加油站,使得传入对象的寿命延长至自己控制范围内。用了AddRef(),就要在相关的Destroy中添加Release()。
在本函数,有三处这样的语句:
pMeshContainer->MeshData.pMesh = pMesh;
pMeshContainer->MeshData.Type = D3DXMESHTYPE_MESH;
pMesh->AddRef();
....
pMeshContainer->pSkinInfo = pSkinInfo;
pSkinInfo->AddRef();
pMeshContainer->pOrigMesh = pMesh;
pMesh->AddRef();
....
将来在DestroyMeshContainer()中,要释放这些指针:
....
SAFE_RELEASE( pMeshContainer->MeshData.pMesh );
SAFE_RELEASE( pMeshContainer->pSkinInfo );
SAFE_RELEASE( pMeshContainer->pOrigMesh );
由于这些指针值的创建、更改等都是用户自己经营的,所以务必要加前后吻合,在CreateMeshContainer()中AddRef(),在DestroyMeshContainer()中Release().
再来看数据的保存部分。
在CreateMeshContainer()的传入参数中,有pMeshData,pMaterials,pEffectInstances,NumMaterials,pAdjacency,pSkinInfo
你需要把这些数据保存到自己的D3DXMESHCONTAINER对象中。并且其中的所有数组所需的空间都要在全局堆中new出来。所以在该代码中,有如下new:
pMeshContainer = new D3DXMESHCONTAINER_DERIVED;//自定义的扩展数据容器对象
memset(pMeshContainer, 0, sizeof(D3DXMESHCONTAINER_DERIVED));//初始化pMeshContainer,清0
...
pMeshContainer->pMaterials = new D3DXMATERIAL[pMeshContainer->NumMaterials];//准备保存材质
pMeshContainer->ppTextures = new LPDIRECT3DTEXTURE9[pMeshContainer->NumMaterials];//准备创建纹理对象。它声明在扩展部分。
pMeshContainer->pAdjacency = new DWORD[NumFaces*3];//准备保存邻接三角形数组,NumFaces = pMesh->GetNumFaces();
然后,对数据进行memcpy保存。pEffectInstances由于在绘制中不需要,并没进行保存。对于没有贴图的赋以默认材质属性。
值得注意的是,所有这些new,必须在DestroyMeshContainer()时进行delete.
接下来的处理中,如果发现Mesh的FVF中没有法向量,要用CloneMeshFVF()重建Mesh,计算顶点平均法向量。以备光照处理。
最后,我们看看蒙皮信息pSkinInfo的处理。这是重头戏。
如果发现pSkinInfo!=NULL,就准备着手从各个蒙皮骨骼信息创建SkinMesh.
首先,用扩展容器结构D3DXMESHCONTAINER_DERIVED中的各属性保存原Mesh指针值,pMeshContainer->pOrigMesh = pMesh, 因为接下来我们要创建SkinMesh替代原Mesh.然后,把SkinInfo中的各骨骼的偏移矩阵保存到pMeshContainer->pBoneOffsetMatrices中
cBones = pSkinInfo->GetNumBones();
pMeshContainer->pBoneOffsetMatrices = new D3DXMATRIX[cBones];
.....
每个“骨骼偏移矩阵”pBoneOffsetMatrices,在将来DrawMeshContainer()中是必须要用的。因为原始Mesh中的顶点数据乘以“骨骼偏移矩阵”,再乘以“变换矩阵”,才能求得各骨骼顶点在世界坐标系中的坐标。 即:
骨骼上各点在世界坐标系中的新坐标=初始网格中的各点坐标*骨骼偏移矩阵*骨骼当前的变换矩阵
其中,“初始网格中的各点坐标*骨骼偏移矩阵” = 骨骼上各点初始时刻在该骨骼坐标系中的局部坐标
做了以上工作后,调用GenerateSkinnedMesh(pMeshContainer),创建SkinMesh. 接下来,我们看看GenerateSkinnedMesh()做了哪些工作。
3.4 怎样生成蒙皮网格SkinMesh? GenerateSkinnedMesh()分析
由于要重定义pMeshContainer->MeshData.pMesh,所以先SAFE_RELEASE( pMeshContainer->MeshData.pMesh ); 释放原pMesh
在这个函数中,是根据当前绘图方式设置进行加载数据的。因为顶点混合,有无索引的顶点混合,有含索引的顶点混合,所使用的函数和对应的SkinMesh数据内容也有所不同。
在示例中,自定义了枚举m_SkinningMethod,主要分为D3DNONINDEXED和D3DINDEXED,以有纯软件渲染等。运行示例后,你可以选择菜单中的Options选择不同的渲染方式。
我们着重分析一下带索引的蒙皮网格。在程序中,就是D3DINDEXED相关的部分。
if (m_SkinningMethod == D3DINDEXED){ ....}
注意! 示例默认工作在D3DNONINDEXED下,如果要跟踪D3DINDEXED部分的代码,必须选择菜单中的Options选择indexed!
最主要的,要通过DX的ConvertToIndexedBlendedMesh()函数,生成支持“索引顶点混合”的SkinMesh.有关索引顶点混合的技术,你可以在DXSDK帮助文件中搜索“Indexed Vertex Blending”主题,对着英文和插图将就看,确有收获。
要想用硬件对顶点进行混合,那么参与混合者不能太多。也就是说同时影响一个顶点的骨骼数不能多。我们假定一个顶点最多同时受4个骨骼的影响(也就是同时最多有4个骨骼矩阵参与加权求和),那么同时影响一个三角形面的骨骼数最多就是3*4=12个。
我们用NumMaxFaceInfl表示影响一个三角面的最多骨骼矩阵数,那么,通过调用pSkinInfo->GetMaxFaceInfluences()获取这个数值,一般也就3-4。如果这个数值太大,我们强制使用NumMaxFaceInfl = min(NumMaxFaceInfl, 12);来最多取值12。
用NumMaxFaceInfl 这个数值干什么呢? 我们用来它分析当前的显卡倒底行不行。
if (m_d3dCaps.MaxVertexBlendMatrixIndex + 1 < NumMaxFaceInfl)//如果显卡达不到该要求
{
//很奇怪。2005年底买的GeForce 6600GT显卡,竟然m_d3dCaps.MaxVertexBlendMatrixIndex=0, 不支持索引顶点混合!是驱动问题还是怎么了?
//但它支持非索引混合。或者,也许要用HLSL支持混合。看起来,3D编程要多考虑。
..
pMeshContainer->UseSoftwareVP = true;//用软件渲染顶点。显然不实用。
}
else
{
pMeshContainer->NumPaletteEntries = min( ( m_d3dCaps.MaxVertexBlendMatrixIndex + 1 ) / 2,
[來源:GameRes.com]; pMeshContainer->pSkinInfo->GetNumBones() );//--什么意思?
pMeshContainer->UseSoftwareVP = false;//采用硬件顶点混合。
Flags |= D3DXMESH_MANAGED;
}
[评论]在上面有一行代码:
pMeshContainer->NumPaletteEntries = min( ( m_d3dCaps.MaxVertexBlendMatrixIndex + 1 ) / 2,pMeshContainer->pSkinInfo->GetNumBones() );
尽管作者加了大段注释,还是让人一头雾水。其实,我们做一个实验,反尔更能理解它的用途。
第一步,你在这句话后面下一个断点,看一下在你机器上这个数值。我的ATI 9550显卡机器上是19。比tiny.x中的骨骼数35少很多。
第二步,你将上面=右边瞎填一个大于4的数字,比如6。编译后照样运行。而且效果上几乎看不出任何差别。
为什么会这样呢? 我们在绘制代码部分,看看这个数值起什么作用。
在DrawMeshContainer()代码中,我们查找D3DINDEXED相关的部分。在mesh各子集的DrawSubset()之前,有如下代码:
for (iAttrib = 0; iAttrib < pMeshContainer->NumAttributeGroups; iAttrib++)
{
// first calculate all the world matrices
for (iPaletteEntry = 0; iPaletteEntry < pMeshContainer->NumPaletteEntries; ++iPaletteEntry)
{
iMatrixIndex = pBoneComb[iAttrib].BoneId[iPaletteEntry];
if (iMatrixIndex != UINT_MAX)
{
D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );
m_pd3dDevice->SetTransform( D3DTS_WORLDMATRIX( iPaletteEntry ), &matTemp );
}
}
...
}
下面仔细评估一下这些代码.
先注意看其中奇怪的D3DTS_WORLDMATRIX()宏,我们以前还没这样用过。它是做什么用的呢?通过查DXSDK帮助,我们在Geometry Blending主题中找到相关说明,并在"Indexed Vertex Blending"主题中给出了内部实现原理。原来,当你用m_pd3dDevice->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE);开启了索引顶点混合后,在硬件上就启用了“palette of matrices”,即矩阵寄存器组,它最多支持同时256个索引。就像过去用256色调色板来表现彩色一样。D3DTS_WORLDMATRIX()宏就是有256-511这256个数表示矩阵索引号。
这些矩阵参与如下计算:
V最终顶点位置=V*M[索引值1]*权重1 + V*M[索引值2]*权重2 + ....+V*M[索引n]*(1-其它权重和)
这个公式的来源,相信大家在众多资料上见过,不赘述。 当然,我们也可以用程序完成这个蒙皮计算过程,但逐个读顶点却很麻烦。现在是由硬件代劳了。我们只设矩阵就行了。
我们用m_pd3dDevice->SetTransform( D3DTS_WORLDMATRIX( iPaletteEntry ), &matTemp );这种方式设定各索引对应的矩阵。
那么权重呢?我们怎么设?原来在上面所说的DX提供的ConvertToIndexedBlendedMesh()函数中,生成SkinMesh时,各网格顶点格式FVF已经有变化了,增加了新格式,D3DFVF_XYZB2,D3DFVF_LASTBETA_UBYTE4,用以记录顶点对应的权重值以及矩阵索引。如下
struct VERTEX
{
float x,y,z;
float weight;
DWORD matrixIndices;
float normal[3];
};
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZB2 | D3DFVF_LASTBETA_UBYTE4 |D3DFVF_NORMAL);
D3DFVF_LASTBETA_UBYTE4对应于DWORD数值,用于矩阵索引时,每个字节表示一个索引,最多可以允许4个索引,同时有4个矩阵参于该点的混合。如果一次绘制中涉及了9块骨骼矩阵,你可以把这9个矩阵全部用SetTransform设置到矩阵寄存器中,但每个顶点在渲染时,最多使用其中的4个。由此可知,pMeshContainer->NumPaletteEntries这个数值,确定了一趟DrawSubset绘制所用到的矩阵个数,个数越多,在一趟绘制中就可以纳入的更多顶点。所以,当我们减少pMeshContainer->NumPaletteEntries这个数值时,pMeshContainer->NumAttributeGroups数值就会增加。也就是说,一趟绘制中所允许涉及的骨骼数越少,那么子集的数量NumAttributeGroups就会增加,需要多绘几趟。
你可以在此下断点观察,当NumPaletteEntries=19时,NumAttributeGroups=3 当NumPaletteEntries=6时,NumAttributeGroups=12 当NumPaletteEntries=4时,NumAttributeGroups=31,几乎和无索引时的分组一样多了。
顶点中的权重weight存放了它当前骨骼的权重。(一个顶点对应的多个骨骼权重怎么存放?是不是在当前子集中有多个同样的顶点,权重不同,对应的矩阵索引不同,然后混合)
由上所述,ConvertToIndexedBlendedMesh()是一个很重要函数,由DX自动将Mesh顶点分组成多个子集,以便DrawSubset. 你必须把它的返回参数都记录下来,在绘制时使用。
四. 怎样绘制显示动画?
DrawFrame()用来绘制整个X框架。它遍历各个框架,找到Mesh不为空的进行绘制。(其实整个.x中通常只有一个不为空,见上文所述)
DrawMeshContainer()是绘制函数。
4.1 怎样开启顶点混合?
注意应用有关的Vertex Blending技术。如在索引方式的绘制中,
m_pd3dDevice->SetRenderState(D3DRS_VERTEXBLEND, pMeshContainer->NumInfl - 1);
其实是设定了D3DVBF_2WEIGHTS或D3DVBF_3WEIGHTS
注意要m_pd3dDevice->SetRenderState(D3DRS_INDEXEDVERTEXBLENDENABLE, TRUE);
4.2 矩阵的刷新:
首先,在FrameMove()调用m_pAnimController->SetTime()设置当前时间(或在DX9.0c中用AdvanceTime()设置时间差),从而刷新各个pFrame->TransformationMatrix,即骨骼转换矩阵
其次,调用UpdateFrameMatrices()做乘法累积,计算出各骨骼坐标系到根世界转换矩阵。
最后,在绘制前,将该转换矩阵左乘偏移矩阵,得到最终的转换矩阵。
D3DXMatrixMultiply( &matTemp, &pMeshContainer->pBoneOffsetMatrices[iMatrixIndex], pMeshContainer->ppBoneMatrixPtrs[iMatrixIndex] );
由此可见,你如果注释掉了m_pAnimController->SetTime,画面肯定停了。
4.3 绘制输出 是在DrawMeshContainer()中,调用SkinMesh的DrawSubset进行绘制。一些细节内容如D3DTS_WORLDMATRIX(),在上面已经有说明,不再罗嗦。
4.4 关于示例中多种绘制方式分析
在示例中,用到了多种渲染方式,包括传统的非索引顶点混合,还有新兴的HLSL方式。而且我发现,ATI RADEON 9550 显卡MaxVertexBlendMatrixIndex=37,而价格更高的Gefoce 6600GT MaxVertexBlendMatrixIndex竟然为0,不支持index vertex blending!
所以,还是有必要分析一下该示例中各种vertex blending方式的处理,以便掌握多种绘制方式适应不同显卡。
经测试,示例中所涉及的多种方式,由慢到快,依次是以下几种:
SOFTWARE,
D3DNONINDEXED,
D3DINDEXED,
D3DINDEXEDVS,
D3DINDEXEDHLSLVS,
从最慢的SW到最快的HLSL,大约相差20%,有时会大到40%。 差别不是特别悬殊的原因,主要是顶点混合并不是瓶颈。
关于顶点处理方式,是在创建D3D设备时指定的。共有三种方式:
D3DCREATE_SOFTWARE_VERTEXPROCESSING 软件顶点运算(简记 sw vp)
D3DCREATE_HARDWARE_VERTEXPROCESSING 硬件顶点运算。必须有这项才支持有HAL (简记 hw vp)
D3DCREATE_MIXED_VERTEXPROCESSING 混合顶点运算,即硬件+软件 (简记 mixed vp)
一旦用D3DCREATE_HARDWARE_VERTEXPROCESSING方式创建设备,就只能在硬件方式下进行顶点处理。如果调用m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)来切换到软件顶点处理,HRESULT会返回失败。
所以,如果你对客户的显卡没有足够的信息,就用D3DCREATE_MIXED_VERTEXPROCESSING方式创建设备。它默认工作方式是HAL。一旦发现进行某种绘制时硬件能力不够,就可以调用调用m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)切换到软件模式。在示例中就是这么做的,启动示例后,运行在mixed模式下。
在Gefoce6600GT显卡中,由于D3DINDEXED方式不支持,采用了软件混合方式,在这种方式下速度甚至比SOFTWARE慢。HLSL还好,还是最快。
要确定设备的硬件顶点处理能力,可以参考D3DCAPS9结构的VertexProcessingCaps成员。可以获取下列属性
MaxActiveLights,MaxUserClipPlanes,MaxVertexBlendMatrices,MaxStreams,MaxVertexIndex
(1)D3DNONINDEXED方式:
首先看GenerateSkinnedMesh()中怎样创建蒙皮网格的。
这种方式下,用ConvertToBlendedMesh()建立蒙皮网格,而不是ConvertToIndexBlendedMesh()
为了绘制蒙皮,在这个函数中对Mesh各子集的顶点再次进行的分组。分组的标准是各顶点(或三角面)所涉及的骨骼矩阵个数不超过pMeshContainer->NumInfl个。(这个数字是由在ConvertToBlendedMesh()时,由参数pMaxFaceInfl返回的)。一个Mesh子集可能被拆开成多个分组。 最后,分组的属性保存在pBoneCombinationBuf中,如子集ID,该子集的各骨骼ID,起始三角面,三角面个数等供绘制时使用,分组的个数保存在pMeshContainer->NumAttributeGroups中。
接下来检查每个分组所涉及的骨骼数,是不是超过硬件允许的最大混合矩阵数---MaxVertexBlendMatrices。如果超过了就把所有分组截为两大部分,前一部分用硬件混合,后一部分采用软件混合。而且,一旦发现有需要软件混合,要采用CloneMeshFVF(D3DXMESH_SOFTWAREPROCESSING|...)的方式重新生成网格。
再来看绘制部分DrawMeshContainer()
用pBoneComb指向骨骼分组属性,扫描各分组。找出其中骨骼数满足硬件性能的用进行绘制。
然后开启软件顶点渲染m_pd3dDevice->SetSoftwareVertexProcessing(TRUE),对那些骨骼数超出硬件性能的进行绘制。
SetSoftwareVertexProcessing()需要当前d3d设备以D3DCREATE_MIXED_VERTEXPROCESSING方式创建。
(2)D3DINDEXED,这种方式上面分析过了,从略。用pMeshContainer->UseSoftwareVP表示是否采用软件绘制。
值得注意的是在这种方式下,一旦硬件性能不足,会彻底使用软件顶点渲染,而不是像上面一样拆为两部分。
(3)D3DINDEXEDVS,D3DINDEXEDHLSLVS
这种情况下使用了着色器和高级着色语言。超出本文主旨,讨论从略。
(4)SOFTWARE--软件方式? 让人有些迷惑,与上面的m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)有何区别?
从代码看,这种方式下反而比较简单。GenerateSkinnedMesh()中,
先直接从原始Mesh克隆一个Mesh,然后读取它的材质属性数组。开辟一个空间m_pBoneMatrices,用以存放各块骨骼的转换矩阵。
在绘制时,从pMeshContainer中的变换矩阵乘以偏移矩阵,放在pBoneMatrices中。把这个矩阵数组,以原Mesh的顶点作为源顶点,以新克隆的MeshData.pMesh做为目标顶点,调用pSkinInfo->UpdateSkinnedMesh(),用软件方式计算各骨骼顶点的新位置(相当于软件计算方式蒙皮)。
然后调用MeshData.pMesh->DrawSubset()绘制。
可见,在SOFTWARE方式下,最终顶点的渲染还是HAL方式的,只不过蒙皮计算是由软件完成的。它和上面的m_pd3dDevice->SetSoftwareVertexProcessing(TRUE)直接设置软件顶点渲染还是有区别的。
相关推荐
### 关于骨骼动画Skinned Mesh的解析 #### 一、骨骼动画原理 骨骼动画是一种广泛应用于游戏和3D动画制作中的技术,它通过模拟人体骨骼结构来实现模型的动画效果。这种技术的核心在于通过控制虚拟骨架上的关节来...
一键将角色的Animator或Animation去掉,将SkinnedMeshRender更换为一般的Mesh Render,完成将动画效果的实现从CPU转移到GPU运算的目的,来利用GPU Instancing技术减少Draw Call。 • Convert assets quickly and ...
通过SkinnedMeshRenderer 实现换装系统,不在只是单纯的设置显示隐藏
SkinnedMeshRenderer 模型贴图网格分块过多,导致 batches过大,影响程序运行,通过将单个模型的网格贴图合并达到优化效果 详解:https://blog.csdn.net/weixin_41843959/article/details/80500930
根据提供的文件信息,本文将详细解析“d3dx_skinnedmesh.pdf”中关于Direct3D中的骨骼动画技术的关键知识点。 ### 骨骼动画在Direct3D中的应用 #### 一、概述 骨骼动画(Skinned Mesh Animation)是现代实时三维...
Unity 中基于skinnedMesh的 贴图插件, 非常方便使用 ,
Character Customization 修改后适用于Unity5.0 写了一个直接从Prefab绕过Assetbundle的SkinnedMesh小用例
《DX9与Skinned Mesh:一个简化的骨骼动画实例》 在计算机图形学领域,DirectX 9(简称DX9)是一种广泛应用于游戏开发和3D应用的技术,它提供了丰富的图形编程接口,允许开发者直接控制硬件加速的图形处理。本文将...
UVPaint (Skinned Mesh Decal System) 1.84e 下载后为unitypackage 无需解压
在DirectX 9.0 SDK的SkinMesh示例中,皮肤网格(SkinnedMesh)的实现通常与应用程序紧密耦合,导致在其他样本中添加一个皮肤网格角色变得相对复杂。Phil Taylor在2003年1月的文章中提出了一种模块化的方法,旨在提高...
学习threejs,通过SkinnedMesh来创建骨骼和蒙皮动画,json模型
3. **骨骼蒙皮动画(Skinned Mesh)**:这是一种高级的动画技术,它结合了Morph动画和关节动画的优点。骨骼蒙皮动画不仅保留了关节动画的灵活性,还通过蒙皮技术解决了裂缝问题。该技术利用骨骼层级结构控制顶点位置的...
### Skinned Mesh Character Animation with Direct3D 9.0c #### 一、概述与基础知识 **1.1 概述** 本章旨在探讨利用Direct3D 9.0c进行实时角色动画制作的方法和技术。现代游戏开发和3D模拟程序中,实时角色动画...
unity UVPaint (skinned mesh Decal System) 1.84e Requires Unity 4.6.6 or higher. Runtime Texturing! UVPaint creates decals and immediately bakes them in needed ShaderProperty texture on 3D ob jects. ...
在实际开发中,你需要考虑如何加载和解析骨骼和动画数据,以及如何有效地更新和应用皮肤矩阵。此外,优化是另一个重要方面,因为骨骼动画可能会对GPU造成较大的负担。例如,通过减少骨骼数量、优化权重分配或使用更...
骨骼蒙皮动画例子,VC6无法编译通过,必须在VC2003以上才能通过
SkinnedMeshRender是Unity3D中用于绘制带有骨骼动画的网格模型的关键组件。它的主要属性包括: 1. bones:存储骨骼(Transform)的数组,定义了角色的身体结构。 2. materials:存储材质(Material)的数组,用于...
2. **支持更多类型的对象**:除了基本的Mesh对象,新版本还支持Skinned Meshes(带骨骼动画的网格)和地形等复杂对象的合并,扩大了应用范围。 3. **更好的UV处理**:3.24.1版改进了UV坐标处理,确保合并后的网格...
在提供的代码示例中,`Generate`函数展示了如何使用CombineInstance结构体来组合多个SkinnedMeshRenderer。这个函数并不真正地将子网格合并成单一的Mesh,而是将它们作为新Mesh对象的子网格,这样可以保留各自不同的...
《MFC DirectX3D Skinned Mesh DLL 0.1:深度解析与应用》 MFC (Microsoft Foundation Classes) 是微软提供的一套C++类库,用于简化Windows应用程序的开发。DirectX3D则是微软的图形API,主要用于游戏开发和3D图形...