BSP技術詳解1------有圖有真相


 我這個人非常懶,到現在也沒有發表幾篇文章,今天有一點時間貼上一些我翻譯的文章.BSP技術作為室內引擎渲染的主流技術雖然已經存在多年,但是生命力仍然非常頑強,最新的DOOM3,HL2仍然將它作為渲染的主流技術,但是在網上對它介紹文章雖然多卻非常淺顯,大多是使用Q3的BSP文件進行渲染,而BSP文件如何產生則介紹非常少,蓋因為這一部分是場景編輯器的工作,而完成一個這樣的BSP編輯器是非常困難的,需要掌握的知識非常多.下面我將對BSP編輯器這一部分需要用到的BSP知識進行一下介紹,這只是一些很初步的知識,如希望了解更多的內容,Q2開源代碼中有一個BSP編輯器的代碼是你研究的重點,還有就是HL2泄露代碼中的編輯器代碼,(一個痛苦的研究過程,可能要花費你幾個月甚至一年的時間,不過這是值得的,如果你想完成一個主流的射擊游戲引擎的話,沒有BSP編輯器是不可想象的).


 第一節 BSP Trees
BSP Trees英文全稱為Binary Space Partioning trees,二維空間分割樹,簡稱為二叉樹。它於1969年被Shumacker在文章《Study for Applying Computer-Generated Images to Visual Simulation》首次提出,並被ID公司第一次使用到FPS游戲Doom中,Doom的推出獲得了空前的成功,不僅奠定了ID公司在FPS游戲開發的宗師地位,也使BSP技術成為室內渲染的工業標准,從BSP產生到現在已經有30多年了,其間雖然產生了大量的室內渲染的算法,但卻無人能撼動它的地位,對於以摩爾定律發展的計算機業來說這不能不是一個奇跡。
為什么使用BSP Trees
一個BSP Trees如同它的名字一樣是一個層次樹的結構,這個樹的葉節點保存了分割室內空間所得到的圖元集合。現在隨着硬件加速Z緩沖的出現,我們只需要用很小的代價就可以對空間中的圖元進行排序,但是在90年代初由於硬件的限制,使用BSP的主要原因是因為它可以對空間中的圖元進行排序來保證渲染圖元的順序是按照由后至前進行的,換句話說,Z值最小的物體總是最后被渲染。當然還有其他的算法可以完成這個功能,例如著名的畫家算法,但是它與BSP比較起來速度太慢了,這是因為BSP通常對圖元排序是預先計算好的而不是在運行時進行計算。從某種意義上說BSP技術實際上是畫家算法的擴展,正如同BSP技術的原始設計一樣,畫家算法也是使用由后至前的順序對場景中的物體進行渲染。但是畫家算法有以下的缺點:
l 如果一個物體從另一個物體中穿過時它不能被正確的渲染;
l 在每一幀對被渲染的物體進行排序是非常困難的,同時運算的代價非常大;
l 它無法管理循環覆蓋的情況,如圖所示

 BSP技術詳解 1-------------有圖有真相 - 磚頭不離身 - 磚頭不離身

          圖6.1

BSP原理
建立BSP Trees的最初想法是獲得一個圖元的集合,這個集合是場景的一部分,然后分割這個圖元集合為更小的子集合,這里必須注意子集合必須為“凸多邊形”。這意味着子集合中任一個多邊形都位於相同集合中其它多邊形的“前面”。是不是有點難以理解呢,舉一個例子,如果多邊形A的每一個頂點都位於由多邊形B所組成的一個面的正面,那么可以說多邊形A位於多邊形B的“前面”,參考左圖。我們可以想象一下,一個盒子是由6個面組成的,如果所有的面都朝向盒子的內部,那么我們可以說盒子是一個“凸多邊形”,如果不是都朝向盒子的內部,那么盒子就不是“凸多邊形”。

 BSP技術詳解 1-------------有圖有真相 - 磚頭不離身 - 磚頭不離身

圖6.2

下面讓我們看一下如何確定一個圖元集合是否是一個“凸多邊形”,偽算法如下:
l 函數CLASSIFY-POINT
l 參數:
l Polygon – 確定一個3D空間中點相對位置的參考多邊形。
l Point – 待確定的3D空間中的點。
l 返回值:
l 點位於多邊形的哪一邊。
l 功能:
l 確定一個點位於被多邊形定義的面的哪一邊。

CLASSIFY-POINT (Polygon, Point)
1 Sidevalue = Polygon.Normal * Point
2 if (Sidevalue == Polygon.Distance)
3 then return COINCIDING
4 else if (Sidevalue < Polygon.Distance)
5 then return BEHIND
6 else return INFRONT

l 函數 POLYGON-INFRONT
l 參數:
l Polygon1 – 用來確定其它多邊形是否在其“前面”的多邊形。
l Polygon2 – 檢測是否在第一個多邊形“前面”的多邊形。
l 返回值:
l 第二個多邊形是否在第一個多邊形的“前面”。
l 功能:
l 檢測第二個多邊形的每一個頂點是否在第一個多邊形的“前面”。

POLYGON-INFRONT (Polygon1, Polygon2)
1 for each point p in Polygon2
2 if (CLASSIFY-POINT (Polygon1, p) <> INFRONT)
3 then return false
4 return true

l 函數 IS-CONVEX-SET
l 參數:
l PolygonSet – 用來檢測是否為“凸多邊形”的圖元集合。
l 返回值:
l 集合是否為“凸多邊形”。
l 功能:
l 相對於集合中的其它多邊形檢查每一個多邊形,看是否位於其它多邊形的“前面”,如果有任意兩個多邊形不滿足這個規則,那么這個集合不為“凸多邊形”。

IS-CONVEX-SET (PolygonSet)
1 for i = 0 to PolygonSet.Length ()
2 for j = 0 to PolygonSet.Length ()
3 if(i != j && not POLYGON-INFRONT(PolygonSet[i], PolygonSet[j]))
4 then return false
5 return true
 
在函數POLYGON-INFRONT中並沒有進行對稱的比較,這意味着如果多邊形A位於多邊形B的“前面”你並不能想當然的認為多邊形B一定位於多邊形B的“前面”。下面的例子簡單的顯示了這一點。

       BSP技術詳解 1-------------有圖有真相 - 磚頭不離身 - 磚頭不離身

圖6.3

在圖6.3中我們可以看到多邊形1位於多邊形2的“前面”,這是因為頂點p3、p4位於多邊形2的“前面”,而多邊形2卻沒有位於多邊形1的“前面”,因為頂點p2位於多邊形1的“后面”。

對於一個BSP層次樹來說可以用下面結構來定義:
class BSPTree
{
BSPTreeNode RootNode // 樹的根節點
}
class BSPTreeNode
{
BSPTree Tree // 接點所屬的層次樹
BSPTreePolygon Divider // 位於兩個子樹之間的多邊形
BSPTreeNode *RightChild // 節點的右子樹
BSPTreeNode *LeftChild // 節點的左子樹
BSPTreePolygon PolygonSet[] // 節點中的多邊形集合
}
class BSPTreePolygon
{
3DVector Point1 // 多邊形的頂點1
3DVector Point3 // 多邊形的頂點2
3DVector Point3 // 多邊形的頂點3
}

現在你可以看見每一個多邊形由3個頂點來定義,這是因為硬件加速卡使用三角形來對多邊形進行渲染。將多邊形集合分割為更小的子集合有很多方法,例如你可以任意選擇空間中的一個面然后用它來對空間中的多邊形進行分割,把位於分割面正面的多邊形保存到右子樹中而位於反面的多邊形保存到左子樹中。使用這個方法的缺點非常明顯,那就是如果想選擇一個將空間中的多邊形分割為兩個相等的子集合的面非常困難,這是因為在場景中有無數個可選擇的面。如何在集合中選擇一個最佳的分割面呢?下面我將對這個問題給出一個比較適當的解決方案。
我們現在已經有了一個函數POLYGON-INFRONT,它的功能是確定一個多邊形是否位於其它多邊形的正面。現在我們要做的是修改這個函數,使它也能夠確定一個多邊形是否橫跨過其它多邊形定義的分割面。算法如下:
l 函數 CALCULATE-SIDE
l 參數 :
l Polygon1 – 確定其它多邊形相對位置的多邊形。
l Polygon2 – 確定相對位置的多邊形。
l 返回值:
l 多邊形2位於多邊形1的哪一邊
l 功能:
l 通過第一個多邊形對第二個多邊形上的每一個頂點進行檢測。如果所有的頂點位於第二個多邊形的正面,那么多邊形2被認為位於多邊形1的“前面”。如果第二個多邊形的所有頂點都位於第一個多邊形的反面,那么多邊形2被認為位於多邊形1的“后面”。如果第二個多邊形的所有頂點位於第一個多邊形之上,那么多邊形2被認為位於多邊形1的內部。最后一種可能是所有的頂點即位於正面有位於反面,那么多邊形2被認為橫跨過多邊形1。

CALCULATE-SIDE (Polygon1, Polygon2)
1 NumPositive = 0, NumNegative = 0
2 for each point p in Polygon2
3 if (CLASSIFY-POINT (Polygon1, p) = INFRONT)
4 then NumPositive = NumPositive + 1
5 if (CLASSIFY-POINT (Polygon1, p) = BEHIND)
6 then NumNegative = NumNegative + 1
7 if (NumPositive > 0 && NumNegative = 0)
8 then return INFRONT
9 else if(NumPositive = 0 && NumNegative > 0)
10 then return BEHIND
11 else if(NumPositive = 0 && NumNegative = 0)
12 then return COINCIDING
13 else return SPANNING

上面的算法也給我們解答了一個問題,當一個多邊形橫跨過分割面時如何進行處理,上面的算法中將多邊形分割為兩個多邊形,這樣就解決了畫家算法中的兩個問題:循環覆蓋和多邊形相交。下面的圖形顯示了多邊形如何進行分割的。

             BSP技術詳解 1-------------有圖有真相 - 磚頭不離身 - 磚頭不離身

圖6.4

 如圖6.4所示,多邊形1為分割面,而多邊形2橫跨過多邊形1,如圖右邊所示,多邊形被分割為2、3兩部分,多邊形2位於分割面的“前面”而多邊形3位於分割面的“后面”。
當建立一個BSP樹時,首先需要確定的問題是如何保證二叉樹的平衡,這意味着對於每一個葉節點的分割深度而言不能有太大的差異,同時每一個節點的左、右子樹需要限制分割的次數。這是因為每一次的分割都會產生新的多邊形,如果在建立BSP樹時產生太多的多邊形的話,在圖形加速卡對場景渲染時會加重渲染器的負擔,從而降低幀速。同時一個不平衡的二叉樹在進行遍歷時會耗費許多無謂的時間。因此我們需要確定一個合理的分割次數以便於獲得一個較為平衡的二叉樹,同時可以減少新多邊形的產生。下面的代碼顯示了如何通過循環多邊形集合來獲得最佳的分割多邊形。

l 函數 CHOOSE-DIVIDING-POLYGON
l 參數:
l PolygonSet – 用於查找最佳分割面的多邊形集合。
l 返回值:
l 最佳的分割多邊形。
l 功能:
l 對指定的多邊形集合進行搜索,返回將其分割為最佳子集合的多邊形。如果指定的集合是一個“凸多邊形”則返回。

CHOOSE-DIVIDING-POLYGON (PolygonSet)
1 if (IS-CONVEX-SET (PolygonSet))
2 then return NOPOLYGON
3 MinRelation = MINIMUMRELATION
4 BestPolygon = NOPOLYGON
5 LeastSplits = INFINITY
6 BestRelation = 0

l 循環查找集合的最佳分割面。
7 while(BestPolygon = NOPOLYGON)
8 for each 多邊形P1 in PolygonSet
9 if (多邊形P1在二叉樹建立過程中沒有作為分割面)

l 計算被當前多邊形定義的分割面的正面、反面和橫跨過分割面的多邊形的數量。
10 NumPositive = 0, NumNegative = 0, NumSpanning = 0
11 for each 多邊形P2 in PolygonSet except P1
12 value = CALCULATE-SIDE(P1, P2)
13 if(value = INFRONT)
14 NumPositive = NumPositive + 1
15 else if(value = BEHIND)
16 NumNegative = NumNegative + 1
17 else if(value = SPANNING)
18 NumSpanning = NumSpanning + 1

l 計算被當前多邊形分割的兩個子集合的多邊形數量的比值。
19 if (NumPositive < NumNegative)
20 Relation = NumPositive / NumNegative
21 else
22 Relation = NumNegative / NumPositive

l 比較由當前多邊形獲得的結果。如果當前多邊形分割了較少的多邊形同時分割后的子集合比值可以接受的話,那么保存當前的多邊形為新的候選分割面。
l 如果當前多邊形和最佳分割面一樣分割了相同數量的多邊形而分割后的子集合比值更大的話,將當前多邊形作為新的候選分割面。

23 if (Relation > MinRelation &&
(NumSpanning < LeastSplits ||
(NumSpanning = LeastSplits &&
Relation > BestRelation))
24 BestPolygon = P1
25 LeastSplits = NumSpanning
26 BestRelation = Relation

l 通過除以一個預先定義的常量來減少可接受的最小比值。
27 MinRelation = MinRelation / MINRELATIONSCALE
28 return BestPolygon

算法分析
對於上面的函數來說,根據場景數據大小的不同它可能花費很長一段時間。常量MINRELATIONSCALE用來確定在每次循環時所分割的子集合多邊形數量的比值每次減少多少,為什么要使用這個常量呢,考慮一下,對於給定的MinRelation如果我們找不到最佳的分割面,通過除以這個常量將比值減少來重新進行循環查找,這樣可以防止死循環的出現,因此當這個比值足夠小時我們必定可以獲得可接受的最佳結果。最壞的事情是我們有一個包含N個多邊形的非“凸”集合,分割多邊形將集合分割為一個包含N-1個多邊形的部分和一個包含1個多邊形的部分。這個結果只有在最小比值小於1/(n-1)才是可以接受的(參考算法的19-23行)。這意味着MinRelation /MINRELATIONSCALEi < 1/(n-1),這里i是循環重復的次數。讓我們假設MinRelation的初始化值為1,由於比值永遠為0-1之間的值因此這是最可能的值(參考算法的19-22行)。我們有
1 / MINRELATIONSCALEi < 1/(n-1)
1 < MINRELATIONSCALEi/(n-1)
(n-1) < MINRELATIONSCALEi
logMINRELATIONSCALE (n-1) < i
這里的i沒有上邊界,但是因為i非常接近於logMINRELATIONSCALE (n-1),我們可以簡單的假設兩者是相等的。另外我們也假設MINRELATIONSCALE永遠大於或等於2,因此我們可以有
logMINRELATIONSCALE (n-1) = i 
MINRELATIONSCALE >= 2
i = logMINRELATIONSCALE (n-1) < lg(n-1) = O(lg n)
在循環的內部,對多邊形的集合需要重復進行兩次循環,因此對我們來說最壞的情況下這個算法的復雜度為O(n2lg n),而通常情況下這個算法的復雜度接近於O(n2)。
在函數CHOOSE-DIVIDING-POLYGON的循環中看起來如果不發生什么事情的話好象永遠不會停止,但是這不會發生,這是因為如果多邊形集合為非“凸”集合的話總能找到一個多邊形來把集合分割為兩個子集合。CHOOSE-DIVIDING-POLYGON函數總是選擇分割集合的多邊形數量最少的多邊形,為了防止選擇並不分割集合的多邊形,分割后的子集合的多邊形數量之比必須大於預先定義的值。為了更好的理解我上面所講解的內容,下面我將舉一個例子來說明如何選擇一個多邊形對一個很少數量多邊形的集合進行分割。

       BSP技術詳解 1-------------有圖有真相 - 磚頭不離身 - 磚頭不離身

圖6.5

在上面的例子中無論你選擇多邊形1、6、7還是多邊形8進行渲染時都不會分割任何其它的多邊形,換句話說也就是所有的其它多邊形都位於這些多邊形的“正面”。

關於分割時選擇產生多邊形最少的分割面另外一個不太好的原因是大多數時候它所產生的層次樹通常是不平衡的,而一個平衡的層次樹在運行的時候通常比不平衡的層次樹性能更好。
當獲得最佳的分割面后伴隨着必然產生一些被分割的多邊形,如何對被分割的多邊形進行處理呢,這里有兩個方法:
1. 建立一個帶葉節點的二叉樹,這意味着每一個多邊形將被放在葉節點中,因此每一個被分割的多邊形也將被分開放在二叉樹的一邊。
2. 另外一個方法是將被分割的多邊形保存到公共節點中,對每一個子樹重復這個過程直到每一個葉節點都包含了一個“凸”多邊形集合為止。
產生帶葉節點的BSP樹的算法如下:
l 函數GENERATE-BSP-TREE
l 參數:
l Node – 欲建立的類型為BSPTreeNode的子樹。
l PolygonSet – 建立BSP-tree的多邊形集合。
l 返回值:
l 保存到輸入的父節點中的BSP-tree。
l 功能:
l 對一個多邊形集合產生一個BSP-tree。

GENERATE-BSP-TREE (Node, PolygonSet)
1 if (IS-CONVEX-SET (PolygonSet))
2 Tree = BSPTreeNode (PolygonSet)
3 Divider = CHOOSE-DIVIDING-POLYGON (PolygonSet)
4 PositiveSet = {}
5 NegativeSet = {}
6 for each polygon P1 in PolygonSet
7 value = CALCULATE-SIDE (Divider, P1)
8 if(value = INFRONT)
9 PositiveSet = PositiveSet U P1
10 else if (value = BEHIND)
11 NegativeSet = NegativeSet U P1
12 else if (value = SPANNING)
13 Split_Polygon10 (P1, Divider, Front, Back)
14 PositiveSet = PositiveSet U Front
15 NegativeSet = NegativeSet U Back
16 GENERATE-BSP-TREE (Tree.RightChild, PositiveSet)
17 GENERATE-BSP-TREE (Tree.LeftChild, NegativeSet)

算法分析
函數CHOOSE-DIVIDING-POLYGON的時間復雜度為O(n2 lg n),除非出現遞歸調用否則它將控制其它的函數,如果我們假設對多邊形集合的分割是比較公平的話,那么我們可以通過公式來對函數GENERATE-BSP-TREE的復雜度進行表達:
T(n) = 2T(n/2) + O(n2 lg n)
通過公式我們可以知道這個函數的復雜度為Q (n2 lg n)。這里n為輸入的多邊形集合的多邊形數量。
下面我要用一個例子來演示如何產生一個BSP-tree。下面的結構是一個多邊形的原始集合,為了表示方便對每一個多邊形都進行了編號,這個多邊形集合將被分割為一個BSP-tree。

 

              BSP技術詳解 1-------------有圖有真相 - 磚頭不離身 - 磚頭不離身
    圖6.6

為了能夠運行這個算法我們必須對常量MINIMUMRELATION和MINRELATIONSCALE進行賦值,在實際運行中我們發現當MINIMUMRELATION=0.8而MINRELATIONSCALE=2時可以獲得比較好的結果。但是你也可以對這些數值進行試驗來比較一下,通常當常數MINIMUMRELATION比較大時獲得的層次樹會比較平衡但同時分割產生的多邊形也會大量增加。在上圖顯示的多邊形集合並不是一個“凸”的,因此首先我們需要選擇一個合適的分割面。在快速的對上面的結構進行一下瀏覽后我們可以知道多邊形(1、2、6、22、28)不能被用來作為分割面,這是因為它們定義了包含所有多邊形集合的外形。但是其它的多邊形都可以作為候選的分割面。分割產生的多邊形最少同時分割為兩個子集合的多邊形數目之比為最佳的多邊形是16與17,它們位於同一條直線上同時並不會分割任何的多邊形。而分割后的子集合的多邊形數目也是一樣的,都是“正面”為13而“反面”為15。讓我們選擇多邊形16作為分割面,那么分割后的的結構如下:

 

                  BSP技術詳解 1-------------有圖有真相 - 磚頭不離身 - 磚頭不離身          

圖6.7

現在從圖6.7我們可以看到無論是左子樹還是右子樹都沒有包含“凸”多邊形集合,因此需要對兩個子樹繼續進行分割。
在左子樹中多邊形1、2、6、7作為多邊形集合的“凸邊”不能用做分割面,而多邊形4、10在同一條直線上同時沒有分割任何多邊形,而分割后的多邊形子集合:“正面”為8而“反面”為7非常的平衡,我們選擇多邊形4為分割面。
在右子樹中多邊形16、17、22、23、28不能作為分割面,而多邊形18、19、26、27雖然沒有分割任何多邊形但是分割后的多邊形子集合:“正面”為11而“反面”為3,3/11這個比值小於最小比值0.5因此我們需要尋找其它更適合的多邊形。多邊形20、21、24、25都只分割了一個多邊形,但是多邊形21看起來分割后的結果更合理,在分割過多邊形22后多邊形子集合的結果為:“正面”為8而“反面”為6。
下圖顯示了操作后的結果:

                  BSP技術詳解 1-------------有圖有真相 - 磚頭不離身 - 磚頭不離身

圖6.8

在圖中每一個子集合還不是一個“凸”集合,因此需要繼續進行分割,按照上面的法則對圖6.8所示的結構進行分割后,結果如下:

              BSP技術詳解 1-------------有圖有真相 - 磚頭不離身 - 磚頭不離身         

圖6.9

上圖顯示了最后的結果,這可能不是最優的結果但是我們對它進行處理所花費的時間並不太長。
渲染BSP
現在我們已經建立好一個BSP樹了,可以很容易對樹中的多邊形進行正確的渲染,而不會產生任何錯誤。下面的算法描述了如何對它進行渲染,這里我們假設函數IS-LEAF的功能為給定一個BSP節點如果為葉節點返回真否則返回假。
函數DRAW-BSP-TREE
參數:
l Node – 被渲染的節點。
l Position – 攝象機的位置。
l 返回值:
l None
l 功能:
l 渲染包含在節點及其子樹中的多邊形。

DRAW-BSP-TREE (Node, Position)
1 if (IS-LEAF(Node))
2 DRAW-POLYGONS (Node.PolygonSet)
3 return
 
l 計算攝象機包含在哪一個子樹中。
4 Side = CLASSIFY-POINT (Node.Divider, Position)

l 如果攝象機包含在右子樹中先渲染右子樹再渲染左子樹。
5 if (Side = INFRONT || Side = COINCIDING)
6 DRAW-BSP-TREE (Node.RightChild, Position)
7 DRAW-BSP-TREE (Node.LeftChild, Position)
 
l 否則先渲染左子樹。
8 else if(value = BEHIND)
9 DRAW-BSP-TREE (Node.LeftChild, Position)
10 DRAW-BSP-TREE (Node.RightChild, Position)
用這個方法進行渲染並沒有減少渲染到屏幕上的多邊形數量,由於一個場景可能包含成百上千個多邊形因此這個方法並不是很好的解決方案。通常情況下有大量的節點和多邊形並沒有處於攝象機的視野范圍之內,它們並不需要被渲染到屏幕上,如何查找這些不可見的節點和多邊形防止它們渲染到屏幕上呢,隱藏面剔除就是為了解決這個問題而提出一項技術,在下一節中我們將對這項技術進行詳細的闡述。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM