3D游戏角色动画(二)

2 骨骼动画的原理与实现

这里的骨骼蒙皮动画特指skinnd mesh,也叫骨骼动画。无论是合金装备,波斯王子,还是魔兽世界,到处都是骨骼动画技术的运用。用它塑造了各种各样,栩栩如生的生物。其中,人类体格的骨骼动画运用最为广泛。现在让我们一步一步揭开它神秘的面纱。

从本质上来讲,所有的3D角色动画系统都是基于一种逻辑,就是用一定的方法去改变Mesh顶点的位置,只是具体改变的方法不同而已。骨骼动画也是一样的。

骨骼动画的基本原理就是首先控制各个骨骼和关节,再使符在上面的skinned mesh与其匹配。在骨骼蒙皮动画中,一个角色由作为皮肤的单一网格模型和按照一定层次组织起来的骨骼组成。骨骼层次描述了角色的结构,就像关节动画中的不同部分一样,骨骼蒙皮动画中的骨骼按照角色的特点组成一个层次结构。相邻的骨骼通过关节相连,并且可以作相对的运动。通过改变相邻骨骼间的夹角,位移,组成角色的骨骼就可以做出不同的动作,实现不同的动画效果。皮肤则作为一个网格蒙在骨骼之上,规定角色的外观。这里的皮肤不是固定不变的刚性网格,而是可以在骨骼影响下变化的一个可变形网格。组成皮肤的每一个顶点都会受到一个或者多个骨骼的影响。在顶点受到多个骨骼影响的情况下,不同的骨骼按照与顶点的几何,物理关系确定对该顶点的影响权重,这一权重可以通过建模软件计算,也可以手工设置。通过计算影响该顶点的不同骨骼对它影响的加权和就可以得到该顶点在世界坐标系中的正确位置。动画文件中的关键帧一般保存着骨骼的位置,朝向等信息。通过在动画序列中相邻的两个关键帧间插值可以确定某一时刻各个骨骼的新位置和新朝向。然后按照皮肤网格各个顶点中保存的影响它的骨骼索引和相应的权重信息可以计算出该顶点的新位置。这样就实现了在骨骼驱动下的单一皮肤网格变形动画。或者简单地说骨骼蒙皮动画。骨骼蒙皮动画的效果比关节动画和单一网格动画更逼真,更生动。而且,随着3D硬件性能的提高,越来越多的相关计算可以通过硬件来完成,骨骼蒙皮动画已经成为各类实时动画应用中使用最广泛的动画技术。

下面讨论骨骼蒙皮动画实现的技术细节。在一个典型的骨骼蒙皮动画模型文件中,会保存如下信息:网格信息,骨骼信息和动画信息。网格信息是角色的多边形模型。该多边形模型一般由三角形面片组成,每一三角形面片有三个指向模型的顶点表的索引。通过该索引,可以确定该三角形的三个顶点。顶点表中的每一顶点除了带有位置,法向量,材质,纹理等基本信息外,还会指出有哪些骨骼影响了该顶点,影响权重又是多少。影响一个顶点的最大骨骼数一般取决于模型的设计和目标硬件平台的限制。比如,对于一个典型的人体骨架,一般只有在关节附近的顶点才会受到相邻几块骨骼的影响,而同时影响某一顶点的骨骼数,也不会超过四块。骨骼信息包括全部骨骼的数量和每一骨骼的具体信息。所有的骨骼按照父子关系组织成一棵树。树根代表整个骨架,其余每一节点包括叶子节点代表一根骨骼。每一根骨骼包括该骨骼在父骨骼坐标系中的变换矩阵,通过该变化矩阵确定了该骨骼在父骨骼坐标系中的位置。在动画信息中则保存了若干关键帧。每一关键帧指出了每一骨骼在该时刻相对于父骨骼坐标系的变换矩阵,当然也可以是该骨骼相对于父骨骼的位置,朝向等变动。在播放动画序列中的任一时刻:

1)首先确定该时刻之前和之后的两个关键帧,然后按照该时刻与前后两个关键帧时刻的时间值插值计算出该时刻该骨骼相对于父骨骼的新变换矩阵,这个变换矩阵往往代表旋转变换,放缩变换也能接受,如果是平移变换矩阵,很容易将Mesh四分五裂。

2)对于皮肤网格中的每一个顶点,计算它在世界坐标中新的位置和朝向。首先找到影响该顶点的所有骨骼。然后计算每一骨骼对该顶点的影响。也就时说,计算在该骨骼独立作用下顶点的新位置。计算按照如下公式:

顶点的新位置 = 最初状态顶点的位置* 最初状态骨骼世界变换矩阵的逆矩阵* 骨骼的新变换矩阵(I)

然后将所有这些新位置按照每一骨骼的影响权重加权求和。注意所有权重的和应该恰好为 1。在公式(I)中,最初状态顶点的位置为什么首先要与最初状态骨骼世界变矩阵的逆矩阵相乘呢?前面说过,骨骼的新变换矩阵是相对于父骨骼变换的,另一方面这个新变换矩阵是世界变换矩阵,它的任何变换是相对于世界坐标系原点的。因此需要把最初状态顶点移动到相当于父骨骼节点是原点的位置上,再进行矩阵变换。

3)根据网格模型顶点的新位置和朝向绘制角色网格。

下面我们结合具体的图形API环境(Direct3D)来进一步介绍骨骼动画。

第一,了解骨骼结构(Skeletal Structures)和骨层级(Bone Hierarchies):

骨骼结构就是连续很多的骨头(Bone)相结合,形成的骨层级。第一个骨头叫做根骨(root bone),是形成骨骼结构的关键点。其它所有的骨骼作为孩子骨(child bone)或者兄弟骨(sibling bone)附加在根骨之上。所谓的“骨”用一个帧(frame)对象表示。在Directx中,用一个D3DXFRAME结构或者X文件中的Frame template来表示帧对象。下面看一下Frame template和D3DXFRAME结构的定义:

template Frame
{
< 3D82AB46-62DA-11cf-AB39-0020AF71E433 >
FrameTransformMatrix frameTransformMatrix; // 骨骼相对于父节点的坐标变换矩阵,就是一个matrix
Mesh mesh; // 骨骼的Mesh
}



typedef struct _D3DXFRAME
{
LPSTR Name; // 骨骼名称
D3DXMATRIX TransformationMatrix; // 相对与父节点的坐标变换矩阵

LPD3DXMESHCONTAINER pMeshContainer; // LPD3DXMESHCONTAINER对象,

//用来加载MESH,还有一些附加属性,见SDK

struct _D3DXFRAME *pFrameSibling; // 兄弟节点指针,和下面的子节点指针

// 一块作用构成骨骼的层次结构。
struct _D3DXFRAME *pFrameFirstChild; // 子节点指针
} D3DXFRAME, *LPD3DXFRAME;



注意D3DXFRAME * pFrameSibling和D3DXFRAME * pFrameFirstChild,主要是利用这两个指针形成骨层级。pFrameSibling把一个骨头连接到兄弟层级,相对的,pFrameFirstChild把一个骨头连接到子层级。通常,你需要用建模软件为你的程序创建那些骨骼结构,输出骨层级到X文件以便使用。Microsoft有3D Studio Max和Maya的输出插件(exporter),可以输出骨骼和动画数据到X文件。很多建模程序也都有这样的功能。

X文件包含了帧数据,用一个帧(frame)模版的数据对象层级代表骨骼层级。如下图:



注意上面的图,利用D3DXFRAME pointers指针形成了一个兄弟帧和孩子帧的链表。

在前面template Frame中已经提及过每个Frame数据对象中存放着一个变换矩阵,这个矩阵描述了该骨骼相对于父骨骼的位置。另外在根Frame数据对象中内嵌了一个标准的Mesh数据对象。Frame定义了骨骼的层级,而Mesh中的SkinWeights数据对象定义了Frame代表的骨头。我们用D3DXFRAME结构容纳从X文件加载进来的Frame数据对象。为了更好的容纳Frame数据对象,我们需要扩展下D3DXFRAME结构:

struct D3DXFRAME_EX : D3DXFRAME

{

D3DXMATRIX matCombined; // 组合变换矩阵,用于储存变换的骨骼矩阵

D3DXMATRIX matOriginal; // 从X文件加载的原始变换矩阵



D3DXFRAME_EX()

{

Name = NULL;

pMeshContainer = NULL;

pFrameSibling = pFrameFirstChild = NULL;

D3DXMatrixIdentity(&matCombined);

D3DXMatrixIdentity(&matOriginal);

D3DXMatrixIdentity(&TransformationMatrix);

}



~D3DXFRAME_EX()

{

delete [] Name; Name = NULL;

delete pFrameSibling; pFrameSibling = NULL;

delete pFrameFirstChild; pFrameFirstChild = NULL;

}

}

利用我们以前介绍的cXParse类可以遍历X文件的数据对象,从而加载出Frame数据对象。下面的代码都是写在方法ParseObject中,如下:

// 判断当前分析的是不是Frame节点
if( objGUID == TID_D3DRMFrame )
{

// 引用对象直接返回,不需要做分析。一个数据段实际定义一次后可以被其他模板引用,例

//如后面的Animation动画模板就会引用这里的Frame

// 节点,标识动画关联的骨骼。
if( pDataObj->IsReference() )
return true;

// D3DXFRAME_EX为D3DXFRAME的扩展结构,增加些数据成员

D3DXFRAME_EX *pFrame = new D3DXFRAME_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必须有效
else if( objGUID == TID_D3DRMFrameTransformMatrix && pData )
{
// 我们可以肯定pData指向某个Frame
D3DXFRAME_EX *pDataFrame = ( D3DXFRAME_EX* )( *pData );

// 先取得缓冲区大小,应该是个标准的4x4矩阵
DWORD size = 0;
LPCVOID buffer = NULL;

hr = pDataObj->Lock( &size, &buffer );
if( FAILED( hr ) )
return false;

// 拷贝数据
if( size == sizeof( D3DXMATRIX ) )
{
memcpy( &pDataFrame->TransformationMatrix, buffer, size );
pDataObj->Unlock();

pDataFrame->matOriginal = pDataFrame->TransformationMatrix;
}
}



第二,修改和更新骨骼层级:

加载完骨骼层级之后,你可以操作它,更改骨骼的方位。你需要创建一个递归函数,按照名字找到相应的Frame数据对象。这个函数如下:

D3DXFRAME_EX *FindFrame(D3DXFRAME_EX *Frame, char *Name)

{

if(Frame && Frame−>Name && Name) {

// 如果名字找到,返回一个Frame指针

if(!strcmp(Frame−>Name, Name)) // strcmp函数比较两个字符串,如果两个字符串相等,返回0

return Frame;

}

// 在sibling frames找匹配的名字

if(Frame && Frame−>pFrameSibling) {

D3DXFRAME_EX *FramePtr =
FindFrame((D3DXFRAME_EX*)Frame−>pFrameSibling,
Name);

if(FramePtr)

return FramePtr;

}

// 在child frames找匹配的名字

if(Frame && Frame−>pFrameFirstChild) {

D3DXFRAME_EX *FramePtr =
FindFrame((D3DXFRAME_EX*)Frame−>pFrameFirstChild,
Name);

if(FramePtr)

return FramePtr;

}

// 如果没有找到,返回 NULL

return NULL;

}

如果你想找到一个叫“Leg”的Frame,可以把“Leg”传入FindFrame函数,并且提供指向RootFrame的指针:

// pRootframe 为D3DXFRAME_EX root frame 指针

D3DXFRAME_EX *Frame = FindFrame(pRootFrame, "Leg");

if(Frame) {

// 可以在这里做一些处理,比如旋转操作

// 你在这里可以稍微的旋转这个骨头

D3DXMatrixRotationY(&Frame−>TransformationMatrix, 1.57f);

}

一旦你修改变换骨头,你需要更新整个骨骼层级,也就是把变换的组合矩阵存入D3DXFRAME_EX结构的matCombined成员中,用于后面的渲染。下面的函数应该增加到D3DXFRAME_EX结构中,如下:

void UpdateHierarchy(D3DXMATRIX *matTransformation = NULL)

{

D3DXFRAME_EX *pFramePtr;

D3DXMATRIX matIdentity;

// 如果为空,用一个全同矩阵

if(!matTransformation) {

D3DXMatrixIdentity(&matIdentity);

matTransformation = &matIdentity;

}

// 把变换矩阵组合到matCombined中

matCombined = TransformationMatrix * (*matTransformation);

// 更新兄弟层级

if((pFramePtr = (D3DXFRAME_EX*)pFrameSibling))

pFramePtr->UpdateHierarchy(matTransformation);

// 更新孩子层级

if((pFramePtr = (D3DXFRAME_EX*)pFrameFirstChild))

pFramePtr->UpdateHierarchy(&matCombined);

}

现在matCombined储存着每个骨骼相对于原点的变换矩阵,然后只要把各个顶点附在相应的骨骼上,就能渲染了。



第三,使用蒙皮网格:

网格可以分为蒙皮网格(Skin Mesh)和普通网格(Mesh)。蒙皮网格就是具有蒙皮信息的普通网格。为了搞清楚蒙皮网格我们需要介绍相关的三个模版:

template Mesh

{

<3D82AB44-62DA-11CF-AB39-0020AF71E433>

DWORD nVertices; //顶点数

array Vector vertices[nVertices]; //顶点坐标数组

DWORD nFaces; //多边形数

array MeshFace faces[nFaces]; //多边形顶点引索

[...]

}
这个模板存储一个表态的网格和网格的材质。在骨骼蒙皮动画中,整个角色只是一个网格,由蒙皮信息确定网格中的每一个部分如何受到骨骼的影响。网格在内部会分成几个子集,每一个子集将受到一些特定骨骼的影响。
template XSkinMeshHeader

{

< 3CF169CE-FF7C-44ab-93C0-F78F62D172E2 >

WORD nMaxSkinWeightsPerVertex; // 网格中受到骨骼影响的顶点数

WORD nMaxSkinWeightsPerFace; // 网格中受到骨骼影响的多边形数

WORD nBones; // 影响网格顶点的骨骼数量

}

这个模版包含于Mesh模版中。包含关于蒙皮信息的属性。
template SkinWeights

{

< 6F0D123B-BAD2-4167-A0D0-80224F25FABB >

STRING transformNodeName; //骨骼的名字

DWORD nWeights; //附属到该骨骼的顶点数

array DWORD vertexIndices[nWeights]; //附属到该骨骼的顶点引索

array float weights[nWeights]; //相应引索的顶点权值

Matrix4x4 matrixOffset; //相对于骨骼位置的偏移矩阵

}

这个模版也包含于Mesh模版中,真正的蒙皮信息就存储在这里。每一个影响到网格的骨骼在模版中都有实例。例如有12个骨骼影响到网格,Mesh模版里将有12个SkinWeights模版的实例。

蒙皮网格和普通网格的唯一不同点就是看XskinMeshHeader和SkinWeights模版是否存在。如果把这两个模版从任何一个蒙皮网格里面移走的话,就可以得到一个普通网格。在X文件中,我们将会发现一个GUID为TID_D3DRMMesh的模版,这表示模版里面存有一个网格。利用D3D的帮助函数D3DXLoadSkinMeshFromXof将会加载蒙皮网格和其它补充性数据。只需要向它传递一个IDirectXFileData指针,然后它将为你做剩下的事情。现在介绍下D3DXLoadSkinMeshFromXof函数:

HRESULT D3DXLoadSkinMeshFromXof(

LPD3DXFILEDATA pxofMesh, //X文件数据接口

DWORD Options, //加载参数

LPDIRECT3DDEVICE9 pD3DDevice, //使用的三维设备

LPD3DXBUFFER * ppAdjacency, //邻接信息缓冲接口

LPD3DXBUFFER * ppMaterials, //材质缓冲接口

LPD3DXBUFFER * ppEffectInstances, //效果实例接口

DWORD * pMatOut, //材质数

LPD3DXSKININFO * ppSkinInfo, //蒙皮信息接口

LPD3DXMESH * ppMesh //加载的网格模型接口

);

需要特别注意是LPD3DXSKININFO * ppSkinInfo接口,储存着蒙皮信息。

当你加载一个网格,并读取了的这些顶点的权值之后,你可以变换这些顶点去匹配骨骼的方向,使用以下步骤:

1)迭代所有的顶点。为每个顶点进行第2步。

2)对当前顶点连接到的每一个骨头,得到骨头的变换矩阵。

3)对于每个骨头的变换矩阵,用顶点的权值乘以这个变换矩阵然后把这个结果应用到顶点的组合变换矩阵。

4)为每个连接的骨头重复第三步,然后为每个顶点通过第四步重复第二步。当你完成以上步骤,把组合变换矩阵应用到具体的被迭代的顶点(从第一步)。

怎样精确的获得顶点的权值?可以利用ID3DXSkinInfo接口的GetBoneVertexInfluence方法得到这些权值。顶点的权值一般储存在Mesh数据对象的末端。执行完以上步骤后,剩下的仅仅是渲染了。结合前面介绍的计时动画技术,可以把关键帧时间和变换矩阵储存在AnimationSet模版中,利用一些变量不断的修改和更新骨骼层级便能创造出各种动画效果。





3 增加场景数据

在游戏中,仅仅有活灵活现的角色动画是远远不够的。因为你需要让它在具体的场景中尽情地表演,这就需要场景数据。场景数据中最具代表性的就是角色模型在游戏世界中的位置,包围球半径。显然,还有很多其它数据,这些数据都是因场合而异的。正如前面所说,X文件可以用于储存任何数据,包括场景数据。但是用X文件储存场景信息会碰到很多麻烦。比如你很难找到能够把模型转化成附有场景信息的X文件的建模工具。当然,你可以自己开发建模工具的插件。下面我介绍一个简单有效的方式得到场景数据。

很多建模软件都支持导出XML文件。在3DSMAX中,就有这样的插件。完全可以在XML文件中储存场景信息。这样,就需要一个XML文件的读取函数。虽然XML文件较为复杂,但是利用一些帮助库,可以很方便的写出XML文件的读取函数。Tinyxml 就是一个小巧稳定的XML帮助库,在互联网上可以找到很多关于它的信息。我们的目的就是把XML文件中的场景数据根据需要加载到自己定义的数据结构中。

假设定义这样一个简单的数据结构:

struct SMeshSceneInfo //Mesh场景信息,为了访问的便利,不使用链表动态分配内存

{

SMeshSceneInfo()

{

int ID=0;

D3DXVECTOR3 position(0.0f,0.0f,0.0f);

}



string name; //Mesh的名字

int ID; //MeshID

D3DXVECTOR3 position; //Mesh世界坐标



};

接着利用帮助库的函数,在程序的初始化阶段加载场景数据到这个数据结构中。之后在程序里就可以利用这些数据进行相关的操作。









4 简介渐变动画

回到20世纪90年代早期,一种革命性的计算机图形动画技术称之为morphing,就是渐变。它一举成功,成为主流技术,并延用至今。游戏中的渐变技术,最好的例子可能就是ID SOFT的雷神之锤(Quake)。所有这些角色的动画序列由一系列的渐变网格模型构成,一个模型缓慢的改变形状变成第2个模型,第2个模型再改变形状匹配到第3个网络模型上,以此类推。

其实渐变动画的原理很简单,就是差值两个关键帧模型的顶点。第一个关键帧模型叫做源网格模型,第二个关键帧叫做目标网格模型。当从第二个关键帧向第三个关键帧变化时,第二个关键帧网格又变成源网格模型,第三个关键帧变成目标网格模型。

下图显示了随着时间的变化,各网格顶点的变化:



每个顶点都共享源网格模型和目标网格模型中相同的引索。在这里,顶点的次序尤为重要,如果次序错误,将产生奇怪的动画效果。

相比骨骼蒙皮动画,渐变动画的逻辑简单许多,如果掌握了前者,很容易就能实现各种基于渐变动画的效果。





四 结束语

游戏的世界正因为各种动画技术的灵活运用才显得绚丽多彩,有趣迷人。在不久的未来,更棒的动画技术将会普及。比如,在游戏中你是一个身怀绝技的武士,当对手跃起,咆哮着从上方挥刀砍来,你不能简单地按游戏手柄的“挡格”键,而是真正的手握战刀,看准时机把对方的攻击化险为夷。各个动作的计算准确无误,如果你愿意的话,可以如实记录你的行为,立即产生出相同的动画效果。或者你是一个高尔夫球的爱好者,并不是任何地方都有打高尔夫球的场地。没有问题,你同样可以在家里尽情地挥棒击球,享受高夫球的乐趣。相信你可以想象出更多有趣的事情。这不仅仅是想象,而是不久就能体会到的快乐。



相关参考资料:Introduction to 3D Game Programming with DirectX 9.0 by Frank Luna

ISBN:1-55622-913-5 Wordware Publishing © 2003 (388 pages)



Advanced Animation with DirectX by Jim Adams

Copyright © 2003 Premier Press, a division of Course Technology.



Advanced 3D Game Programming with DirectX 9.0 by Peter Walsh

ISBN:1-55622-968-2 (pbk.) Copyright © 2003 Wordware Publishing, Inc.



DirectX 9.0 Programmer's Reference by Microsoft

2005 Microsoft Corporation. All rights reserved.



硬件支持下骨骼蒙皮动画的实现 by Octane3d
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值