高效率3D图形程序中的骨骼

骨骼 皮肤动画技术是 3D 动画领域的一项比较高级的技术。由于其生动、逼真的效果,在影视制作、动态仿真等领域起着重要的作用。只有使用骨骼 皮肤技术,才能制作出广播级的动画作品。
  顾名思义,骨骼 皮肤动画的含义是使用一系列的骨骼去带动一张皮肤进行运动。其特点是:
  第一,作为皮肤的网格是一个整体,而不是分成区段的。
  在简单的区段动画中,一个复杂的物体是由许多“坚硬”的段组成的,最典型和常见的例子是人体,是由头、躯干、手臂、腿、脚等组成,而躯干又分上身、下身,手臂又分上臂、前臂和手,腿部又分为大腿和小腿。分别为这些段定义运动,就可以组成人体的较复杂的运动了。这种技术的优点是实现方便,而且运算速度快,适用于对视觉效果要求不高的场所。但是有一个致命的缺点是会在段与段之间相联接的地方出现明显的接缝,而在进行某些动作的时候会出现段与段分离的现象,这些在广播级的动画中是绝对不允许出现的。但是由于骨骼 皮肤动画中的皮肤是一个整体,所以避免了这些情况的发生。
  第二,皮肤的形状是可以改变的,并且完全是由与其相关的骨骼决定的。
  皮肤不再是一个“硬梆梆的”网格,而是“有弹力的、能拉伸的”。相比区段动画中所有的由形状不变的生硬网格组成的区段,骨骼 皮肤动画中的皮肤能在任何时刻保持光滑、生动的外表,在制作一些像蛇、动物的尾巴等软的东西时尤其出色。
  既然骨骼 皮肤动画有这些好处,它又是怎么实现的呢?
  首先,需要有一个做皮肤用的网格和一系列骨骼。对于网格有一些要求,例如,在关节处的多边形数目应该多一些等等。而骨骼的大小和位置关系应该与皮肤相对应,因为要靠位置来判断骨骼影响了皮肤网格上的哪些点。
  然后,决定网格上的点是由哪快骨骼影响的。每一块骨骼都有一个作用范围,在这个范围中的点都要受该骨骼的影响。在关节处的点往往会受到多于一块的骨骼的影响,这些骨骼的影响通过不同的权值叠加在点上,使网格的关节部分尽量保持平滑。每一块骨骼都包括一组位置、朝向信息,借助这组信息,网格上的点确定自己的位置和朝向。这样就实现了骨骼对皮肤的影响。
  最后,设置骨骼的运动信息,带动受其影响的网格上的各点运动,就形成了动画。
  在一些 3D 动画软件中,第二步是即时算出来的,也就是说,一开始网格并不知道受哪些骨骼控制,只有建立了骨骼,并吧它付给网格后,软件才开始计算各点的位置是否在某一块骨骼的影响区域中,在所有的点都找到了自己的骨骼后,才可以进行动画。这种机制一般运用于各种 3D 动画设计软件中。
  在要求高效率的场合(如游戏中),这种方式是不可取的。为了实现高效率,预处理是一种普遍而有用的的方法。所谓预处理,就是在程序之外尽可能的把所有固定的数据处理好,并且为程序中优化的算法奠定基石。预处理的一个比较成功的例子是 Id 公司的 Quake 中对地图的处理。为了使 Quake 一帧场景中所需处理的多边形数目最少,需要对地图数据进行优化。采用 BSP 树和创立可能可见集可以达到优化的要求,但是为每一幅地图生成一棵 BSP 的过程是极其缓慢的。据 ID 公司的资料纪录,当时生成一棵 BSP 树用了十多分钟,而创立可能可见集的工作在一台有四个 CPU 的机器上用了约一个小时。然而每一地图的 BSP 树和可能可见集一旦生成,就可以在游戏中不再改变。这种情况下就可以采用预处理。借助于这种方法, Id 公司的人员将 Quake 引擎的效率提高了一半以上。由这个例子我们可以看出预处理的道理就是“长痛不如短痛”。
  在高效率 3D 程序中要实现骨骼 皮肤动画,也需要预先制作好皮肤和骨骼,并且在数据中记录皮肤网格上的各点分别受哪些骨骼控制。这就是一个预处理过程。另外,为了实现简化,在预处理时可以不靠骨骼的“影响区域”来确定骨骼影响的点。因为这样做需要确定网格的点与骨骼作用区域的相包含关系,这将是一个繁琐的过程。
  下面我们通过一个骨骼 皮肤动画的程序例子(下载)来分析一下其具体实现。
  我们选用的这个例子为一工作台模式的程序,采用 VC++ 6.0 编译,使用 OpenGL 加速,需要 OpenGL 实用工具库 Glut 的支持,并且使用了一个提供从 3DS 文件中读取数据的辅助库。此程序的功能包括两大部分:读入一个皮肤网格和一系列骨骼,确定网格和骨骼的关系;读入一系列动画信息,实现一个简单的动画。前者就是我们提到过的所谓预处理过程,是程序的重点。
  程序中首先定义了如下结构:

   typedef float MATRIX[4][3];    /* 矩阵的定义 */
   struct Bone
   {
     struct   Bone *NextPtr;    /* 使用链表存储骨骼 */
     char    Name[12];      /* 这块骨骼的名字 */
     long    NrVerts;       /* 这块骨骼影响的顶点的个数 */
     MATRIX   Matrix;        /* 骨骼的初始化矩阵 */
     MATRIX*   AnimPtr;       /* 指向表示动画信息的矩阵数组的指针 */
   };

   struct Skin
   {
     long    NrFrames;      /* 动画的帧数 */
     point3ds *PointPtr;       /* 顶点缓存,用于暂时存储变换位置后的顶点 */
     Bone *    BonePtr;      /* 骨骼链表的头节点 */
     mesh3ds * MeshPtr;       /* 网格皮肤 */
   };

   struct BonePoint
   {
     point3ds   Point;       /* 顶点的位置 */
     int      Index;        /* 在网格中的原始位置索引 */
     Bone*     BonePtr;       /* 指向影响此顶点的骨骼的指针 */
   };

  然后,定义一些宏和全局变量如下,很简单:

   #define WORLD_NEAR 1000
   #define WORLD_FAR 25000
   #define ONEDEGREE ((1.0f/180.0f)*3.1415926)
   long      CurFrame = 0;         /* Current frame in animation */
   float     AngleY = -25*ONEDEGREE;    /* Current angle of the camera */
   float     Distance = -12000;       /* Current camera distance from the
                        world center (0,0,0) */
   float     Height = 0;          /* Current camera height from the
                        world center (0,0,0) */
   BOOL      Paused = FALSE;        /* Is the animation playback paused or not */
   Skin *SkinPtr;               /* pointer to the character/skin displayed*/

  下面是程需的核心部分 检索顶点位置来确定其受哪一块骨骼影响。

   void SolveBoneInfluences(database3ds *db, Skin *skinptr)
   {
     /* 参数说明: db in : 一个 3ds 数据库对象的指针,其中存储了一些网格等信息。 skinptr in & out )如果函数成功,将以一个新的皮肤结构对象填写此结构 */
     /* Allocate a big workbuffer */
     BonePoint *bonepointptr=(BonePoint*)malloc(30000*sizeof(BonePoint));
     BonePoint *curbonepoint=bonepointptr;

     long NrBoneVerts=0;

     /* 建立一个新的顶点缓冲,将所有骨骼的点依次放进去,每一个点都记录了是从哪一块骨骼来的。由于此程序例中所有的骨骼都是由一个网格和一个矩阵组成,而且所有骨骼网格的点都与皮肤网格的点一一对应。实际上骨骼的网格是皮肤网格的一部分,每一块骨骼网格上的所有点都是皮肤网格所有点的子集,本程序的核心思想就是通过这种关系确定皮肤上的点是由哪块骨骼影响的 */

     MATRIX tmpmat;
     Bone *boneptr=skinptr->BonePtr;   /* 获得皮肤的骨骼链表 */
     while(boneptr)           /* 遍历骨骼链表 */
     {
       mesh3ds *bonemesh=NULL;
       /*bonemesh 对象是一个 mesh3ds 结构的对象,定义见 3dsftk.h*/

       GetMeshByName3ds(db,boneptr->Name,&bonemesh);
       /* 按照已有的 boneptr 中的名字从 db 中读取一个骨骼网格给 boneptr ,保证读取的是与骨骼对应的。 */
       assert(bonemesh);

       Copy3dsMatrix(tmpmat,bonemesh->locmatrix);
       /* 将骨骼网格的矩阵也一并读出 */

       InverseMatrix(tmpmat,boneptr->Matrix);
       /* 因为 3ds OpenGL 中的矩阵定义方式不同,需要做转换。 InverseMatrix 函数定一见源代码。这两步重要的操作实现了将 bonemash 中的矩阵付给 boneptr 的矩阵对象 */

       /* 下面的操作将每一块骨骼网格的所有顶点写入前面开辟的顶点缓冲中 */
       point3ds *bonemeshpoints=bonemesh->vertexarray;

       NrBoneVerts+=bonemesh->nvertices;
       assert(NrBoneVerts<30000);

       for(int i=0;invertices;i++)
       {
         /* 把骨骼顶点的位置信息写入缓冲: */
         curbonepoint->Point.x=bonemeshpoints->x;
         curbonepoint->Point.y=bonemeshpoints->y;
         curbonepoint->Point.z=bonemeshpoints->z;

         /* 纪录缓冲中当前顶点是骨骼链表中哪一块骨骼的 */
         curbonepoint->BonePtr=boneptr;
         bonemeshpoints++;
         curbonepoint++;
       }
       RelMeshObj3ds(&bonemesh);      /* 释放 bonemash 对象 */
       boneptr=boneptr->NextPtr;
     }

     /* 这里的定义有些混乱 */

     mesh3ds *skinmesh = skinptr->MeshPtr;
     /* 定义一个网格对象指针指向 skinptr 中的网格 */

     point3ds *skinmeshpoints = skinmesh->vertexarray;
     /* 定义一个顶点数组指针并令其指向网格对象中的顶点们 */

     BonePoint *skinpointptr = (BonePoint*)malloc(skinmesh->nvertices*sizeof(BonePoint));
     /* 由于顶点数组不包含骨骼信息,程序申请一片与顶点数组同样大小的骨骼顶点数组空间 */

     BonePoint *curskinpoint = skinpointptr;
     /* 设一个指针记录骨骼顶点数组中的当前位置 */

     /* 对于每一个网格顶点数组中的顶点,找到离它最近的骨骼顶点。 */
     int i;
     for (i=0;invertices;i++)
     /* 遍历顶点数组,并用顶点数组中的位置信息填充与之等大的骨骼顶点数组 */
     {
       curskinpoint->Point.x = skinmeshpoints->x;
       curskinpoint->Point.y = skinmeshpoints->y;
       curskinpoint->Point.z = skinmeshpoints->z;
       curskinpoint->Index = i;   /* 按由小到大的顺序纪录原始的骨骼顶点序号 */
       curskinpoint->BonePtr = NULL;

       /* 现在的骨骼顶点数组是由网格数组直接得来的,其中还没有任何骨骼信息,暂且称之皮肤骨骼顶点数组 */
       curbonepoint=bonepointptr;
       /* 先前由一块块骨骼网格依次建立的骨骼顶点数组,称之骨骼骨骼顶点数组 */

       float mindist=1e6;
       for(int j=0;j< NrBoneVerts;j++)
       /* 内层循环,为皮肤骨骼顶点在骨骼骨骼顶点数组中找最近的顶点 */
       {
         float dist=CalcDistNotSquared(skinmeshpoints,&curbonepoint->Point);
         if(dist< mindist)
         {
           mindist=dist;
           curskinpoint->BonePtr=curbonepoint->BonePtr;
         }
         curbonepoint++;
       }
       curskinpoint++;         /* 双层嵌套循环 */
       skinmeshpoints++;
     }

     /* 按照骨骼对皮肤顶点排序,并且对多边形进行新的分配 */
     skinmeshpoints = skinmesh->vertexarray;

     /* 网格对象的顶点数组 */
     face3ds *skinfaces = skinmesh->facearray;

     /* 网格对象的多边形数组 */
     long CurIndex=0;

     boneptr=skinptr->BonePtr;
     while(boneptr)
     {
       curskinpoint=skinpointptr;
       for (i=0;invertices;i++)
       {
         if(curskinpoint->BonePtr==boneptr)
         {
           Transform(boneptr->Matrix,(float*)&curskinpoint->Point,(float*)skinmeshpoints);
           RemapFaceList(skinmesh,curskinpoint->Index,CurIndex++);
           boneptr->NrVerts++;
           skinmeshpoints++;
         }
         curskinpoint++;
       }
       boneptr=boneptr->NextPtr;
     }/* 双层循环完成对网格的顶点的按骨骼排序 */

     /* 清理工作 */
     CleanUpFaceList(skinmesh);
     free(skinpointptr);
     free(bonepointptr);
   }

  到这里,简单介绍了此原程序的核心算法。这里有几点需要注意:首先,在程序中,是在运行期间调用此函数生成骨骼和皮肤的关系,而在一般的应用时,这一部和动画等操作应该是分离的,这个函数应该用于生成一个确定了骨骼 皮肤对应关系的文件。另外,程序给出的算法并不是唯一的骨骼 皮肤系统的的实现。这只是一个简单的例子,有许多种算法比这个要优越,例如在 DirectX 8.0 D3DX 辅助库中的算法。该算法加入了各点受骨骼影响的权值的概念,因此动画更平滑。还有一点,就是本程序对模型文件的要求,即骨骼网格是整张皮肤的一部分,具体应该如何,在 3DS MAX 中导入本程序所用的 3ds 文件一看就明白了。本站提供此源程序和所用到的库( 3dsftk.lib glut32 实用库)的下载。
   glut32 实用库的用法:解压缩后将 *.dll 文件与例子程序放在同一目录下,即可使程序正常运行。如果要编译源代码,需要将 *.h 文件拷贝到 VC include/GL 目录下,将 *.lib 文件拷贝到 VC lib 目录下,将 *.dll 文件放在 windows system32 目录下







=================================================
【角色骨胳动画处理系统】以两种不同的方式来处理人物角色之动画资料,一种是利用骨胳架构来牵动皮肤,这样子的作法较花费计算时间,但较省内存,另一种则是直接对外表皮肤做动画的变形,这么做的好处是计算量少,速度较快,
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值