BSP技术详解3---有图有真相
第三节 室内场景中光照运算
关于Radiosity的算法最早是由Goral、Cindy M、Torrance、Kenneth E、Greenberg、Donald P、Battaile和Bennett在论文《Modelling the interaction of light between diffuse surfaces》提出的。他们使用Radiosity来模拟能量在漫反射表面之间进行传送,漫反射表面对照到表面上的光线在所有的方向上都进行相同的反射,和它相反的是镜面反射表面,它只在反射方向上传播反射光。由于漫反射表面的这个特性,这就意味着对于所有的观察角度而言看起来表面都是相同的,这样对于场景中的每一个表面只需要进行一次光照运算,而且可以在场景的预渲染时进行,因此这项技术被大量的3D游戏所采用。
下面我再简短的讲解一下Radiosity是如何工作的,而将主要的精力放在如何使用BSP树来加速Radiosity的计算,对于Radiosity的详细介绍请参考前面的章节。Radiosity技术是设计用来使场景中光照看起来更加真实和光滑,如果我们使用一个一直向前传播而不考虑反射的光照模型,那么当场景中的灯光照亮场景中的物体时,并不会计算远处经过反射过来的光线,这样场景中的阴影看起来非常尖锐而物体表面也看起来非常不真实。为了使用radiosity技术我们需要把场景分割成一块一块很小的部分,每一部分我们称它为patch,每一个patch都有一个初始化的能量级别,如果它不是一个灯光这样的发光体的话通常为0,有许多方法来分配场景中的能量,这里我们将要使用的方法称为交互式radiosity。这个方法的过程是我们从场景中未发送能量的级别最高的patch开始发送能量,能量经过传递后将不再发送能量的patch的等级设为0,重复这个过程直到场景中的每一个patch的能量等级都小于一个预定值为止。
当能量从一个patch(j)开始发送到另一个patch(i)时我们使用下面的公式:
Bi = Bi + Bj * Fij * Ai / Aj
这里Bi = patch(i)的能量级别 Bj = patch(j)的能量级别
Ai= patch(i)的作用区域 Aj = patch(j)的作用区域
Fij = patch(i)与patch(j)之间的系数
在公式中系数Fij是由以下公式来确定的:
Fij = (cos qi * cos qj) / d2 * Hij
这里Fij = patch(i)与patch(j)之间的系数
qi = patch(i)与patch(j)法线之间的夹角
qj = patch(i)与patch(j)法线之间的夹角
d = patch(i)与patch(j)之间的距离
Hij = patch(i)与patch(j)之间的可见性系数。如果在两个patch之间只有一条光线可以跟踪,这个值为1,如果没有光线可以跟踪为0。一般情况下由于每一个patch都不是一个点而是一个区域,因此光线有很多条。
从上面的公式中我们可以看到在场景中进行radiosity计算是非常耗费时间的。这个函数的复杂度为O(n3),这里的n为场景中patch的数量。由于对于场景中每一个patch你需要发送最少一条光线到其它patch上,因此需要对场景中的几乎所有的多边形都进行光线跟踪计算。在上面的公式中系数H的计算非常耗费时间,下面我们将看一下如何在BSP树中对它的计算进行优化。
BSP树中的radiosity计算
在进行场景中的光照计算之前需要把场景中的面分割为patch,一个方法是在开始的时候设定每一个patch为预定的大小,当计算每一个patch的能量时,如果在patch上的能量足够大,对这个patch进行分割。不过这个方法是非常耗费时间的,因此必须寻找一个更好的方法来通过BSP树对计算进行优化。
在radiosity的一般算法中场景中的每一个光源都被看作为一个或多个patch,这里我们可以改进一下,将每一个光源放在它所位于的叶节点中,接下来每一个光源都发送自己的能量到场景中所有的patch上,当这个过程完成后radiosity计算也就结束了。为了使最后的结果看起来更好可以使用一种称为“渐进精选”(progressive refinement)的技术来对这个方法进行很小的修改。在每一次过程中,叶节点中具有高能量的patch将发送能量到其它低能量的patch上,这样做的结果是高亮度的patch将发送能量到处于阴影中patch上。这是因为在实际生活中并没有真正黑暗的地方,它多多少少要获得一些其它物体反射过来的光亮。
由于计算非常耗费时间需要做一下优化,使用渲染BSP树时获得的PVS信息可以在选择哪些patch将接受能量时剔除一些无用的计算。因为在计算PVS时使用了相同的方法来进行光线跟踪。
通过场景来分配能量的算法如下:
l 函数RADIOSITY
l 参数:
l Tree – 进行radiosity计算的BSP树。
l 返回值:
l None
l 功能:
l 在场景中的patch之间发送能量。
RADIOSITY (Tree)
1 for(each leaf L in Tree)
2 for(each light S in L)
3 for(each leaf V that is in L’s PVS)
4 Send S’s energy to the patches in V
l 下面语句5是为了让地图编辑者在任何时候都可以检查场景渲染的效果,如果他感到看起来已经足够好了可以中断能量的传播。
5 while(not looks good enough)
6 for(each leaf L in Tree)
7 for(each leaf V that is in L’s PVS)
8 Send energy from the patch with the most unsent energy in L
to all patches in V.
复杂度分析
这个函数的运算费用实在是太高昂了,可以称为时间杀手,在最坏的情况下每一条光线将不得不检测场景中的每一个多边形,此时复杂度为O(n3),这里n为树中patch的数量。一般情况下由于进行了优化可以减少大量的计算,但是减少多少并不能计算出来,因为这依赖于树结构的复杂度。
上面的函数给出了一个充分利用BSP树的优点来加速场景光照运算的方法,尤其是可以显著的减少光线跟踪的计算量,而且地图设计者可以来决定当场景渲染时如果渲染的效果可以接受中断渲染循环。这对地图的预渲染实在是太方便了,运行的时间可以根据渲染的效果来决定。
第四节 BSP树的预渲染
现在我们需要完成一个完整BSP引擎的预处理过程,下面的算法显示如何将场景渲染到BSP树中。
l 函数RENDER-SCENE
l 参数:
l Scene – 被渲染的场景
l 返回值:
l 一个BSP树。
l 功能
l 预渲染来获得一个包含场景信息的BSP树。
RENDER-SCENE (Scene)
l 使用描述场景中图元的物体来渲染BSP树。
1 GeometryPolygons = {}
2 for (每一个包含场景图元的物体object O)
3 GeometryPolygons = GeometryPolygons U O.PolygonSet
4 GENERATE-BSP-TREE (Tree.RootNode, GeometryPolygons)
l 分配叶节点上的取样点。
5 DISTRIBUTE-SAMPLE-POINTS (Tree.RootNode, {})
6 TRACE-VISIBILITY (Tree)
7 for 每一个场景中的静态物体object O
8 for 物体O中每一个多边形P
9 PUSH-POLYGON (Node, P)
l 函数CREATE-PATCHES是一个未定义的函数,由于我们的解决方案效率并不是太好,因此没有对它进行详细的介绍。
10 CREATE-PATCHES (Tree)
11 RADIOSITY (Tree)
复杂度分析
函数的复杂度如下:
函数 最坏情况 一般情况 描述
GENERATE-BSP-TREE O(n2 lg n) O(n2) n为场景中多边形的数量
DISTRIBUTE-SAMPLE-POINTS Q (np + xy) Q (np + xy) n为树中多边形的数量,p为树中典型点的数量,x和y为分割面的宽度和高度。
TRACE-VISIBILITY O(n2) O(n lg n), n为树中多边形的数量。
RADIOSITY O(n3) O(n2 lg n) n为树中patch的数量
在一般情况这一列中显示了算法通常所需运行的时间,对算法时间影响最大的是函数RADIOSITY,它使整个算法的复杂度趋向于O(n3)。