骨骼动画及微软示例Skinned Mesh的解析.docx
- 文档编号:28605993
- 上传时间:2023-07-19
- 格式:DOCX
- 页数:23
- 大小:30.42KB
骨骼动画及微软示例Skinned Mesh的解析.docx
《骨骼动画及微软示例Skinned Mesh的解析.docx》由会员分享,可在线阅读,更多相关《骨骼动画及微软示例Skinned Mesh的解析.docx(23页珍藏版)》请在冰豆网上搜索。
骨骼动画及微软示例SkinnedMesh的解析
骨骼动画及微软示例:
SkinnedMesh的解析
骨骼动画是D3D的一个重要应用。
尽管微软DXSDK提供了示例SkinnedMesh,但由于涉及众多概念和技术局,顺利上手。
文中所述都是参照各种资料加上自己的理解,也有可能出些偏差,有则回贴拍砖,无则权当一笑一骨骼动画原理
原理方面在网上资料比较多,大家都基本明白。
在此说一下重点:
!
|'s9_9L;M!
x"_总体上,绝大部分动画实现原理一致,就是“提供一种机制,描述各顶点位置随时间的变化”。
有三种方法:
1.1关节动画:
由于大部分运动,都是皮肤随骨骼在动,皮肤相对于它的骨骼本身并没有发生运动,所以只要知,从子骨骼用矩阵乘法累积到最顶层根骨骼,就可以得到每个子骨骼相对于世界坐标系的转换矩阵。
这种动画,只须用普通Mesh保存最初始的各顶点坐标,以及一系列后续时刻所对应的各骨骼的运动矩阵。
不通过运算调节动作。
缺点是在两段骨骼交接处,容易产生裂缝,影响效果。
1.3骨骼蒙皮动画(skinnedMesh)
/A'R4r5L;R4C
1.2渐变动画:
通过保存一系列时刻的顶点坐标来完成动画。
虽然比较逼真,但占用大量空间,灵活性也不高
$T3P#J,y4F9Q&r"C-R6U
相当于上面两方法的折中。
现在比较流行。
在关节动画的基础上,利用顶点混合(VertexBlend)技术,对于关节附近的顶点,由影响这些顶点的两段(或裂缝问题。
+r2E/c+O+R:
o
这里,引入一个D3D技术概念:
“VertexBlending”---顶点混合技术。
比如说,你肯定用过SetTransform个问题后文会讲到。
你也可以在微软的DXSDK的帮助文件中搜索“GeometryBlending”主题,有裂缝及其二X文件如何保存骨骼动画
理解X文件格式,对用好相关的DX函数是非常重要的。
不含动画的普通X文件,有一个Mesh单元,保存了各顶点信息、各三角面的索引信息、材质种类及定义等。
动画X文件,则在这个单元中增加了“各骨骼蒙皮信息”、“骨骼层次及结构信息”、“各时刻骨骼矩阵信息”等。
(?
2.1网格蒙皮信息:
首先,在Mesh{}单元中,在原有的普通网格顶点数据基础上,新增了XSkinMeshHead其中,XSkinMeshHeader是总括,举一实例,如下:
XSkinMeshHeader{&a2`8J-o(l-p%mR$p8RK-t.?
"?
9J3r9G0b2G8
D.|
x7`&l!
A0d)b#O9{5@7G2,//一个顶点可以受到骨骼影响的最大骨骼数,可用于计算共同作用时减少遍历次数35//当前Mesh的骨骼总数。
}4,//一个三角面可以受到骨骼影响的最大骨骼数。
这个数字对硬件顶点混合计算提出了基本要求。
由于每个骨骼的蒙皮信息都需要用SkinWeights结构去描述,所以有多少块骨骼,在Mesh中就有多少个Sk注意,一般把SkinWeights视作Mesh的一部分。
这种Mesh又称SkinnedMesh(蒙皮网格)SkinWeights结构如下:
{STRINGtransformNodeName;//骨骼名
8p-J7G/e-y+B8[
+t4\+`0B/q&`3Y4G;K
DWORDnWeights;//权重数组的元素个数,即该骨骼相关的顶点个数arrayDWORDvertexIndices[nWeights];//受该骨骼控制的顶点索引,实际上定义了该骨骼的蒙皮arrayfloatweights[nWeights];//蒙皮各顶点的受本骨骼影响的权值
$LG(`*C$p2}-Q!
S1M%l)QMatrix4matrixOffset;//骨骼偏移矩阵,用来从初始Mesh坐标,反向计算顶点在子骨骼坐标}
0K2Q,Y)@/Y;q0d#R
*d*J2\:
S-{*L)W4N在有的书中,把上面的matrixOffset叫骨骼权重矩阵,是不恰当的。
应该称为骨骼偏移矩阵比较合适。
[问题]在整个动画过程中,子骨骼运动矩阵的数值是不断变化的。
上面的骨骼偏移矩阵变化吗?
有没有必要重答:
各骨骼的偏移矩阵matrixOffset专门用来从原始Mesh数据计算出各顶点相对于骨骼坐标系的原始坐标。
矩阵是与原始Mesh顶点数值相关联的,在整个动画过程中是不变的,也不应该变。
在动画过程中变化是当前码如下:
D3DXMatrixMultiply(&matTemp,&pMeshContainer->pBoneOffsetMatrices[iMatrixIndex],pMesh即,D3DXMatrixMultiply(输出最终世界矩阵,该骨骼的偏移矩阵,该骨骼的变换矩阵)'F8z;~#v.W"S
4M2H6]&^3?
-^2E
2.2骨骼层次信息
在X文件中,Frame是基本的组成单元。
又称框架Frame。
一个.x可以有多个Frame。
(注意此处的Fram框架Frame允许嵌套,这样就存在父子框架了。
而并列的框架,称为兄弟框架。
这两种关系组合在一起,即可每个框架结构的最前面,有一个FrameTransformMatrix矩阵数据,描述了该框架相对于父框架的变换矩阵。
这种层次结构,使得X文件能描述许多复杂的物体。
如地形场景。
在骨骼动画文件中,框架结构可直接拿来描述人物骨骼的层次结构。
框架的名字通常为对应的骨骼名。
如“左上臂->左前臂->手掌->手指”就形成一个父子骨骼链。
而左上臂与右上臂是并行关系。
-n2D5V-]&G数据示例:
D:
\D9XSDK\Samples\Media\tiny.x
P7P5~+r2T1e
Frame...{
F;J:
uy&m5c
.....
I!
|:
y:
l)?
!
b+_%\,]
FrameBip01_R_Calf{//子骨骼
FrameTransformMatrix{
(K&f&y%q'Mf
1.000,-
0."000691,-
0."000,
0."000,
0."000691,
1."000,-
0."000,
0."000,
0."00}FrameBip01_R_Foot{//--孙子骨骼:
m;A3U(^-m6Z!
c*o7\5gZ.c7i,o+N!
l,J:
X4q8l2n:
v/Q'}#M2\
FrameTransformMatrix{}....缩进}}
&\3K/[+S7i+l4Z.|
0."988831,
0."124156,
0."082452,
0."000,-
0."122246,
0."992109,-
0."027835,
0."000,-
0."08
答:
一般来说,每个动画文件只有一个Mesh网格,包含物体所有顶点信息。
[问题]查看示例tiny.x文件,发现只有根框架下有一个Mesh,包含了所有顶点信息。
其它各个Frame都没有3L;_3f(c*M0?
1R其它Frame,只是借用来描述各骨骼的层次信息,没必要再定义骨骼网格。
每块骨骼对应的蒙皮顶点信息,坐标,要借助SkinWeights中的顶点索引来进行重新计算。
2.3动画信息:
&O&H*M6q4F:
f7|*J#I6]$x/Z3p
由一系列AnimatonKey组成,数据示例如下:
AnimationKey{
3]&_+z)|1S;[0b3R6I-v7Q8G0H4a
4;--动画类型4表示矩阵
62;--动画帧数,即下面矩阵个数
3Y%m1k/j/g.|2t&W
0;16;
1."000,-
0."000691,-
0."000,
0."000,
0."000691,
1."000,
0."000,
0."000,
0."0
0.".上面红数字表示时刻tick,兰数字表示数值的个数。
...其它各时刻矩阵...
R:
T#v&D'FI"H'H#n
80;16;
0."992696,-
0."120646,-
0."000,
0."000,
0."120646,
0."992696,
0."000,
0."000,-0
3|;H7u0U6j)N,l6D&b
{Bip01_R_Calf}--对应的骨骼对象引用}-p$h,h#i*G"O{-t'
)n*m!
Y+j/W4q+e/Z.a
注意:
9K;y8z/?
9u5A2F
(1)每块骨骼都有一个AnimationKey{}.
(2)在上面数据结构中,主要保存了各典型时刻的该骨骼相对于父的变换矩阵.
(3)在0时刻的矩阵,与该骨骼对应的前面的Frame所对应的矩阵是相同的。
如FrameBip01_R_Calf{}中的后动画运行时,DX会提供一种功能,用AnimatonKey中的对应数据刷新初始的变换矩阵(也可能启用关键帧三怎样从X文件加载骨骼动画信息?
3.1负责加载的函数:
可能有多种加载方式,在此以SDK中的示例为准,叙述一种标准加载方式,需要用到DX函数D3DXLoadMHRESULTWINAPI
%F(B)^%b1t
D3DXLoadMeshHierarchyFromX(
LPCSTRFilename,//.x文件名
&T:
@!
O8R.X0z1W8~#Q9N
0@&H6a!
V(j7Z1i0G$@DWORDMeshOptions,//Mesh选项,一般选D3DXMESH_MANAGEDLPDIRECT3DDEVICE9pD3DDevice,//指向D3D设备Device
LPD3DXALLOCATEHIERARCHYpAlloc,//自定义数据容器
#["K3Y"^2S5I/P8@#p4n*o0a/~*v9x(W5e4@&oLPD3DXLOADUSERDATApUserDataLoader,//一般选NULL
LPD3DXANIMATIONCONTROLLER*ppAnimController//返回相应的动画控制器
;`8A"g.~+?
+hLPD3DXFRAME*ppFrameHierarchy,//返回根Frame指针,指向代表整个骨架的Frame层);
这个函数后面的两个输出参数很重要,也很好理解,但输入参数中的自定义数据容器是怎么回事呢?
原来,鉴于动画数据的复杂性,需要你配合完成加载过程。
比如你是否用到自定义扩展结构,Mesh等数据保存DX提供了ID3DXALLOCATEHIERARCHY接口,提供了这个自定义的机会,你重载这个接口的虚函数,在加你需要像下面这样建立一个自定义数据容器类:
y3Y,d'i,y/Y-c$H6`
classCAllocateHierarchy:
publicID3DXAllocateHierarchy{public:
9q1\6d:
V0a.T+`3B&}!
_
9^!
d0B)z/_$H(XSTDMETHOD(CreateFrame)(THIS_LPCTSTRName,LPD3DXFRAME*ppNewFrame);STDMETHOD(CreateMeshContainer)(THIS_LPCTSTRName,LPD3DXMESHDATApMeshData,
DWORD*pAdjacency,LPD3DXSKINFOpSkinInfo,LPD3DXMESHCONTAINER*ppNewMeshContainer);STDMETHOD(DestroyFrame)(THIS_LPD3DXFRAMEpFrameToFree);CAllocateHierarchy(CMyD3DApplication*pApp):
m_pApp(pApp){}public:
CMyD3DApplication*m_pApp;
};
9?
2N*A%D8f.Z2t-h:
J;j6R0j$P*?
1I-c6CG.n*O9A:
W2a"LPD3DXMATERIALpMaterials,LPD3DXEFFECTINSTANCEpEffectInstancesSTDMETHOD(DestroyMeshContainer)(THIS_LPD3DXMESHCONTAINERpMeshContainerBase
4A%q"`'P;{%M1U8\0x3X:
C;M$k
[问题]上面的STDMETHOD是什么意思?
答:
相当于virtualHRESULT__stdcall的宏。
<评论>因为这种类要与D3D的COM接口打交道,不#defineSTDMETHOD(method)virtualHRESULTSTDMETHODCALLTYPEmetho#defineSTDMETHODCALLTYPE__stdcall
这样当写一个函数STDMETHOD(op1(inti))
3.2自定义数据容器以及具体的读取过程:
2d1z5K&`.^5}:
[,Z
"`.W/n1T8t%J$b.L'w
展开后成为:
virtualHRESULT__stdcallop1(inti);
5S'W2Q%j4[(N.M6WO
根据.X文件,在加载过程中,主要有两方面数据需要保存,一个是骨架Frame信息,一个是网格蒙皮Mesh框架信息(对应于骨骼)
typedefstruct_D3DXFRAME{LPSTRName;
4v$s8V&F8O+g5p
2R*N0s4h/e"x9X*`:
}
%F*T!
y%b$s3O3|/|)i$`6D3DXMATRIXTransformationMatrix;//本骨骼的转换矩阵struct_D3DXFRAME*pFrameSibling;//兄弟骨骼
struct_D3DXFRAME*pFrameFirstChild;//子骨骼
}D3DXFRAME,*LPD3DXFRAME;LPD3DXMESHCONTAINERpMeshContainer;//本骨骼所对应Mesh数据5^;c3]-i+m+k
9Y/U.b8H,v!
g9g
自定义数据容器,其数据来源由上面接口的CreateMeshContainer()函数提供
Uh'f'T:
Ctypedefstruct_D3DXMESHCONTAINER{LPSTRName;//容器名
D3DXMESHDATAMeshData;//Mesh数据,可创建SkinMesh取代这个MeshLPD3DXMATERIALpMaterials;//材质数组
LPD3DXEFFECTINSTANCEpEffects;
DWORDNumMaterials;//材质数
5}6I_6X-H(k1E9s
DWORD*pAdjacency;//邻接三角形数组
LPD3DXSKINFOpSkinInfo;//蒙皮信息,其中含.x中的各个skinweight蒙皮顶点索引及各struct_D3DXMESHCONTAINER*pNextMeshContainer;
}D3DXMESHCONTAINER,*LPD3DXMESHCONTAINER;
[评论]
}3},P+r%~#@6?
.在动画文件中,框架通常用来描述骨骼。
可以把Frame视做骨骼,所以不细加区分。
.在D3DXFRAME结构中有一个pMeshContainer指针,难道框架与Mesh是一一对应的吗?
有一个框架(骨骼)就有一个Mesh吗?
怎么.X文件中只有一个Mesh?
难道加载时拆开存放?
答:
从D3DXFrame结构上看,每个Frame都有一个pMeshContainer指针。
这就有三种解释:
第一种,加载到内存后所有的pMeshContainer都指向同一个全局Mesh
(m4z:
|6k#w&v#k9z0}3sl4Z4d2^/q.在上面D3DXFRAME结构中,pFrameSibling,pFrameFirstChild两个指针,常用于递归函数中,遍历整个第二种,加载到内存后,只有一个主框架的pMeshContainer不为空,其它Frame的pMeshContainer第三种,加载到内存后,D3D将Mesh拆分,分开到各骨骼所对应的Frame,每个Frame都有自己的Me这个问题我以前也不是很清楚,通过查看示例源码及跟踪发现,正确解释应该是第2种。
唯一的一个全局MNULL.应该大致如此。
这个问题之所以让人困绕,是因为从后续代码上看,在渲染DrawFrame时,是遍历每mesh中。
mesh,那么变换矩阵怎么组织运算等等。
所以,根据第二种解释,由于只有一个pMeshContainer不为NU0b"d+m7}*z"]1}4q
所以,读取tiny.x文件后,会产生多个D3DXFRAME对象,但只有一个D3DXMESHCONTAINER对象。
在示例代码的CMyD3DApplication:
:
InitDeviceObjects()中,有:
hr=D3DXLoadMeshHierarchyFromX(strMeshPath,D3DXMESH_MANAGED,m_pd3dDevice,&
if(FAILED(hr))
returnhr;
你在运行完这句话后,下一个断点,观察m_pFrameRoot,会发现如下内容:
3u3j8c3D6^1}!
w1E0Q+w!
o
其中的Alloc是就自定义的数据容器对象。
m_pFrameRoot是根骨骼,对遍历很重要。
m_pAnimControlle-Y%b4g)i#Q/L(am_pFrameRoot0x00c59380{Name=0x00c53630"Scene_Root".....}//根框架pMeshContainer0x0000
pFrameSibling0x0000
;qI!
i*@3l:
A0I8]"c;R5r0WP!
@5c*I4w4?
*f
pFrameFirstChild0x00c59428{Name=0x00c53ca8"body"pMeshContainer=0x
0000."..}//子+---pMeshContainer0x0000
+---pFrameSibling0x01419f00{Name=0x00c5ffd8"Box01"pMeshContainer=0x000+---pFrameFirstChild0x00c594d0{Name=0x0000pMeshContainer=0x00c59828//4c*X;_7P!
L8r*E5K(z&_7g
可见,在内存中的Frame布局是与.x中一一对应的。
除了pFrameFirstChild0x00c594d0这个地方的Fram
3.3分析CAllocateHierarchy类
另外一点可以看出,并不是每个Frame都对就一块骨骼,有的是别的用途。
也就是说Frame对象的个数可能下面继续研究自定义数据容器CAllocateHierarchy,顾名思义,该类是在加载过程中自行分配层次数据空间。
它的成员CreateFrame()是用来创建D3DXFrame对象的,而CreateMeshContainer()是用来创建MeshCreateMeshContainer只运行一次,再次验证了上面的说法。
'Q.^/x:
_;y&X)Z&B;Z+n+b/x
值得注意的是,示例对上面的D3DXFRAME,D3DXMESHCONTAINER两个结构做了扩展,分别代之以D3序处理。
%L'z+N8}-G7r1lp%z2H!
["T3p
CreateFrame()处理比较简单,你只是new一个Frame对象空间,填入传进来的Name,其它内容由DX负CreateMeshContainer()较为复杂。
它的任务一是保存传入的网格数据,二是根据这些数据及蒙皮信息调支持顶点混合,完成蒙皮的显示。
在D3DXMESHCONTAINER_DERIVED结构中,用pOrigMesh保存旧的在这个函数中,多次用到了AddRef(),对COM不熟悉的新手容易困惑。
D3D是COM组件,它在服务进程中运返回接口指针,这些接口及其占用内存什么时候释放,要通过“引用计数”的技术来解决。
AddRef()给这个接口了。
由此可知,每次调用Rlease()后,并不一定会释放内存,而是当引用计数归0时释放内存。
这样,对接口指针的使用,就像维护堆栈的平衡一样,要仔细,而且按照某种约定规则使用。
*f!
\;_!
T~#p/G7n1d但平时D3D编程中,怎么不用AddRef()呢?
这是由于一个接口指针,如ID3DDevice,或VertexBuf指针作了。
只要你在不用时,调用p指针->Relase()就释放了。
一般编程,特别是小型示例程序,都是初始化时建但在CreateMeshContainer()函数中,以多种方式使用了指针,在局部指针变量中来回传递,所以问题复杂化期前,再Release().但许多程序员都不是严格这么做。
因为在局部变量用完就废了,先AddRef()增加计数再数不对,如果没有统一的习惯,不好排查。
在CreateMeshContainer()中,对接口指针的使用有三种方式,例方式一:
不使用AddRef()。
和普通指针一样,临时变量是左值,接口指针是右值,直接赋值使用。
如:
pMesh=pMeshData->pMesh;
+@"X(K5o9k%d#u(F/K
这是由于pMesh是局部变量,它只是临时引用一下,没必要为它先AddRef(),后Release()。
pMesh->GetDevice(&pd3dDevice);//此处d3d设备引用计数已经加
1."...
SAFE_RELEASE(pd3dDevice);//--此处将引用计数减1,并不是真的释放d3d设备
;H0{6_%F+L8g4J+X方式二:
隐式的使用AddRef()。
由于用到了一些内部有AddRef()动作的函数,就要按照COM约定,在子程在本例中,pd3dDevice在GetDevice()中已经Addref()过了,所以,在退出CreateMeshContaine方式三:
显式的使用AddRef()。
如果一个指针值,不是由D3DXCreate出来的,而是通过赋值方式复制给一不AddRef(),极有可能在函数返回该对象就可能释放了。
它就像一个加油站,使得传入对象的寿命延长至自己在本函数,有三处这样的语句:
"h&o!
e6x
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 骨骼动画及微软示例Skinned Mesh的解析 骨骼 动画 微软 示例 Skinned Mesh 解析