1 简介
为了从不同的角度可视化一个场景(Scene),虚拟相机(virtual camera)就经常派上用场了。虚拟相机的设置(OpenGL中常通过gluPerspective和gluLookAt函数设置)决定了从屏幕中可以看到什么。
View平截头体(frustum)包罗了当前屏幕上一切潜在的可见实体(之所以说潜在的可见实体,是因为有些实体可能被遮挡住了)。它通过相机的设置来定义,当使用透视投影(perspective projection)时,平截头体就像一个被截掉塔尖的金字塔。
金字塔的顶点就是相机的位置,塔底就是远平面(far plane)。金字塔在近平面(near plane)位置截断,因此叫做平截头体。
平截头体内部的所有元素都是潜在可见的,至少部分可见。所以没有必要渲染那些平截头体外部的元素,因为它们根本看不到。
上图中,所有绿色(完全在View的平截头体内)和黄色(部分可见)的图形都将被渲染。但是红色的图形不会被渲染。需要留意的是绿色的球体不可见,它被黄色椭圆遮挡住了,但它也会被渲染,因为它在View的平截头体内。
View平截头体筛选目的就是要甄别什么在平截头体内部,筛选所有不在其内部的。只有其内部的实体被送往图形硬件,最后请求硬件渲染它们,同时保存那些不可见的节点。由于3D世界中仅有可见节点驻留在显存内,所以某种程度上提高了应用程序的性能。这更有希望描述整个3D世界。
如果平截头体内这部分比整个场景小得多,那么这个试验就显得意义非凡。但是小得多的标准是什么呢?这取决于应用程序。极端的情况是整个场景总是完全可见,这时View平截头体筛选只是浪费时间,因为没什么可筛选的。幸运的是这种筛选技术非常容易实现,并且相比性能上的显著提升,这个值得一试。
http://www.lighthouse3d.com/opengl/viewfrustum/index.php?intro
2 View平截头体的形状
在这一节里,View平截头体的形状通过OpenGL程序教学来展开。假设用gluPerspective函数定义了一个透视投影,并且相机位置通过gluLookAt来设置。
首先,让我们看看这些函数的参数(均为float型):
l gluPerspective(fov, ratio, nearDist, farDist);
l gluLookAt(px, py, pz, lx, ly, lz, ux, uy, uz);
P(px, py, pz)是相机的位置,View射线通过d=L(lx, ly, lz)-P(px, py, pz)定义。远近平面都垂直于View射线,它们距离相机位置的距离分别为nearDist,farDist。其矩形边界大小事距离和fov,ratio(ratio between the horizontal and vertical fields of view)的函数。
Hnear = 2 * tan(fov/2) * nearDist
Wnear = Hear * ratio
Hfar = 2 * tan(fov/2) * farDist;
Wfar = Hfar * ratio
为了执行View平截头体筛选,需要按下面两步操作:
l 提取平截头体的数据 – 每次平截头体改变后都需要做,i.e.,相机或透视模式改变时。
l 根据平截头体测试对象是否被筛选 – 这一步需要在每一帧执行。如果每个对象在每一帧之间保持筛选状态不变,那么仅仅需要在相机移动(比如平截头体更新或透视模式改变)时做测试。
http://www.lighthouse3d.com/opengl/viewfrustum/index.php?defvf
3 几何方法 – 提取平面
在自然空间中运用几何方法,可以通过View平截头体的数据计算定义frustum边界的六个平面:near, far, top, bottom, left & right。
这些平面的法向量(normal)指向View平截头体内部。通过检测对象在平面的哪一侧,就可以确定一个对象是否在平截头体内。具体可以计算点到平面的有符号距离(signed distance)来实现,如果它在法向量所指的一侧,即距离为正值,该对象在平面右侧。如果一个对象位于六个平面的右侧,则它在平截头体内。
本节论述了如何计算定义平截头体的六个平面的方法。Testing将在后面的详述。
一种方法是,首先确定平截头体的八个顶点,然后用这些顶点来定义六个平面。
下图显示了上面提及的顶点,它们可以用来计算这六个平面。
定义点的标记法是这样的:第一个字母表示在near plane(n)还是far plane(f)上;第二个字母表示改点的top(t)或bottom(b)位置;第三个字母表示left(l)或right(r)位置。
回顾上一节提到的一些知识:
l P - 相机位置
l D - 相机View射线的矢量方向。这里假设该向量已标准化。
l nearDist - 相机到near plane的距离
l Hnear - near plane的高
l Wnear - near plane的宽
l farDist - 相机到far plane的距离
l Hfar - far plane的高
l Wfar - far plane的宽
另外还需要一些单位向量,也就是up vector和right vector。前者通过单位化向量(ux, uy, uz)得到(即gluLookAt函数的后三个参数);后者通过计算up vector和d vector的叉积得到。如下图,显示了如何得到far plane的左上角点ftl。
具体可以通过下面的式子计算ftl点,
fc = p + d * farDist
ftl = fc + (up * Hfar/2) - (right * Wfar/2)
其他点这样计算,
ftr = fc + (up * Hfar/2) + (right * Wfar/2)
fbl = fc - (up * Hfar/2) - (right * Wfar/2)
fbr = fc - (up * Hfar/2) + (right * Wfar/2)
nc = p + d * nearDist;
ntl = nc + (up * Hnear/2) - (right * Wnear/2)
ntr = nc + (up * Hnear/2) + (right * Wnear/2)
nbl = nc - (up * Hnear/2) - (right * Wnear/2)
nbr = nc - (up * Hnear/2) + (right * Wnear/2)
三个点可以定义一个平面。例如,可以用ftl, ftr, fbr定义far plane,但应该保持法向方向的一致性,所有法向都指向view frustum内部。
这种方法计算near/far planes可以做一些优化。一个平面可由一个法向和一个点定义,这些平面都可以基于相机的定义计算出来。near plane可由平面上的点nc和法向d定义,far plane可由-d和点fc定义。
其他平面也可以用一种更高效的方式计算出来,即利用一个法向量和一个点来定义平面。下面的代码给出了right plane的法向量。
nc = p + d * nearDist
fc = p + d * farDist
a = (nc + right * Wnear/2) - p
a.normalize();
normalRight = up x a;
http://www.lighthouse3d.com/opengl/viewfrustum/index.php?gaplanes