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)
用这个方法进行渲染并没有减少渲染到屏幕上的多边形数量,由于一个场景可能包含成百上千个多边形因此这个方法并不是很好的解决方案。通常情况下有大量的节点和多边形并没有处于摄象机的视野范围之内,它们并不需要被渲染到屏幕上,如何查找这些不可见的节点和多边形防止它们渲染到屏幕上呢,隐藏面剔除就是为了解决这个问题而提出一项技术,在下一节中我们将对这项技术进行详细的阐述。

posted @ 2012-07-08 20:42  einyboy  阅读(8802)  评论(0编辑  收藏  举报