第二节 隐藏面剔除
对不可见物体进行剔除是游戏行业为了满足提高画面渲染速度的要求而产生的一项技术,就是在硬件加速技术飞跃发展的今天,虽然现在已经可以完成许多在过去被认为是不可能实现的工作,但是对于隐藏面进行剔除仍是加速图形渲染的一项重要技术。通常当一个游戏运行的时候,它最少需要以每秒30帧的速度运行。在几年前这意味着如果每一帧你渲染的带纹理的多边形数量超过5000个就被认为是不可接受的,而现在几乎所有的商业显卡每一秒都可以渲染几千万个多边形。可是现在仍然需要使用隐藏面剔除这项技术,这是为什么呢?显而易见,对不可见物体渲染以后将会被可见物体遮挡住,这样做无谓的浪费了显卡的带宽,但是同时它也增加了场景的细节,使游戏画面看起来更加吸引人。现在的问题是多大程度上来剔除隐藏的多边形,象view frustum culling和portal渲染这样的技术来剔除一个不可见多边形是非常耗费时间的,用来去做这些计算的CPU时间可以用来完成其它诸如AI或碰撞检测这样的工作,因此开发一个隐藏面剔除算法必须注意到这一点。对于现在的游戏来说几乎没有一个是将每一个隐藏的多边形都进行剔除,而是剔除一个多边形的集合如一个节点或一个物体等等。对于一个单独的多边形它并不进行剔除,因此一个正确的隐藏面剔除方案是允许一定的重复渲染来适当的减少计算量。
当建立一个FPS游戏时进行隐藏面剔除最通常的方法是使用portal渲染。这项技术可以非常充分的利用BSP的优点,但是请注意portal技术并不仅仅只能用于BSP中。Portal技术还可以用来产生一些特效如镜子和监视器等等。
Portal渲染
在这里我将介绍一下portal技术的原理,通常对于一个室内场景来说它可以被描述为由一个个“洞口”相互连接的“房间”组成,这里“洞口”被称为portal而“房间”被称为sector,通常sector被定义为一个“凸”的“闭合”的多边形集合,定义中的“凸”阅读过前面的内容你应当已经能很好的理解了,而“闭合”是什么意思呢?它意味着在sector内部任意连接两个顶点做一条线段,这条线段不会和其它的多边形相交。换句话说如果你想在sector内部任意画一条线段通到sector的外部必定与组成sector的多边形相交。这也意味着连接每一个sector的“洞口”必须被一个组成portal的多边形所填充,而对于放置portal多边形来说你既可以手工放置也可以由程序自动产生,在我讲解这项技术之前我必须提醒一下,由于硬件加速Z缓冲的出现对sector必须为“凸”的限制已经消除,因此有许多游戏引擎已经不再遵守这个标准,但是在这里我还是要对过去的方法进行一下介绍。
一个portal引擎的基本方法是当你通过一个指定观察位置的可视平截体(view frustum)进行渲染时,如果一个portal出现在可视范围内,那么portal将对可视平截体进行剪切,这样与其相连的sector将会通过一个观察位置相同但已经改变过的可视平截体进行渲染。这是一个非常简单而且非常适合进行递归调用的方法,由于可视平截体被portal进行了精确的限制,因此被隐藏的物体可以很简单进行剔除。下面的例图显示了一个portal引擎中的可视平截体是如何被剪切的。
图6.10
在图6.10中观察者的位置位于V,而初始的可视平截体为F1,当它通过一个portal多边形P1后被P1剪切产生新的可视平截体F2,接着当它通过portal多边形P2、P3后F2被剪切为F3、F4,而F3通过P4后被剪切为F5而F4被剪切为F6。观察这个过程我们可以发现portal技术非常适合进行递归调用。
接着需要考虑的是如何对物体进行拣选剔除,通常对于所有的3D引擎来说都需要通过一系列步骤来加速这个处理过程,回忆一下前面讲解的内容,这个过程首先要计算出物体的“最大包围球”或是“最大包围盒”,它是包含了物体所有顶点的最小包围体,接着用包围体来和“可视平截体”每一个剪切面进行碰撞检测,如果包围体位于每一个剪切面的“反面”那么物体将不会进行渲染,下图显示了这个过程:
图6.11
图中物体1位于右剪切面的“正面”但位于左剪切面的“反面”因此它将不会被渲染,而物体2不仅位于左剪切面的“正面”而且有一部分位于右剪切面的“正面”因此将会被渲染出来。
Portal技术最初的想法是通过剪切多边形来保证只有物体可见的部分被渲染出来,也就是无效渲染的多边形数量为0。但是现在这种想法被认为不是很理想,因为它无谓的浪费了处理时间。但是由于一个多边形在递归循环过程中将被遍历多次因此我们需要知道在渲染场景时它是否已经被渲染过,一个较好的方法是使用一个帧数来标识这个多边形,这样可以很容易的来描述这个多边形在上一帧是否被渲染过。再看一下图6.10中最右边的墙,它同时通过F5和F6来进行渲染,通过对它进行标识我们可以知道它是否被渲染过,否则就会产生Z缓冲错误。
为了便于在portal引擎中进行渲染我们需要对“可视平截体”进行一下定义,一个“可视平截体”是一个保存了多个剪切面的结构,每一个剪切面的法线都将指向“可视平截体”的内部,因此在它内部将产生一个闭合的锥体。下面的算法显示了如何计算一个多边形是否包含在“可视平截体”的内部。在这个算法中我们使用了一个函数CLASSIFY-POINT,它使用一个剪切面和一个点作为输入参数。
l 函数INSIDE-FRUSTUM
l 参数:
l Frustum – 用于检测多边形是否位于其中的“可视平截体”。
l Polygon – 用于检测的多边形。
l 返回值:
l 多边形是否位于“可视平截体”的内部。
l 功能:
l 使用“可视平截体”的每一个剪切面来对多边形的每一个顶点进行检测,如果所有的顶点都位于所有剪切面的“正面”,那么多边形处于“可视平截体”的内部。
INSIDE-FRUSTUM (Frustum, Polygon)
1 for each point Pt in Polygon
2 Inside = true
3 for each plane Pl in Frustum
4 if (CLASSIFY-POINT (Pl, Pt) <> INFRONT)
5 Inside f false
6 if (Inside)
7 return true
8 return false
在一个portal引擎中它的主渲染函数可以简单的用下面的方法来实现。
函数RENDER-PORTAL-ENGINE
参数:
Sector – 观察者所处的sector。
ViewFrustum – 当前的“可视平截体”。
返回值:
None
功能:
渲染portal引擎中的多边形,场景被描述为由多个portal连接的sectors所组成。
RENDER-PORTAL-ENGINE (Sector, ViewFrustum)
1 for each polygon P1 in Sector
2 if (P1是一个portal and INSIDE-FRUSTUM (ViewFrustum, P1))
3 NewFrustum = CLIP-FRUSTUM (ViewFrustum, P1)
4 NewSector = 获得由当前sector通过portal P1相连接的sector
5 RENDER-PORTAL-ENGINE (NewSector, NewFrustum)
6 else if (P1任然没有被渲染)
7 draw P1
8 return
如何放置portal
正如我前面提到的在一个portal引擎中最大的问题就是如何放置portal,如果手工来放置它的话非常花费时间,同时要求地图的设计者有熟练的技巧。因此一个良好的自动放置portal的算法非常有必要,在这里我将要介绍一下关于这方面的几个解决方案,这些方案都使用了BSP。
我介绍的第一个解决方案是由瑞典DICE公司的Andreas Brinck提出的,它的原理非常简单,观察一下一个完整的BSP层次树,可以发现这样一个现象,对于每一个portal来说它必定与BSP层次树中由分割多边形定义的分割面位置相同,因此在相同的位置上我们可以在分割面之外建立一个portal多边形,portal多边形被初始化为一个矩形,这个矩形的大小将超过portal所处的BSP节点的“最大包围盒”的大小,接着将portal多边形放入包含它的节点所位于的子树中,当节点不是叶节点时,那么portal将继续被传送到节点的子树中,这样子节点中的分割面将对它进行分割,而当包含它的节点为叶节点时,它也会被节点中的多边形进行剪切,因为portal初始化的大小超过了节点的范围。当portal被分割后,被分割的两个部分会继续传送到最顶层的节点重复这个过程,而当portal不需要进行分割时,根据它所处于分割面的位置来放置到相应的子节点中,如果位于分割面的“正面”它将被放置在右子树中,而当它位于分割面的“反面”将被放置于左子树中。如果portal正好位于分割面之上它将同时放在左右子树中。
为了方便方便将所有的portal放置到BSP中我们需要定义如何对一个多边形进行分割,为了方便使用我们假设完成这个功能的函数为INTERSECTION-POINT,它将返回一个面与一个线段的交点。
l 函数CLIP-POLYGON
l 参数:
l Clipper – 去分割其它多边形的多边形或面。
l Polygon – 被分割的多边形。
l 返回值:
l 被分割后的两个部分。
l 功能:
l 由指定的分割面来分割多边形。如果多边形没有被分割将会返回一个空的多边形。
CLIP-POLYGON (Clipper, Polygon)
1 RightPart = {}
2 LeftPart = {}
3 for each point edge E in Polygon
4 Side1 = CLASSIFY-POINT (Clipper, E.Point1)
5 Side2 = CLASSIFY-POINT (Clipper, E.Point2)
6 if (Side1 <> Side2 and
Side1 <> COINCIDING and
Side2 <> COINCIDING)
7 Ip = INTERSECTION-POINT (Clipper, E)
8 if (Side1 = INFRONT)
9 RightPart = RightPart U E.Point1
10 RightPart = RightPart U Ip
11 LeftPart = LeftPart U Ip
12 LeftPart = LeftPart U E.Point2
13 if (Side1 = BEHIND)
14 LeftPart = LeftPart U E.Point1
15 LeftPart = LeftPart U Ip
16 RightPart = RightPart U Ip
17 RightPart = RightPart U E.Point2
18 else
19 if (Side1 = INFRONT or Side2 = INFRONT or
Side1 = COINCIDING and Side2 = COINCIDING)
20 RightPart = RightPart U E.Point1
21 RightPart = RightPart U E.Point2
22 if (Side1 = BEHIND or Side2 = BEHIND)
23 LeftPart = LeftPart U E.Point1
24 LeftPart = LeftPart U E.Point2
25 return (RightPart, LeftPart)
现在我们可以定义如何在一个BSP层次树中对portal进行分配了,在算法中portal被初始化为一个大于BSP根节点“最大包围盒”的多边形。
l 函数PLACE-PORTALS
l 参数:
l PortalPolygon – 放置到BSP中的多边形。
l Node – 我们当前遍历的节点。
l 返回值:
l None
l 功能:
l 放置一个portal多边形到BSP层次树中,如果需要的话对它进行剪切。这个函数将会产生一个节点将由portal连接而每一个节点将包含一个portal多边形列表的BSP层次树。
PLACE-PORTALS (PortalPolygon, Node)
1 if (IS-LEAF (Node))
l 将portal和节点中的每一个多边形进行检测。当portal所包含的多边形和由一个多边形定义的面相交时它将被这个面进行分割,分割后的两个部分将被重新传送到最顶层的节点中继续进行检测。
2 for (each polygon P2 in Node)
3 IsClipped = false
4 if (CALCULATE-SIDE (P2, PortalPolygon) = SPANNING)
5 IsClipped = true
6 (RightPart, LeftPart) = CLIP-POLYGON (P2, PortalPolygon)
7 PLACE-PORTALS (RightPart, RootNode)
8 PLACE-PORTALS (LeftPart, RootNode)
9 if (not IsClipped)
10 从当前节点中将portal的多边形剔除,因为它的位置和节点中一个多边形的位置相同。
l 参考下面的描述。
11 添加当前节点到portal多边形所连接的节点集合中。
12 else
13 if (当前节点的分割多边形没有放置在树中)
14 建立一个多边形P,它的大小将超过包含当前节点所有多边形的最大包围盒的范围,并且和分割多边形的位置相同。
15 PLACE-PORTALS (P, Node.LeftChild)
16 PLACE-PORTALS (P, Node.RightChild)
17 Side = CALCULATE-SIDE (Node.Divider, PortalPolygon)
18 if (Side = POSITIVE)
19 (RightPart, LeftPart) = CLIP-POLYGON(P2, PortalPolygon)
20 PLACE-PORTALS (RightPart, RootNode)
21 PLACE-PORTALS (LeftPart, RootNode)
22 if (Side = POSITIVE or COINCIDING)
23 PLACE-PORTALS (PortalPolygon, Node.RightChild)
24 if (Side = NEGATIVE or COINCIDING)
25 PLACE-PORTALS (PortalPolygon, Node.LeftChild)
下面我将对算法中的第10行进行一下解释,当剔除和当前节点中一个多边形位置相同的portal多边形部分将会产生什么样的结果。参考一下图6.12。
图6.12
在图6.12中一个portal已经到达了一个叶节点,图中灰色的区域是是portal多边形在遍历BSP树过程中被剔除的区域称它为1。而图中标注为2、3、4的亮灰色部分是和portal多边形位置相重叠的多边形,因此这一部分的portal多边形也将会被剔除,而剩下的部分5将被用来做为一个portal。
在上面的算法第一次看的话会觉的非常复杂,但实际上它非常简单和容易理解,最终的结果就是每一个portal将会在两个节点上结束而对于任一个portal来说它必定可以和一个portal之间相互可见。在下面我将用一个实际的例子来解释一下这个算法。
图6.13
对于图6.13中的结构,需要进行下面的几步:
第一步,portal多边形s1进入节点n1中。
在节点n1中portal多边形s1将会被分割,因为它的一部分和节点中间的一个多边形位置相同,因此portal多边形s1将会被分割为两部分分别为p1、p2,重叠的部分被剔除。
第二步,p1、p2进入节点s2中。
在节点s2中由于p1位于分割面s2的“正面”,因此将和分割面s2一起被送入到节点n2中,而p2由于位于分割面s2的“反面”,因此将和分割面s2一起被送入到节点s3中。由于p1、p2没有和分割面相交因此这一步没有出现分割操作。
第三步,p1、s2进入节点n2中。
在节点n2中n2被认可作为一个portal,因此在节点n1和n2中它不会再发生任何变化。而多边形p3的一部分将会被剔除,因为它的一部分和节点中间的一个多边形位置相同,在上一步中多边形s2也被送入到节点s3中,而在这里它又被称为p3。
第四步,p3和s3进入节点n3中。
在节点n3中多边形p3被认可为一个portal,而多边形s3的一部分因为和上面的原因一样被剔除,同时在这里它又被称为p4。
第五步,p2和p4进入节点s4中。
没有任何多边形要进行分割,因此多边形p2和p4将与s4一起被送入节点n4中,而s4将被单独送入节点n5中。
第六步,p2、p4和s4一起进入节点n4中。
无论是p2还是p4都不需要分割,但是由于s4和中间的一个多边形完全重叠因此将会被剔除。
第七步,结束,没有任何多边形进入节点n5中。
这个节点将没有portal,因为对于任何节点来说它都是不可见的,而从这个节点位置上将不会看见任何一个节点。
结果:
portal p1同时位于节点n1和n2中;
portal p2同时位于节点n1和n4中;
portal p1同时位于节点n2和n3中;
portal p1同时位于节点n3和n4中。
上面我所讲解的内容是建立一个简单的portal引擎所必备的功能,它能给我们提供一个较高的运行帧速。
PVS
一个portal引擎虽然能够提供许多非常好的特性,但是它的结构太复杂。当你使用portal技术来构建一个游戏引擎时你会发现它存在许多问题,最大的一个问题是在渲染场景的每一帧都需要进行可视性检测,这会产生大量的多边形剪切操作,在场景非常复杂的情况下,运算的费用会非常的高,因此需要寻找一种技术来对场景中可视性检测进行预计算而不是在运行期间进行计算。PVS(Potentially Visible Set)可视性集合,就是为了解决这个问题而出现的一项技术,可以通过对BSP中每一个叶节点设置一个PVS,这个PVS保存了从第一个叶节点开始看到的叶节点集合,它不仅可以用来帮助加速场景渲染,还可以用来加速场景中光照运算和进行网络优化。
PVS是在场景进行预渲染时计算出来的,每一个BSP的叶节点都保存一个可视节点的集合,当对场景进行渲染时,摄象机所在的叶节点将被渲染,同时保持在PVS中的叶节点也将会被渲染出来,这里需要一些算法来避免场景重复渲染,由于今天硬件加速卡的发展,它所提供的硬件Z缓冲的大小已经可以方便的解决这个问题。
计算PVS
如果要求PVS必须在BSP的叶节点之间进行标准的光线跟踪计算,来查找一下在一个叶节点中任一个点是否在其它叶节点中可见。为了加速光线跟踪的计算,在每一个叶节点中必须指出一些典型的位置点来避免无谓的计算,现在的问题就是如何放置这些典型的点。
对于一个portal引擎的portal来说,典型点可以沿着树的分割面放置,这是因为只有在两个portal是相互开放的情况下才可以进行可视化检测。如果位于一个叶节点中间的一个点被从另一个叶节点发射出来的光线认为是可见的话,那么这条光线必然通过连接两个节点的portal,参考下图:
图6.14
在图6.14中我们可以看见如果在一个节点中的一个点可以从其它节点看见,那么视线必然通过两个节点相互连通的区域。这是非常明显的,如果视线被物体阻断的话那么两点之间必定不可见。因此将典型点放置在两个节点之间所开放的区域是非常合适的,下面描述的算法将在一个BSP树上放置典型点。对于这个函数需要一组帮助函数来把点放置在一个节点上,它们是
l DISTRIBUTE-POINTS (Node) 这个函数将按照一定的间隔沿着给定节点的分割面来放置点,点的位置会处于节点包围盒的内部。它将返回一个点的集合,复杂度为O(xy),这里x为节点包围盒内分割面的宽度,而y为高度。
l CLEANUP-POINTS (Node, PointSet) 从点的集合中剔除不合格的点,这些点可能和节点中的一个多边形位置相同,也可能是位于节点包围盒的外部。函数的复杂度为O(np),这里n为节点中多边形的数量而p为集合中点的数量。
l 函数 DISTRIBUTE-SAMPLE-POINTS
l 参数:
l Node – 当前我们遍历的节点。
l PointSet – 放置到给定节点子树中的点的集合。
l 返回值:
l None
l 功能:
l 沿着给定节点的分割面放置点。它将按照分割面的位置来对输入的点进行检测,如果这些点和节点中的一个多边形位置相同,或者位于节点包围盒的外部那么将会被剔除。新产生的点将会被添加到左、右两个集合中,当一个点的集合遍历到一个叶节点时那么它就是这个叶节点的典型点。
DISTRIBUTE-SAMPLE-POINTS (Node, PointSet)
1 CLEANUP-POINTS (Node, PointSet)
2 if (IS-LEAF (Node))
3 设置点的集合为当前节点的典型点。
4 else
5 RightPart = NewPoints
6 NewPoints = DISTRIBUTE-POINTS (Node)
7 RightPart = NewPoints
8 LeftPart = NewPoints
9 for each point P in PointSet
10 Side = CLASSIFY-POINT (Node.Divider, P)
11 if (Side = COINCIDING)
12 RightPart = RightPart U P
13 LeftPart = LeftPart U P
14 if (Side = INFRONT)
15 RightPart = RightPart U P
16 if (Side = BEHIND)
17 LeftPart = LeftPart U P
18 DISTRIBUTE-SAMPLE-POINTS (Node.LeftChild, LeftPart)
19 DISTRIBUTE-SAMPLE-POINTS (Node.RightChild, RightPart)
算法分析
每一次调用这个函数的复杂度为O(np + xy)(参考函数CLEANUP-POINTS和
DISTRIBUTE-POINTS),为了计算完整的复杂度我们可以用下面的公式来表示(我们假设典型点的集合同时分布在两个集合之中):
T(n) = 2T(n/2) + O(np + xy)
这个函数第一次调用时将会从BSP树的根节点开始并会传入一个空的集合。换句话说它是按照下面的方法进行的,它是从被BSP树根节点所定义的分割面上的点开始的,由于一个面的大小没有限制因此也会有无限个典型点,因此就需要对典型点进行限制,这个现在就是根节点的包围盒。
在根节点上获得合格的典型点后需要把所有的点发送到根节点的两个子树中,当一个典型点的集合进入一个节点后会被分割为两个部分,一个部分包含了所有位于节点分割面的“正面”的点,而另一部分包含了所有位于节点分割面的“反面”的点。而位于分割面上的点将会同时放在两个集合中。接着“正面”集合将会被传入到右子树中而“反面”集合将会被传入到左子树中。重复这个过程直到进行到叶节点时结束,在这些操作后每一个叶节点将包含一个典型点的集合,这些点都位于节点所连通的地方。
如果我们现在在这个阶段对每一个节点进行光线跟踪那么是非常耗费时间的,但是如果我们知道叶节点是与哪一个节点相连的,那么这个过程将会变的简单,因为这样可以减少一些不必要的光线跟踪运算。查找相互连接的叶节点非常简单,可以通过检测每一个叶节点的典型点来查找,如果同时有两个节点共享一个点那么这两个节点必然是相互连通的,因为在遍历BSP树放置典型点的过程中,点不是没有放置到节点上就是同时放置在两个节点上。当我们知道哪些节点是相互连通的,就可以定义进行光线跟踪的算法了,不过首先我们需要定义一些帮助函数。
为了方便进行光线跟踪我们需要一些基本的光线跟踪函数,BSP树是非常适合进行光线跟踪的结构,因为大部分不可见的区域通过BSP树可以很简单的剔除掉,不用进行遍历,因此花费的时间也非常少。我们需要的函数如下:
POLYGON-IS-HIT (Polygon, Ray) 返回光线是否和多边形相交。
RAY-INTERSECTS-SOMETHING-IN-TREE (Node, Ray) 返回光线是否和节点中的子树相交。
INTERSECTS-SPHERE (Sphere, Ray) 返回光线是否和球体相交。
CREATE-RAY (Point1, Point2) 通过两个点来建立一条光线。
上面的函数RAY-INTERSECTS-SOMETHING-IN-TREE是一个非常有趣的函数,因为它显示了一些BSP树的优点,同时也显示了如何使用BSP树来加速光线跟踪的处理。它是一个递归函数,首先在树的根节点上使用,伪算法如下:
l 函数RAY-INTERSECTS-SOMETHING-IN-TREE
l 参数:
l Node – 用来进行跟踪的节点。
l Ray – 用来进行求交的光线。
l 返回值:
l 光线是否和节点中的物体相交。
l 功能:
l 检测光线是否与给定节点极其子树中的物体相交。
RAY-INTERSECTS-SOMETHING-IN-TREE (Node, Ray)
1 for each polygon P in Node
2 POLYGON-IS-HIT (P, Ray)
3 startSide = CLASSIFY-POINT (Ray.StartPoint, Node.Divider)
4 endSide = CLASSIFY-POINT (Node.EndPoint, Node.Divider)
l 如果光线和节点的分割面相交或和分割面的位置重叠,对节点的两个子节点进行检测。
5 if ((startSide = COINCIDING and endSide = COINCIDING) or
startSide <> endSide and startSide <> COINCIDING and
endSide <> COINCIDING)
6 if (RAY-INTERSECTS-SOMETHING-IN-TREE (Node.LeftChild, Ray))
7 return true
8 if (RAY-INTERSECTS-SOMETHING-IN-TREE (Node.RightChild, Ray))
9 return true
l 如果光线只位于分割面的“正面”对节点的右子树进行检测。在if语句中使用or操作符是因为光线的一个端点可能与分割面的位置重叠。
10 if (startSide = INFRONT or endSide = INFRONT)
11 if(RAY-INTERSECTS-SOMETHING-IN-TREE (Node.RightChild, Ray))
12 return true
l 如果光线只位于分割面的“反面”对节点的左子树进行检测。在if语句中使用or操作符是因为光线的一个端点可能与分割面的位置重叠。
13 if (startSide = BEHIND or endSide = BEHIND)
14 if (RAY-INTERSECTS-SOMETHING-IN-TREE (Node.LeftChild, Ray))
15 return true
l 光线没有和任何物体相交,返回到上一层。
16 return false
算法分析
最坏的情况是光线遍历了BSP树中每一个节点,这样光线将与节点中每一个单独的多边形进行检测,这时函数的复杂度将为O(n),这里n为BSP树中所有多边形的数量。一般情况下光线并不会检测树中的每一个节点,这样将会大大减少进行检测的多边形数量。最佳的情况是光线被限制在一个节点中,这时函数的复杂度接近于O(lg n).这取决于BSP树的结构大小。
l 函数CHECK-VISIBILITY
l 参数:
l Node1 – 开始处的节点。
l Node2 – 结束处的节点。
l 返回值:
l Node2是否可以被node1看见。
l 功能:
l 在两个叶节点的典型点之间进行跟踪,检测两个节点之间是否可见。
CHECK-VISIBILITY (Node1, Node2)
1 Visible = false
2 for each 典型点P1 in Node1
3 for each 典型点P2 in Node2
4 Ray = CREATE-RAY (P1, P2)
5 if(not RAY-INTERSECTS-SOMETHING-IN-TREE(Node1.Tree.RootNode,
Ray)
6 Visible = true
7 return Visible
算法分析
函数CHECK-VISIBILITY非常耗费时间,当我们在两个叶节点之间进行光线跟踪来检测两者之间是否可见时,我们不得不从Node1的每一个典型点开始对Node2的每一个典型点进行跟踪,最坏的情况是每一次跟踪都将对树中所有的多边形进行检测,这时函数的复杂度O(s1 s2 p),这里s1为Node1中典型点的数量,s2为Node2中典型点的数量,p为树中多边形的数量。通常它的性能是比较好的,接近于O(s1 s2 lg p)。
l 函数TRACE-VISIBILITY
l 参数:
l Tree – 去进行光线跟踪的BSP-tree。
l 返回值:
l None
l 功能:
l 对于树中每一个叶节点,它将进行光线跟踪运算来检测和它相连的节点的可见性。每一个被发现是可见的节点将会被添加到当前节点的PVS中。当一个叶节点发现是可见的,我们还要对和它相连的节点进行光线跟踪运算来检测可见性。
TRACE-VISIBILITY (Tree)
1 for (each 树中的叶节点L)
2 for (each 和叶节点L相连的叶节点C)
3 添加节点C到节点L的PVS中
4 for (each 树中的叶节点L1)
5 while (在叶节点L的PVS中存在一个叶节点L2,它所连接的节点没有进行可见性检测)
5 for (each 连接到节点L2中的叶节点C)
6 if (节点C没有位于节点L1的PVS中 and
CHECK-VISIBILITY (L1, C))
7 添加节点C到节点L1的PVS中
7 添加节点L1到节点C的PVS中
算法分析
如果我们没有使用根据叶节点的相连情况来进行处理的优化技术,那么必须对树中的每一个叶节点进行检测,这时函数的复杂度为O(n2),这里n为树中叶节点的数量。要对上面的算法估计一个对处理过程提高了多少性能的具体数值非常困难,因为它依赖于所处理的结构的大小。对于每一个叶节点与其它节点都是可见的结构,这个算法不会做任何优化。对于每一个叶节点只有一到二个可见的节点的结构,这个算法将会产生非常大优化,它的复杂度接近与O(n)。
现在在一个设计良好的地图上所产生的结构在每一帧上将会避免检测大量的多边形。一个设计良好的地图在建立时将会考虑物体的可见性,这意味着应尽可能的在地图中放入一些能障碍视线的物体,如墙壁等。如果在地图中包含一个有大量细节的大房间,那么在上面的算法中(或在一个portal引擎中)进行的隐藏面剔除工作对它不会产生任何效果。这时我们需要使用其它类似于LOD这样的技术来进行多边形剔除。
静态物体
考虑一下这样的场景,一个球体位于一个立方体的中心,如果用BSP对它进行渲染那么将会产生大量的节点,并会产生大量的分割多边形,这是因为在球体上的每一个多边形都会送入到叶节点中。如果球体有200个多边形当渲染时就会产生200个叶节点的BSP树。这样做非常影响运行的速度,因此必须寻找一个方法来避免这种现象的发生。
为了解决这个问题地图的设计者可以选择组成地图的几何体,在上面的例子中也就是立方体,接着将剩余的物体做为静态物体,它们将不会用来对BSP树进行渲染或进可见性检测,但是它们会参与到地图的光照运算当中去。当进行可见性运算时每一个静态物体会被添加到BSP树中,每一个静态物体的多边形将会被添加到BSP树中,这个过程如下:
l 函数PUSH-POLYGON
l 参数:
l Node – 多边形当前所在的节点。
l Polygon – 将被添加的多边形。
l 返回值:
l None
l 功能:
l 将多边形添加到树中。如果多边形的一些点与节点的分割面相交将会被分割。分割后的部分将会被继续向下传送。当一个多边形进入一个叶节点后它将被添加到叶节点的多边形集合中。
PUSH-POLYGON (Node, Polygon)
1 if (IS-LEAF (Node))
2 Node.PolygonSet = Node.PolygonSet U Polygon
3 else
4 value = CALCULATE-SIDE (Node.Divider, Polygon)
5 if (value = INFRONT or value = SPANNING)
6 PUSH-POLYGON (Node.RightChild, Polygon)
7 else if (value = BEHIND)
8 PUSH-POLYGON (Node.LeftChild, Polygon)
9 else if (value = SPANNING)
10 Split_Polygon28 (P1, Divider, Front, Back)
11 PUSH-POLYGON (Node.RightChild, Front)
12 PUSH-POLYGON (Node.LeftChild, Front)
PUSH-POLYGON是一个递归函数,它将一个多边形添加到BSP树中,这个函数对每一个静态物体的每一个多边形都会调用一次,
在这个过程处理之后叶节点可能不再成为一个“凸”集合,这在进行碰撞检测时可能会产生一些问题,后面的内容将会对这个问题进行阐述。