高效率3D圖形程式中的骨骼-皮膚系統實現(轉)

post0發表於2007-08-12
高效率3D圖形程式中的骨骼-皮膚系統實現(轉)[@more@]

  骨骼--皮膚動畫技術是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;

  }

  

  /*這裡的定義有些混亂*/

  

  mesh3d

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-952132/,如需轉載,請註明出處,否則將追究法律責任。

相關文章