骨骼动画(Skeletal Animation)

来源:互联网 发布:http协议 适用端口号 编辑:程序博客网 时间:2024/04/30 05:09

骨骼动画(Skeletal Animation)(一)

相信这里没有人没玩过采用骨骼动画技术的游戏,看看那些热门的动作游戏,例如《波斯王子》、 《分裂细胞》和《战神》,你就知道骨骼动画的威力了(我承认是猜的)。骨骼动画技术用来使我们的3D模型在屏幕上动起来,通过和动作捕捉技术结合,可以让 模型做出非常逼真的动作。而这样一个极具威力的技术,其原理却相当简单。

假设我们要让游戏主角做出一个动作,例如波斯王子拿弯刀往前一劈。最简单的方法,就是让模型 师建一个动画序列,然后在程序中逐帧播放,就像放电影一样。不过这样一来工作量就太大了,玩家也需要N个G的硬盘来安装这个游戏。与此不同,骨骼动画技术 采用了一种很聪明的方式。首先,建模师完成一个标准姿势的3D模型,通常是双手沿着肩的方向伸展平放,双脚打开。所有的后继动作将由这个基础动作演变得 到。在完成这个基准模型之后,建模师再建一个骨骼结构,一系列相互关联的顶点,就像一个骨架一样,与人体模型各个关节匹配并且都会有一定数量的顶点与之关 联。想象一下人和人身上的骨骼就很容易知道我在说什么。之后,在我们想要完成的动画序列中,挑选一些关键帧,对每个关键帧,将骨骼的位置与关键帧匹配。然 后把这一系列的关键帧骨骼保存起来,除了骨骼的位置,同时保存的还有从基准位置变换到当前关键帧的旋转、平移、缩放或者一个混合的坐标变换矩阵。在我们引 擎中,首先根据当前时间查找这时候角色是处于哪两个关键帧中间。找到之后以时间为参数在关键帧的坐标变换矩阵之间求插值,用插值结果来决定骨骼当前的位 置。骨骼位置求出来后,所有和骨骼关联的顶点的坐标也可以相应求出来了。通过使用骨骼动画技术,我们用相对较少的数据就可以播放很平滑的动画!

了解了相关原理,来看看如何在directx中播放骨骼动画。我的参考书是《Advanced Animation with DirectX》。

 

 骨骼动画(Skeletal Animation)(二)

现在我们知道为了播放骨骼动画,需要有骨骼(bone)的数据,模型(mesh)的数据,关 联骨骼和模型上每个顶点的关联数据,以及关键帧的坐标变换数据。所有这些数据必须以某种形式存在于某个地方供我们获取才行。这里要介绍的MS的x文件格式 以及从中获取数据的方法。强烈建议大家都来学习一下x文件格式!你会发现它即简单又强大,即使用来存放自定义数据也是相当的方便,一旦掌握之后我保证你会 对它爱不释手。

典型的x文件以数据模板和实际数据两部分组成。数据模板类似c++中的结构定义,不过更为灵活和开放。实际数据就是遵守模板定义的数据段。看一个例子,

template Employee {

<3D82AB43-62DA-11cf-AB39-0020AF71E433> // 每个模板关联唯一的GUID

STRING Name; // 姓名

DWORD Sex; // 性别

[ContactEntry] // 联系方式, 另一个模板,模板可以嵌套

}

template ContactEntry {

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

STRING PhoneNumber; // 电话号码

STRING Address; // 地址

}

Employee David{

"David";

1;

ContactEntry{

"100-100000000";

"far far away";

}

}

从上面这个简单的例子我们就可以看出x文件的大概模样了,详细的情况大家可以参考《Advanced Animation with DirectX》。下面我们看如何来读取这样一个x文件,借助下几个对象,

ID3DXFile -- x文件格式文档对象。例如Employee.x这样一个文件。

ID3DXFileEnumObject -- 用来枚举x文档的顶级模板数据。所谓顶级模板数据是指那些没有

父模板的数据,例如上面的David数据段。

ID3DXFILEDATA -- 模板数据。上面的David和他的联系方式都是ID3DXFILEDATA

对象,自包含。

下面看实际的分析函数, 下面的代码适用于DirectX 9.0 SDK Update (October 2004),原书的代码有点过时了...

//-----------------------------------------------------------------------------

// 名称 : Parse

// 描述 : 分析x文件格式文档

//-----------------------------------------------------------------------------

bool Parse( char *filename, void **pData )

{

LPD3DXFILE lpD3DXFile;

LPD3DXFILEENUMOBJECT lpD3DXFileEnumObj;

LPD3DXFILEDATA lpD3DXFileData;

// 参数检查

if( NULL == filename )

return false;

// 创建X文件对象

HRESULT hr = D3DXFileCreate( &lpD3DXFile );

if( FAILED( hr ) )

return false;

// 注册标准模板

hr = lpD3DXFile->RegisterTemplates(

( LPVOID )D3DRM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES );

if( FAILED( hr ) )

{

Release<LPD3DXFILE>( lpD3DXFile );

return false;

}

// 创建X文件枚举对象

hr = lpD3DXFile->CreateEnumObject(

filename, D3DXF_FILELOAD_FROMFILE, &lpD3DXFileEnumObj );

if( FAILED( hr ) )

{

Release<LPD3DXFILE>( lpD3DXFile );

return false;

}

// 解析开始

bool parseResult = BeginParse( pData );

if( true == parseResult )

{

// 查询顶级模板数

SIZE_T childCount = 0;

lpD3DXFileEnumObj->GetChildren( &childCount );

// 分析每个订级模板

for( DWORD i=0; i<childCount; i++ )

{

// 获取当前模板

hr = lpD3DXFileEnumObj->GetChild( i, &lpD3DXFileData );

if( FAILED( hr ) )

break;

// 分析

parseResult = ParseObject( lpD3DXFileData, NULL, 0, pData );

// 释放FileData对象

Release<LPD3DXFILEDATA>( lpD3DXFileData );

// 出现错误,中断分析

if( false == parseResult )

break;

}

// 解析结束

if( parseResult )

parseResult = EndParse( pData );

}

// 释放相关对象

Release<LPD3DXFILEENUMOBJECT>( lpD3DXFileEnumObj );

Release<LPD3DXFILE>( lpD3DXFile );

// 解析结束

return parseResult;

}

//-----------------------------------------------------------------------------

// 名称 : ParseObject

// 描述 : 递归解析顶级模板

//-----------------------------------------------------------------------------

bool ParseObject(

LPD3DXFILEDATA pDataObj,

LPD3DXFILEDATA pParentDataObj,

DWORD depth,

void **pData )

{

LPD3DXFILEDATA pSubDataObj;

bool parseResult = true;

HRESULT hr;

// 获取子模板数目

DWORD childCount;

pDataObj->GetChildren( &childCount );

// 遍历模板并分析

for( DWORD i=0; i<childCount; i++ )

{

// 取子模板对象

hr = pDataObj->GetChild( i, &pSubDataObj );

if( FAILED( hr ) )

break;

// 分析子模板

parseResult = ParseObject( pSubDataObj, pDataObj, depth+1, pData );

// 释放数据对象

Release<LPD3DXFILEDATA>( pSubDataObj );

// 出现错误,停止分析

if( false == parseResult )

break;

}

return parseResult;

}

就那么简单,相信大家都看得明白。通过重载ParseObject方法,我们以判断当前分析的模板类型,然后创建实际的模板对象,从文档中复制数据。有了上面的工具,我们就可以自己来读取和解析x格式的骨骼动画文件了。

 

骨骼动画(Skeletal Animation)(三)

下面我们就来看看如何重载ParseObject方法来获得我们感兴趣的数据,不要担心,绝 对简单。仔细看代码,你会发现只需要做一件事情,判断当前数据段的类型(通过GUID),分配对应的结构对象,然后从数据段拷贝数据(所有SDK自定义模 板的GUID都在头文件rmxfguid.h中定义, 你需要把它加入你的工程中。所有预定义模板在这里可以找到)。先来看看如何获取当前数据段的GUID,

GUID objGUID;

pDataObj->GetType( &objGUID );

简单吧,下面开始我们的分析之旅。x动画文件中骨骼是用Frame模板定义的,

template Frame

{

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

FrameTransformMatrix frameTransformMatrix; // 骨骼相对于父节点的坐标变换矩阵

Mesh mesh; // 骨骼的Mesh

}

只有两个字段。FrameTransformMatrix就是一个matrix。Mesh稍 微复杂,详细格式大家自己参考MSDN,我们也会有专门的代码来加载Mesh,现在关注Frame。为了加载Frame,我们要在程序中定义一个和 Frame模板对应的数据结构,SDK中经默认提供了一个,那就是D3DXFRAME,

typedef struct _D3DXFRAME

{

LPSTR Name; // 骨骼名称

D3DXMATRIX TransformationMatrix; // 相对与父节点的坐标变换矩阵

LPD3DXMESHCONTAINER pMeshContainer; // LPD3DXMESHCONTAINER对象,用来

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

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

// 一块作用构成骨骼的层次结构。

struct _D3DXFRAME *pFrameFirstChild; // 子节点指针.

} D3DXFRAME, *LPD3DXFRAME;

这样一个结构已经足够容纳Frame模板中的数据并形成一个层次结构,不过为了我们程序的需要,我们还需要其他字段,为此我们通常会扩展D3DXFRAME,

typedef struct _D3DXFRAME_EX : public D3DXFRAME

{

D3DXMATRIX matCombined; // 存储当前节点相对于根节点的位置偏移矩阵,沿着到

// 到根骨骼的路径把所有的坐标变换矩阵相乘得到。

D3DXMATRIX matOriginal; // 在播放动画的时候有可能会改变原来结构中的

// TransformationMatrix,因此我们声名一个新的字段

// 将原来的坐标变换矩阵保存起来以便在需要的时候恢

// 复回去。

... // 忽略一些方法定义

}

我知道有些人已经按捺不住了,那么动手吧,

// 判断当前分析的是不是Frame节点

if( objGUID == TID_D3DRMFrame )

{

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

// 被其他模板引用,例如后面的Animation动画模板就会引用这里的Frame

// 节点,标识动画关联的骨骼。

if( pDataObj->IsReference() )

return true;

// 创建D3DXFRAME_EX结构,准备拷贝数据

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 --

骨骼。正如大家已经看到的那样,我们可以用pFrameSibling和pFrameFirstChild两个字段来构成骨骼的层次结构。骨骼模板包含了当前骨骼相对父骨骼的坐标变换矩阵和骨骼对应的模型

Mesh --

模型。角色的顶点数据,包含vertex buffer, index buffer等。我们可以直接用普通的ID3DXMesh来加载其中的数据。除此之外,Mesh中还包含了SkinWeight模板。

SkinWeight --

骨骼关联的顶点已经该骨骼的坐标变换对该顶点的权重。实际中我们并不需要特殊处理这类模板数据,ID3DXMesh已经包含了对应的代码。

AnimationSet --

动画集合。例如角色的各种动作“Kill”,“Jump”等等,包含多个Animation。

Animation --

动画。由对应骨骼的名称和一组AnimationKey组成。

AnimationKey --

动画键。包含一组时间戳以及在对应时间戳应用到骨骼上的平移、缩放、旋转向量或者复合的坐标变换矩阵。

以上就是我们需要了解的全部了。至此,所有原料都已经准备齐全,各位大厨们下一步要做的就是骨骼动画这道小菜啦!

 

骨骼动画(Skeketal Animation)(四)

我们的目标是根据骨骼动画来更新模型。看看手上的材料,

1)骨骼动画数据。上一节中我们已经读出了AnimationSet、Animation和 AnimationKey这些动画数据,我们现在要做的就是把它们应用到骨骼上面去。AnimationSet只是标明了我们要播放的动画名称,关键的处 理在Animation和AnimationKey上面。Animation包含了所对应的骨骼名称,下属的AnimationKey包含了坐标变换的类 型以及对应的时间戳,我们也把AnimationKey看做一个关键帧。下面要做的就是根据当前时间判断动画落在哪两个关键帧中间,例如key1和 key2,然后求出插值系数scaler,

scaler = (当前时间-key1.时间)/(key2.时间-key1.时间)

 

求出插值系数后,骨骼的当前位置就可以用下面的方法求出,注意各种key类型求插值的方法不一样,

 

switch( Key的类型 )

{

case 旋转:

...

// 四元数插值

D3DXQUATERNION RotationQuaternion;

D3DXQuaternionSlerp(

&RotationQuaternion,

&pAnimationKey->pQuaternionKeys[Key1].value,

&pAnimationKey->pQuaternionKeys[Key2].value,

Scaler);

 

// 应用旋转矩阵

D3DXMATRIX RotationMatrix;

D3DXMatrixRotationQuaternion( &RotationMatrix, &RotationQuaternion );

pAnimation->pBone->TransformationMatrix *= RotationMatrix;

 

break;

 

case 平移和缩放:

...

 

// 矢量插值

D3DXVECTOR3 InterpolatedVector =

pAnimationKey->pVectorKeys[Key1].value + Scaler *

( pAnimationKey->pVectorKeys[Key2].value - pAnimationKey->pVectorKeys[Key1].value );

 

if( pAnimationKey->Type == XAnimationKey::KeyType::Scaling )

{

// 应用缩放矩阵

D3DXMATRIX ScalingMatrix;

D3DXMatrixScaling(

&ScalingMatrix, InterpolatedVector.x, InterpolatedVector.y, InterpolatedVector.z );

pAnimation->pBone->TransformationMatrix *= ScalingMatrix;

}

else

{

// 应用平移矩阵

D3DXMATRIX TranslationMatrix;

D3DXMatrixTranslation(

&TranslationMatrix, InterpolatedVector.x, InterpolatedVector.y, InterpolatedVector.z );

pAnimation->pBone->TransformationMatrix *= TranslationMatrix;

}

 

break;

 

case 坐标变换矩阵:

 

...

 

// 矩阵插值

D3DXMATRIX TransformMatrix =

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

// 描述: 计算本节点及所有兄弟、子节点相对于根节点的偏移矩阵

//-----------------------------------------------------------------------------

void UpdateHierarchy( D3DXMATRIX *matTrans = NULL )

{

D3DXMATRIX matIdentity;

 

// 根节点的偏移矩阵为单位矩阵

if( NULL == matTrans )

{

D3DXMatrixIdentity( &matIdentity );

matTrans = &matIdentity;

}

 

// 计算偏移矩阵

matCombined = TransformationMatrix * ( *matTrans );

 

// 更新兄弟节点

if( pFrameSibling )

( ( D3DXFRAME_EX* )pFrameSibling )->UpdateHierarchy( matTrans );

 

// 更新子节点

if( pFrameFirstChild )

( ( D3DXFRAME_EX* )pFrameFirstChild )->UpdateHierarchy( &matCombined );

}

 

通过在根骨骼上的一次调用,我们就可以在自定义的matCombined字段中得到各个骨骼相对于根骨骼的坐标变换矩阵.

 

3)mesh数据。mesh数据相对简单,ID3DXMesh和ID3DXSkinInfo 接口为我们了做大部分的工作。不过天底下没有免费的午餐,为了让它们运转起来,我们还是要做一些额外的努力。在步骤1里面我们已经通过插值得到了骨骼当前 的坐标变换矩阵,不过这个坐标变换是相对于模型本地坐标的,为了应用到mesh上,我们需要将坐标对齐到mesh的中心,

 

// 首先bone转换到以mesh中心为坐标原点的坐标系,然后再应用frame的坐标变换矩阵

for( DWORD i=0; i<m_pRootMeshContainer->pSkinInfo->GetNumBones(); i++ )

{

m_pRootMeshContainer->pBoneMatrices[i] =

*( m_pRootMeshContainer->pSkinInfo->GetBoneOffsetMatrix( i ) );

 

if( m_pRootMeshContainer->ppFrameMatrices[i] )

m_pRootMeshContainer->pBoneMatrices[i] *= *m_pRootMeshContainer->ppFrameMatrices[i];

}

 

这样所有的数据都准备好了,用ID3DXSkinInfo的方法来更新骨骼关联的每个顶点的坐标,(每个顶点根据关联的所有骨骼的坐标变换矩阵乘以对应的权重再相加来得到最终应用到顶点上的坐标变换矩阵)

 

m_pRootMeshContainer->pSkinInfo->UpdateSkinnedMesh(

m_pRootMeshContainer->pBoneMatrices, NULL, pSrcVertex, pDestVertex );

 

到此一切准备都结束了! 绘制Mesh的动作和平常一样,设置材质和纹理,然后调用DrawSubSet方法,这个想来对大家是没有什么难度的事情。怎么样,是不是想从头再看一遍回味一下呢? 呵呵。

原创粉丝点击