我有点喜欢三角学,所以我知道将角度映射到我们通常拥有的某些值的最佳方法是切线。当然,如果我们想要一个有限的数字来避免比较 {sign(x),y/x} 的麻烦,那就有点混乱了。
但是有一个函数可以将 [1,+inf[ 映射到 [1,0[ 称为逆函数,这将允许我们有一个有限的范围来映射角度。正切的倒数是众所周知的余切,因此是 x/y(是的,就这么简单)。
一个小插图,显示单位圆上正切和余切的值:
当|x| 时,您会看到这些值是相同的。 = |y|,您还可以看到,如果我们对两个圆圈上输出值介于 [-1,1] 之间的部分进行着色,我们就可以为整个圆圈着色。要使这种值映射连续且单调,我们可以这样做:
- 使用余切的对立面与切线具有相同的单调性
- 将 2 添加到 -cotan,使 tan=1 处的值一致
- 将 4 添加到圆的一半(例如,在 x=-y 对角线下方)以使值适合不连续点之一。
这给出了以下分段函数,它是角度的连续且单调的函数,只有一个不连续性(这是最小的):
double pseudoangle(double dx, double dy)
{
// 1 for above, 0 for below the diagonal/anti-diagonal
int diag = dx > dy;
int adiag = dx > -dy;
double r = !adiag ? 4 : 0;
if (dy == 0)
return r;
if (diag ^ adiag)
r += 2 - dx / dy;
else
r += dy / dx;
return r;
}
请注意,这与Fowler angles 非常接近,具有相同的属性。正式地,pseudoangle(dx,dy) + 1 % 8 == Fowler(dx,dy)
说到性能,它远没有 Fowler 的代码那么繁琐(而且通常也不那么复杂 imo)。在 gcc 6.1.1 上使用 -O3 编译,上面的函数生成一个带有 4 个分支的汇编代码,其中两个来自 dy == 0(一个检查两个操作数是否“无序”,因此如果 dy 是 NaN ,另一个检查它们是否相等)。
我认为这个版本比其他版本更精确,因为它只使用尾数保留操作,直到将结果转移到正确的间隔。这在 |x| 时尤其明显。 > |x|,则运算 |x| + |y|失去了相当多的精度。
如图所示,角度-伪角度关系也非常接近线性。
看看分支是从哪里来的,我们可以做如下注释:
如果我们选择得到大致相同的结果(直到舍入误差)但没有分支,我们可能会滥用 copsign 并编写:
double pseudoangle(double dx, double dy)
{
double s = dx + dy;
double d = dx - dy;
double r = 2 * (1.0 - copysign(1.0, s));
double xor_sign = copysign(1.0, d) * copysign(1.0, s);
r += (1.0 - xor_sign);
r += (s - xor_sign * d) / (d + xor_sign * s);
return r;
}
如果 dx 和 dy 的绝对值接近,则由于 d 或 s 中的取消,可能会发生比以前的实现更大的错误。没有检查除以零以与其他实现进行比较,因为这仅在 dx 和 dy 都为 0 时发生。