深度探索Skinned Mesh【翻译】

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

译注

我在学习蒙皮骨骼动画的技术实现时,阅读了大量的中英文文档。但绝大部分文档,要么是语焉不详过度抽象,并且流于纸上谈兵。要么就是翻译得实在让人难以理解。Frank Luna的这篇文章是我所阅读过的最棒的蒙皮骨骼动画论文。Frank不仅有生花妙笔,把一些难以理解的技术要点以深入浅出的方式说得清清楚楚,而且还有着很强的实作能力,给文章配上了对应的示例程序。让读者能深入到每一个实现细节中。因此,我把Frank的这篇文章翻译出来,以供大家交流。欢迎批评指正。

前言

在大量的3D仿真程序,尤其是3D计算机游戏中扮演了一个重要的角色。本论文描述了用以驱动一个现代的实时的角色动画系统所需的数据结构和算法。另外,本文还对D3DX9.0c动画系统的进行了详细的研究。

本文第1章描述了用来表征3D角色及其运动的数据结构。第2章聚焦在用来描述一个动画序列(animation sequence)的数据集。第3章考察了基于刚体(rigid body)的动画技术,并且强调了使用该技术的问题所在。第4章解析一个新的动画技术:顶点混合(vertex blending),也通常称为蒙皮动画(skinned mesh animation)。该技术可以避免刚体动画带来的问题。第5章则展示了如何通过使用D3DX动画相关的API去实现一个角色蒙皮动画。第6章展示了如何使用多个不同的动画序列。第7章则演示了通过使用D3DX动画混合(animation blending)相关的API,如何用既有的动画,产生新的动画。最后,第8章则解释了通过使用D3DX的动画回调(animation callback)相关的API,来并行执行代码。

1 角色网格层级概述

图1展现了一个角色网格(character mesh)。途中高亮的链接成条的骨块(chain of bones)被称之为一个骨架(skeleton)。骨骼动画系统就自然而然地以该骨架结构为实现底层,构架而成,骨架外部被一个皮肤(skin)所环绕。在此,“皮肤”将被建模成一个三维的几何体(也即顶点及其构成的多边形)。和真实世界中一样,骨架中的每一个骨块将会影响皮肤的形状和位置。。从数学的观点来说,这些骨块通过变换矩阵(transformation matrix)的方式来表征的。这些变换矩阵可以适当地变换皮肤的几何体。因而,当我们操纵骨架运动时,附着在该骨架的皮肤也会随着当前骨架的姿势(pose)来运动。

图1:角色网格,高亮的骨块链条【bone chain】表示了角色的骨架【skeleton】黑色的多边形则表示了角色的皮肤

1.1 骨块及其继承式的变换

首先,所有的骨块都初始布局在骨块空间(bone space)中。骨块B有两个关联的变换。第一个是其局部变换(local transform),称之为L,另一个是组合变换(combined transform),称之为C。局部变换负责在骨块空间中,让骨块B绕着它的关节(joint)旋转(如图2左图所示)。如果骨块B的关节还连接到另一个称为B的父骨块(parent bone)的骨块时。局部变换L还负责了骨块B相对其父骨块的平移量(如图2右图所示)

图2: a图中的骨块在骨块空间中,绕着它的环枢关节【pivot joint】旋转,b图中的骨块发生偏移,为其父骨块腾出了空位

与局部变换形成对照,组合变换则负责对构成角色骨架的骨块进行摆姿势(posing)的操作,如图3所示。也就是说。组合变换将把一个骨块,从它自身的骨块空间,变换到角色空间(character space)中去。。因而,组合变换就是用来操作皮肤的在角色空间中的位置和形状的。

图3: “组合变换”把骨块从骨块空间变换到角色空间,在本图中,在骨块中间的骨块,变成了角色的右上臂的骨块

我们该如何确定组合变换。这个确定过程并不是很直截了当。因为骨块之间并不是相互独立的,而是相互之间会影响其位置。暂时先不考虑旋转,首先考虑如图4所示的一个手臂的骨架。如下图:

图4: 图中所示的是一个手臂的骨架。观察如何使用T(v0), T(v1)和T(v2)定位手掌的位置。同样地,观察如何使用T(v0), T(v1)定位前臂,使用T(v0)定位上臂。【实际上T(v0)并不起什么作用,因为上臂已经是根骨块不需要进行平移】

在骨块空间中,给定一个上臂,前臂,手掌的骨块。我们需要找出每个骨块的组合变换,用来将这些骨块定位在图4所示的位置。因为每个骨块的局部变换使得该骨块与其父骨块发生偏移。从图4中我们可以容易看到:一个骨块的位置,首先通过使用(applying)它自身的局部变换,然后使用它的父骨块的局部变换,再使用其父骨块的父骨块的局部变换,依次类推。

图5:层级式变换。观察一个骨块的父骨块的变换,如何影响该骨块和该骨块的子骨块

现在我们看到,处于骨块层级架构中的每个骨块,都从各自的父骨块中继承得到平移量和旋转量。这也就是说,得到一个骨块的组合变换,是通过:首先由作用其自身的局部变换(先平移接着旋转);然后再作用其父骨块的局部变换;再作用其祖父骨块的局部变换,依次类推。一直到作为根节点的那个祖先骨块为止。从数学观点上来说。第i个骨块的组合变换矩阵C i是由以下公式得到:

$ C_i = L_i \times P_i $

其中 $ L_i $ 是编号为第i号的骨块的局部变换矩阵。 $ P_i $ i是编号为第i号的骨块的父骨块的组合变换矩阵。注意,通过做矩阵级乘,得到某骨块的组合变换时,需要先乘以骨块自身的局部变换矩阵,而后再乘以父骨块的组合变换矩阵。

1.2 D3DXFRAME

现在介绍D3DX库提供的用以构建骨骼层次数据的结构体D3DXFRAME。我们将使用这个结构体来表征角色的骨块。通过对一些指针变量进行赋值,我们可以连接起整个骨架中的骨块。如图6中所示的骨架。

图6: 图1所示的角色模型的骨骼层次示意图)。垂直向下的箭头表示下部的骨块是上部的骨块的第一个子骨块。水平向右的箭头表示左右两边的骨块是兄弟关系。

应当承认,在角色动画范畴内的上下文概念中。名字BONE通常就是指D3DXFRAME。但D3DXFRAME是一个通用的数据结构,也可用来描述一个非角色网格层级(non-character mesh hierarchies)的数据。无论如何,只要在角色动画范畴内,我们可以交替使用“bone”和“frame”表示同一个概念。

1typedef struct  _D3DXFRAME{
2    LPSTR Name; 
3    D3DXMATRIX TransformationMatrix; 
4    LPD3DXMESHCONTAINER pMeshContainer; 
5    struct  _D3DXFRAME *pFrameSibling; 
6    struct  _D3DXFRAME *pFrameFirstChild; 
7} D3DXFRAME, *LPD3DXFRAME; 

D3DXFRAME结构的数据成员的描述的如下表:

Data Member Description
Name The name of the node
TransformationMatrix The local transformation matrix.
pMeshContainer Pointer to a D3DXMESHCONTIANER. This member is used in the case that you want to associate a container of meshes with this frame. If no mesh container is associated with this frame, set this pointer to null. We will ignore this member for now and come back to D3DXMESHCONTIANER in Section 5 of this paper.
pFrameSibling Pointer to this frame’s sibling frame; one of two pointers used to connect this node to the mesh hierarchy—see Figure 6.
pFrameFirstChild Pointer to this frame’s first child frame; one of two pointers used to connect this node to the mesh hierarchy—see Figure 6.

D3DXFRAME的问题是它不含有一个用来表示combined transform的成员数据。为了解决这个问题我们可以扩展D3DXFRAME,如下代码:

1struct FrameEx : public D3DXFRAME
2{
3     D3DXMATRIX combinedTransform;
4};

1.3 计算得到组合变换矩阵的C++代码

我们可以通过递归遍历访问(recursively traversing)层级结构的方法,去计算每一个层级中的节点(node),也即每一个骨块的组合变换矩阵,如下便是C++代码。

 1void CombineTransforms(FrameEx* frame,D3DXMATRIX& P) 
 2// P就是本骨块的父骨块的组合变换矩阵
 3{
 4    D3DXMATRIX& L = frame->TransformationMatrix;
 5    D3DXMATRIX& C = frame->combinedTransform;
 6    C = L * P;
 7    FrameEx* sibling = (FrameEx*)frame->pFrameSibling;
 8    FrameEx* firstChild = (FrameEx*)frame->pFrameFirstChild;
 9    if( sibling ) // 如果本骨块存在兄弟骨块,那么计算兄弟骨块的组合变换矩阵
10        combineTransforms(sibling, P);
11    if( firstChild ) // 如果本骨块存在子骨块,那么计算子骨块的组合变换矩阵
12        combineTransforms(firstChild , C);
13}

开始执行该递归遍历操作的函数的方式如下:

1D3DXMATRIX identity;
2D3DXMatrixIdentity(&identity);
3CombineTransforms( rootBone, identity );

因为作d为根骨块的那个骨块是没有父骨块的。所以给作为父骨块组合变换矩阵的那个参数,传一个单位矩阵进去。

2 关键帧和动画

在本文中,我们将会考虑预先录制的动画数据(prerecorded animation data)。也就是说动画数据在3D建模工具或者是运动捕捉设备中就已经预先建立好了。通过骨块的变换矩阵,附着在骨架上的皮肤也将会相对应地发生运动,以反映出当前骨架的姿势。所以,现在的问题就是:怎样使得一副骨架发生运动。

为了使得问题表述得更具体化形象化。我们举一个特定的例子。假定一个3D美工被指派了一个任务,要求他创建一个机器人手臂的动画序列(animation sequence),并且该序列必须要持续5秒。在时间段[0.0s, 2.5s]内,机器人的上臂要绕它的肩关节旋转60°,前臂相对于上臂不发生任何移动。在时间段(2.5s, 5.0s]内,上臂绕肩关节旋转-30°。前臂相对于上臂也还不发生任何移动。为了创建这样的一个动画序列,艺术家粗略地为上臂骨块创建了三个关键帧(key frame)。这三个关键帧这分别取在时刻t0 = 0s, t 1 = 2.5s, 和t2 = 5s处,如图7所示

图7: 在[0.0s-2.5s]时间段内,手臂绕着肩连接点旋转了60度,在 (2.5s, 5.0s]时间段内,手臂绕着肩连接点旋转了-30度

一个关键帧可以视为:在某一时刻,骨架中某一骨块的一个有显著含义的姿势(significant pose)。在动画序列中,骨架中的每一个骨块都特别含有若干个关键帧。一般地,关键帧由一个旋转值四元数,放缩值向量,平移值向量所构成。

我们注意到关键帧定义了动画中的极值姿势(extreme pose)。那也就是说,动画中所有其他的姿势,都分布在其中某两个关键帧之间。。现在我们注意到了每个骨块只有三个关键帧的话是不能构成一个5秒长的运动轨迹足够平滑(smoothly)的动画。

图8:关键帧插值 回到图7所示的原始示例在[0.0s, 2.5s] 时间段内,手臂将会从1号关键帧运动到2号关键帧。然后,在(2.5s, 5.0s],时间段内,从2号关键帧运动到3号关键帧)

2.1 计算中间姿势

每两个关键帧之间的中间姿势(intermediate pose)是通过插值计算而得。也就是说,给定关键帧K0 和K1,K0描述了骨块的一个姿势,K1描述了骨块的另一个姿势,在这两个姿势之间的表示某个姿势的帧,可以通过数学插值得到。图8展示了通过对关键帧K0和K1进行插值计算的方法。通过指定取值范围在[0,1]之间的不同的插值参数s,我们可以看到当插值参数从0到1变化时,表示中间姿势的帧也在K0和K1间变化。因而,参数s表示了从一个关键帧过渡到另一个关键帧时,得到的中间姿势关键帧中,K0和K1两个关键帧各自所占的百分比。

我们如何在两个骨块间进行插值?对于平移量和放缩量来说,使用 线性插值(linear interpolation) ,在三维空间中的旋转相对复杂些。我们必须使用四元数去描述旋转,使用 球面插值(spherical interpolation) 方法去对四元数进行正确的插值。D3DX库对这两种插值方法提供了函数: D3DXVec3LerpD3DXQuaternionSlerp

注意:我们从不对矩阵进行插值,因为旋转量使用矩阵去描述的话,难以正确地进行插值计算,所以我们使用四元数来进行插值。因而,关键帧的变换量通常都是分别“旋转值四元数”,“放缩值向量”,“平移值向量”来描述(简称RST-Value)。而不直接使用一个矩阵。对所有的骨块的RST-Value进行插值后。可以把RST-value转换成一个变换用矩阵。最后,如果一个文件是以矩阵的形式去存储关键帧数据的话,我们首先要把这个矩阵分解转换成RST-Value值来进行插值计算。

现在我们知道,如何去计算中间姿势了。让我们重新看看整个处理过程。给定一个时刻t,求该时刻的中间姿势。第一步要找到时刻$ t_0 $和$ t_1 $ (t0 ≤ t ≤ t1)的关键帧 $ K_i $ 和 $ K_{i+1}$ ,然后对着两个关键帧,在时刻t处进行插值。第二步是把时刻t从取值范围[t0, t1]变换到取值范围[0, 1]。以便能正确地反映在中间姿势帧中,K i和Ki+1各占的插值计算百分比。接着我们遍历每一块骨块,执行相同的插值计算。最后,利用所有骨块的经过插值计算后的RST值,更新骨块的局部变换矩阵,以反映当前时刻下t的骨块姿势。

指定在时刻t下,对某个骨块,在两个关键帧K0和K1中间计算中间姿势的伪代码如下。

 1struct Keyframe
 2{
 3    float time;
 4    D3DXQUATERNION R;
 5    D3DXVECTOR3 S;
 6    D3DXVECTOR3 T;
 7};
 8
 9void interpolateBone(Keyframe& K0, Keyframe& K1, D3DXMATRIX& L)
10{
11    // Transform to [0, 1]
12    float t0 = K0.time;
13    float t1 = K1.time;
14    float lerpTime = (t - t0) / (t1 - t0);
15
16   // Compute interpolated RST-values.
17   D3DXVECTOR3 lerpedT;
18   D3DXVECTOR3 lerpedS;
19   D3DXQUATERNION lerpedR;
20   D3DXVec3Lerp( &lerpedT, &K0.T, &K1.T, lerpTime);
21   D3DXVec3Lerp( &lerpedS, &K0.S, &K1.S, lerpTime);
22   D3DXQuaternionSlerp(&lerpedR, &K0.R, &K1.R, lerpTime);
23
24   // Build and return the interpolated local transformation matrix for this bone.
25   D3DXMATRIX T, S, R;
26   D3DXMatrixTranslation(&T, lerpedT.x, lerpedT.y, lerpedT.z);
27   D3DXMatrixScaling(&S, lerpedS.x, lerpedS.y, lerpedS.z);
28   D3DXMatrixRotationQuaternion(&R, &lerpedQ);
29   L = R * S * T;
30}

注意:在一般情况下,关键帧的平移值和放缩值都会是一个常数,所以不必进行插值。

上面的代码只是展示了对一个骨块的插值计算。当然为了使得整个骨架运动。我们必须对骨架中的每一个骨块进行相同操作。我们称含有插值后骨块的骨架为插值后骨架(interpolated-skeleton).

在实际代码中我们并不会自行地计算任何中间姿势,这些工作是通过使用D3DX库提供的ID3DXAnimationController接口完成。但无论如何我们需要理解插值计算中间姿势的工作原理,理解幕后发生的一切。

3 刚体动画及其一个问题

通过使用一个骨架,我们现在知道怎么表现一个会运动的角色网格了。也知道如何使用关键帧插值来让角色动起来。但我们还没深入讨论骨块和皮肤之间的关系。在两者之间建立最直截了当的关联的办法就是:一个骨块对应于一个表示皮肤的独立的网格(separatemesh)。而且这样的网格还可以很方便地在对应的骨块的骨块空间中进行建模。因为这些骨块的组合变换信息,能够正确地把其对应的独立的网格定位在该角色正确的位置上。因而,给定一个经过重新计算的,表示当前的动画姿势的组合变换后,我们可以使用它,逐一地在正确的位置上,渲染出每一个骨块对应的皮肤网格。如下面的代码所示。

 1void RecursRender(D3DXFRAME* node, D3DXMATRIX& parentTransform)
 2{
 3    D3DXMATRIXC = node->TransformationMatrix * parentTransform;
 4    _device->SetTransform(D3DTS_WORLD, &C);
 5    for(uint32 i = 0; i < node->pMeshContainer->NumMaterials; ++i)
 6    {
 7        D3DXMESHCONTAINER* mc = node->pMeshContainer;
 8        D3DMATERIAL9& mtrl =mc->pMaterials[i].MatD3D;
 9        _device->SetMaterial(&mtrl);
10       node->pMeshContainer->MeshData.pMesh->DrawSubset(i);
11    }
12   if( node->pFrameSibling )
13       recursRender(node->pFrameSibling, parentTransform);
14   if( node->pFrameFirstChild )
15       recursRender(node->pFrameFirstChild, C);
16}

这个技术叫做刚体动画(rigid bodyanimation) 。通过这个技术我们也可以让一个角色网格动起来 。尽管如此,图 9演示了该技术的瑕疵,使得这种技术可能无法为同时代的游戏所接受。

4 解决方案:顶点混合

因为把角色的皮肤分割成若干不连接的部分,所以使用刚体动画的角色模型在运动时会不可避免地产生裂缝。要解决此问题,我们可以先把角色的皮肤处理成一个联系的网格。现在我们先看图10中所演示的。

仔细观察,图中的皮肤网格是连续的,在手臂中某部位的皮肤,它延伸并紧贴到其他部分。特别地,靠近关节的顶点,看起来是同时受上臂和前臂同时影响的。这也就是说,靠近关节的顶点,是由两个对其施加了影响的骨块的变换值,加权平均决定的。这是顶点混合(vertex algorithm)算法的核心思想。即某处的皮肤,可能受到多于一个的骨块影响。

4.1 偏移变换

在继续实现顶点混合算法的细节之前,我们先解决一个问题。之前,在一个有关节的(articulated )角色网格模型中,一个骨块是对应一个独立的网格。而且网格是建模在它对应的骨块的骨块空间中的 。骨块本身的组合变换可以把它对应的皮肤网格正确地定位到角色模型上。尽管如此,在顶点混合算法中。我们是把角色的整张皮肤,连续地建模在角色空间中的。这样导致的后果就是,因为这些顶点不是在骨块空间中,所以我们不能简单地使用骨块变换去把皮肤网格定位。

为了解决这个问题。我们要介绍一个新的变换,我们称之为偏移变换(offset transform)。骨架中每一个骨块都有一个对应的偏移变换矩阵。一个偏移变换矩阵,把在绑定空间(bind space)中的顶点 ,变换到各自对应的骨块的骨块空间中去。图 11概述了这个变换过程。

因而,通过使用偏移变换矩阵对骨块 B 的顶点进行变换。我们将顶点移到骨块B的骨块空间。一旦我们在骨块空间拥有了顶点,我们可以使用 B 的组合变换矩阵来将其定位到角色空间中当前的动画姿势下的位置中去。所以我们还接着介绍一个变换,称之为最终变换(final transform)。该变换由骨块的偏移变换和组合变换组合而成。在数学上,第 $ i $ 个骨块的最终变换矩阵 $ F_i $ 可由下式指定。

(2) $ F_i = M_i \times C_i $

其中,Mi是骨块的偏移矩阵,Ci 是骨块的组合矩阵。

4.2 顶点混合的实现细节

实际上,Real Time Rendering 一书中提及到,通常,不要超过4个骨块影响同一个顶点。因此我们的设计中,也是同一个顶点最多受到4个骨块的影响。为了实现顶点混合技术。我们现在把角色网格建模成一个连续的网格。每一个顶点含有 4个索引值,这些索引值将指向一个矩阵调色板(matrix palette)中的四项。矩阵调色板是一个矩阵数组,里面含有的每一个骨块的最终变换矩阵。每一个顶点也含有四个权重值,分别表述了影响该顶点的各个最终变换矩阵,各自所占的比例值。因此我们可以得到以下的顶点结构。

如果一个连续的网格,它的顶点有上述的结构体所描述的信息的话,我们将称这些网格为蒙皮网格(skinned mesh) 。任意一个顶点 v 的最终位置 v′可以由下面的公式进行计算。

观察上述的等式。 对一个给定的顶点 v,我们使用了所有的影响了该顶点的骨块最终变换矩阵去对其进行变换。然后我们对其进行加权平均计算。得到最后的位置 v′。因此,顶点的最后的位置是由所有对该顶点有影响的骨块的权重值决定的。注意:所有的混合权重值相加必须要为 1.0

最后我们使用 DirectX HLSL语言撰写一个顶点着色器执行顶点混合操作,并返回一个经过合适地混合计算的(blendedvertex)顶点。我们使用2.0 版本的顶点着色器。这样子每个顶点所受的骨块影响的个数, 可以动态调整的。 每个顶点可以受到 2 个, 3个或者 4个骨块的影响。 如果你的显卡不支持2.0版本的顶点着色器。这个实例程序可以在 REF 格式的设备上运行之。请看接下来的提示,以了解如何修改实例代码,使得它能在较低版本的顶点着色器上运行。

 1//////////////////////////////////////////////////////////////////
 2//
 3// File: vertblendDynamic.txt
 4//
 5// Author:Frank Luna
 6//
 7// Desc: Vertexblending vertex shader. Supports mesheswith2-4
 8// bone influences per vertex.We can dynamically set
 9// NumVertInfluencesso that the shader knowshow many
10// weightsit is processing per vertex. In order to support
11// dynamicloops, wemustuse at least vertexshader
12// version2.0.
13//
14//////////////////////////////////////////////////////////////////
15extern float4x4WorldViewProj;
16extern float4x4FinalTransforms[35];
17extern texture Tex;
18extern intNumVertInfluences =2; //<--- Normally set dynamically.
19
20sampler S0= sampler_state 
21{
22    Texture = <Tex>;
23    MinFilter = LINEAR;
24    MagFilter = LINEAR;
25    MipFilter = LINEAR;
26};
27
28struct VS_OUTPUT
29{
30    float4 pos: POSITION0;
31    float2 texCoord: TEXCOORD;
32    float4 diffuse : COLOR0;
33};
34
35
36VS_OUTPUT VertexBlend(float4 pos : POSITION0,
37float2 texCoord: TEXCOORD0,
38float4 weights: BLENDWEIGHT0,
39int4 boneIndices : BLENDINDICES0)
40{
41     VS_OUTPUT output = (VS_OUTPUT)0;
42     float4 p = float4(0.0f, 0.0f, 0.0f,1.0f);
43     float lastWeight = 0.0f;
44     int n= NumVertInfluences-1;
45     // This next code segmentcomputes formula (3).
46     for(int i = 0; i < n; ++i)
47     {
48         lastWeight+= weights[i];
49         p += weights[i]*mul(pos, FinalTransforms[boneIndices[i]]);
50    }
51    lastWeight= 1.0f - lastWeight;
52    p += lastWeight* mul(pos, FinalTransforms[boneIndices[n]]);
53    p.w =1.0f;
54    output.pos= mul(p, WorldViewProj);
55    output.texCoord= texCoord;
56    output.diffuse = float4(1.0f, 1.0f,1.0f, 1.0f);
57    return output;
58}
59
60technique VertexBlendingTech
61{
62    pass P0
63   {
64      vertexShader = compile vs_2_0 VertexBlend();
65      Sampler[0]= <S0>;
66      Lighting =false;
67    }
68}

注意:当使用低于 2.0 版本的顶点着色器(例如1.1 版)进行顶点混合时,会发生两个问题。第一个问题是:低版本的着色器无法保证有足够的常量寄存器来存放矩阵调色板数据。可以通过查询D3DCAPS9::MaxVertexShaderConst属性检查这版本的常量寄存器的数量。我们可以知道 1.1 版本的着色器仅仅能保证有 96 个常量寄存器。除以4 的话,也只能刚好够24 个 4X4 的矩阵使用。解决这个问题的一个而方法是把这些蒙皮网格切割成多个网格,再逐次渲染它们。这样子可以使得每一部分的网格的矩阵调色板能有足够的常量寄存器使用。( h ID3DXSkinInfo::ConvertToIndexedBlendedMesh能切割这些蒙皮网格.) 而相反地,2.0版本的顶点着色器可以保证提供 256个常量寄存器,一般来说这么多的寄存器是够用了的,不需要把蒙皮网格切分。

低版本顶点着色器的第二个问题是一些旧显卡(如Geforce3)并不支持D3DDECLTYPE_UBYTE4类型的着色器输入参数宣告类型。而这个类型的数据一般是用来输入顶点骨块索引值的。解决的方式是把这个类型的数据转换成旧显卡支持的D3DDECLTYPE_D3DCOLOR类型。然后可以通过使用 HLS L的本征函数 D3DCOLORtoUBYTE4,把 D3DCOLOR类型的参数解出这些索引值来。

使用低版本顶点着色器最后一个不便之处就是它不支持动态分支。这也就是说不能在着色器代码中使用循环和分支。因此,如果一个网格,它的每一个顶点所受的骨块影响的数目存在着不同的话,就无法使用一个通用的着色器代码去处理它。而在实际中我们肯定会有很多不同的角色网格。这些网格,有些是顶点受两个骨块影响,有些是四个。很显然使用一个通用的着色器代码去应付各种不同的情况是非常方便的。

5.用D3D9.0的实现细节

在本章中,我们将通过使用 D3DX 库和顶点混合算法,把一个蒙皮网格动起来,对前面章节中所提及的理论进行实践。本节所讨论的示例代码名字是“d3dx_skinnedMesh” 。在深入讨论其实现的细节之前。我们先回忆一下,为了实现顶点混合动画,所需要的理论知识是什么。从而先把“实现顶点混合动画”这一任务的基本要素掌握在手。

首先,我们需要一个骨块层级结构,和一个蒙皮网格。另外,为了计算每个骨块在当前动作姿势下的最终变换信息。我们还需要骨块的偏移变换矩阵,和更新过的组合变换矩阵。对于动画播放而言 ,我们需要每一个骨块的关键帧。这些关键帧数据可以从 X 文件中导入。我们还需要在两个关键帧中插值的功能。给定了上述所需的数据和功能后。我们便可以通过顶点混合算法去让一个蒙皮网格动起来了。后续的章节将详细描述如何获取,创建和使用这些数据和功能。

5.1. D3DXMESHCONTAINER

在 1.2节中提到过 D3DXFRAME 结构体。该结构体含有一个 D3DXMESHCONTAINER 类型的指针。该指针允许我们把一个存储 mesh 的容器(该容器就是一个链表)和本frame建立起关联。之前我们没有详细描述 D3DXMESHCONTAINER 结构。现在我们就详细分析它,该结构定义如下:

 1typedef struct _D3DXMESHCONTAINER {
 2    LPSTR Name;
 3    D3DXMESHDATA MeshData;
 4    LPD3DXMATERIAL pMaterials;
 5    LPD3DXEFFECTINSTANCEpEffects;
 6    DWORD NumMaterials;
 7    DWORD* pAdjacency;
 8    LPD3DXSKININFO pSkinInfo;
 9    struct _D3DXMESHCONTAINER* pNextMeshContainer;
10}D3DXMESHCONTAINER,*LPD3DXMESHCONTAINER;

D3DXMESHCONTAINER 数据成员描述如下表

Data Member Description
Name The name of themesh.
MeshData A D3DXMESHCONTAINER is a general structure and canbean ID3DXMesh, ID3DXPMesh, or ID3DXPatchMesh.TheD3DXMESHDATA structure specifies what type of meshthisis,andcontains a valid pointer to that type of mesh.
pMaterials Pointer to an array of D3DXMATERIAL structures.
pEffects Pointer to a D3DXEFFECTINSTANCE structure,which contains effectfile information. We will not be loading any effect datafrom the.X file, and therefore we can ignore this variable.
NumMaterials The number of elements in thematerial array
pAdjacency points to.pAdjacency Pointer tothe adjacencyinfo of the mesh.
pSkinInfo Pointer to anID3DXSkinInfo interface,which contains information needed for performing vertex blending.That is, it contains offset matrices for each bone, vertex weights,andvertex bone indices.The important methods of ID3DXSkinInfo will be discussed later onat the time they are used.

5.2. ID3DXAnimationController

ID3DXAnimationController 接口负责处理动画。对于每一个动画序列而言。它存储了每一个骨块的所有关键帧。并且它还包含了在两个关键帧的插值的功能,以及其他的如动画混合动画回调的功能。

在一个动画序列中,把角色从当前时刻 t 的姿势,更新到t+Δt时刻对应的另一个姿势。为了能渲染出平滑连续的动画,需要把Δt 取得足够小。ID3DXAnimationController::AdvanceTime方法能正确达到这个目的。通过使用关键帧插值机制,该方法把角色的骨块在时刻t 的姿势,更新到 t +Δt时刻对 应的姿势。该方法的原型如下:

1HRESULT ID3DXAnimationController::AdvanceTime( DOUBLE TimeDelta,
2LPD3DXANIMATIONCALLBACKHANDLERpCallbackHandler);

参数 TimeDelta就是Δt,第二个参数我们目前可以忽略,传递一个NULL值给他。注意当动画序列播放到末尾端的时候。缺省地,当前的播放轨计时器(track timer)将会复位,回滚到动画序列的起始端。

一旦动画控制器对所有的骨块进行插值以对应更新后的姿势后。我们需要访问它们。在哪可以获得这些插值后的骨块?动画控制器有指向层级中所有 frame的 D3DXFRAME::TransformationMatrix 的指针。这一点是很重要的,这是因为当动画控制器对骨块进行插值计算时,会对该骨块所对应的 D3DXFRAME::TransformationMatrix 进行更新写入。因而,通过 D3DXFRAME::TransformationMatrix ,我们可以直接在任何时刻访问这些经过插值的骨块。下面的代码就是我们对 ID3DXAllocateHierarchy 的子类的实现。

 1class AllocMeshHierarchy : public ID3DXAllocateHierarchy
 2{
 3public:
 4    HRESULT STDMETHODCALLTYPECreateFrame(THIS_ PCSTR Name,D3DXFRAME** ppNewFrame);
 5    
 6    HRESULT STDMETHODCALLTYPECreateMeshContainer(
 7                                 PCSTR Name,
 8                                 const D3DXMESHDATA*pMeshData,
 9                                 const D3DXMATERIAL*pMaterials,
10                                 const D3DXEFFECTINSTANCE* pEffectInstances,
11                                 DWORD NumMaterials,
12                                 const DWORD *pAdjacency,
13                                 ID3DXSkinInfo*pSkinInfo,
14                                 D3DXMESHCONTAINER**ppNewMeshContainer);
15
16    HRESULT STDMETHODCALLTYPEDestroyFrame( THIS_D3DXFRAME* pFrameToFree);
17    
18    HRESULT STDMETHODCALLTYPEDestroyMeshContainer(THIS_ D3DXMESHCONTAINER*pMeshContainerBase);
19};

ID3DXAllocateHierarchy 的抽象成员方法描述如下表:

Function Description CreateFrame Given the frame name as input, createand return a newly allocate D3DXFRAME through ppNewFrame. CreateMeshContainer Given all the parameters, except the last, as valid input values,create and return anewly allocated D3DXMESHCONTAINER through ppNewMeshContainer DestroyFrame Free any memory or interfaces pFrameToFree owns, and delete pFrameToFree. DestroyMeshContainer Free any memory or interfaces pMeshContainerBase owns,and delete pMeshContainerBase

这些函数的实现是简单易懂的。所以在此就不作讨论。你可以查阅本文所对应的示例代码中对AllocMeshHierarchy 类的完全实现

5.4 D3DXLoadMeshHierarchyFromX和D3DXFrameDestroy

我们实现 ID3DXAllocateHierarchy 接口之后,我们可以使用D3DX库提供的函数去从X 文件中载入网格层级了。

1HRESULT WINAPI  D3DXLoadMeshHierarchyFromX(
2    LPCSTR Filename,
3    DWORD MeshOptions,
4    LPDIRECT3DDEVICE9 pDevice,
5    LPD3DXALLOCATEHIERARCHY pAlloc,
6    LPD3DXLOADUSERDATA pUserDataLoader,
7    LPD3DXFRAME* ppFrameHeirarchy,
8    LPD3DXANIMATIONCONTROLLER* ppAnimController);

通过使用 D3DXFrameDestroy 函数我们可以销毁 frame hierarchy。接下来的代码演示了如何使用D3DXLoadMeshHierarchyFromX 函数和D3DXFrameDestroy 函数。

5.5 找到一个唯一的网格

为了简化的目的。我们将作如下的假定:我们约定在所读取的 X 文件中,只包含一个蒙皮网格。这个假定并不是一个特别的限制。比如 DX SDK 的名为 tiny的 X 文件就含且仅含一个蒙皮网格。基于这个假定,在 CreateMeshContainer方法中我们将会忽略掉非蒙皮网格。因此,我们可以断定,在网格层次中,仅有一个 frame,是含有一个指向 D3DXMESHCONTAINER 类型指针,即一个有效的 pMesh数据成员值。加之我们仅读取蒙皮网格。所以我们读进来的这个 mesh container,是含有蒙皮信息(skinning info)的,也即,pSkinInfo 数据成员不是一个空指针。所以现在让我们找到这个唯一的 mesh container。下面的方法将递归搜索整个层级,找到那个含有指向 mesh 的指针的 frame。

 1D3DXFRAME*SkinnedMesh::findNodeWithMesh(D3DXFRAME* frame)
 2{
 3      if( frame->pMeshContainer && frame->pMeshContainer->MeshData.pMesh != 0 )
 4         return frame;
 5      D3DXFRAME*f = 0;
 6      if(frame->pFrameSibling && f= findNodeWithMesh(frame->pFrameSibling)
 7        return f;
 8     if(frame->pFrameFirstChild && f= findNodeWithMesh(frame->pFrameFirstChild )
 9       return f;
10     return 0;
11}

后面的代码开始执行递归搜索,保存一个指向 mesh container 的局部指针变量,保存指向skin info的指针。

1// Find the one and only mesh in the tree hierarchy.
2D3DXFRAME*f = findNodeWithMesh(_root);
3if( f== 0)
4  THROW_DXERR(E_FAIL);
5D3DXMESHCONTAINER* meshContainer = f->pMeshContainer;
6_skinInfo = meshContainer->pSkinInfo;
7_skinInfo->AddRef();

应当注意,我们仅仅保存了指向mesh container 的指针。而且我们不需要负责释放它。因为网格是保留在层级中的,当摧毁层级时将会自动释放它。相反地我们要对 skin infoobject 进行引用计数加 1.我们要在 SkinnedMesh::deleteDeviceObject方法中负责释放该 skin infoobject。

5.6 转换为一个蒙皮网格

到目前为止我们拥有指向唯一的一个网格容器的指针。该网格容器也含且仅含有一个网格。但这时候,网格的顶点格式并不包含顶点权重 (vertex weight), 或者是指向骨块的索引值 (bone indexdata) ,这两个是顶点混合算法所必须的。因而,我们必须把网格转换成一个“索引顶点混合网格 ”(indexed-blended-mesh) 。或者把其转成一个带有使用顶点混合算法所必须顶点格式的蒙皮网格。接下来的这个函数,传递进来的参数是指向那个唯一的网格的指针值,这函数就做上面提到的转换。

 1void SkinnedMesh::buildSkinnedMesh(ID3DXMesh*mesh)
 2{
 3    DWORDnumBoneComboEntries= 0;
 4    ID3DXBuffer* boneComboTable = 0;
 5    THROW_DXERR( _skinInfo->ConvertToIndexedBlendedMesh(mesh,
 6         D3DXMESH_MANAGED | D3DXMESH_WRITEONLY,
 7         SkinnedMesh::MAX_NUM_BONES_SUPPORTED,
 8         0,  //  ignore adjacency  in
 9         0,  //  ignore adjacency out
10         0,  // ignore face remap
11         0,  // ignore x vertex remap
12         &_maxVertInfluences,
13         &numBoneComboEntries,
14         &boneComboTable,
15         &_skinnedMesh) )
16    // We do not need the bone table, so just release it.
17   ReleaseCOM(boneComboTable);
18}

成员函数 buildSkinnedMesh 中调用的关键函数是ID3DXSkinInfo::ConvertToIndexedBlendedMesh 。回到第 5.1节,ID3DXSkinInfo 接口包含了每一个骨块的偏移矩阵,顶点权重和顶点与指明哪块骨块相关联的索引值。因而 ID3DXSkinInfo 接口可以把一个输入的网格转化成蒙皮网格。

ID3DXSkinInfo::ConvertToIndexedBlendedMesh 方法的大部分参数在所给的示例程序中已经得到解释了。但其中个别参数要特别注意。因为本文中不使用骨块联合表(bonecombination table),因此相关的两个参数要被忽略。返回值_maxVertInfluences 得到在本蒙皮网格中,一个顶点,被若干骨块影响它,最多的骨块个数。最后我们可以看到经过转换得到的蒙皮网格,将会存储在_skinnedMesh 成员变量中。

5.7 创建一个组合变换矩阵组

因为我们最后需要给顶点着色器程序设置 4.2 节中提到的矩阵调色板数组。 该数组把组合矩阵存成一个便于使用的数组格式。但我们并不需要去对这些组合矩阵做一个拷贝。这是因为当层级中的组合变换矩阵发生变化时(每帧都会发生变化),我们还得去更新这个拷贝。因此我们只需要得到使用一个包含指向这些组合矩阵的指针的数组,去直接访问和更新组合矩阵,而不用去复制一份。下面的代码展示了如何去保存指向组合矩阵的指针到数组中。

 1void SkinnedMesh::buildCombinedTransforms()
 2{
 3    for(UINT i= 0;i < _numBones;++i)
 4    {
 5        // Find the frame that corresponds with the ith bone offset matrix.
 6        const char* boneName= _skinInfo->GetBoneName(i);
 7        D3DXFRAME*frame = D3DXFrameFind(_root, boneName);
 8        if( frame )
 9        {
10            FrameEx* frameEx = static_cast<FrameEx*>( frame );
11            _combinedTransforms[i] = &frameEx->combinedTransform;
12        }
13    }
14}

从上面代码可以看到。我们保存指向了组合变换矩阵的指针到数组。这些第 i 个指针也对应了第 i个骨块的偏移矩阵。所以,一旦给定了第 i个骨块。我们就可以获取第 i 个偏移矩阵和第i个组合变换矩阵。然而还有另一个原因要如此去配置矩阵数组的布局。在上面的代码中我们把源网格转换成一个 索引顶点混合网格。 “顶点的骨块索引”和_skinInfo 接口所管理的骨块数组(bone array)是相关联的 。所以把组合变换矩阵数组与偏移矩阵数组建立起关联也是很重要的。

5.8 初始化过程的概述

首先让我们概述一下开始创建和准备渲染一个蒙皮网格时的初始化步骤。首先,我们继承实现ID3DXAllocateHierachy 的四个纯虚方法, 这四个方法将会在使用 D3DXLoadMeshHierarchyFromX 函数和 D3DXFrameDestroy函数去创建和销毁骨骼层次时被调用到。调用D3DXLoadMeshHierarchyFromX 函数从 X 文件中装载一个角色网格,然后在骨骼层次中,找到包含有指向角色蒙皮数据的指针的那个骨块。接着,因为在刚装载进来的网格的顶点格式,不是一个蒙皮网格所需的顶点格式(没有顶点权重值) 。所以我们必须使用 ID3DXSkinInfo::ConvertToIndexedBlendedMesh 方法去将其转换之。最后,我们创建一个装有指向骨块的组合变换矩阵的指针的数组。一遍我们可以快速地访问这些矩阵。执行这些步骤之后,就可以让蒙皮网格动起来,并且渲染之。

5.9 运行起来的工作,使角色网格活动起来

初始化之后,我们拥有了偏移矩阵,指向每个骨块的组合矩阵的指针。还有用以进行骨块插值计算的动画控制器,以及一个配置好的使用顶点混合算法的蒙皮网格。这也就是说,一切就绪。可以让蒙皮网格动起来,画出来。为了达成这个任务,可以将其划分为五个步骤。

1 使用 ID3DXAnimationController::AdvanceTime 方法,通过插值计算。把骨骼摆到当前姿势。因为动画控制器含有指向层次中的骨块的变换矩阵(D3DXMATRIX::TransformationMatrix) 。所以动画控制器通过在动画序列中的两个关键帧之间进行插值,能够更新这些矩阵,以反映当前时刻下角色的姿势。 2 现在,骨块被摆放到当前姿势了。通过经过插值的 D3DXMATRIX::TransformationMatrix 递归遍历计算骨块的组合变换矩阵。如 1.3节所提到的。 3 对于每一个骨块。取得它的偏移矩阵和组合变换矩阵。相乘这两个矩阵,得到该骨块的最终变换矩阵。 4 把已经包含了能正确地把皮肤网格变换到当前姿势的最终矩阵的数组,传给顶点着色器 5 最后,在当前的姿势渲染角色网格。下面的代码中,SkinnedMesh::frameMove 函数内包含了前四个步骤的实现。

 1void SkinnedMesh::frameMove(float deltaTime,D3DXMATRIX&worldViewProj)
 2{
 3    _animCtrl->AdvanceTime(deltaTime, 0);
 4    D3DXMATRIX identity;
 5    D3DXMatrixIdentity(&identity);
 6    combineTransforms(static_cast<FrameEx*>(_root), identity);
 7    D3DXMATRIXoffsetTemp, combinedTemp;
 8    for( UINT i= 0;i < _numBones;++i)
 9    {
10        offsetTemp= *_skinInfo->GetBoneOffsetMatrix(i);
11        combinedTemp = *_combinedTransforms[i];
12        _finalTransforms[i] = offsetTemp * combinedTemp;
13    }
14    _effect->SetMatrix(_hWorldViewProj,&worldViewProj);
15    _effect->SetMatrixArray(_hFinalTransforms,&_finalTransforms[0], _finalTransforms.size());
16}

下面的代码是第五个步骤的实现,在 SkinnedMesh::render 函数中。

 1void SkinnedMesh::render()
 2{
 3    _effect->SetTechnique(_hTech);
 4    UINT numPasses = 0;
 5    _effect->Begin(&numPasses, 0);
 6    for (UINT i= 0;i < numPasses;++i)
 7    {
 8        _effect->BeginPass(i);
 9        // Draw the oneand only subset.
10        _skinnedMesh->DrawSubset(0);
11        _effect->EndPass();
12    }
13    _effect->End();
14}

我们的示例程序使用 DirectXSDK 中的 tiny.x 文件中的角色网格模型。图 13 显示了该截图。

6 多序列动画

一个动态的角色可以含有若干个动画序列的数据。这些数据我们可以称之为“动画集”(animationset)。例如,一个角色可以含有一个行走的动画, 一个奔跑的动画,一个开枪的动画,一个跳跃的动画 ,还有一个死亡的动画。在本节中,我们将会开发一个名为 d3dx_multiAnimation 的程序,用来演示如何在一个角色的多个动画中进行切换播放。我们注意到我们做的程序,将要比 DirectXSDK 中的“MultiAnimation”示例要简单得多。

在我们开始之前, 我们首先需要一个含有多个动画序列的X 文件 (tiny.x文件中的模型只有一个动画集) 。DirectX SDK 发布了一个名为“tiny_4anim.x”的文件。该文件含有与 tiny.x 文件相同的网格模型,但是有四个动画集。这四个动画集分别是挥手动画,慢跑动画,行走动画,徘徊动画,Tiny_4anim.x可以在 DirectX SDK 中的“MultiAnimation”示例中找到。如下图所示。

现在我们拥有了一个有多个动画集的 x文件,使用上节中相同的方法去装载模型,装载动画数据 。当动画集装载完毕后。我们可以使用动画控制器去在动画序列中进行切换。为了从播放一个动画序列切换到另一个动画序列。我们必须当前的播放序列设置成另一个序列。 可通过下面的方法来完成。

HRESULT ID3DXAnimationController::SetTrackAnimationSet(UINT Track, LPD3DXANIMATIONSET pAnimSet);

参数值 Track 是用来标识我们将要设置动画集 pAnimSet 上去的播放轨的索引值。在下一节中将会深入解释播放轨。当前我们仅需知道我们仅仅在一个时刻内播放一个动画就可以了。所以,我们使用第一个播放轨。0 号播放轨。所以,当我们把一个新的动画集设置到 0 号播放轨时,我们就把这新的动画集替掉旧的,从而改变当前播放的动画序列。这也就是说,ID3DXAnimationController::AdvanceTime方法将用当前这个新的动画序列来操纵骨块。但我们从哪里得到指向动画控制器的动画集的指针呢?可通过以下的方法:

HRESULT ID3DXAnimationController::GetAnimationSet(UINT Index,LPD3DXANIMATIONSET *ppAnimSet);

这个方法将返回参数 Index 指定的动画集的指针。该指针可通过参数 ppAnimSet 活得。动画控制器中的动画集个数需要通过方法 ID3DXAnimationController::GetNumAnimationSets 来获得。

最后,当在两个动画序列中切换时,我们想重置全局动画时间(globalanimationtime)。使得新的动画能从起始处开始播放。可以通过调用 ID3DXAnimationController::ResetTime方法来完成。

让我们通过解释“d3dx_multiAnimation”示例程序来总结本节的实现细节。为了模块服用的目的 ,我们将继承 SkinnedMesh类,用来特别处理 tiny_4anim.x 文件。这个新类在其父类的基础上增加了在四个动画序列中切换的功能。

// Note ha we hardcode these index values and they are // specific to tiny_4anim.x.They were found by actually examining the .x file. enum ANIMATIONSET_INDEX { WAVE_INDEX= 0, JOG_INDEX = 1, WALK_INDEX= 2, LOITER_INDEX = 3 };

class Tiny_X : public SkinnedMesh { public: void playWave(); void playJog(); void playWalk(); void playLoiter(); };

为了使得文章简洁些我们仅展示其中一个函数的实现,如下:

void Tiny_X::playLoiter() { ID3DXAnimationSet* loiter= 0; _animCtrl->GetAnimationSet(LOITER_INDEX,&loiter); _animCtrl->SetTrackAnimationSet(0, loiter); _animCtrl->ResetTime(); }

在实际的“d3dx_multiAnimation”示例代码中。我们在每一帧都会检查一个计数器(counter) ,该计数器响应用户的输入以更新其计数值。该计数器的值将决定了要播放哪个动画序列,代码如下:

HRESULT MultiAnimDemoApp::FrameMove() { switch( _animationIndex ) { case LOITER_INDEX: _tinyxMesh.playLoiter(); break; case WALK_INDEX: _tinyxMesh.playWalk(); break; case JOG_INDEX: _tinyxMesh.playJog(); break; case WAVE_INDEX: _tinyxMesh.playWave(); break; } _tinyxMesh.frameMove(m_fElapsedTime, _worldViewProj); … }

例如,如果变量值 animationIndex 为 LOITER_INDEX,那么_tinyxMesh.playLoiter()将会被调用,徘徊动画将会代替当前的动画而播放之。然后当 ID3DXAnimationController::AdvanceTime 方法在_tinyxMesh.frameMove 函数内被调用时,角色模型的骨块将会按徘徊动画所指定的姿势播放之。所以 就产生播放徘徊动画的效果。

7 动画混合

假定你的游戏角色有一个奔跑动画序列和一个开枪动画序列。你也许希望你的角色能够边跑边开枪。 现在你明显希望你的3D 美工给你创建一个奔跑中开枪的动画序列。但为什么我们不通过对两个已有的动画序列进行数学上的混合处理,让计算机来产生这个所需的动画,从而节省一些开发实践和内存呢?事实上,动画混合(animation blending)技术允许你这样子做。这个技术允许你通过两个现有的动画序列,通过混合它们,产生一个新的动画序列。

D3DX 库提供的 API很方便地支持动画混合。在上一节中我们提到动画控制器可以有若干个不同的播放轨,你可以把一个动画集给附着(attach)到播放轨上去。到目前为止我们只是使用了第一个播放轨,0 号播放轨。使用D3DX 库去使用动画混合技术的关键点在于,动画控制器能自动地对当前所 有的已启动的(enabled)播放轨进行自动混合。所以为了执行动画混合。我们所需的是把若干的动画集,设置到若干不同的播放轨上,然后启动它们。然后ID3DXAnimationController::AdvanceTime方法便会混合所有的动画播放轨,从而操纵骨骼。特别重要的是,仅仅往不同的播放轨上添加动画集是不 够的。播放轨还得启动它。可以通过以下的方法启动或者是停用播放轨:

1HRESULT ID3DXAnimationController::SetTrackEnable(
2UINT Track,
3BOOL Enable  // True to enable and false to disable.
4);

当启动一个新的播放轨时我们还需要定义它的播放速度。我们可以通过下面的方法来设置:

1HRESULT ID3DXAnimationController::SetTrackSpeed( UINT Track,FLOAT Speed);

大多数情况下,播放速度设置为1,当然了,可以通过设置播放速度从1 变化为其他值,以达到“快进”和“慢动作”等特殊的播放效果

如果我们能制定每一个播放轨对最终混合的动画,各自所贡献的权重值的话,是很有利的。例如 。你可能想其中一个播放轨贡献 30%的权重,另一个播放轨贡献70%的权重。不必惊讶,D3DX 动画库函数支持这个功能,如下:

1HRESULT ID3DXAnimationController::SetTrackWeight(UINT Track,FLOATWeight);

这个方法设置编号为 Track 的播放轨在最终的混合动画中所贡献的权重值。注意,所有的播放轨的权重值之和必须要等于 1.一般地。在“权重贡献”这一个话题上。D3DX 库提供了一个很好的控制 。我们可以通过给一个动画播放轨设置 D3DXPRIORITY_HIGH 或 D3DXPRIORITY_LOW,来指定该播 放轨的播放优先级。在进行混合计算时,所有高优先的播放轨会单独进行混合。低优先级的也会单独混合。然后这两个独立混合计算所得的结果,将会再次进行混合,以达到最终的混合动画。可以通过如下的方法来设置优先级:

1HRESULT ID3DXAnimationController::SetTrackPriority(UINTTrack,D3DXPRIORITY_TYPE Priority);

通过分析“d3dx_blendAnim”示例程序的具体实现来总结本节的内容。首先我们先继承上届所实现的 Tiny_X类。添加播放混合动画的函数。

1class Tiny_X : public SkinnedMesh
2{
3public:
4    ...
5    void playLoiterWaveBlend();
6    void playLoiterJogBlend();
7    void playJogWaveBlend();
8    void playWalkWaveBlend();
9};

为了使文章简单我们仅展示其中一个函数,如下:

 1void Tiny_X::playJogWaveBlend()
 2{
 3    ID3DXAnimationSet* jog = 0;
 4    ID3DXAnimationSet* wave =0;
 5    _animCtrl->GetAnimationSet(JOG_INDEX, &jog);
 6    _animCtrl->GetAnimationSet(WAVE_INDEX, &wave);
 7    _animCtrl->SetTrackAnimationSet(0, jog);
 8    _animCtrl->SetTrackAnimationSet(1, wave);
 9    _animCtrl->SetTrackWeight(0, 0.4f);
10    _animCtrl->SetTrackWeight(1, 0.6f);
11    _animCtrl->SetTrackEnable(0, true);
12    _animCtrl->SetTrackEnable(1, true);
13    _animCtrl->SetTrackSpeed(0, 1.0f);
14    _animCtrl->SetTrackSpeed(1, 1.0f);
15    _animCtrl->SetTrackPriority(0,D3DXPRIORITY_HIGH);
16    _animCtrl->SetTrackPriority(1,D3DXPRIORITY_HIGH);
17    _animCtrl->ResetTime();
18}

正像上一个示例“d3dx_multiAnimation” 。实际的程序中将会读取文件。每一帧查询由用户输入所更新的计数器值,以决定播放哪个动画。

8 动画回调机制

经常我们希望在响应一个动画回调(animationcallback)时能执行一些代码。一个动画回调是指在一个动画序列的某个指定的时间点被触发, 然后和动画播放并行实行的一段代码。例如在 DirectX SD K中的“MultiAnimation”示例。当模型的脚踏到地面时它会播放脚步声。同样地我们也可以在播放一个开火动画时播放开枪的声音。另一个例子,当一个角色播放一个特殊的战斗技能时,我们可以拉近镜头,给角色一个特写。以增加动作的强度感。所以动画回调的关键点就是在于:我们可以执行一段和动画序列相耦合的任意的代码。此外,因为回调的代码和动画紧耦合。你也许希望它不是在主线程循环中执行。而是希望动画在适当的时刻自行执行之。这个任务可以由动画回调机制来完成。

本节中对应该技术的实现的程序是“d3dx_animCallbacks ” 。回调代码将会让镜头旋转 90 度。在本示例中我们使用 tiny.x文件。接下来将解释如何去实现动画回调

8.1 回调操作器

回调执行器(callback handler)是一段包含了需要在动画播放过程中执行的代码的函数。我们可以继承并实现 ID3DXAnimationCallbackHandler接口的唯一方法 HandleCallback。如下:

1class TinyXCallbackHandler : public ID3DXAnimationCallbackHandler
2{
3public:
4    HRESULT CALLBACK HandleCallback(THIS_UINTTrack, LPVOID pCallbackData);
5};

实现回调操作器之后我们必须将其挂接到动画控制器上,使得当一个动画回调被触发后,控制器能够知道调用哪个对应的回调操作器。为了达成需求,我们将会把TinyXCallbackHandler的实例对象指针当做AdvnaceTime方法的第二个参数,传递进去,如下:

1TinyXCallbackHandler callbackHandler;
2_animCtrl->AdvanceTime(deltaTime, &callbackHandler);

通过继承实现ID3DXAnimationCallbackHandler接口产生不同的回调操作器,然后把这些回调操作器的实例对象的指针传递给 AdvanceTime 方法。我们可以在不同的回调操作器实例之间进行切换。

8.2 回调导向键

一个支持回调机制的动画序列会包含一系列的回调导向键。一个回调导向键定义了该回调的执行时刻(相对于该动画序列而言) 。还定义了一个动画回调被触发时将要执行的操作(如一个回调操作器将被执行) 。此外。一个回调导向键还包含了要传递给被触发的动画回调器的输入参数。D3DX库定义的回调导向键如下:

1typedef struct _D3DXKEY_CALLBACK
2{
3    FLOAT Time;  // Time callback handler should be executed
4    LPVOID pCallbackData;  // Input data to callback handler
5}D3DXKEY_CALLBACK, *LPD3DXKEY_CALLBACK;

注意到回调导向键的设计思想是类似于关键帧的。关键帧是定义了一个动画序列的运动。回调导向则是定义了一段将会被并行执行的代码段。

在 " d3dx_animCallbacks " 示例中我们给回调操作器传递的输入参数很简单。就是一直指向摄像机theta 坐标值的指针,如下:

1struct TinyXCallbackInfo
2{
3    float* theta;
4};

当然你可以如上代码所示,给回调操作器传递任意数据。例如,你可以在输入参数中包含一些条件变量,使得回调操作器能够根据不同的条件执行不同的代码分支段。

8.3 操作回调

操作一个动画回调是很直接明了的:给定输入数据, 执行你想要的操作。 在 d3dx_animCallbacks"示例中我们在每一次的回调操作中,简单地旋转摄像机 90 度。如下代码:

1HRESULT TinyXCallbackHandler::HandleCallback(UINT Track, LPVOID pCallbackData)
2{
3    // Given your callback data,execute any desired code.
4    TinyXCallbackInfo* data =(TinyXCallbackInfo*)pCallbackData;
5    // rotate camera 0 to 90 degrees
6    *(data->theta) += D3DX_PI* 0.5f;
7    return D3D_OK;
8}

8.4 设置回调键

我们已经讨论了回调导向键,以及如何操作他们。但到目前为止我们还没讨论如何在一个动画序列中添加回调导向键。如果没有添加回调导向键的话是不会有任何的回调操作被触发的。接下来的这些加入了注解的代码将会展现如何增加回调导向键。注意这里使用了的一些 D3DX函数和类型没有加入注释。在大多数情况下这些函数和类型都是可以顾其名思其义的。如果还有不甚了解的地方请查阅DirectXSDK文档去了解更多的细节。

 1void Tiny_X::setupCallbackKeyframes(float* theta)
 2{
 3    // Remark:  theta is pointer to the  camera's theta coordinate.
 4    // Grab the current animation set for 'tiny.x' (we know there is only one.)
 5    ID3DXKeyframedAnimationSet* animSetTemp = 0;
 6    _animCtrl->GetAnimationSet(0, (ID3DXAnimationSet**)&animSetTemp);
 7    // Compress  it.
 8    ID3DXBuffer* compressedInfo = 0;
 9    animSetTemp->Compress(D3DXCOMPRESS_DEFAULT,0.5f,0, &compressedInfo);
10    // Setup two callback keys.
11    UINT numCallbacks = 2;
12    D3DXKEY_CALLBACK keys[2];
13    // Make static so it does not pop off the  stack.
14    static TinyXCallbackInfo CallbackData;
15    CallbackData.theta =theta;
16    // GetSourceTicksPerSecond() returns the number of 
17    // animation keyframe ticks that occur per second.
18    // Callback keyframe times are tick based. 
19    double ticks = animSetTemp->GetSourceTicksPerSecond();
20    // Set the first callback key to triggeraa  callback half way  through the animation  sequence.
21    keys[0].Time = float(animSetTemp->GetPeriod()/2.0f*ticks);
22    keys[0].pCallbackData = (void*)&CallbackData;
23    // Set the second callback key to trigge  callback at the end of the animation  sequence.
24    keys[1].Time = animSetTemp->GetPeriod()*ticks;
25    keys[1].pCallbackData = (void*)&CallbackData;
26    // Create the ID3DXCompressedAnimationSet  interface with the callback  keys.
27    ID3DXCompressedAnimationSet* compressedAnimSet = 0;
28    D3DXCreateCompressedAnimationSet(animSetTemp->GetName(),
29    animSetTemp->GetSourceTicksPerSecond(),
30    animSetTemp->GetPlaybackType(), compressedInfo,numCallbacks, keys, &compressedAnimSet);
31    compressedInfo->Release();
32    // Remove the old (non  ) compressed) animation  set.
33    _animCtrl->UnregisterAnimationSet(animSetTemp);
34    animSetTemp->Release();
35    // Add the  w new  ) (compressed) animation  set.
36    _animCtrl->RegisterAnimationSet(compressedAnimSet);
37    // Hook up the animation set to the first  track.
38    _animCtrl->SetTrackAnimationSet(0, compressedAnimSet);
39    compressedAnimSet->Release();
40}

9 总结

本文概述了蒙皮网格和角色动画。我们已经学习了如何去表征一个角色模型的骨骼层次。学习了一个骨块时如何继承它的父骨块的变换。学习了如果通过给每一个骨块增加一组关键帧,去描述一个动画序列。学习了关于使用了顶点混合技术的蒙皮动画及其优点,并且学习了如何使用 D3DX 动画相关的 API 去实现一个顶点混合。我们还学习了如何通过使用 D3DX 动画相关的API 对多重动画序列进行混合操作。最后,我们学习了如何通过使用回调操作器去并行执行一段代码。

10 参考书目

Akenine-Möller, Tomas,and Eric Haines. Real-Time Rendering. 2nd ed.Natick,Mass : A K Peters,Ltd., 2002.

Freidlin, Benjamin. “DirectX 8.0: Enhancing Real-Time Character Animation with Matrix Palette Skinning and Vertex Shaders.” MSDN Magazine, June 2001.

Lander,Jeff. “Slashing ThroughReal-Time CharacterAnimation.” Game Developer Magazine, April1998.

Lander,Jeff. “Skin Them Bones: GameProgramming for theWeb Generation.” Game DeveloperMagazine, May 1998.

Lander,Jeff. “Over MyDead,PolygonalBody.” Game Developer Magazine, October 1999.

Microsoft Corporation. Microsoft DirectX 9.0c SDK Documentation. Microsoft Corporation,2003.