骨骼动画.docx
- 文档编号:4027122
- 上传时间:2022-11-27
- 格式:DOCX
- 页数:10
- 大小:22.04KB
骨骼动画.docx
《骨骼动画.docx》由会员分享,可在线阅读,更多相关《骨骼动画.docx(10页珍藏版)》请在冰豆网上搜索。
骨骼动画
骨骼动画(SkeletalAnimation)
相信这里没有人没玩过采用骨骼动画技术的游戏,看看那些热门的动作游戏,例如《波斯王子》、《分裂细胞》和《战神》,你就知道骨骼动画的威力了(我承认是猜的)。
骨骼动画技术用来使我们的3D模型在屏幕上动起来,通过和动作捕捉技术结合,可以让模型做出非常逼真的动作。
而这样一个极具威力的技术,其原理却相当简单。
假设我们要让游戏主角做出一个动作,例如波斯王子拿弯刀往前一劈。
最简单的方法,就是让模型师建一个动画序列,然后在程序中逐帧播放,就像放电影一样。
不过这样一来工作量就太大了,玩家也需要N个G的硬盘来安装这个游戏。
与此不同,骨骼动画技术采用了一种很聪明的方式。
首先,建模师完成一个标准姿势的3D模型,通常是双手沿着肩的方向伸展平放,双脚打开。
所有的后继动作将由这个基础动作演变得到。
在完成这个基准模型之后,建模师再建一个骨骼结构,一系列相互关联的顶点,就像一个骨架一样,与人体模型各个关节匹配并且都会有一定数量的顶点与之关联。
想象一下人和人身上的骨骼就很容易知道我在说什么。
之后,在我们想要完成的动画序列中,挑选一些关键帧,对每个关键帧,将骨骼的位置与关键帧匹配。
然后把这一系列的关键帧骨骼保存起来,除了骨骼的位置,同时保存的还有从基准位置变换到当前关键帧的旋转、平移、缩放或者一个混合的坐标变换矩阵。
在我们引擎中,首先根据当前时间查找这时候角色是处于哪两个关键帧中间。
找到之后以时间为参数在关键帧的坐标变换矩阵之间求插值,用插值结果来决定骨骼当前的位置。
骨骼位置求出来后,所有和骨骼关联的顶点的坐标也可以相应求出来了。
通过使用骨骼动画技术,我们用相对较少的数据就可以播放很平滑的动画!
了解了相关原理,来看看如何在directx中播放骨骼动画。
我的参考书是《Advanced Animation with DirectX》。
现在我们知道为了播放骨骼动画,需要有骨骼(bone)的数据,模型(mesh)的数据,关联骨骼和模型上每个顶点的关联数据,以及关键帧的坐标变换数据。
所有这些数据必须以某种形式存在于某个地方供我们获取才行。
这里要介绍的MS的x文件格式以及从中获取数据的方法。
强烈建议大家都来学习一下x文件格式!
你会发现它即简单又强大,即使用来存放自定义数据也是相当的方便,一旦掌握之后我保证你会对它爱不释手。
典型的x文件以数据模板和实际数据两部分组成。
数据模板类似c++中的结构定义,不过更为灵活和开放。
实际数据就是遵守模板定义的数据段。
看一个例子,
templateEmployee{
<3D82AB43-62DA-11cf-AB39-0020AF71E433>//每个模板关联唯一的GUID
STRINGName;//姓名
DWORDSex;//性别
[ContactEntry]//联系方式,另一个模板,模板可以嵌套
}
templateContactEntry{
<4C9D055B-C64D-4bfe-A7D9-981F507E45FF>//GUID
STRINGPhoneNumber;//电话号码
STRINGAddress;//地址
}
EmployeeDavid{
"David";
1;
ContactEntry{
"100-100000000";
"farfaraway";
}
}
从上面这个简单的例子我们就可以看出x文件的大概模样了,详细的情况大家可以参考《AdvancedAnimationwithDirectX》。
下面我们看如何来读取这样一个x文件,借助下几个对象,
ID3DXFile--x文件格式文档对象。
例如Employee.x这样一个文件。
ID3DXFileEnumObject--用来枚举x文档的顶级模板数据。
所谓顶级模板数据是指那些没有
父模板的数据,例如上面的David数据段。
ID3DXFILEDATA--模板数据。
上面的David和他的联系方式都是ID3DXFILEDATA
对象,自包含。
下面看实际的分析函数,下面的代码适用于DirectX9.0SDKUpdate(October2004),原书的代码有点过时了。
//-----------------------------------------------------------------------------
//名称:
Parse
//描述:
分析x文件格式文档
//-----------------------------------------------------------------------------
boolParse(char*filename,void**pData)
{
LPD3DXFILElpD3DXFile;
LPD3DXFILEENUMOBJECTlpD3DXFileEnumObj;
LPD3DXFILEDATAlpD3DXFileData;
//参数检查
if(NULL==filename)
returnfalse;
//创建X文件对象
HRESULThr=D3DXFileCreate(&lpD3DXFile);
if(FAILED(hr))
returnfalse;
//注册标准模板
hr=lpD3DXFile->RegisterTemplates(
(LPVOID)D3DRM_XTEMPLATES,D3DRM_XTEMPLATE_BYTES);
if(FAILED(hr))
{
Release
returnfalse;
}
//创建X文件枚举对象
hr=lpD3DXFile->CreateEnumObject(
filename,D3DXF_FILELOAD_FROMFILE,&lpD3DXFileEnumObj);
if(FAILED(hr))
{
Release
returnfalse;
}
//解析开始
boolparseResult=BeginParse(pData);
if(true==parseResult)
{
//查询顶级模板数
SIZE_TchildCount=0;
lpD3DXFileEnumObj->GetChildren(&childCount);
//分析每个订级模板
for(DWORDi=0;i { //获取当前模板 hr=lpD3DXFileEnumObj->GetChild(i,&lpD3DXFileData); if(FAILED(hr)) break; //分析 parseResult=ParseObject(lpD3DXFileData,NULL,0,pData); //释放FileData对象 Release //出现错误,中断分析 if(false==parseResult) break; } //解析结束 if(parseResult) parseResult=EndParse(pData); } //释放相关对象 Release Release //解析结束 returnparseResult; } //----------------------------------------------------------------------------- //名称: ParseObject //描述: 递归解析顶级模板 //----------------------------------------------------------------------------- boolParseObject( LPD3DXFILEDATApDataObj, LPD3DXFILEDATApParentDataObj, DWORDdepth, void**pData) { LPD3DXFILEDATApSubDataObj; boolparseResult=true; HRESULThr; //获取子模板数目 DWORDchildCount; pDataObj->GetChildren(&childCount); //遍历模板并分析 for(DWORDi=0;i { //取子模板对象 hr=pDataObj->GetChild(i,&pSubDataObj); if(FAILED(hr)) break; //分析子模板 parseResult=ParseObject(pSubDataObj,pDataObj,depth+1,pData); //释放数据对象 Release //出现错误,停止分析 if(false==parseResult) break; } returnparseResult; } 就那么简单,相信大家都看得明白。 通过重载ParseObject方法,我们以判断当前分析的模板类型,然后创建实际的模板对象,从文档中复制数据。 有了上面的工具,我们就可以自己来读取和解析x格式的骨骼动画文件了。 下面我们就来看看如何重载ParseObject方法来获得我们感兴趣的数据,不要担心,绝对简单。 仔细看代码,你会发现只需要做一件事情,判断当前数据段的类型(通过GUID),分配对应的结构对象,然后从数据段拷贝数据(所有SDK自定义模板的GUID都在头文件rmxfguid.h中定义,你需要把它加入你的工程中。 所有预定义模板在这里可以找到)。 先来看看如何获取当前数据段的GUID, GUIDobjGUID; pDataObj->GetType(&objGUID); 简单吧,下面开始我们的分析之旅。 x动画文件中骨骼是用Frame模板定义的, templateFrame { <3D82AB46-62DA-11cf-AB39-0020AF71E433> FrameTransformMatrixframeTransformMatrix;//骨骼相对于父节点的坐标变换矩阵 Meshmesh;//骨骼的Mesh } 只有两个字段。 FrameTransformMatrix就是一个matrix。 Mesh稍微复杂,详细格式大家自己参考MSDN,我们也会有专门的代码来加载Mesh,现在关注Frame。 为了加载Frame,我们要在程序中定义一个和Frame模板对应的数据结构,SDK中经默认提供了一个,那就是D3DXFRAME, typedefstruct_D3DXFRAME { LPSTRName;//骨骼名称 D3DXMATRIXTransformationMatrix;//相对与父节点的坐标变换矩阵 LPD3DXMESHCONTAINERpMeshContainer;//LPD3DXMESHCONTAINER对象,用来 //加载MESH,还有一些附加属性,见SDK struct_D3DXFRAME*pFrameSibling;//兄弟节点指针,和下面的子节点指针 //一块作用构成骨骼的层次结构。 struct_D3DXFRAME*pFrameFirstChild;//子节点指针. }D3DXFRAME,*LPD3DXFRAME; 这样一个结构已经足够容纳Frame模板中的数据并形成一个层次结构,不过为了我们程序的需要,我们还需要其他字段,为此我们通常会扩展D3DXFRAME, typedefstruct_D3DXFRAME_EX: publicD3DXFRAME { D3DXMATRIXmatCombined;//存储当前节点相对于根节点的位置偏移矩阵,沿着到 //到根骨骼的路径把所有的坐标变换矩阵相乘得到。 D3DXMATRIXmatOriginal;//在播放动画的时候有可能会改变原来结构中的 //TransformationMatrix,因此我们声名一个新的字段 //将原来的坐标变换矩阵保存起来以便在需要的时候恢 //复回去。 ...//忽略一些方法定义 } 我知道有些人已经按捺不住了,那么动手吧, //判断当前分析的是不是Frame节点 if(objGUID==TID_D3DRMFrame) { //引用对象直接返回,不需要做分析。 一个数据段实际定义一次后可以 //被其他模板引用,例如后面的Animation动画模板就会引用这里的Frame //节点,标识动画关联的骨骼。 if(pDataObj->IsReference()) returntrue; //创建D3DXFRAME_EX结构,准备拷贝数据 D3DXFRAME_EX*pFrame=newD3DXFRAME_EX(); //拷贝名称 pFrame->Name=GetObjectName(pDataObj); //注意观察文件就可以发现一个Frame要么是根Frame,父节点不存在, //要么作为某个Frame的下级Frame而存在。 if(NULL==pData) { //作为根节点的兄弟节点加入链表。 pFrame->pFrameSibling=m_pRootFrame; m_pRootFrame=pFrame; pFrame=NULL; //将自定义数据指针指向自己,供子 //节点引用。 pData=(void**)&m_pRootFrame; } else { //作为传入节点的子节点 D3DXFRAME_EX*pDataFrame=(D3DXFRAME_EX*)(*pData); pFrame->pFrameSibling=pDataFrame->pFrameFirstChild; pDataFrame->pFrameFirstChild=pFrame; pFrame=NULL; pData=(void**)&pDataFrame->pFrameFirstChild; } } 结束了! 是不是很简单,呵呵,记住我们只需要做一件事情,判断类型,分配匹配的对象然后拷贝数据,下面来分析Frame中的matrix, //frame的坐标变换矩阵,因为matrix必然属于某个Frame所以pData必须有效 elseif(objGUID==TID_D3DRMFrameTransformMatrix&&pData) { //我们可以肯定pData指向某个Frame D3DXFRAME_EX*pDataFrame=(D3DXFRAME_EX*)(*pData); //先取得缓冲区大小,应该是个标准的4x4矩阵 DWORDsize=0; LPCVOIDbuffer=NULL; hr=pDataObj->Lock(&size,&buffer); if(FAILED(hr)) returnfalse; //拷贝数据 if(size==sizeof(D3DXMATRIX)) { memcpy(&pDataFrame->TransformationMatrix,buffer,size); pDataObj->Unlock(); pDataFrame->matOriginal=pDataFrame->TransformationMatrix; } } 这样大家应该对其他类型的模板数据分析代码都应该大致猜的出来了。 具体的代码我就不在这里提供,只是简单的介绍一下它们的作用和关系,大家可以参考最后附上的工程。 Frame-- 骨骼。 正如大家已经看到的那样,我们可以用pFrameSibling和pFrameFirstChild两个字段来构成骨骼的层次结构。 骨骼模板包含了当前骨骼相对父骨骼的坐标变换矩阵和骨骼对应的模型 Mesh-- 模型。 角色的顶点数据,包含vertexbuffer,indexbuffer等。 我们可以直接用普通的ID3DXMesh来加载其中的数据。 除此之外,Mesh中还包含了SkinWeight模板。 SkinWeight-- 骨骼关联的顶点已经该骨骼的坐标变换对该顶点的权重。 实际中我们并不需要特殊处理这类模板数据,ID3DXMesh已经包含了对应的代码。 AnimationSet-- 动画集合。 例如角色的各种动作“Kill”,“Jump”等等,包含多个Animation。 Animation-- 动画。 由对应骨骼的名称和一组AnimationKey组成。 AnimationKey-- 动画键。 包含一组时间戳以及在对应时间戳应用到骨骼上的平移、缩放、旋转向量或者复合的坐标变换矩阵。 以上就是我们需要了解的全部了。 至此,所有原料都已经准备齐全,各位大厨们下一步要做的就是骨骼动画这道小菜啦! 我们的目标是根据骨骼动画来更新模型。 看看手上的材料, 1)骨骼动画数据。 上一节中我们已经读出了AnimationSet、Animation和AnimationKey这些动画数据,我们现在要做的就是把它们应用到骨骼上面去。 AnimationSet只是标明了我们要播放的动画名称,关键的处理在Animation和AnimationKey上面。 Animation包含了所对应的骨骼名称,下属的AnimationKey包含了坐标变换的类型以及对应的时间戳,我们也把AnimationKey看做一个关键帧。 下面要做的就是根据当前时间判断动画落在哪两个关键帧中间,例如key1和key2,然后求出插值系数scaler, scaler=(当前时间-key1.时间)/(key2.时间-key1.时间) 求出插值系数后,骨骼的当前位置就可以用下面的方法求出,注意各种key类型求插值的方法不一样, switch(Key的类型) { case旋转: ... //四元数插值 D3DXQUATERNIONRotationQuaternion; D3DXQuaternionSlerp( &RotationQuaternion, &pAnimationKey->pQuaternionKeys[Key1].value, &pAnimationKey->pQuaternionKeys[Key2].value, Scaler); //应用旋转矩阵 D3DXMATRIXRotationMatrix; D3DXMatrixRotationQuaternion(&RotationMatrix,&RotationQuaternion); pAnimation->pBone->TransformationMatrix*=RotationMatrix; break; case平移和缩放: ... //矢量插值 D3DXVECTOR3InterpolatedVector= pAnimationKey->pVectorKeys[Key1].value+Scaler* (pAnimationKey->pVectorKeys[Key2].value-pAnimationKey->pVectorKeys[Key1].value); if(pAnimationKey->Type==XAnimationKey: : KeyType: : Scaling) { //应用缩放矩阵 D3DXMATRIXScalingMatrix; D3DXMatrixScaling( &ScalingMatrix,InterpolatedVector.x,InterpolatedVector.y,InterpolatedVector.z); pAnimation->pBone->TransformationMatrix*=ScalingMatrix; } else { //应用平移矩阵 D3DXMATRIXTranslationMatrix; D3DXMatrixTranslation( &TranslationMatrix,InterpolatedVector.x,InterpolatedVector.y,InterpolatedVector.z); pAnimation->pBone->TransformationMatrix*=TranslationMatrix; } break; case坐标变换矩阵: ... //矩阵插值 D3DXMATRIXTransformMatrix= pAnimationKey->pMatrixKeys[Key1].value+Scaler* (pAnimationKey->pMatrixKeys[Key2].value-pAnimationKey->pMatrixKeys[Key1].value); //应用坐标变换矩阵 pAnimation->pBone->TransformationMatrix*=TransformMatrix; break; }//switch 这样我们就把根据当前时间计算出来的插值坐标变换矩阵应用到骨骼上了。 2)骨骼数据。 在从文件中读出来的时候,我们已经利用pFrameSibling和pFrameFirstChild两个字段构造了一个层次结构。 注意骨骼中的TransformationMatrix包含的是当前骨骼相对于父骨骼的坐标变换,应用到mesh上的时候,我们需要的相对于根骨骼的坐标变换。 因此我们要做一下处理,简单的一个递归调用, //----------------------------------------------------------------------------- //名称: UpdateHierarchy //描述: 计算本节点及所有兄弟、子节点相对于根节点的偏移矩阵 //----------------------------------------------------------------------------- voidUpdateHierarchy(D3DXMATRIX*matTrans=NULL) { D3DXMATRIXmatIdentity; //根节点的偏移矩阵为单位矩阵 if(NULL==ma
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 骨骼 动画
![提示](https://static.bdocx.com/images/bang_tan.gif)