·天新网首页·加入收藏·设为首页
首页|笔记本|手机|数码相机|摄像机|MP3/MP4|主板|内存|显示器|办公|打印机|下载|开发|汽车|学院|业界
硬件|台式机|数码|数字家庭|投影仪|GPS/CPU|显卡|硬盘|服务器|网络|一体机|驱动|源码|游戏|考试|报价
您现在的位置:天新网 > 软件开发 > 游戏开发
3D角色动画
http://dev.21tx.com 2007年01月06日 迷糊小亚

3D游戏角色动画

                                    作者:迷糊小亚

                                         

(这篇文章在gameres也有下的,这里的没有图片)

 

摘要:本文主要描述了3D游戏角色动画的原理及应用,从介绍微软的X文件到最为广泛应用的骨骼蒙皮动画,另外简要的介绍了下渐变动画的原理。

关键词Role Animation   Skeletal Animation   Morphing Animation   Skinned Mesh 

AbstractIntroduce 3D Game Role Animation, for example Skeletal Animation and Skinned Mesh etc.

目录

 

概述3D角色动画的应用

 

3D游戏动画基础------基于时间的运动

 

3D游戏角色动画

   1 介绍微软的X文件

   2 骨骼蒙皮动画的原理与实现

   3 增加场景数据

   4 简介渐变动画

 

结束语

 

 

 

正文:

 

概述3D角色动画的应用

3D角色动画是计算机动画技术的一个重要组成部分,也是计算机图形学的一个分支。无论是在离线渲染环境下,还是在实时渲染环境下,3D角色动画都得到了广泛的应用。在离线渲染环境下,主要应用于动画电影制作和各类广告制作。动画电影制作中所使用的3D角色动画技术的一个重要特点是动画数据量大,渲染需要耗费大量时间,因此动画作品必须预先制作,渲染,然后转化成视频文件播放。在实时渲染环境下,主要应用于虚拟现实,视频游戏,甚至是建模软件,动画制作软件。现在,随着计算机硬件技术的发展,特别是带有硬件加速功能的显卡性能的提高,很多曾经只能在离线环境下应用的技术,都转移到实时渲染环境中来。其中,实时渲染的角色动画技术得到了发展且被广泛的应用。目前,实时角色动画技术大体可分为三种类型。

第一类是关节动画(Skeletal Animation)。关节动画中的角色由若干独立的部分组成。每一个部分对应着一个独立的网格模型,不同的部分按照角色的特点组织成一个层次结构。比如说,一个人体模型可以由头,上身,左上臂,左前臂,左手,右上臂,右前臂,右手,左大腿,左小腿,左脚,右大腿,右小腿,右脚等各部分组成。而某个部分,可能是另一个部分的子节点,同时又是另一个部分的父节点。比如上面的人体模型中,右前臂就是右上臂的子节点,同时也是右手的父节点。而右上臂是上身的子节点,后者则是躯体的子节点。通过改变不同部分之间的相对位置,比如夹角,位移等等,就可以实现所需要的各种动画效果。这类动画的优点很多。首先,在动画序列的关键帧中只需要存储节点间的相对变化,因此动画文件占用的空间很小。其次,可以实现很多复杂的动画效果,如果应用程序支持反向动力学还可以动态实现预先存储的动画序列之外的新的动画效果。当然这类动画也有不少缺点。其中之一是由于角色模型是一个层次模型,要获得某一个部分相对于世界坐标的位置,必须从根结点开始遍历该节点所有的祖先节点累计计算模型的世界变换。但最关键的问题是在不同部分的结合处往往会有很明显的接缝,这会严重的影响模型的真实感。

第二类是渐变动画(Morphing Animation)。这种动画中的角色由一系列的渐变网格模型构成。在动画序列的关键帧中记录着组成网格的各个顶点的新位置或者是相对于原位置的改变量。通过在相邻关键帧之间插值来直接改变该网格模型中各个顶点的位置就可以实现动画效果。相对于关节动画,单一网格模型动画的角色看上去更真实,也不会有关节动画所面临的接缝问题。由于没有使用层次模型,获得模型网格顶点在世界坐标中位置的计算量也很小。但是,这类动画的适应性很弱,角色很难通过实时计算来与环境进行良好的互动,以获得预先存储的动画序列之外的动画效果。另一方面,由于关键帧要存储网格模型所有的顶点信息,动画文件占用的空间比较大。

第三类是骨骼蒙皮动画(Skinned Mesh)。骨骼蒙皮动画可以看作是关节动画和渐变动画的结合。他同时兼有关节动画的灵活和渐变动画的逼真。后面将详细介绍骨骼蒙皮动画的技术细节。

3D角色动画技术和其它动画技术相结合,就能创造出绚丽多彩的游戏世界。

 

 

 

 

3D游戏动画基础------基于时间的运动

在一个游戏项目中,计时扮演了一个重要的角色。基于时间的运动,也就是创建计时器来控制运动。它能够产生这样一种动画效果:同样处理10000毫秒的动画,在性能好的计算机上得到平滑完整的动画效果,在性能不好的计算机上显得跳帧,但也能够在10000毫秒的时候完成任务,和性能好的计算机是同步的。

基本思路是事先设置好动画关键帧序列,在主循环中判断出第一个动画关键帧和下一个动画关键帧的编号,利用一个时间计数器去定位相对于第一动画关键帧的位置。随着时间计数器的增长,不断从第一个动画关键帧的位置移动至下一个动画关键帧的位置。主要分为以下几个步骤:

1设置动画关键帧序列。

2计算出每一帧的时间TimeTime是相对于程序开始运行的毫秒数。

3定位出第一个动画关键帧和下一个动画关键帧。

4利用Time计算出相对于第一个动画关键帧的毫秒数,再利用这个偏移毫秒数计算出相对于第一个动画帧的偏移位置。

5设置变换矩阵。

6回到第2步。

 

上图表示了4帧的关键帧动画,其中第0帧和第3帧变换矩阵相同。下面直接看代码,我将结合代码详细叙述。

 

typedef struct sKeyframe

{

  DWORD     Time;

  D3DMATRIX matTransformation;

} sKeyframe;//关键帧的结构,DWORD Time为执行该帧的时间,D3DMATRIX //matTransformation为在该帧时模型的变换矩阵。

 

sKeyframe g_Keyframes[4] =

{

  // 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.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.999999f,  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, 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变量不断改变,为本程序运行的毫秒数。

Time %= (g_Keyframes[3].Time+1);//得到一个不断从01200变化的毫秒数。

  DWORD Keyframe = 0;  // 从第0帧开始。

  for(DWORD i=0;i<4;i++) {

 

    // 如果Time>= 某一关键帧的时间,将关键帧定位于此帧。

    if(Time >= g_Keyframes[i].Time)

      Keyframe = i;

  }

 

  DWORD Keyframe2 = (Keyframe==3) ? Keyframe:Keyframe + 1;//得到接下来的关键帧,如//Keyframe为第3关键动画帧,Keyframe2也为第3关键动画帧。

//Keyframe=1200时这种情况才成立,几率很小。一般情况下,Keyframe2=Keyframe+1

   DWORD TimeDiff = g_Keyframes[Keyframe2].Time -

                   g_Keyframes[Keyframe].Time;

  if(!TimeDiff)

    TimeDiff=1;//计算两个sKeyframe的时间差,当Keyframe=Keyframe2=3时,TimeDiff=0//此时另TimeDiff=1

    float Scalar = (float)(Time - g_Keyframes[Keyframe].Time) / (float)TimeDiff;  // Scalar //值为[01),利用Time计算出相对于Keyframe的偏移毫秒数,再除以两//帧的时间差。

 

  D3DXMATRIX matInt = D3DXMATRIX(g_Keyframes[Keyframe2].matTransformation) -

                      D3DXMATRIX(g_Keyframes[Keyframe].matTransformation);

  matInt *= Scalar; //用于计算相对于Keyfrme的偏移位置。

  matInt += D3DXMATRIX(g_Keyframes[Keyframe].matTransformation); // 计算出该帧处相//对于Keyfrme的偏移位置。

 

  g_pD3DDevice->SetTransform(D3DTS_WORLD, &matInt);  // 设置 world transformation matrix

   设置完变换矩阵,剩下的事情就只是渲染了。创建计时器控制动画的技术是非常简单有效的,这是现代计算机游戏动画的基础,因此,必须深刻理解它的内容。

 

 

  

 

3D游戏角色动画

1 介绍微软的X文件

    制作3D游戏角色动画需要与之相关的“动画容器”。这个“容器”中储存着动画的数据。微软的X文件正是这样的一种容器。由于微软的影响力,它们公司设计的X文件也广为流传。X文件是一套基于模版定义的文件,理论上它能够容纳任何数据。也就意味着不仅仅是3D模型文件,任何用于游戏引擎加载的外部资源都可以被包含于X文件之中。下面我们详细的介绍下X文件。为了得到一个直观的印象,我们首先浏览下它的全貌。

 

xof 0302txt 0032

 

template Header {

<3D82AB43-62DA-11cf-AB39-0020AF71E433>

DWORD major;

DWORD minor;

DWORD flags;

}

 

template Frame {

<3D82AB46-62DA-11cf-AB39-0020AF71E433>

[FrameTransformMatrix]

[Mesh]

}

 

Header {

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.000000, 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.00000;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; 0.000000; 0.000000;;

                   }

                 }

                 }

                    }

}

首先看头文件xof 0302txt 0032xof表示这是一个真正的X文件。0302txt表示通知程序使用DirectxX文件,版本为3.2的模版,其中txt表示此文件为文本文件,可读,并非是一个2进制文件。0032表示一个浮点数的位数为32,如果想要用64位的浮点数,可以写成0064

下面我们将按照以下七个步骤进行介绍和说明。

 

第一,声明一个模版:

假设声明 template ContactEntry ,首先需要用guidgen.exe产生一个GUID。产生的GUID如下:

// {4C9D055B-C64D-4bfe-A7D9-981F507E45FF}

DEFINE_GUID(<<name>>,

0x4c9d055b, 0xc64d, 0x4bfe, 0xa7, 0xd9, 0x98, \

0x1f, 0x50, 0x7e, 0x45, 0xff);

之后需要在程序代码中加入:

#include "initguid.h"

// At beginning of source code file - add DEFINE_GUIDs

DEFINE_GUID(ContactEntry, \

0x4c9d055b, 0xc64d, 0x4bfe, 0xa7, 0xd9, 0x98, \

0x1f, 0x50, 0x7e, 0x45, 0xff);

还要在X文件中加入:

template ContactEntry {

<4C9D055B-C64D-4bfe-A7D9-981F507E45FF>}

这里介绍下声明模版用到的数据类型:

关键字       描述

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-terminated string (char[]))

array        Signifies an array of following data type to follow ([])

使用数据类型的举例:

DWORD Value;  

array STRING Text[20];//定义一个名为Text的数组,类型为STRING,大小为20

DWORD ArraySize; array STRING Names[ArraySize]; //可以将大小设置为变量。

现在,我们声明一个ContactEntry模版:

template ContactEntry {

<4C9D055B-C64D-4bfe-A7D9-981F507E45FF>

STRING Name; // The contact's name

STRING PhoneNumber; // The contact's phone number

DWORD Age; // The contact's age

}

实例化一个模版对象:

ContactEntry JimsEntry {

"Jim Adams";

"(800) 555-1212";

30;

}

{JimsEntry} 可以用这样的形式引用一个数据对象。例如,在一个animation sequence template中引用一个Frame data object做为其内嵌数据对象。也可以利用引用表示一个数据对象的副本,没有必要重复书写这个数据对象。

 

第二,内嵌数据对象和模版约束:

首先,我们分别声明了三个不同的模版,请仔细看它们的区别。

template ClosedTemplate {

<4C9D055B-C64D-4bfe-A7D9-981F507E45FF>

DWORD ClosedData;

}

template OpenTemplate {

<4C9D055B-C64D-4bff-A7D9-981F507E45FF>

DWORD OpenData;

[...]

}

template RestrictedTemplate {

<4C9D055B-C64D-4c00-A7D9-981F507E45FF>

DWORD RestrictedData;

[ClosedTemplate]

[OpenTemplate]

}

ClosedTemplate看起来没有什么不同,因为它就是标准的模版声明。在OpenTemplate中包含一个[...],表示这是一个开放模版。开放模版允许在[]中内嵌任何数据对象。例如,你可以实例化OpenTemplate,在里面定义一个OpenData变量和内嵌一个ClosedTemplate的实例。最后的RestrictedTemplate为约束模版。约束模版实例化时只允许包含它列出的数据对象,如,不能在RestrictedTemplate包含[ClosedTemplate][OpenTemplate]以外的数据对象。

 

第三,充分利用DirectX .X Standard Templates