14

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

我想使用第二个atan将 XY 坐标转换为角度。但是,atan()在 GLSL 中,除了不能处理 when 之外x = 0,还不是很稳定。尤其是在x接近零的情况下,除法可能会溢出,从而导致相反的结果角度(您会得到接近 -PI/2 的东西,而您假设会得到大约 PI/2)。

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

4

5 回答 5

13

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

两个地区

我们知道这atan(x,y)在绿色区域要稳定得多——当 x 接近于零时,我们只是有一个接近 atan(0.0) 的值,它在数值上非常稳定,而通常atan(y,x)在橙色区域更稳定。你也可以说服自己这种关系:

atan(x,y) = PI/2 - atan(y,x)

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

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);
}

作为旁注,数学函数的恒等式atan(x)实际上是:

atan(x) + atan(1/x) = sgn(x) * PI/2

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

图形

于 2014-09-27T01:27:09.607 回答
12

根据您的目标平台,这可能是一个已解决的问题。atan(y, x)的 OpenGL 规范规定它应该在所有象限中工作,只有当 x 和 y 都为 0 时才定义行为。

所以人们会期望任何体面的实现在所有轴附近都是稳定的,因为这是 2 参数atan(或atan2)背后的全部目的。

提问者/回答者是正确的,因为某些实现确实走捷径。然而,公认的解决方案假设当 x 接近于零时,糟糕的实现总是不稳定的:在某些硬件(例如我的 Galaxy S4)上,当x接近于零时值是稳定的,但当y接近于零时不稳定。

要测试 GLSL 渲染器的 实现atan(y,x),这里有一个 WebGL 测试模式。按照下面的链接,只要您的 OpenGL 实现不错,您应该会看到如下内容:

GLSL atan(y,x) 测试模式

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

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

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

这是已接受解决方案的测试页面。注意:宿主的 WebGL 版本缺少mix(float,float,bool),所以我添加了一个符合规范的实现。

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

于 2015-07-10T20:00:02.643 回答
8

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

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

这是另一个为我解决问题的修复程序:

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

于 2014-12-01T12:51:47.340 回答
5

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

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

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

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

很容易陷入眼前的问题,以至于您可能会忽略为什么首先需要这些信息。并不是说这在每种情况下都有效,但有时它有助于跳出框框思考......

于 2015-10-13T20:30:30.300 回答
0

给出任意值在四个象限中的角度的公式

坐标 x 和 y。对于 x=y=0,结果未定义。

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

   -sign(x*y)*atan((abs(x)-abs(y))/(abs(x)+abs(y))) 
于 2019-01-17T12:38:11.230 回答