永远也不完美的程序

不断学习,不断实践,不断的重构……

常用链接

统计

积分与排名

好友链接

最新评论

人物动画系统(转)

我最近在学习人物动画的方面,做个总结,由于刚刚接触这个方面,所以有什么问题请大家指出。

在这篇日志里面,你可以获得这些信息:

1 人物动画的框架
2 骨骼动画及蒙皮技术
3 doom 3和quake 4中模型和动画格式md5及原理
4 可能的扩展


先来看一下人物动画的几种方法:

一、简单关键祯的动画

  像quake3中就是用的这样的方法。这种方法最简单,缺点就是空间上的浪费。由于每个关键祯中都要储存整个网格的几何信息,所以用这种方法生成的动画文件相当的庞大,在游戏进行中也会占用大量的内存。现在的游戏,一般都不用了。

二、简单的骨骼动画及蒙皮技术

  现在的很多游戏,都是用的这种方法,具体的原理后面再解释。这种方法可以节省大量的空间,对于美工来说,工作量也相对较小(可以利用动作捕捉的数据),真实性方面,简单的应用中也表现得比较好。

三、改进的蒙皮方法和基于物理的骨骼动画

  改进的蒙皮方法可以避免简单的蒙皮中产生的“糖纸失真现象”;

  基于物理的骨骼动画,已经有很多的游戏、物理引擎支持这一特性了,但是还有很多的技术问题需要处理。这是次时代游戏引擎必须很好实现技术之一。



基本的蒙皮原理

拿md5格式为例,来简单的解释一下蒙皮的原理。在doom3和quake4中md5mesh文件,用来记录一个人物的静态模型。有这样几个结构:

Joint: 用来记录骨骼的关节的信息;
Weight: 用来记录顶点相对于关节的权值;
Vertex: 顶点信息,和一般的顶点不同,这里的顶点不直接的包含几何坐标信息,而是记录了对应的Weight;

现在就来解释一下这三者之间的关系:

  Joint(关节)是会动的,而皮肤上的顶点是会随着顶点做相应的运动。我们保持皮肤上面的各个顶点和它相对应的关节的“位置关系”,就可以通过旋转关节,使得真个皮肤跟着旋转。这个“位置关系”,就是Weight。在运动的过程中,我们获得当前的骨骼的几何信息,也就是每个关节的几何位置,然后在根据每个顶点对于这些关节的权值,分别计算每个顶点的实际几何位置,这样,整个人物网格就计算出来了。

  很显然,这其中有一个预处理过程和两个关键的步骤。预处理就是需要由静态的模型(美工做出的人物模型)和骨骼来计算得到一组Weight;两个关键的步骤是,1、获得的整个骨骼的几何信息(有可能从关键祯混合得到);2、由顶点对应的Weight来计算出每个顶点的实际几何信息。

  了解这些基本的概念,下面就来介绍人物动画系统的框架。


骨骼蒙皮基本框架

基本的类型:

关节信息:

typedef struct _CharJoint
{
    Vector3 pos;
    Vector4 startPoint;
    int parentID;
    char name[32];
} CharJoint;

其中,parentID为父关节,pos为对应父关节的偏移值,startPoint为旋转角度;

权值信息:

typedef struct _CharWeight
{
    Vector3 pos;
    int jointID;
    float bias;
} CharWeight;

其中,pos为偏移量,jiontID为对应的joint,bias偏向值;


顶点信息:

typedef struct _CharVert
{
    float u, v;
    int startWeight;
    int weightCount;
} CharVert;

  其中,startWeight为该顶点对应的Weight在Weight列表中的偏移地址,weightCount记录该顶点对应多少个权值;对于简单的顶点,比如头顶上的某个点,动画的时候涉及到的变化并不多,所以,对应的权值数也就少,可以只有一个;对于动画中涉及变化比较复杂的点,比如手肘区域的顶点,可能由较多的权值(4个或更多),这样才能够很好的表示运动中对于多个关节的相对位置。


大概还涉及到这样一些类:

CharSkeleton: 记录整个骨骼的信息,包含了关节的链表;
CharMesh: 记录整个人物模型的静态信息,包括顶点,权值,关节等;
CharBlender: 基类,根据CharMesh和CharSkeleton来计算出实际的网格,基本成员函数为Blender,用CPU来计算蒙皮,可以被子类Blend覆盖(比如可以写一个用Vertex Shader实现的Blender);
CharAnimation: 每个CharAnimation实例对应一个动作序列,比如“人物蹲下动作”;动作序列保存的是人物骨骼动画的关键祯,也就是在某一祯时,骨骼中各个关节的几何信息;注意这里的祯的概念并不是平常说的渲染的祯,在动画中,为了进一步节省空间,一般设定了一个动作为几个格,就像动漫制作过程中的“故事板”,只是整个过程中的几个缩略图,在后期制作过程中,在“填满”中间缺省的图片;这里的骨骼动画关键祯也是如此,文件中只保存了间断的几个状态,在渲染的时候,还是要实时的生成中间的某个状态,来把整个动作序列“填满”;
CharAnimCtrl: 这个类的作用就是完成上面所说的,将动作序列“填满”的功能,输入是CharAnimation和时间,输出是一个基本的骨架,也就是CharSkeleton(当然这是靠传引用参数进行输出);


解决关键问题

  刚才提到了,整个系统中由三个关键的问题:一个预处理过程,关键祯混合以及从权值计算出实际顶点。预处理过程,基本上是编写一个建模工具导出插件的工作,这里就不讨论了。

关键祯混合:

  简单的办法,就是直接用线性的方法混合,比如现在的动画时间标识为40,而我只有标识为20和50的两个关键祯,于是:40-20 = 20,50-40 = 10;而20:10 = 2:1;所以,我们现在的状态离关键祯20的差异,以及离关键祯50的差异,这两个差异的比,就是2:1;好了,所以现在很自然地,我们取倒数,1:2;于是,我们做混合的时候,用“1份”关键祯20的骨骼,和“2份”50关键祯的骨骼,然后相加两者的结果(也就是“混合”过程)最后,除以3,得到最终的“1份”关键祯为40的骨骼。恩,就这么简单。(不过注意不要把这个比值的含义搞反了);

  际应用中还有其他的混合形式,后面再来介绍。


计算实际顶点:

我们看一下软件的(用CPU做蒙皮)Blend过程:

void CharBlender::Blend( Mesh &outputMesh, PE::CharMesh &inputMesh, PE::CharSkeleton &inputSk )
{
    if ( outputMesh.GetNumVertices() < inputMesh.GetNumVerts() )
        return;

    CharOutVert *pOutVerts = ( CharOutVert* )outputMesh.LockVertexBuffer();
    int numVerts = inputMesh.GetNumVerts();
    int numTris = inputMesh.GetNumTris();

    for ( int i = 0; i < numVerts; i++ )
    {
        const CharVert *pVert = inputMesh.GetVertAt( i );
        pOutVerts->x = pOutVerts->y = pOutVerts->z = 0.0f;

        /* u v initial */
        pOutVerts->u0 = pOutVerts->u1 = pOutVerts->u2 = pVert->u;
        pOutVerts->v0 = pOutVerts->v1 = pOutVerts->v2 = pVert->v;

        for ( int j = 0; j < pVert->weightCount; j++ )
        {
            const CharWeight *pWeight = inputMesh.GetWeightAt( pVert->startWeight + j );
            int index = pWeight->jointID;
            const CharJoint *pJoint = & ( inputSk.GetJointAt( pWeight->jointID ) );
            vec3_t wv;
            Quat_rotatePoint( &pJoint->startPoint.x, &pWeight->pos.x, wv );
            pOutVerts->x += ( pJoint->pos[0] + wv[0] ) * pWeight->bias;
            pOutVerts->y += ( pJoint->pos[1] + wv[1] ) * pWeight->bias;
            pOutVerts->z += ( pJoint->pos[2] + wv[2] ) * pWeight->bias;
        }
    }

    outputMesh.UnlockVertexBuffer();

    CharTri *pOutTri = ( CharTri* )outputMesh.LockIndexBuffer();

    for ( int i = 0; i < numTris; i++ )
    {
        const CharTri *pTri = inputMesh.GetTriAt( i );
        pOutTri->index[0] = pTri->index[0];
        pOutTri->index[1] = pTri->index[1];
        pOutTri->index[2] = pTri->index[2];
    }

    outputMesh.UnlockIndexBuffer();
}

其中黑体的部分,就是关键的代码,应该很容易看懂。其中,Quat_rotatePoint函数的作用就是将点进行旋转,得到新的坐标。


关于md5anim文件

  Doom3和Quake4中的动画文件都是用md5anim文件保存的。md5anim文件只含有该动作所涉及到的骨骼关节的动画信息。也就是所,文件中关键祯的关节列表,是它所对应的md5mesh文件中基本关节列表的一个子集;这样做当然是有道理的,因为,有些动作,可能只涉及到身体的一个部分,比如眨眼,换弹夹等等,那么,把一个完整的骨骼框架放在mesh文件中,把若干不同的局部或者整体的关节序列放在不同的动画文件中,这样,可以最大限度的节省空间。


可能的扩展

一、复杂动作的混合

  有时候,我们需要将两个动作混合,比如,一个人物同时的在做两种动作,一边向左平移,一边向右方开枪;不可能为每种可能的混合动作做大量的美工工作,而且空间上,我们也不允许这样做;可行的办法是,混合两个不同的动作序列,比如上半身动作和下半身动作的混合,这当然是最简单的方式。还有很多比较麻烦的混合方式,比如,人物在行走时中了枪,需要混合“行走”和“中枪”两个动作,而简单的线性混合是无法真实模拟的。

二、基于物理的动画

  这不再仅是图形方面的问题了,这其中涉及到了大量的物理模型,这个,我也不懂。。。可以从第三方的物理引擎获得帮助,ODE好像就支持了;

三、基于GPU的蒙皮

  原理和CPU蒙皮的原理一致,只是用了Shader,会比CPU蒙皮的效率快很多。在前面的代码中,只需实现CharBlender的子类就可以了。

四、非常流行的“换装”系统

  这在RPG游戏里面简直就是不可少的一条。就现在的框架来说,还不能达到随意“换装”的要求。修改CharMesh以及Character的底层,需要能够添加和删除基本的骨架,支持多层皮肤(衣服)(多个Mesh的开关)。还可以更换不同的武器(底层实现还是通过添加骨架完成)。。。

posted on 2008-11-12 11:39 狂烂球 阅读(625) 评论(0)  编辑 收藏 引用 所属分类: 图形编程


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理