非常快速的3D距离检测?

37

有没有一种快速而粗略的方法来进行3D距离检查,以便结果很快,但非常快?我需要进行深度排序。我使用STL sort 如下:

bool sortfunc(CBox* a, CBox* b)
{
    return a->Get3dDistance(Player.center,a->center) <
      b->Get3dDistance(Player.center,b->center);
}

float CBox::Get3dDistance( Vec3 c1, Vec3 c2 )
{
    //(Dx*Dx+Dy*Dy+Dz*Dz)^.5 
    float dx = c2.x - c1.x;
    float dy = c2.y - c1.y;
    float dz = c2.z - c1.z;

return sqrt((float)(dx * dx + dy * dy + dz * dz));
}

有可能不使用平方根或乘法的方式来实现吗?


执行数据集的前一半吗? - Mikhail
13个回答

64
你可以忽略平方根,因为对于所有正数(或者说非负数)的xy,如果sqrt(x) < sqrt(y),那么x < y。由于你要对实数的平方求和,每个实数的平方都是非负数,而任何正数的和都是正数,所以平方根条件成立。
但是,你不能消除乘法而不改变算法。这里有一个反例:如果x是(3, 1, 1),而y是(4, 0, 0),则|x| < |y|,因为sqrt(1*1+1*1+3*3) < sqrt(4*4+0*0+0*0)1*1+1*1+3*3 < 4*4+0*0+0*0,但是1+1+3 > 4+0+0
既然现代CPU可以比从内存加载操作数更快地计算点积,那么消除乘法可能并没有什么优势(我认为最新的CPU有一种特殊指令,可以每3个周期计算一次点积!)。
在进行任何算法更改之前,我建议先进行一些性能分析。你选择的算法将严重依赖于数据集的大小(它是否适合缓存?),你需要多频繁地运行它以及你如何处理结果(碰撞检测?接近性?遮挡?)。

2
反例实际上并不能证明在这种情况下你不能省略乘法 - 深度排序不必完美,因为它主要是一个性能问题(z缓冲处理偶尔的不准确性),所以如果你通过不完美的深度排序获得足够的性能提升,超过了失去的渲染性能... 在现实中可能不太可能发生,但我只是说这里的证明是错误的 - 它证明了错误的事情。 - user180247
Steve314:好观点。我修饰了我的陈述,希望它是正确的。 - Gabe
5
我同意,当一个人只需要比较两个距离而不是求出距离本身时,省略平方根符号是相当普遍的。 - Matthieu M.
1
看起来你非常关注性能。 因此,你不应该使用stl::sort。 实现自己的排序算法(例如从互联网上获取开源快速排序实现的代码并进行修改)。 由于函数调用数量巨大,stl::sort比自定义实现要慢得多。 - Tara
@Dudeson:只要采取一些预防措施,std::sort 就能击败很多人试图放弃的垃圾代码。例如,大量函数调用的问题在 g++ 中基本上消失了,如果你传递一个 lambda 而不是一个函数指针。(没有尝试过其他编译器;如果指针值在编译时可知,它们甚至可以完全优化掉它。) - cHao
@CHao:我猜你在谈论C++11的特性?我还没有真正使用过它们中的很多。另外:我所说的是真的,因为我做到了并注意到了相当大的加速。 - Tara

30

我通常会先按曼哈顿距离进行过滤。

float CBox::Within3DManhattanDistance( Vec3 c1, Vec3 c2, float distance )
{
    float dx = abs(c2.x - c1.x);
    float dy = abs(c2.y - c1.y);
    float dz = abs(c2.z - c1.z);

    if (dx > distance) return 0; // too far in x direction
    if (dy > distance) return 0; // too far in y direction
    if (dz > distance) return 0; // too far in z direction

    return 1; // we're within the cube
}

如果您更了解您的环境,实际上可以进一步优化此过程。例如,在像飞行模拟器或第一人称射击游戏这样有地面的环境中,水平轴比垂直轴要大得多。在这样的环境中,如果两个物体相距很远,则它们很可能更多地由x和y轴分隔而不是z轴(在第一人称射击游戏中,大多数物体共享相同的z轴)。因此,如果您首先比较x和y,则可以从函数中提前返回并避免进行额外的计算:

float CBox::Within3DManhattanDistance( Vec3 c1, Vec3 c2, float distance )
{
    float dx = abs(c2.x - c1.x);
    if (dx > distance) return 0; // too far in x direction

    float dy = abs(c2.y - c1.y);
    if (dy > distance) return 0; // too far in y direction

    // since x and y distance are likely to be larger than
    // z distance most of the time we don't need to execute
    // the code below:

    float dz = abs(c2.z - c1.z);
    if (dz > distance) return 0; // too far in z direction

    return 1; // we're within the cube
}

抱歉,我没有意识到这个函数是用于排序的。你仍然可以使用曼哈顿距离来进行第一次粗略的排序:

float CBox::ManhattanDistance( Vec3 c1, Vec3 c2 )
{
    float dx = abs(c2.x - c1.x);
    float dy = abs(c2.y - c1.y);
    float dz = abs(c2.z - c1.z);

    return dx+dy+dz;
}

在粗略的第一次排序之后,您可以选择最靠前的结果,例如最接近的前10名玩家,并使用正确的距离计算进行重新排序。


我不确定如何找出玩家到一个立方体的距离是否小于玩家到另一个立方体的距离? - jmasterx
他想做的是按距离从某个点排序,而不是确定两个点是否相距小于一定距离。 - Gabe
1
我对第二部分感到担忧。在我的看法中,> 可能和 - 一样慢,而 abs 的成本并不高,但每个 if 都会创建一个条件分支 - 因此可能会被错误地预测。对于简单的曼哈顿距离(或者说简单的三个乘积之和的距离平方),我不会在没有良好的分析证据的情况下增加复杂性。消除平方根是值得的,但这个曼哈顿距离需要小心处理,即使在第一个版本中,我也会使用一个带有 and 运算符的单个返回。 - user180247
1
Steve314: 实际上,(x1-x2)^2+(y1-y2)^2+(z1-z2)^2 运算可以轻松地进行向量化处理,只需使用4个操作:加、乘、加、加。考虑到两组向量的操作可以轻松地进行流水线处理,你可能会发现完整比较版本的速度平均比曼哈顿距离版本更快。 - Gabe

17

这里有一个方程式,可能会帮助您同时消除平方根和乘法:

max(|dx|, |dy|, |dz|) <= distance(dx,dy,dz) <= |dx| + |dy| + |dz|

这将为您提供一个距离范围估计,将其定位到3倍因子内(上下界最多相差3倍)。 然后您可以按较低的数字排序。 然后,直到您达到第一个遮挡对象的3倍远的对象时,您需要处理数组。 然后,保证在数组中后面不会找到更接近的任何对象。

顺便说一句,在这里进行排序是过度的。 一种更有效的方法是制作一系列具有不同距离估计的桶,例如[1-3],[3-9],[9-27],....然后将每个元素放入桶中。 从最小到最大处理桶,直到你找到一个遮挡物体。 多处理1个桶以确保。

顺便说一句,浮点乘法现在非常快。 我不确定将其转换为绝对值能带来多少好处。


15
我很失望那些古老的数学技巧似乎正逐渐被遗忘。这里是你所要求的答案。来源于 Paul Hsieh 的优秀网站:http://www.azillionmonkeys.com/qed/sqroot.html。请注意,你不需要关心距离;使用距离的平方就可以完成排序,速度会更快。
在二维中,我们可以使用以下公式得到一个粗略的距离度量近似值而不需要进行平方根计算:
distanceapprox(x, y) = (1 + 1/(4-2*√2))/2 * min((1 / √2)*(|x|+|y|), max (|x|, |y|)) 该公式计算结果与真实答案最多相差约8%。类似地,在三维中进行类似推导,得到如下公式:

distanceapprox (x, y, z) = (1 + 1/4√3)/2 * min((1 / √3)*(|x|+|y|+|z|), max (|x|, |y|, |z|))

最大误差约为16%。

然而,需要指出的是,在许多情况下,距离仅用于比较。例如,在经典曼德博集合(z←z2+c)计算中,复数的大小通常与半径长度为2的边界进行比较。在这些情况下,可以通过实质上平方比较两侧(因为距离始终为非负),简单地放弃平方根。也就是说:

    √(Δx2+Δy2) < d is equivalent to Δx2+Δy2 < d2, if d ≥ 0

我还应该提到,Richard G. Lyons的《理解数字信号处理》第13.2章收录了令人难以置信的二维距离算法(也称为复数幅度近似)。以下是一个例子:

Max = x > y ? x : y;

Min = x < y ? x : y;

if ( Min < 0.04142135Max )

|V| = 0.99 * Max + 0.197 * Min;

else

|V| = 0.84 * Max + 0.561 * Min;
这个方法最大误差只有实际距离的1.0%。当然,代价是要进行几个分支;但即使对于此问题“被广泛接受”的答案至少也有三个分支。
如果您真的想以特定精度进行超快速的距离估计,可以通过编写自己简化的fsqrt()估计来实现,该方法使用与编译器供应商相同的基本方法,但精度较低,通过执行固定数量的迭代。例如,您可以消除极小或极大数字的特殊情况处理,并/或减少Newton-Rapheson迭代次数。这是所谓的“Quake 3” fast inverse square root 实现的关键策略——它是具有仅一个迭代的经典Newton算法。
不要假设您的fsqrt()实现很慢,而没有进行基准测试和/或阅读源代码。大多数现代fsqrt()库实现都是无分支的并且非常快速。例如这里是旧IBM浮点fsqrt实现。过早优化是,且始终是,万恶之源。

6
请注意,对于两个(非负)距离AB,如果sqrt(A) < sqrt(B),则A < B。创建一个专门用于sortfunc()Get3DDistance()GetSqrOf3DDistance())的特殊版本,该版本不调用sqrt()。

或者更好的是,如果Vec3实现了减法和点积运算,那么这样就足够了:Vec3 d = A - B; float dist = dot(d,d); - greyfade

5
如果您担心性能问题,您还应该注意发送参数的方式:
float Get3dDistance( Vec3 c1, Vec3 c2 );

意味着需要两个Vec3结构的副本。请使用引用代替:

float Get3dDistance( Vec3 const & c1, Vec3 const & c2 );

3
您可以比较距离的平方而不是实际距离,因为d2 = (x1-x2)2 + (y1-y2)2+ (z1-z2)2。这并不能消除乘法,但它可以消除平方根运算。

1
输入向量更新的频率以及排序的频率是多少?根据您的设计,将“Vec3”类与预先计算的距离扩展并按照该距离进行排序可能非常高效。特别是如果您的实现允许您使用矢量化操作。
除此之外,请参阅flipcode.com关于近似距离函数的文章,以了解另一种方法的讨论。

1

根据您用于比较的点数略有不同,以下内容几乎可以保证在所有迭代中假设所有点都发生变化的情况下以近似顺序获取点列表。

1)将数组重写为曼哈顿距离的单个列表,其中out [i] = abs(posn [i] .x - player.x)+ abs(posn [i] .y - player.y)+ abs(posn [i] .z - player.z);

2)现在您可以对浮点数使用基数排序进行排序。

请注意,在实践中,这比对3D位置列表进行排序要快得多,因为它显着降低了排序操作中的内存带宽要求,而所有时间都会花费在其中不可预测的访问和写入。 这将在O(N)时间内运行。

如果许多点在每个方向上都是静止的,则有更快的算法,例如使用KD-Tree,尽管实现要复杂得多,并且很难获得良好的内存访问模式。


0
如果这只是用于排序的值,那么您可以将sqrt()替换为abs()。如果您需要将距离与设定值进行比较,请获取该值的平方。
例如,您可以将abs(...)与a*a进行比较,而不是检查sqrt(...)是否大于a。

2
由于他在计算平方和,所以 sqrt 的参数始终为正数,使得使用 abs 是多余的。 - Gabe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接