三維模型冗餘面的stan melax縮減算法


Stan Melax 正在加拿大亞伯達大學攻讀計算機科學博士學位,致力於研究交互式3D技術和算法。他同時是Bioware的技術總監,曾經參與《超鋼戰神》這款遊戲的製作,現在正在爲他們的下一款遊戲實現非常酷的3D效果,你可以通過電子郵件跟他聯絡:[email protected]

 

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄

  如果你是一個遊戲開發者,那麼3D 多邊形模型已經成爲你日常生活中的一部分,並且你一定對一些3D概念例如每秒多邊形數量、低面模型以及細節層次等等非常熟悉了。你可能也同樣知道多邊形減面算法的目的在於通過一個有着大量多邊形的高細節的模型生成一個多邊形數量比它少、但是看起來卻跟原模型很相像的低面模型。這篇文章解釋了一種實現自動減面的方法,並且附帶的討論了多邊形減面的有用之處。在我們開始之前,我建議你去下載我的一個程序:BUNNYLOD.EXE,它展示了我將要闡述的這項技術。你可以在Game Develop網站上找到它。

  • 問題的由來

  在深入這個很“牛X”的3D算法之前,你可能會問你自己真的有必要關注它嗎?要知道,已經有一些商業的插件和工具來爲你減少多邊形數量了。
  然而,下面的幾條理由會告訴你爲什麼需要實現自己的減面算法:

  • 你使用的多邊形減面工具生成的結果無法滿足你的特殊需求,因此你希望做一個自己的工具。
  • 你當前使用的多邊形減面工具可能無法產生減面過程中的變化信息,而你卻希望利用這些變化信息來使不同的細節層次之間的轉換更加平滑。
  • 你希望將生產過程自動化,這樣的話美術人員就僅僅需要創建一個細節適當的模型,然後遊戲引擎就能自動創建模型其餘的細節層次。
  • 你正在製作一個VRML瀏覽器,你希望提供一個菜單項來簡化那些巨大的VRML文件。那些把這些巨大的文件放到網上的超級計算機用戶沒有想到這些文件在普通家用電腦上顯示幀速率會比較低。
  • 你在你的遊戲中使用的特效改變了物體的幾何形狀,增加了多邊形的數量,你需要一個方法來使你的引擎能夠實時地快速減少多邊形數量。

  你對此懷疑?圖1展示了一個具體的實例,一個遊戲引擎對減少多邊形這種特性的需求。

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄
圖1 爆炸效果對多邊形數量的影響

  在Bioware,我實現了實時的爆炸效果,並且把它們應用在了我們開發的一個遊戲原型上,以便給我們的出版商留下深刻印象。玩家可以射擊和爆破他們瞄準的實心物體表面的任意塊。通過子彈撞擊而改變遊戲環境比典型的“定點爆破”這種只能在遊戲世界中改變預先設定項的技術更棒。遺憾的是,重複不斷的使用爆破效果會在物體上產生大量附加的三角形,如同你在圖1中看到的一樣。許多添加的面是很小的或者是碎片,不會對遊戲的視覺效果產生絲毫的影響——它們僅僅是讓遊戲更慢。這種情況下就要求有實時的多邊形減面功能,所以我開始尋找一種能夠高效地完成這項工作的算法。

  • 坍塌邊

  在我着手處理這個問題之前,我跟亞伯達大學圖形實驗室的一些人學習了多邊形減面。(它讓我跟一個團隊一起工作,從而弄明白這個非常難的算法是如何工作的,並且弄明白什麼樣的技術適用於什麼樣的任務。)最近這個領域出現了很多研究成果,但其中大多數比較好的技術都是 H.Hpppe 的漸進網格算法的改進和變形(參見“更多的信息”)。那些技術都是通過重複不斷的使用一個簡單的邊坍塌操作來降低模型的複雜度,見圖2。

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄
圖2 邊塌陷

  在這個操作裏面,u和v兩個頂點(邊uv)被選中並且其中一個頂點(這裏是u)“移動”或者說“坍塌”到另一個頂點(這個例子裏是v)。下面這些步驟說明如何實現這個操作:
1. 去除所有既包含頂點u又包含頂點v的三角形(換一種說法,去除所有以uv爲邊的三角形)。
2. 更新所有剩下的三角形,把所有用到頂點u的地方都用頂點v代替。
3. 移除頂點u。
重複以上的過程,直到多邊形的數量達到了預期數量。每一次重複的過程中,通常會移除一個頂點、兩個面、三條邊。圖3展示了一個簡單的例子。

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄
圖3 多邊形經過一系列邊坍塌之後減少了面數

  • 選擇下一條邊進行坍塌

  要產生效果比較好的底面模型的訣竅在於要正確地選擇坍塌的邊,能夠在坍塌的時候最小程度的影響模型的視覺變化。研究者提出了各種各樣的方法來使在每一次坍塌的時候能夠選擇出“最小影響”的邊。但遺憾的是,最好的那種方法非常非常複雜(也就是說,很難實現),並且要花大量時間用於運算。因此這推動我要找到一種能夠在遊戲運行階段減少多邊形面數的方法,我做了很多實驗,最後終於爲這個選擇邊的過程開發了一種簡單又超快的方法來生成相當不錯的低面模型。

  顯然,先要去除那些小細節。同時要注意的是,對於那些在同一平面上的表面,只需要很少的多邊形就可以表示,同時高度彎曲的曲面則需要更多的多邊形來表示。根據以上這些,我們定義了:一條邊是否要坍塌,取決於它的邊長與曲率值的乘積。爲了找到在uv方向上距離別的三角形最遠的u的臨接三角形,我們通過比較兩個面的法線的點積得到坍塌邊uv的曲率值。方程式1展現了用更多正式符號表示的求邊坍塌值的公式。詳見源碼(你可以在Game Developer網站上下載到源代碼)。

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄
Tu是包含頂點u的三角形的集合,Tuv是同時包含頂點u和頂點v的三角形的集合。
方程式1 求邊坍塌值的方程式

  你可以看到,這個算法在決定哪一條邊坍塌的時候對於面的曲率和大小做了平衡。要注意的是頂點u到v的坍塌值不一定和頂點v到u的坍塌值相同。此外,這個公式對於脊狀的邊的坍塌也是有效的。即使這條脊有可能是一個銳角,或者是直角,都沒有關係。圖4舉例說明了這種情況。非常明顯的,在平面區域中間的頂點B,可以被坍塌到頂點A或者頂點C。角上的頂點C應該最後被保留下來。如果把上面的頂點A坍塌到內部的頂點B,那就會非常糟糕。不過,頂點A可以沿着脊坍塌到頂點C,這絲毫不會影響這個模型的外觀。

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄
圖4 好的和差的邊坍塌

  如果你正在實作你自己的減面算法,你可能希望能夠用這個公式做實驗,來看看是否滿足你的要求。例如,對於一個動畫模型,你可能希望能夠改進公式,使它能夠在判斷潛在的坍塌邊的時候可以參考不止一個動畫關鍵幀的數據。如果對於你來說,模型質量比減面算法所需要的執行時間更重要的話,你應該考慮使用Hoppe的函數。我們已經添加了很多擴展用來處理貼圖座標、頂點法線、鄰接邊,以及表面斷裂(比如貼圖接縫)。

  • 結果

  先顯示一個原來的模型,然後顯示簡化後的模型,這是對多邊形減面算法效果的最好證明。大多數的研究論文都用非常高面高細節的模型減面來證明它們的效果,原始模型接近100,000個多邊形,簡化後的模型只有10,000個多邊形。對於3D遊戲來說,更恰當(並且跟有挑戰性)的測試是生成一個只有幾百個多邊形的模型,以此展示算法的強大威力。

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄
圖5 453個、200個以及100個頂點的小兔子模型(從左到右)

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄
圖6 隨機選擇坍塌邊(200個頂點)

  舉個例子,圖5展示了一個小兔子的模型,它是從一個由 Viewpoint Datalabs 製作的VRML文件中提取出來的。模型的最初版本(左邊)包含有453個頂點和902個多邊形。後邊顯示的是減少到200個頂點(中間)和100個頂點(右邊)的模型。希望你能夠對圖中不同數量多邊形模型的視覺外觀看起來感到滿意。圖6展示了由於沒有選擇出正確的坍塌邊而簡化的模型,這裏坍塌邊的選擇是隨機的。
  

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄
圖7 一個女性的人物模型,左邊100%多邊形數量,中間20%多邊形數量,右邊是4%多邊形數量

 當我們完成了動物實驗之後,就要開始把這種算法應用在人物模型上了。圖7展示了一個Bioware製作的女性人物模型的三個版本——4,858;1,000以及200個頂點。(根據歐拉公式,我們知道多邊形的數量大致爲頂點數的兩倍。)這些模型圖片是用平坦的方式渲染的,你能夠明顯的看到模型之間的不同之處。當我們使用平滑的方式渲染並且應用上貼圖的話,那麼這些差別就不會那麼明顯了。
 

  • 實際應用

  我們最初的目標比較簡單:我們想要找到一種方法可以減少由於過多的爆破特效造成的過多的多邊形。但是,經過開發這個多邊形減面算法並且在人物模型上得出的比預期好的結果,我們覺得這個技術完全可以用於在遊戲引擎中生成模型的細節層次(LOD)。預計這個在基本算法基礎上改進的新的版本可以整合進Bioware的3D引擎中。現在,我們的美術人員只需要爲每一個遊戲中的物體創建一個細緻的模型就可以了。一個預處理的過程就可以爲模型減面。然後,如果遊戲每秒的幀速率低於預定的限度,或者遊戲中的一個物體離攝像機相當遠的時候,我們就可以拿一個低面的模型來代替高細節的模型。可以在遊戲運行期間來做這些事情從而增加遊戲的可伸縮性。遊戲可以根據當前運行系統的馬力來調整這些東西。
 

  • 實現細節

  這種算法僅僅能夠運用於三角形。如果需要的話可以把其它更多邊的多邊形簡單地分解爲三角形,除了這點就沒有別的限制了。事實上,許多應用只用三角形。

  大多數儲存多邊形物體的數據結構都是用一組頂點數據和一組三角形數據組成,其中三角形數據中包含了指向頂點數據的頂點索引數據。比如說:
Vector vertices[];
class Triangle {
 int v[3]; // indices into vertex list
} triangles[];
 VRML中使用的索引面集合節點數據是這種數據結構的另一個例子。當一個物體中的兩個三角形有相同的頂點的時候,它們有相同的索引值(因此它們共享頂點列表中的相同的頂點)。

 

class Triangle {

public:

Vertex * vertex[3];// the 3 points that make this tri

Vector normal; // orthogonal unit vector

Triangle(Vertex *v0,Vertex *v1,Vertex *v2);

~Triangle();

void ComputeNormal();

void ReplaceVertex(Vertex *vold,Vertex *vnew);

int HasVertex(Vertex *v);

};

class Vertex {

public:

Vector position; // location of this point

int id; // place of vertex in original list

List<Vertex *> neighbor; // adjacent vertices

List<Triangle *> face; // adjacent triangles

float cost; // cached cost of collapsing edge

Vertex * collapse; // candidate vertex for collapse

Vertex(Vector v,int _id);

~Vertex();

void RemoveIfNonNeighbor(Vertex *n);

};

List<Vertex *> vertices;

List<Triangle *> triangles;

 

程序清單1 擴展後的數據結構

  我們根據我們的多邊形減面算法的需要對這個數據結構進行了添加。一個主要的改進是我們現在需要訪問的信息已經不僅僅是每個三角形使用哪些頂點——我們同樣要知道每個頂點被哪些三角形使用。此外,我們應該可以直接訪問每一個頂點的鄰接頂點(也就是邊)。程序清單1展示了添加後的數據結構。
  
  

float ComputeEdgeCollapseCost(Vertex *u,Vertex *v) {

// if we collapse edge uv by moving u to v then how

// much different will the model change, i.e. the “error”.

float edgelength = magnitude(v->position - u->position);

float curvature=0;

// find the “sides” triangles that are on the edge uv

List<Triangle *> sides;

for(i=0;i<u->face.num;i++) {

if(u->face[i]->HasVertex(v)){

sides.Add(u->face[i]);

}

}

// use the triangle facing most away from the sides

// to determine our curvature term

for(i=0;i<u->face.num;i++) {

float mincurv=1;

for(int j=0;j < sides.num;j++) {

// use dot product of face normals.

float dotprod = u->face[i]->normal ^ sides[j]->normal;

mincurv = min(mincurv,(1-dotprod)/2.0f);

}

curvature = max(curvature,mincurv);

}

return edgelength * curvature;

}

void ComputeEdgeCostAtVertex(Vertex *v) {

if(v->neighbor.num==0) {

v->collapse=NULL;

v->cost=-0.01f;

return;

}

v->cost = 1000000;

v->collapse=NULL;

// search all neighboring edges for “least cost” edge

for(int i=0;i < v->neighbor.num;i++) {

float c;

c = ComputeEdgeCollapseCost(v,v->neighbor[i]);

if(c < v->cost) {

v->collapse=v-neighbor[i];

v->cost=c;

}

}

}

void Collapse(Vertex *u,Vertex *v){

// Collapse the edge uv by moving vertex u onto v

if(!v) {

// u is a vertex all by itself so just delete it

delete u;

return;

}

int i;

List<Vertex *>tmp;

// make tmp a list of all the neighbors of u

for(i=0;i<u->neighbor.num;i++) {

tmp.Add(u->neighbor[i]);

}

// delete triangles on edge uv:

for(i=u->face.num-1;i>=0;i--) {

if(u->face[i]->HasVertex(v)) {

delete(u->face[i]);

}

}

// update remaining triangles to have v instead of u

for(i=u->face.num-1;i>=0;i--) {

u->face[i]->ReplaceVertex(u,v);

}

delete u;

// recompute the edge collapse costs in neighborhood

for(i=0;i<tmp.num;i++) {

ComputeEdgeCostAtVertex(tmp[i]);

}

}


程序清單2 坍塌值的確定以及進行邊的坍塌操作
   
  成員函數 ReplaceVertex() 在多邊形減面的過程中被用來處理邊坍塌。數據結構中的頂點、三角形的添加、刪除、或者替換必須保持正確,構造函數、析構函數以及另外的成員函數保證了這個過程的正確性。我們保存了面法線,因爲它們在邊選擇的方程運算中被大量地用到。爲了避免每次重新運算,我們還把每個頂點選擇最優坍塌邊以及坍塌值記錄了下來。因爲那些成員函數的實現是非常直觀的,因此我沒有將它們包含到這篇文章裏面。如果你感興趣,就去Game Developer網站上找到這個算法的源代碼,然後簡單的找一下就可以了。程序清單2包含了坍塌值的計算代碼和進行邊坍塌操作的代碼。
    
  有了這幾個函數之後,多邊形減面的操作就變得很簡單了。先初始化物體的頂點和三角形數據,然後按照下面這樣做:
while(vertices.num > desired) {
  Vertex *mn = MinimumCostEdge();
  Collapse(mn,mn->collapse);
}
  在BUNNYLOD.EXE這個演示中,沒有使用這麼簡單的循環。它還爲了動畫創建了一個附加的數據結構。 

  • 更好地利用數據

  相比把用過之後移除的頂點、三角形數據信息丟棄,還不如把它們都保留下來,以便以後需要使用這些數據的時候不必重新運算多邊形減面。這個特性很容易就能實現,只要把每一個坍塌後的頂點以及坍塌的順序保存下來就可以了。
  BUNNYLOD.EXE這個演示就是使用這個方法。一開始,小兔子模型在大約1秒鐘時間內從450個頂點減少到0個。然後,模型會不斷的增加細節,並且是在一些特殊的多邊形數量的階段,左邊的進度條會同時通過動畫方式來表示這個過程。另外還有一種動畫方式是從0到全部頂點不斷增加。
    
  邊坍塌序列也可以用在漸進傳輸中。就像交錯存儲的.GIF和.JPG圖片可以在網絡傳輸中不斷增加細節一樣,一個物體的頂點可以通過坍塌過程的倒序排列來進行數據廣播。接受的計算機可以不斷的根據接收到的數據流來重建並且顯示這個模型。這個主意非常棒,但是或許現在來看和遊戲開發者還沒有什麼關係。

  模型的LOD在很多遊戲中是一個非常重要的組件。根據我們的算法生成的坍塌序列,可以生成很多細節層次的模型來表示模型的不同的LOD。在交換模型的時候有一個問題就是玩家常常會注意到它的發生(這種現象叫做“跳出”)。一個對付“跳出”現象的解決方案是在兩個模型中間做平滑變形。爲了能夠在兩個模型之間做變形,必須把其中一個模型的頂點映射到另一個模型上。幸運的是,這些信息可以從邊的坍塌序列中提取出來。BUNNYLOD.EXE也演示了變形的例子。

  • 可供選擇的邊坍塌技術

  多邊形減面算法不是創建低面模型的唯一選擇。美術人員做出來的底面模型往往比通過算法算出來的低面模型更好。其中一個原因是算法無法從宏觀上把握模型。從一方面來講,美術人員瞭解他(她)所創建的模型(比如兔子、椅子,等等),並且能夠從審美的角度上決定如何去減少物體的面數。人類的視覺系統會偏向於某幾個細節,比如說眼睛和嘴部,並且很少去關心其它部位的細節,比如鎖骨或者膝蓋。另一方面,我們這個簡單的算法僅僅比較了很少的點積和邊長,並且明顯缺乏一種智慧來自動識別那些人感覺比較重要的部位並進行優化。使用多邊形減面算法的優勢在於能夠讓這個過程自動化。
  

一種簡單、快速、高效的多邊形減面算法 - SD-Gundam - 聯盟世紀-啓示錄  
  圖8 技術對比

 
  另一種在遊戲中使用的製作LOD的技術是使用參數化曲面來描述幾何物體,參數曲面片可以鑲嵌在需要細化的部位。Shiny的MESSIAH引擎使用了相似的方法。當然,這些基於表面的方法更加可取(也許是最佳的)。圖8舉了一個2D的例子說明了這個優勢。一個正八邊形通過參數化的方法去掉一條邊生成了一個正七邊形。如果用坍塌掉一條邊的話正八邊形就生成了一個不規則的圖形。

  遺憾的是,並不是所有的情況都適用參數化曲面的。有一些情況要求物體在渲染時生成多邊形的時候相鄰的表面能夠很好的吻合在一起(沒有裂縫和T型連接)。並且,有很多鋸齒狀的物體使用參數化表面無法取得良好的效果,這是因爲需要的表面也許並不會比多邊形的數量少。以多邊形爲基礎的減面的方法一般來說更加有用,並且可以工作在當前類型的模型上。

  我希望我提供的這些信息和例子程序能夠派上用場,雖然這篇文章沒有觸及到貼圖座標、頂點法線、鄰接邊、單一拓撲、貼圖接縫等等。這些題目都留作給讀者的練習題。此外,對此算法的變化和改進都是有探索價值的。一個令人興奮的主題是適應性簡化,可以根據運行時的參數來使模型的不同部分使用不同的細節層次進行渲染。這個對於室外地形環境非常有用,這樣的話離視點近的部分就可以又更多的細節表現。

  • 更多的信息

  最近多邊形減面已經成爲一個很熱門的搜索主題,並且大部分的文獻都能夠在計算機圖形學會議的會議記錄裏面找到。另外一些資料你可以看:

  • Cohen, J., M. Olano, and D. Manocha. “Appearance-Preserving Simplification”, SIGGRAPH ‘98.
  • Hoppe, H. “Progressive Meshes,” SIGGRAPH ‘96, pp. 99-108.
  • Luebke, D. and C. Erikson. “View-Dependent Simplification of Arbitrary Polygonal Environments”, SIGGRAPH ‘97, pp. 199-207.
  • I have a demo on my university web site at http://www.cs.ualberta.ca/~melax/polychop
  • H. Hoppe, the Guru of polygon reduction, maintains a web site at p://research.microsoft.com/~hoppe/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章