GLSL上的鲁棒atan(y,x)用于将XY坐标转换为角度

时间:2014-09-27 01:27:10

标签: c++ glsl coordinates atan2 numerical-stability

在GLSL(特别是我正在使用的3.00)中,有两个版本 atan()atan(y_over_x)只能返回-PI / 2,PI / 2之间的角度,而atan(y/x)可以考虑所有4个象限,因此角度范围涵盖-PI,PI,很像C ++中的atan2()

我想使用第二个atan将XY坐标转换为角度。 但是,GLSL中的atan()除了无法处理x = 0时也不稳定。特别是在x接近于零的情况下,除法会溢出,导致相反的结果角度(你得到的东西接近-PI / 2,你可以得到大约PI / 2)。

我们可以在GLSL atan(y,x)之上构建一个好的,简单的实现,以使其更强大?

5 个答案:

答案 0 :(得分:10)

我将回答我自己的问题,分享我的知识。我们首先注意到当x接近零时发生不稳定。但是,我们也可以将其翻译为abs(x) << abs(y)。首先,我们将平面(假设我们在单位圆上)划分为两个区域:一个位于|x| <= |y|,另一个位于|x| > |y|,如下所示:

two regions

我们知道atan(x,y)在绿色区域更稳定 - 当x接近于零时,我们只需要接近atan(0.0)的东西,这在数值上非常稳定,而通常{{1} }在橙色区域更稳定。你也可以说服自己这种关系:

atan(y,x)

适用于未定义的所有非原点(x,y),我们讨论的是atan(x,y) = PI/2 - atan(y,x) 能够在-PI,PI的整个范围内返回角度值,而不是{{ 1}}只返回-PI / 2,PI / 2之间的角度。因此,我们对GLSL的强大atan(y,x)例程非常简单:

atan(y_over_x)

作为旁注,数学函数atan2()的标识实际上是:

float atan2(in float y, in float x)
{
    bool s = (abs(x) > abs(y));
    return mix(PI/2.0 - atan(x,y), atan(y,x), s);
}

这是真的,因为它的范围是(-PI / 2,PI / 2)。

graph

答案 1 :(得分:10)

根据您的目标平台,这可能是一个已解决的问题。 The OpenGL spec for atan(y, x)指定它应该适用于所有象限,只有当x和y都为0时才会保持行为不确定。

因此,期望任何体面的实现在所有轴附近都是稳定的,因为这是2-argument atan 背后的全部目的(或 atan2 )。

提问者/回答者是正确的,因为某些实现确实采用了快捷方式。然而,接受的解决方案假设当x接近零时,错误的实现总是不稳定的:在某些硬件(例如我的Galaxy S4)上,当 x 接近时,该值稳定零,但 y 接近零时不稳定。

为了测试你的GLSL渲染器atan(y,x)的实现,这里是一个WebGL测试模式。按照下面的链接,只要你的OpenGL实现得体,你应该看到这样的东西:

GLSL atan(y,x) test pattern

使用原生atan(y,x)测试模式: http://glslsandbox.com/e#26563.2

如果一切顺利,你应该看到8种不同的颜色(忽略中心)。

链接的演示样本atan(y,x)用于x和y的多个值,包括0,非常大和非常小的值。中心框是atan(0.,0.) - 在数学上未定义,实现方式各不相同。我在硬件测试中看到过0(红色),PI / 2(绿色)和NaN(黑色)。

这是已接受解决方案的测试页面。 注意:主机的WebGL版本缺少mix(float,float,bool),因此我添加了与规范匹配的实现。

使用接受回答的atan2(y,x)测试模式: http://glslsandbox.com/e#26666.0

答案 2 :(得分:6)

x=y=0的情况下,您提出的解决方案仍然失败。这里两个atan()函数都返回NaN。

此外,我不会依赖mix来在两种情况之间切换。我不确定这是如何实现/编译的,但是x * NaN和x + NaN的IEEE浮点规则再次在NaN中产生。因此,如果您的编译器确实使用了混合/插值,则结果应为x=0y=0的NaN。

这是另一个解决了我的问题的解决方法:

float atan2(in float y, in float x)
{
    return x == 0.0 ? sign(y)*PI/2 : atan(y, x);
}

x=0时角度可以是±π/ 2。这两者中的哪一个仅取决于y。如果y=0也是,则角度可以是任意的(向量的长度为0)。在这种情况下,sign(y)会返回0,这是正常的。

答案 3 :(得分:3)

有时,提高代码性能的最佳方法是避免首先调用它。例如,您可能想要确定矢量角度的原因之一是,您可以使用此角度使用角度的正弦和余弦的组合来构造旋转矩阵。然而,矢量的正弦和余弦(相对于原点)已经隐藏在矢量本身内部。您需要做的就是通过将每个矢量坐标除以矢量的总长度来创建矢量的标准化版本。这是计算向量角度的正弦和余弦的二维示例[x y]:

double length = sqrt(x*x + y*y);
double cos = x / length;
double sin = y / length;

一旦得到正弦和余弦值,您现在可以直接使用这些值填充旋转矩阵,以相同的角度执行任意向量​​的顺时针或逆时针旋转,或者您可以连接第二个旋转矩阵以旋转到零以外的角度。在这种情况下,您可以将旋转矩阵视为将任意向量的角度“归一化”为零。这种方法也可以扩展到三维(或N维)情况,尽管例如,你将有三个角度和六个正/成对对象来计算(每个平面一个角度)用于3D旋转。

在你可以使用这种方法的情况下,你可以通过完全绕过atan计算获得巨大的胜利,这是可能的,因为你想要确定角度的唯一原因是计算正弦和余弦值。通过跳过转换到角度空间和后退,您不仅可以避免担心被零除,而且还可以提高靠近极点的角度的精度,否则会受到乘以/除以大数的影响。我已经成功地在GLSL程序中使用了这种方法,该程序将场景旋转到零度以简化计算。

很容易陷入一个直接的问题,你可能会忽视你为什么首先需要这些信息。并不是说这在每种情况下都有效,但有时候开箱即用是有帮助的......

答案 4 :(得分:0)

一个公式,该公式给出了四个象限中任意值的角度

坐标x和y的

。对于x = y = 0,结果是不确定的。

f(x,y)= pi()-pi()/ 2 *(1+符号(x))*(1-符号(y ^ 2))-pi()/ 4 *(2+符号(x))* sign(y)

   -sign(x*y)*atan((abs(x)-abs(y))/(abs(x)+abs(y)))