{Horizon Culling} 地平线剔除
From : http://cesiumjs.org/2013/04/25/Horizon-culling/
在虚拟地球(如, Cesium)的开发过程中, 我们需要能够快速确定场景中的对象, 如地形瓦片(terrain tiles), 卫星(satellites), 建筑物(buildings), 交通工具(vehicles)等, 何时是不可见的。因为这些对象不可见, 所以也就没有必要被渲染。我们当然需要进行视域(view-frustum)的剔除。但是, 地平线剔除(horizon culling)是另一个重要的剔除类型。
在上图中, 绿色的点是观察者(viewer)是可见的。红色的点是不可见的,因为它们位与视域(两条白色的粗线中间的那部分区域)之外。蓝色的点虽然处于视域的范围内, 但它们依然是不可见的, 因为它们被地球遮挡住了。换句话说, 它们处于地平线之下。地平线剔除是一个非常浅显的一种思路: 你不需要去渲染那些相对当前观察者的位置处于地平线之下的对象。尽管听起来很简单易懂,细节的处理非常棘手, 尤其是这个过程要求得到非常快的处理。Cesium 需要对每个渲染帧做成千上万次的这种测试以确定地形瓦片的可见性。但, 这种测试是非常重要的。在上图的配置中,覆盖整个地球的地形瓦片都处于视域中。然而, 有一半以上的地形瓦片处于地平线以下,不需要被渲染。
几年前, Deron Ohlarik 写了两篇关于地平线剔除的非常优秀的文章。自此以来, 我们一直致力于扩展他的技术,我将在这里分享我们的成果。虽然它只适用于静态数据如地形瓦片,但我们发现这非常有用, 因为它比先前的技术处理地更快、更精确。对水平线剔除的精度的改善源于地球的椭球模型而非近似一个球面。
在此之前,我应该提到对这种技术的信心完全是因为我的同事——Frank Stoner. 我所做的贡献只是在 Cesium 中实现了它,以及写在这里,而他做了最艰难的工作是得到这种技术。
Horizon culling a point against a sphere
正如Ohlarik所描述的, 为了进行地平线剔除, 我们可以为一个静态对象(如一个地形瓦片),即便仅是一个点,计算一个边界范围。如果这个点处于地平线下以下,那么我们就能确信这个瓦片也处于地平线以下。我们的新技术限制于参照一个椭球来剔除单个点,因此我们开始假定这个被遮挡的点已经被计算过了。关于具体做法,请查看后续博客。
我承诺,我们将实现参照一个一般椭球的水平剔除。我也会兑现这个诺言,但是让我们先从一个简单的单位球出发来介绍地平线剔除。然后, 我会说明如何将这个单位球推广到一个任意的椭球。考虑下面这幅图:
在上面这幅图中, 蓝色的圆就是我们的单位球。从相机位置引出的两条单位球的切线表示了视域。图中黑色的竖线表示了所有的地平线上的点。在我们的单位球面上,地平线上的点处在一个平面上,并形成了一个圆。从相机位置到所有地平线上的点的向量构成了一个无限延伸的圆锥体。
灰色阴影的部分球体以及它周围的空间表示了位于地平线以下的区域。阴影区域中的任何点从相机位置上来看,都是不可见的。直观地说,如果一个点处于正切向量构成的无限圆锥体内部并且位于地平线上所有点构成的平面的后面,那么这个点位于地平线之下。
The Plane test
首先, 让我们进行一项廉价的测试来确定一个点位于一个平面的那一侧。考虑下面的这幅图:
我们知道向量VC 和向量VT: 它们分别表示观察者到目标点和椭球中心的向量。我们也知道HC是个单位向量, 因为我们现在处理的是一个单位球。根据毕氏定理(Pythagorean theorem):
因此, 观察者到地平面的距离是:
向量VT在向量VC上的投影小于||VP||,目标点位于平面前侧。换句话说, 目标点位于地平面后侧当:
我们可以简化这个公式,通过两边同时乘以||VC||2, 结果变成:
为了确定目标点是否位于地平面后侧,取观察者到目标点的向量与观察者到椭球中心的向量的点积。如果得到的值比观察者到椭球中心的向量的大小平方值减1,那么目标点就位于该平面后侧。不需要进行平方根或三角函数运算。
The Cone Test
如果目标点位于地平面前方,那么目标点显然不会被球体遮挡。 如果目标点位于平面的后面, 那么, 目标点可能被遮挡也可能不被遮挡。如果目标点也位于无限的视锥(由所有水平点与相机链接构成),那么它就会被遮挡。如果它位于视锥体之外, 那么它就不会被遮挡。那么如何来测试相对于视锥的目标点呢?
先看一下下面的图, 角HVC, TVC 分别被标了出来:
我们可以看到, 点 T 是处于 视锥体内部的, 当满足:
或, 当角
角 HVC 是直角三角形 VCH 的一部分, 所以,我们可以使用余弦公式重写不等式的右侧部分:
接下来, 重写不等式的左侧部分,使用点积和两边同时乘以
现在, 对不等式的两边同时进行平方以消除大小的影响, 虽然反过来涉及到一个平方根:
通过两边进行平方运算,可以有效地测试相对视锥体的目标点, 从相机看去是一个椭圆形的视锥体。但这并不影响结果。然而, 因为相机后面的目标点一定是位于地平面的前方。地平面前方的任何点都不能被水平剔除, 因此第二个视锥体测试是不需要的。
我们进行到了哪一步? 向量VC和VT是很容易计算的, 因为它们从椭圆圆心、目标点和相机位置形成的。VH不是那么明显。但是不要忘了之前一节的内容, 我们发现:
这个不仅容易计算,而且也是确定目标点处于地平面的哪一面。类似的, 我们以及计算得到了。
因此, 我们最终的不等式, 仅需要做一些算术运算:
如果这个不等式为true,那么目标点就处于视锥体之内。如果它处于地平面的后方, 那么这个点就会被遮挡。
Generalizing to an ellipsoid
在单位球下, 是正确的. 我们如何将它推广到任何一个椭球呢?
单位球满足的方程:
椭球满足的方程:
其中, a, b, 和 c 分别是椭球在三个坐标轴上的半径。
给定一个球心在原点的椭球, 相机位置, 一个目标点, 可以对所有的坐标应用伸缩变换以创建一个单位球环境。可用的一个伸缩变换具有如下形式:
我们称变换后的坐标系统为椭球伸缩变换空间,这种方法对于解决椭球问题很有效。
Code
相机位置变换时,需执行的操作:
// Ellipsoid radii - WGS84 shown here var rX = 6378137.0; var rY = 6378137.0; var rZ = 6356752.3142451793; // Vector CV var cvX = cameraPosition.x / rX; var cvY = cameraPosition.y / rY; var cvZ = cameraPosition.z / rZ; var vhMagnitudeSquared = cvX * cvX + cvY * cvY + cvZ * cvZ - 1.0;
然后, 对每个点做遮挡剔除测试:
// Target position, transformed to scaled space var tX = position.x / rX; var tY = position.y / rY; var tZ = position.z / rZ; // Vector VT var vtX = tX - cvX; var vtY = tY - cvY; var vtZ = tZ - cvZ; var vtMagnitudeSquared = vtX * vtX + vtY * vtY + vtZ * vtZ; // VT dot VC is the inverse of VT dot CV var vtDotVc = -(vtX * cvX + vtY * cvY + vtZ * cvZ); var isOccluded = vtDotVc > vhMagnitudeSquared && vtDotVc * vtDotVc / vtMagnitudeSquared > vhMagnitudeSquared;
在 Cesium 中, 我们会预先计算伸缩变换后的位置, 而不是在每个测试之前进行。