Bsp分割算法簡述


來源:中國IT實驗室收集整理 作者:CC

 

BSP分割算法也是有不少文章可以借鑒的,就我目前能掌握的資料來看,泛泛而談者大有人在,實際去作的時候卻總是抓瞎。知道是什么永遠不如知道怎么做,BSP分割是BSP分析的基礎,雖然它很簡單,但是,如果連簡單的都不會做,又怎么能勝任復雜的工作呢?
    趁這段時間有空,遂埋頭鑽研BSP,一周之后,分割和自動Portal生成均已解決,遂做此文,希望能對初學者有所幫助,亦希望能拋磚引玉,眾位高手能不吝賜教。
    本文先就BSP中相對簡單的分割部分做一個簡單的介紹,自動Portal生成的資料正在整理,希望能盡快放出。
    BSP的基本原理
    試想我們生活的空間,肯定是由為數眾多的天花板、牆壁和地板組成,對於每一個“板”,都將空間分為“板前”和“板后”兩個部分。已知人的位置,就可根據人在“板前”還是在“板后”,知道人所能看到的物體的遮擋順序(e.g.如果人在板前,則板前的物體遮擋所有板后的物體)。
    BSP者,原理很簡單:它試圖將所有的板(在BSP中叫做平面)組織成一棵樹,每個平面均將它所在的空間分割為前后兩個部分,這兩個部分又分別被另外的平面分割成更小的空間……直到最后,按照前面所說的算法,確定每一個房間(在BSP中叫做葉子)相對於眼睛的遮擋順序。
    這是一個非常標准的二分法,僅按照“前”和“后”兩個邏輯上的概念來切分空間,這使得它在以“房間”為單位組成的室內場景里是不二之選。為什么?請接着看:
    在判斷遮擋順序的時候,BSP空間的算法極為簡單:只需要從樹根開始,簡單判斷人的位置與所有平面的前后關系:前則正子樹(在平面“前”方的空間)在前,負子樹(在平面“后”方的空間)在后;后則正子樹在后,負子樹在前。以此遞歸到葉子(葉子總是一個房間),就可以確定人處於哪一個房間之中、其他房間的遮擋關系如何。
    這個其實很簡單:因為所有的平面均將其所處的空間分為前后兩個部分,所以,每一個房間,均是由若干平面的“前”“后”來決定的,通過人與這些平面前后關系的判斷,自然而然就可以直接定位到所需的房間之中了。這就是BSP算法的特別之處。

    如圖:空間ABC由A、B、C三個獨立的房間組成,首先,分割平面1將空間分成了平面正向的A房間和平面負向的BC空間,BC空間被2緊接着分割為平面2正向的C房間和負向的B房間。注意這里平面的方向一般由牆壁面向的方向而定。
    如果有一個人處於C房間內,那么如何判斷所有房間的遮擋順序呢?從樹根開始,由於人處於平面1的“后”面,所以,BC空間應該先於A房間(后:先負后正),然后,由於人處於分割平面2的“前”面,所以,C房間應該先於B房間(前:先正后負)。這樣,整個房間離人由近到遠的順序就可以確定了:C-B-A。僅需要通過兩次平面的前后判斷(總共六次乘法、兩次加法、兩次大小判斷),就可以確定空間的先后順序,算法的威力可見一斑!
    BSP分割的目標是將空間划分為一個個葉凸體,也就是一個凸面體。一個個凸面體才有排序的可能,很難想象一個非凸面體在空間中如何排序。如圖左:從箭頭方向看過去,到底凹多面體A是在B的前面?還是B在凹多面體A的前面?而如果是右邊的,兩個凸多面體,情況就不一樣了,A和B方向的前后,根據視點的位置永遠是唯一的。這就是BSP的優勢,只需要知道視點的位置,空間所有凸體的位置順序都可以馬上確定,但如果是凹體,對不起,那就確定不了了,所以,BSP划分空間結構化面的結果必然是一個個凸面體。


    這里面唯一想強調的一點是,如果您分析過Quake3的BSP格式,那么您會發現過去有時候一個房間會被幾個柱子分割得亂七八糟,只是為了少渲染幾個面。現在大不必這么興師動眾,一個房間就留外面的6個結構化面,柱子什么的只算作細節Mesh,不參與分割,這樣產生的結果,與Portal篩選結合之后,效率未必就差。而且,結構更符合邏輯,在以后自動路點和路徑計算的時候,會有一些優勢。想想看,被一個很不規則的柱子(或箱子等其他物體)划分得亂七八糟的空間,一個房間就有很多個葉子,到底哪些葉子是人能走到的?哪些葉子是人走不到的?哪些葉子需要在AI中被考慮?哪些葉子可以排除?一個不以邏輯構成的空間,必然在邏輯的處理上要處處碰壁。所以,最好還是一個葉子就是一個房間、或者一個走廊;柱子、箱子啊什么的全都用細節Mesh,就可以了。
    注意,BSP划分出的凸體其實主要是為了后面BSP分析而進行的,而不是渲染。早先的時候硬件很糟糕,沒有Z緩沖,那時候省一個三角形比現在重要得多。現在?有時候寧可多畫一堆三角形也不會去浪費那個CPU資源進行三角形的逐個篩選。所以,盡量減少結構化面,使結構化面的房間組成凸體,但細節面把房間裝點成什么樣,那就無所謂了,即便細節面將這個空間又變成了一個凹體,也是無所謂的,如圖:



    由於是一個老算法了,因此BSP分割算法早已經不是什么神秘的東西,這個算法有很多例子,推薦《BSP技術詳解》(翻譯后的名稱如此),唯一的遺憾是這篇文章的偽代碼需要花點心思。另外,《3D游戲 卷2 動畫與高級實時渲染技術》所帶的FLY 3D引擎也有很完整的代碼,雖然整個看下來比偽代碼還難懂,但是每個函數基本上都還算清晰,也是一個難得的備選資料。
    當然,可能大部分人還是傾向於去看Quake和HL2的代碼。為了使自己加深印象,我所選擇的是自己從零開始,僅按照資料上的觀點和流程進行DIY,而沒有參考代碼。因為經常參考着、參考着就“拿來主義”了,雖然開發效率保證了,但是記性不清,一旦擴展起來,基本抓瞎^_^b。所以這次狠狠心,決定享受一次DIY的樂趣。
    准備工作:場景數據
    進入工作狀態,第一個問題是場景數據的配置。BSP的難度一定程度上不是算法本身帶來的,BSP算法很簡單也很明確,並沒有太多復雜的東西在里面。復雜的是大凡好的BSP都需要和編輯器結合起來,以進行Portal、Brush、Entity和Path Point諸如此類的定制,直接從3D Max導出一個Mesh然后就進行分析,這個從實踐上限制太多、意義不大,所以,與其說BSP分割很難,倒不如說是BSP的編輯器難做。記得一本老書上曾經說過,BSP編輯器的代碼是BSP分割算法的10倍有余,仔細想想,確實如此,而且只會有過之而無不及。
    在實踐中,我采用了《3D游戲》的方法,這個方法是,通過在3D Max中物體的名稱來區分一個物體的這些面是屬於“結構化面”(分割平面)、“細節面”(不參與分割的面)還是Entity。由於3D Max Script支持使用前綴將一組物體放入一個Array中,所以,使用一個簡單而明確的前綴是一個很好的思路,《3D游戲》使用了*、&這些符號,而我則使用了S(Split)、D(Detail)、E(Entity)。例如SBox01說明這個Box01的所有面均是結構化面,要參與BSP分割和分析,而DSphere01則說明這個Sphere01在BSP的分割和分析中將會被忽略。這中間的主要工作集中在3DMAX Script的撰寫(或者插件的撰寫),所以就不再多說了,對這個技術還比較生疏的,可以參考網上相關的內容。
    從3DMAX中讀出來Object后,其所有的頂點和面索引都已知了,將所有頂點組織成一個頂點表,所有的結構化三角形組織成一個結構化三角形表(這里的三角形是指頂點索引),這個比較簡單,應該不是問題。
    數據進入我們的程序,第一件事情就是要首先計算出所有的平面,因為不同的結構化面可能共用一個平面,所以,這里先需要計算出所有的平面並在平面和結構化面中建立關系,以防止同一個平面被兩次以上使用,影響BSP二分邏輯的正確性。D3DX給出了專門的函數D3DXPlaneFromPoints,可以很方便地從一個三角形產生出一個平面來。一個新的平面算出來后,檢查一下這個平面是否已經生成過了,如果沒有,就算作一個新平面並記錄其ID,否則就要舍棄這個新平面,轉而采用原有平面的ID。直到最后,為所有的結構化三角形給出其對應的平面ID。這中間注意一下D3DX的平面公式是ax+by+cz+d=0,用的是+d,不是-d,在之后的計算中需要注意。
    准備好頂點表、結構化三角形表和平面表之后,分割就可以正式開始了。相對於3DMax Script和插件而言,BSP分割的算法本身容易得讓人崩潰,不多說了,下面開始!
    BSP分割
    首先,自然是要先產生一個根節點,並把所有的頂點表、結構化三角形表和平面表一股腦塞進這個根節點中咯。
    然后,分割的流程大抵如下:
    1 遍歷當前節點的所有備選平面,尋找一個合適的分割平面。
    2 如果找不到合適的分割平面,這個節點是一個葉子,Return。
    3 如果找到了,Mark這個平面已經被使用過。
    4 New兩個新節點,一個為正向節點,一個為負向節點,掛接到本節點下。
    5 遍歷所有結構化面。
    6 如果結構化面在分割平面的:
    正向:將這個結構化面和結構化面所對應的平面放入到正向節點。
    負向,放入到負向節點。
    如果結構化面被分割平面分割,則分割此三角形,並將分割后的結果放入相應的子節點。
    (注意,這一步當發現結構化面所對應的平面已經被Mark的時候,就只放結構化面,不放分割平面了,以防止同一個平面用於分割兩個以上空間,違反BSP空間二分邏輯的唯一性)
    7 遍歷所有細節面。細節面的處理與結構化面類似,只不過這里不用考慮到細節面對應的平面問題,更簡單。
    8 遍歷完畢,由於所有的結構化三角形、平面和細節面已經轉移到兩個子節點中了,因此從本節點中解掉所有的結構化三角形、平面和細節面的引用。節點所需保留的數據只需要是分割平面和兩個下級節點的指針即可。

 

 9 對兩個子節點,分別從1開始遞歸執行。
    這樣,等一切結束的時候,就是一棵完整的BSP樹了,所有的節點中僅保留有節點的分割平面和兩個下級節點,而渲染嚴重相關的結構化三角形和細節面則全都在葉子里。最后,只需要順根遞歸,將所有的節點組織成節點表就可以了。我在這里分別是將節點組織成了節點表,將葉子組織成了葉子表。您也可以通過為節點加一個Is Leaf屬性來將它們統一放到一個節點表里。
    現在,面臨最主要的問題是,在所有的結構化面中,如何尋找一個分割平面?
    首先,分割平面必須是對於凹多面體而言的,已經形成了凸多面體的空間就不必要分割了。對於一個凹體而言,分割平面必須在平面的正負方向均出現三角形。如此遞歸分割下去,就能保證將空間最終分割成大量凸多面體集合。如下圖左,1、4在平面的一方沒有出現三角形,應被舍棄,2、3均可以作為備選的分割平面:



    分割平面的選取是一個比較“笨”的辦法,可偷懶的機會不多,只能是for each的判斷。對於每一個平面,算出一個用於判斷的值,在所有值中最大(或者最小,視算法而定)的那個平面就是最佳分割平面。最簡單的,永遠只選取第一個結構化三角形的平面分割,但是這樣分割下來的空間會慘不忍睹。分割出來的結果最好是讓一棵樹平衡的那個做法。因為平衡二叉樹的操作比不平衡二叉樹要快,冗余度要小很多。
    計算出最優平衡二叉樹幾乎是不可能的,但在近似層面上保證二叉樹盡可能平衡的算法很多,《3D游戲》采用的是:
    P=分割后處於正向的三角形數
    N=分割后處於負向的三角形數
    S=被從中切開的三角形數
    Value = P – N + 8 × S
    這個值最小的那個就是最好的平面。也就是說,正負向三角形數量最接近、且切開三角形最少的那個平面就是最好的分割平面。
    如上圖右,7、3、5均可以作為分割平面,但是,非常明顯:3就比7和5要好得多,因為其正負方向的三角形數最接近,且沒有切割任何三角形。
    這個算法在實際使用上,並不一定能生成最優樹,但它簡單而且直觀。沒有最好的算法,只有最適合的算法,算法的選擇不是唯一的,基本上應該根據空間的特點進行,所以這里就不再多說了。總之,能盡量分割出平衡二叉樹的方法就是好的方法。
    BSP分割完后,產生出來的節點表和葉子表,其中,節點表構成了BSP樹的樹干,葉子表存有所有的結構化三角形和細節Mesh,將被用作之后分析的基本數據源。而在所有的分析中,首先應該進行的就是Portal的分析,Portal分析完畢后,PVS等分析才有可能。而Portal和PVS,則是BSP空間分割最有魅力的兩個部分,在最新的商業引擎中,仍能看到他們的影子,而且,比起90年代初,只會有過之而無不及……
    補充和校正
    關於分割一個三角形
    上篇文檔寫完后,做了一個比較復雜的場景,進行分割后發現原算法的一些問題,在此做一個補充。根據此補充,原文檔將被刪掉重新修改完后再發,對各位讀者造成的不便,希望大家能夠見諒。
    在上篇文章里談到的分割算法里有關被分割面的處理,采取的是直接將被分割面正負都放的策略。當時認為這只會對AABB的計算產生影響,所以也就堂而皇之這么寫上去了。這個算法雖然簡單,但是在之后的Portal處理時會面臨很多困難,這一點也是我開始沒有考慮到的。
    在將場景變得復雜之后,這個問題就越發顯現出來:在有些葉子,將會僅包括若干被分割的共享三角形,且這些三角形根本無法構成封閉空間。然而,這些葉子卻被送入了Portal計算,最后出來的Portal非常詭異,甚至包括了在同一面上的若干個Portal。
    用更多的思路更改Portal算法,倒不如從根本上將空間分割得更為合理,也就是采取標准的做法:將被分割三角形分割開,分割為多個三角形,分別放入相應空間。
    其實這個算法很簡單,一個三角形如果被一個平面分割,直觀上看,有且只有兩種情況:一種是在正負各生成一個三角形;另一個是在一側有一個三角形,另一側有兩個三角形。直觀上說,無論哪種情況,關鍵算法流程都是:
    順序訪問原三角形的邊,設邊的第一個頂點是v0,第二個頂點是v1。
    如果這個邊的兩個頂點均在平面一側,則兩個頂點算入平面相應一側的新多邊形。
    如果有一個點在平面上,則這個點如果是這個邊的第一個頂點,應該在平面兩側的新多邊形中都要放。如果是第二個頂點,則需要判斷第一個頂點在平面的哪一側,並將之放入相應空間(只放一次)。可參考下圖(左)來進行理解。
    如果這個邊被平面切割,則:首先算出來切割后的頂點vip,注意這里需要根據頂點格式分割,法線、紋理坐標均應分割。這時,同樣是判斷第一個頂點在平面哪一側,根據此,把v0、vip、v1按照相應順序組合,分別放到兩側的多邊形中(在這過程中,vip會兩側都放)。



    這個算法有幾個需要注意的地方:
    首先,為了生成頂點順序與原三角形一致的三角形(即順時針三角形生成后仍是順時針,逆時針三角形生成后仍是逆時針),我們必須要按照相應的順序遍歷原三角形的邊:v0-v1、v1-v2、v2-v0,只有順序訪問原三角形的邊才能保證生成后的三角形的順序。如果一開始的順序就很詭異,那么最后生成出來的三角形順序將很難保證,代碼也會很不直觀。
    第二,分割出來兩側的是多邊形而不是三角形,這需要分開判斷,如果多邊形的頂點數量是3,說明這一側生成的是一個三角形,那么就好辦了,直接使用這個三角形即可。如果是4,說明是一個四邊形。如果設四邊形頂點順序是v0 v1 v2 v3那么,組成這個四邊形的兩個三角形分別應該是v0-v1-v2和v0-v2-v3。具體的推導過程就不說了,如果覺得難於理解,可以參考下面的圖,就容易明白了。其中,OV是指原始三角形的三個頂點,V是分割后的這個四邊形的四個頂點,請注意順序。


    第三,注意法線的切分,如果兩個頂點的法線方向正好相反(當然,這是特殊情況),那么最后生成的新頂點的法線會是0!在這種情況下,法線需要單獨作一下處理,我的處理是將整個面的法線賦給這個頂點,當然,您也可能有更好的方式。
    第四,在分割中會生成新的頂點和面,所以最后BSP的頂點數和面數經常會超過在模型原始數據里的頂點數和面數。但現在由於沒有被兩個葉子共同共享的三角形了,所以,一個葉子中的三角形可以統一建一張IB,一次渲染了,速度當然會比使用共享面要快。
    切分算法並不是唯一的,正如BSP分割的方式也並不唯一一樣,關鍵還是選擇對自己最容易掌握,最有利的算法。


免責聲明!

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



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