一种快速而肮脏的方法是使用递归球面细分。从地球表面的三角剖分开始,递归地将每个三角形从顶点延伸到最长边的中间。(理想情况下,您会将三角形分成两个相等直径的部分或相等面积的部分,但是由于这些部分涉及一些复杂的计算,因此我只是将边精确地分成了一半:这会导致各个三角形的大小最终有所不同,但是对于这个应用来说似乎并不重要。)
当然,您将在数据结构中维护此细分,以便快速识别任意点所在的三角形。二叉树(基于递归调用)很好地工作:每次拆分三角形时,树都会在该三角形的节点处拆分。保留有关分割平面的数据,以便您可以快速确定任意点位于平面的哪一侧:这将确定是在树上向左还是向右移动。
(我是否说过要分割“平面”?是的-如果将地球表面建模为球体并使用地心(x,y,z)坐标,则我们的大部分计算都将在三个维度上进行,其中三角形的边是球体与通过其原点的平面的交点。这使得计算变得快速而简单。)
我将通过显示一个球体的一个八分圆点的过程进行说明。其他七个八分圆以相同的方式处理。这样的八分圆是90-90-90的三角形。在我的图形中,我将绘制跨越相同角的欧几里得三角形:它们看起来不会很好,直到变小为止,但是可以轻松,快速地绘制它们。这是与八分圆对应的欧几里得三角形:这是过程的开始。
由于所有边的长度相等,因此随机选择一个边作为“最长边”并细分:
对每个新三角形重复此操作:
在n步之后,我们将有2 ^ n个三角形。这是经过10个步骤后的情况,显示了八分圆中的1024个三角形(总球面上的8192个三角形):
作为进一步的说明,我在该八分圆内生成了一个随机点,然后遍历了细分树,直到三角形的最长边小于0.05弧度。显示(笛卡尔)三角形,探测点为红色。
顺便说一句,要将点的位置缩小到一个纬度(大约),您会注意到这大约是1/60弧度,因此覆盖了(1/60)^ 2 / /(Pi / 2)= 1/6000弧度。总表面。由于每个细分大约将三角形大小减半,因此八分圆的大约13到14个细分将达到目的。这并不需要太多的计算,正如我们将在下面看到的那样,这使它完全不必存储树,而是动态地进行细分是很有效的。在开始时,您会注意到该点位于哪个八分圆中-由其三个坐标的符号确定,可以将其记录为三位数的二进制数-并且在每个步骤中,您都需要记住该点是否位于在三角形的左(0)或右(1)中。这给出了另一个14位二进制数。 您可以使用这些代码对任意点进行分组。
(通常,当两个代码接近于实际的二进制数时,对应的点也接近;但是,这些点仍然可以接近并且具有明显不同的代码。例如,将相距一米的两个点分隔为赤道,例如:它们的代码必须相同,甚至在二进位点之前,因为它们位于不同的八分位。任何固定的空间划分都是不可避免的。)
我使用Mathematica 8来实现这一点:您可以将其按原样或作为伪代码在您喜欢的编程环境中实现。
确定平面0-ab点p位于哪一侧:
side[p_, {a_, b_}] := If[Det[{p, a, b}] >= 0, left, right];
根据点p细化三角形abc。
refine[p_, {a_, b_, c_}] := Block[{sides, x, y, z, m},
sides = Norm /@ {b - c, c - a, a - b} // N;
{x, y, z} = RotateLeft[{a, b, c}, First[Position[sides, Max[sides]]] - 1];
m = Normalize[Mean[{y, z}]];
If[side[p, {x, m}] === right, {y, m, x}, {x, m, z}]
]
最后一个图是通过显示八分圆并在其上方将以下列表呈现为一组多边形来绘制的:
p = Normalize@RandomReal[NormalDistribution[0, 1], 3] (* Random point *)
{a, b, c} = IdentityMatrix[3] . DiagonalMatrix[Sign[p]] // N (* First octant *)
NestWhileList[refine[p, #] &, {a, b, c}, Norm[#[[1]] - #[[2]]] >= 0.05 &, 1, 16]
NestWhileList
refine
在条件适用(三角形较大)时或直到达到最大操作计数(16)时,重复应用操作()。
为了显示八分圆的完整三角剖分,我从第一个八分圆开始,并对精化进行了十次迭代。首先对以下内容进行一些修改refine
:
split[{a_, b_, c_}] := Module[{sides, x, y, z, m},
sides = Norm /@ {b - c, c - a, a - b} // N;
{x, y, z} = RotateLeft[{a, b, c}, First[Position[sides, Max[sides]]] - 1];
m = Normalize[Mean[{y, z}]];
{{y, m, x}, {x, m, z}}
]
不同之处在于,它split
返回输入三角形的两个一半,而不是给定点所在的三角形。完整的三角剖分是通过以下方式获得的:
triangles = NestList[Flatten[split /@ #, 1] &, {IdentityMatrix[3] // N}, 10];
为了进行检查,我计算了每个三角形的大小并查看了范围。(此“大小”与每个三角形和球体中心对接的金字塔形图形成比例;对于像这样的小三角形,此大小基本上与其球形区域成比例。)
Through[{Min, Max}[Map[Round[Det[#], 0.00001] &, triangles[[10]] // N, {1}]]]
{0.00523,0.00739}
因此,大小与平均值的上下差异约为25%:这对于实现近似统一的分组方式似乎是合理的。
在扫描此代码时,您不会注意到三角函数:唯一需要的地方就是在球坐标和笛卡尔坐标之间来回转换。该代码也不会将地球表面投射到任何地图上,从而避免了随之而来的变形。否则,它仅使用平均值(Mean
),勾股定理(Norm
)和3×3行列式(Det
)来完成所有工作。(还有一些简单的列表操作命令,例如RotateLeft
和Flatten
,以及对每个三角形最长边的搜索。)