发信人: nhyjq(人类·晤知想点) 
整理人: nhyjq(2003-06-10 11:09:18), 站内信件
 | 
 
 
版权:A.r.t     
 
   骨骼--皮肤动画技术是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; i<bonemesh->nvertices; 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; i<skinmesh->nvertices; 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; i<skinmesh->nvertices; 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 );
 }
 
 
   到这里,简单介绍了此原程序的核心算法。这里有几点需要注意:首先,在程序中,是在运行期间调用此函数生成骨骼和皮肤的关系,而在一般的应用时,这一部和动画等操作应该是分离的,这个函数应该用于生成一个确定了骨骼--皮肤对应关系的文件。另外,程序给出的算法并不是唯一的骨骼--皮肤系统的的实现。这只是一个简单的例子,有许多种算法比这个要优越,例如在DirectX8.0的d3dx辅助库中的算法。该算法加入了各点受骨骼影响的权值的概念,因此动画更平滑。还有一点,就是本程序对模型文件的要求,即骨骼网格是整张皮肤的一部分,具体应该如何,在3DMAX中导入本程序所用的3ds文件一看就明白了。
 
 
 
  ---- ∵我是人类(♂)㊣,天蝎座
 ∴我冷静、深沉     
 My QQ is 726556     
 欢迎来广州社区的游戏开发版逛逛,我是斑竹            
 欢迎来北京社区的C语言版逛逛,我是Shadow斑竹             | 
 
 
 |