1、3D游戏角色动画 摘要:本文主要描述了3D游戏角色动画的原理及应用,从介绍微软的X文件到最为广泛应用的骨骼蒙皮动画,另外简要的介绍了下渐变动画的原理。关键词:Role Animation Skeletal Animation Morphing Animation Skinned Mesh Abstract:Introduce 3D Game Role Animation, for example Skeletal Animation and Skinned Mesh etc. 目录:一 概述3D角色动画的应用二 3D游戏动画基础-基于时间的运动三 3D游戏角色动画 1 介绍微软的X文件 2 骨
2、骼蒙皮动画的原理与实现 3 增加场景数据 4 简介渐变动画四 结束语正文:一 概述3D角色动画的应用3D角色动画是计算机动画技术的一个重要组成部分,也是计算机图形学的一个分支。无论是在离线渲染环境下,还是在实时渲染环境下,3D角色动画都得到了广泛的应用。在离线渲染环境下,主要应用于动画电影制作和各类广告制作。动画电影制作中所使用的3D角色动画技术的一个重要特点是动画数据量大,渲染需要耗费大量时间,因此动画作品必须预先制作,渲染,然后转化成视频文件播放。在实时渲染环境下,主要应用于虚拟现实,视频游戏,甚至是建模软件,动画制作软件。现在,随着计算机硬件技术的发展,特别是带有硬件加速功能的显卡性能的
3、提高,很多曾经只能在离线环境下应用的技术,都转移到实时渲染环境中来。其中,实时渲染的角色动画技术得到了发展且被广泛的应用。目前,实时角色动画技术大体可分为三种类型。第一类是关节动画(Skeletal Animation)。关节动画中的角色由若干独立的部分组成。每一个部分对应着一个独立的网格模型,不同的部分按照角色的特点组织成一个层次结构。比如说,一个人体模型可以由头,上身,左上臂,左前臂,左手,右上臂,右前臂,右手,左大腿,左小腿,左脚,右大腿,右小腿,右脚等各部分组成。而某个部分,可能是另一个部分的子节点,同时又是另一个部分的父节点。比如上面的人体模型中,右前臂就是右上臂的子节点,同时也是右
4、手的父节点。而右上臂是上身的子节点,后者则是躯体的子节点。通过改变不同部分之间的相对位置,比如夹角,位移等等,就可以实现所需要的各种动画效果。这类动画的优点很多。首先,在动画序列的关键帧中只需要存储节点间的相对变化,因此动画文件占用的空间很小。其次,可以实现很多复杂的动画效果,如果应用程序支持反向动力学还可以动态实现预先存储的动画序列之外的新的动画效果。当然这类动画也有不少缺点。其中之一是由于角色模型是一个层次模型,要获得某一个部分相对于世界坐标的位置,必须从根结点开始遍历该节点所有的祖先节点累计计算模型的世界变换。但最关键的问题是在不同部分的结合处往往会有很明显的接缝,这会严重的影响模型的真
5、实感。第二类是渐变动画(Morphing Animation)。这种动画中的角色由一系列的渐变网格模型构成。在动画序列的关键帧中记录着组成网格的各个顶点的新位置或者是相对于原位置的改变量。通过在相邻关键帧之间插值来直接改变该网格模型中各个顶点的位置就可以实现动画效果。相对于关节动画,单一网格模型动画的角色看上去更真实,也不会有关节动画所面临的接缝问题。由于没有使用层次模型,获得模型网格顶点在世界坐标中位置的计算量也很小。但是,这类动画的适应性很弱,角色很难通过实时计算来与环境进行良好的互动,以获得预先存储的动画序列之外的动画效果。另一方面,由于关键帧要存储网格模型所有的顶点信息,动画文件占用的
6、空间比较大。第三类是骨骼蒙皮动画(Skinned Mesh)。骨骼蒙皮动画可以看作是关节动画和渐变动画的结合。他同时兼有关节动画的灵活和渐变动画的逼真。后面将详细介绍骨骼蒙皮动画的技术细节。3D角色动画技术和其它动画技术相结合,就能创造出绚丽多彩的游戏世界。二 3D游戏动画基础-基于时间的运动在一个游戏项目中,计时扮演了一个重要的角色。基于时间的运动,也就是创建计时器来控制运动。它能够产生这样一种动画效果:同样处理10000毫秒的动画,在性能好的计算机上得到平滑完整的动画效果,在性能不好的计算机上显得跳帧,但也能够在10000毫秒的时候完成任务,和性能好的计算机是同步的。基本思路是事先设置好动
7、画关键帧序列,在主循环中判断出第一个动画关键帧和下一个动画关键帧的编号,利用一个时间计数器去定位相对于第一动画关键帧的位置。随着时间计数器的增长,不断从第一个动画关键帧的位置移动至下一个动画关键帧的位置。主要分为以下几个步骤:1设置动画关键帧序列。2计算出每一帧的时间Time,Time是相对于程序开始运行的毫秒数。3定位出第一个动画关键帧和下一个动画关键帧。4利用Time计算出相对于第一个动画关键帧的毫秒数,再利用这个偏移毫秒数计算出相对于第一个动画帧的偏移位置。5设置变换矩阵。6回到第2步。 上图表示了4帧的关键帧动画,其中第0帧和第3帧变换矩阵相同。下面直接看代码,我将结合代码详细叙述。t
8、ypedef struct sKeyframe DWORD Time; D3DMATRIX matTransformation; sKeyframe;/关键帧的结构,DWORD Time为执行该帧的时间,D3DMATRIX /matTransformation为在该帧时模型的变换矩阵。sKeyframe g_Keyframes4 = / Keyframe 0, 0ms 0, 1.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 1.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 1
9、.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 1.000000f , / Keyframe 1, 40ms 400, 0.000796f, 1.000000f, 0.000000f, 0.000000f, -1.000000f, 0.000796f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 1.000000f, 0.000000f, 50.000000f, 0.000000f, 0.000000f, 1.000000f , / Keyframe 2, 80ms 800, -0.9999
10、99f, 0.001593f, 0.000000f, 0.000000f, -0.001593f, -0.999999f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 1.000000f, 0.000000f, 25.000000f, 25.000000f, 0.000000f, 1.000000f , / Keyframe 3, 120ms 1200, 1.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 1.000000f, 0.000000f, 0.000000f, 0.000000f,
11、0.000000f, 1.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 1.000000f ;/定义了4帧的关键动画。其中第3帧和第0帧的变换矩阵一样,为了使动画能进入循环状态。void DoFrame() /此函数在循环内 static DWORD StartTime = timeGetTime(); DWORD Time = timeGetTime() - StartTime;/用timeGetTime()得到一个操作系统运行的毫秒数,储存到static变量以后将不再改变,/DWORD Time变量不断改变,为本程序运行的毫秒数。
12、Time %= (g_Keyframes3.Time+1);/得到一个不断从0到1200变化的毫秒数。 DWORD Keyframe = 0; / 从第0帧开始。 for(DWORD i=0;i= 某一关键帧的时间,将关键帧定位于此帧。 if(Time = g_Keyframesi.Time) Keyframe = i; DWORD Keyframe2 = (Keyframe=3) ? Keyframe:Keyframe + 1;/得到接下来的关键帧,如/果Keyframe为第3关键动画帧,Keyframe2也为第3关键动画帧。/当Keyframe=1200时这种情况才成立,几率很小。一般情况
13、下,Keyframe2=Keyframe+1。 DWORD TimeDiff = g_KeyframesKeyframe2.Time - g_KeyframesKeyframe.Time; if(!TimeDiff) TimeDiff=1;/计算两个sKeyframe的时间差,当Keyframe=Keyframe2=3时,TimeDiff=0,/此时另TimeDiff=1。 float Scalar = (float)(Time - g_KeyframesKeyframe.Time) / (float)TimeDiff; / Scalar取 /值为0,1),利用Time计算出相对于Keyfra
14、me的偏移毫秒数,再除以两/帧的时间差。 D3DXMATRIX matInt = D3DXMATRIX(g_KeyframesKeyframe2.matTransformation) - D3DXMATRIX(g_KeyframesKeyframe.matTransformation); matInt *= Scalar; /用于计算相对于Keyfrme的偏移位置。 matInt += D3DXMATRIX(g_KeyframesKeyframe.matTransformation); / 计算出该帧处相/对于Keyfrme的偏移位置。 g_pD3DDevice-SetTransform(D3
15、DTS_WORLD, &matInt); / 设置 world transformation matrix 设置完变换矩阵,剩下的事情就只是渲染了。创建计时器控制动画的技术是非常简单有效的,这是现代计算机游戏动画的基础,因此,必须深刻理解它的内容。三 3D游戏角色动画1 介绍微软的X文件 制作3D游戏角色动画需要与之相关的“动画容器”。这个“容器”中储存着动画的数据。微软的X文件正是这样的一种容器。由于微软的影响力,它们公司设计的X文件也广为流传。X文件是一套基于模版定义的文件,理论上它能够容纳任何数据。也就意味着不仅仅是3D模型文件,任何用于游戏引擎加载的外部资源都可以被包含于X文件之中。下
16、面我们详细的介绍下X文件。为了得到一个直观的印象,我们首先浏览下它的全貌。xof 0302txt 0032template Header DWORD major;DWORD minor;DWORD flags;template Frame FrameTransformMatrixMeshHeader 1;0;1;Frame Scene_Root FrameTransformMatrix 1.000000, 0.000000, 0.000000, 0.000000,0.000000, 1.000000, 0.000000, 0.000000,0.000000, 0.000000, 1.00000
17、0, 0.000000,0.000000, 0.000000, 0.000000, 1.000000; Frame Pyramid_Frame FrameTransformMatrix 1.000000, 0.000000, 0.000000, 0.000000,0.000000, 1.000000, 0.000000, 0.000000,0.000000, 0.000000, 1.000000, 0.000000,0.000000, 0.000000, 0.000000, 1.000000;Mesh PyramidMesh 5;0.00000;10.00000;0.00000;,-10.00
18、000;0.00000;10.00000;,10.00000;0.00000;10.00000;,-10.00000;0.00000;-10.00000;,10.00000;0.00000;-10.00000;6;3;0,1,2;,3;0,2,3;,3;0,3,4;,3;0,4,1;,3;2,1,4;,3;2,4,3;MeshMaterialList 1;6;0,0,0,0,0,0;Material Material0 1.000000; 1.000000; 1.000000; 1.000000;0.000000; 0.050000; 0.050000; 0.050000;0.000000;
19、0.000000; 0.000000; 首先看头文件xof 0302txt 0032。xof表示这是一个真正的X文件。0302txt表示通知程序使用Directx的X文件,版本为3.2的模版,其中txt表示此文件为文本文件,可读,并非是一个2进制文件。0032表示一个浮点数的位数为32,如果想要用64位的浮点数,可以写成0064。下面我们将按照以下七个步骤进行介绍和说明。第一,声明一个模版:假设声明 template ContactEntry ,首先需要用guidgen.exe产生一个GUID。产生的GUID如下:/ 4C9D055B-C64D-4bfe-A7D9-981F507E45FFDE
20、FINE_GUID(,0x4c9d055b, 0xc64d, 0x4bfe, 0xa7, 0xd9, 0x98, 0x1f, 0x50, 0x7e, 0x45, 0xff);之后需要在程序代码中加入:#include initguid.h/ At beginning of source code file - add DEFINE_GUIDsDEFINE_GUID(ContactEntry, 0x4c9d055b, 0xc64d, 0x4bfe, 0xa7, 0xd9, 0x98, 0x1f, 0x50, 0x7e, 0x45, 0xff);还要在X文件中加入:template Contact
21、Entry 这里介绍下声明模版用到的数据类型:关键字 描述WORD 16-bit value (short)DWORD 32-bit value (32-bit int or long)FLOAT IEEE float value (float)DOUBLE 64-bit floating-point value (double)CHAR 8-bit signed value (signed char)UCHAR 8-bit unsigned value (unsigned char)BYTE 8-bit unsigned value (unsigned char)STRING A NULL-
22、terminated string (char)array Signifies an array of following data type to follow ()使用数据类型的举例:DWORD Value; array STRING Text20;/定义一个名为Text的数组,类型为STRING,大小为20。DWORD ArraySize; array STRING NamesArraySize; /可以将大小设置为变量。现在,我们声明一个ContactEntry模版:template ContactEntry STRING Name; / The contacts nameSTRING
23、 PhoneNumber; / The contacts phone numberDWORD Age; / The contacts age实例化一个模版对象:ContactEntry JimsEntry Jim Adams;(800) 555-1212;30;JimsEntry 可以用这样的形式引用一个数据对象。例如,在一个animation sequence template中引用一个Frame data object做为其内嵌数据对象。也可以利用引用表示一个数据对象的副本,没有必要重复书写这个数据对象。第二,内嵌数据对象和模版约束:首先,我们分别声明了三个不同的模版,请仔细看它们的区别。
24、template ClosedTemplate DWORD ClosedData;template OpenTemplate DWORD OpenData;.template RestrictedTemplate DWORD RestrictedData;ClosedTemplateOpenTemplateClosedTemplate看起来没有什么不同,因为它就是标准的模版声明。在OpenTemplate中包含一个.,表示这是一个开放模版。开放模版允许在中内嵌任何数据对象。例如,你可以实例化OpenTemplate,在里面定义一个OpenData变量和内嵌一个ClosedTemplate的实例
25、。最后的RestrictedTemplate为约束模版。约束模版实例化时只允许包含它列出的数据对象,如,不能在RestrictedTemplate包含ClosedTemplate,OpenTemplate以外的数据对象。第三,充分利用DirectX .X Standard Templates:正如上面提到的,X文件广泛用于包含一个mesh信息。一个Standard Templates包含了各种信息。Table 3: DirectX .X Standard TemplatesTemplate Name DescriptionAnimation: Defines animation data fo
26、r a single frame.AnimationKey: Defines a single key frame for the parent animation template.AnimationOptions: Contains animation playback information.AnimationSet: Contains a collection of animation templates.Boolean: Holds a Boolean value.Boolean2d: Holds two Boolean values.ColorRGB: Contains red,
27、green, and blue color values.ColorRGBA: Contains red, green, blue, and alpha color values.Coords2d: Defines two coordinate values.FloatKeys: Contains an array of floating-point values.FrameTransformMatrix: Holds the transformation matrix for a parent Frame template.Frame: A frame-of-reference templa
28、te that defines a hierarchy.Header: The .X file header that contains version numbers.IndexedColor: Contains an indexed color value.Material: Contains material color values.Matrix4x4: Holds a 4x4 homogenous matrix container.Mesh: Contains a single meshs data.MeshFace: Holds a meshs face data.MeshFace
29、Wraps: Contains the texture wrapping for mesh faces.MeshMaterialList: Contains the material for face-mapping values.MeshNormals: Holds normals used for mesh data.MeshTextureCoords: Holds texture coordinates used for mesh data.MeshVertexColors: Holds vertex color information used for mesh vertices.Pa
30、tch: Defines a control patch.PatchMesh: Contains a patch mesh (much like the Mesh template).Quaternion: Holds a quaternion value.SkinWeights: Contains an array of weight values mapped to a meshs vertices. Used in skinned meshes.TextureFilename: Contains the texture file name to use for a material.Ti
31、medFloatKeys: Contains an array of FloatKeys templates.Vector: Holds a 3D coordinate value.VertexDuplicationIndices: Informs you which vertices are duplicates of other vertices.XSkinMeshHeader: Used by skinned meshes to define the number of bones contained in a mesh.我们可以在DirectX9SDK的安装目录下搜索到“rmxfgui
32、d.h”字样的头文件。在rmxfguid.h中定义了各个模版的宏,例如:/* 3D82AB44-62DA-11cf-AB39-0020AF71E433 */DEFINE_GUID(TID_D3DRMMesh,0x3d82ab44, 0x62da, 0x11cf, 0xab, 0x39, 0x0, 0x20, 0xaf, 0x71, 0xe4, 0x33);每个模版名加上前缀TID_D3DRM就是宏定义名。既然微软已经帮助我们定义了那么多的模版,我们可以根据需求,充分的去利用这些模版。另外,这些模版往往会有相关的帮助函数,我们使用它们可以事半功倍。第四,创建X文件接口: 我们已经对X文件的模版概
33、念有所了解,现在将使用它们。当然,要想使用首先得访问X文件。访问任何X文件首先要调用DirectXFileCreate函数创建一个IDirectXFile接口,这个接口就代表了一个X文件。IDirectXFile *pDXFile = NULL;HRESULT Result = DirectXFileCreate(&pDXFile);/用&pDXFile返回指向接口的指针。用SUCCEEDED或者FAILED宏判断返回值是否有效。创建完IDirectXFile接口,我们要注册一个定制模版或者标准模版。定制模版是自己定义的模版,标准模版是微软帮我们定义的模版,其实它们之间没有本质的区别,你现在要
34、做的就是告诉IDirectXFile接口,使用二者之中的哪种。如何注册定制模版呢?下面举个例子更容易理解。你可以把X文件中的模版移除,直接在代码里定义那些模版。IDirectXFile接口支持这样的特性。需要调用IDirectXFile:RegisterTemplates函数。HRESULT IDirectXFile:RegisterTemplates(LPVOID pvData, / 一个定义模版数据的缓存,应该精确无误。DWORD cbSize); / pvData缓存的字节数。可以如下定义一个模版数据:char *Templates = xof 0303txt 0032 /标准X文件头。
35、template CustomTemplate DWORD Length; array DWORD ValuesLength; ;之后在用RegisterTemplates将其注册:pFile-RegisterTemplates(Templates, strlen(Templates);如何注册标准模版呢?首先需要在代码中包含rmxfguid.h和rmxftmpl.h。rmxfguid.h定义了各个标准模版的GUDI,rmxftmpl.h以2进制数据形式定义了标准模版数据的缓存和其字节数。然后调用RegisterTemplates将其注册:pFile-RegisterTemplates(D3D
36、RM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES);第五,打开X文件:创建完IDirectXFile接口,注册模版之后需要打开X文件,枚举其数据对象。调用IDirectXFile:CreateEnumObject函数。HRESULT IDirectXfile:CreateEnumObject(LPVOID pvSource, / .X filenameDXFILELOADOPTIONS dwLoadOptions, / Load optionsLPDIRECTXFILEENUMOBJECT* ppEnumObj); / Enum interface当调用CreateEn
37、umObject函数,用pvSource指定一个文件的名字,用ppEnumObj返回一个枚举对象接口指针。用dwLoadOptions指定load操作方式。当指定DXFILELOAD_FROMFILE值,告诉DirectX从磁盘载入一个文件。还有DXFILELOAD_FROMRESOURCE,DXFILELOAD_FROMMEMORY和DXFILELOAD_FROMURL分别表示从一个资源,内存缓冲和Internet上加载X文件。当从Internet加载文件时,需要为其指定完整的网址。下面代码从磁盘加载X文件:/ Filename = filename to load (test.x for
38、example)IDirectXFileEnumObject *pEnum;pFile-CreateEnumObject(LPVOID)Filename, DXFILELOAD_FROMFILE, &pEnum);Filename指向一个有效的文件名,pEnum返回一个枚举对象接口指针。第六,枚举数据对象:注册完模版,打开X文件并且得到一个枚举对象接口,下面需要从X文件读出数据。枚举对象接口指针指向文件的第一个数据对象,因为每一个数据对象可能包含内嵌数据对象或者引用的数据对象,所以与第一个数据对象同在一层级的其它数据对象为同层级数据对象。至于包含的子数据对象的类型,需要对其分别的行进询问。可以
39、使用HRESULT IDirectXFileEnumObject:GetNextDataObject (LPDIRECTXFILEDATA* ppDataObj)得到一个IDirectXFileData接口。它只有一个参数:IDirectXFileData *pData;HRESULT hr = pEnum-GetNextDataObject(&pData);利用此函数,可以不断地访问同一层级的数据对象接口,具体代码如下:while(SUCCEEDED(pEnum-GetNextDataObject(&pData) / 这里可以对pData数据对象进行操作。pData-Release();/释
40、放接口。当返回值为FAILED,表示已经访问完所有的接口。当访问值为SUCCEEDED,你需要继续判断这个数据对象是否包含子对象。利用接口IDirectXFileObject,和HRESULT IDirectXFileData:GetNextObject( LPDIRECTXFILEOBJECT* ppChildObj)函数,代码如下:IDirectXFileObject *pObject;while(SUCCEEDED(pData-GetNextObject(&pObject) / 如果一个子对象存在,需要继续询问它,判断出它的类型为内嵌数据对象或者引用的数/ 据对象。pObject-Rel
41、ease();/ 释放接口。接下来询问接口,看其是否为内嵌数据对象:IDirectXFileData *pSubData;if(SUCCEEDED(pObject-QueryInterface( IID_IDirectXFileData, (void*)&pSubData) / 如果询问内嵌数据对象成功,可以对pSubData数据对象进行操作pSubData-Release();/释放接口。看其是否为引用数据对象:IDirectXFileDataReference *pRef;IDirectXFileData *pSubData;if(SUCCEEDED(pSubObj-QueryInterf
42、ace( IID_IDirectXFileDataReference, (void*)&pRef) / 如果询问引用的数据对象成功,解析出引用的原型。pRef-Resolve(&pSubData);/这里可以对pData数据对象进行操作。pRef-Release();pSubData-Release();/释放接口。现在整理下思路:大体的思路其实很简单,首先枚举最顶层的数据对象,然后判断其是否有子对象,这个子对象可能是内嵌对象或者引用对象二者之一,分别询问其接口,就可以判断出具体的类型。下面是完整的Parse模版的函数:BOOL Parse(char *Filename)IDirectXFile *pFile = NULL;IDirectXFileEnumObject *pEnum = NULL;IDirectXFileData *pData = NULL;