18

In GLSL (specifically 3.00 that I'm using), there are two versions of atan(): atan(y_over_x) can only return angles between -PI/2, PI/2, while atan(y/x) can take all 4 quadrants into account so the angle range covers everything from -PI, PI, much like atan2() in C++.

I would like to use the second atan to convert XY coordinates to angle. However, atan() in GLSL, besides not able to handle when x = 0, is not very stable. Especially where x is close to zero, the division can overflow resulting in an opposite resulting angle (you get something close to -PI/2 where you suppose to get approximately PI/2).

What is a good, simple implementation that we can build on top of GLSL atan(y,x) to make it more robust?

HuaTham
  • 7,486
  • 5
  • 31
  • 50
  • "atan() in GLSL, besides not able to handle when x = 0..." -- This is incorrect: `atan(y,x)` is designed to handle cases where either x or y (but not both) is zero. `atan(y,0.)` returns `PI/2` when y is positive, and `-PI/2` when y is negative. – Jonathan Lidbeck Jul 10 '15 at 20:17
  • 2
    Based on the spec, https://www.opengl.org/sdk/docs/man/html/atan.xhtml, the result is undefined when x = 0. – HuaTham Jul 15 '15 at 03:30
  • HuaTham: you're correct. yikes. Well, it seems at least that it's commonly implemented--I haven't seen the atan(y,0) cases fail on any devices I've run the test pattern on, but evidently it's not behavior to depend on. Thanks for linking to the spec. – Jonathan Lidbeck Jul 16 '15 at 01:20
  • 4
    @Jonathan I was puzzled by this too, but it turns out that web page is **not accurate**. In fact, the [full spec](https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.pdf#page=149) (§8.1, p143) states what you would expect - that either x or y can be 0 but not both. Not sure how the web page ended up like that. – AnOccasionalCashew Feb 20 '18 at 05:22
  • 2
    @AnOccasionalCashew from your full spec, `atan(y,x)` returns "an angle whose tangent is y / x." Taking this as-is, you can see that its spec doesn't guarantee a usable result when `x = 0` because `y/0` is undefined. This is still consistent with the web version. – HuaTham Mar 12 '19 at 07:37

5 Answers5

14

I'm going to answer my own question to share my knowledge. We first notice that the instability happens when x is near zero. However, we can also translate that as abs(x) << abs(y). So first we divide the plane (assuming we are on a unit circle) into two regions: one where |x| <= |y| and another where |x| > |y|, as shown below:

two regions

We know that atan(x,y) is much more stable in the green region -- when x is close to zero we simply have something close to atan(0.0) which is very stable numerically, while the usual atan(y,x) is more stable in the orange region. You can also convince yourself that this relationship:

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

holds for all non-origin (x,y), where it is undefined, and we are talking about atan(y,x) that is able to return angle value in the entire range of -PI,PI, not atan(y_over_x) which only returns angle between -PI/2, PI/2. Therefore, our robust atan2() routine for GLSL is quite simple:

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

As a side note, the identity for mathematical function atan(x) is actually:

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

which is true because its range is (-PI/2, PI/2).

graph

HuaTham
  • 7,486
  • 5
  • 31
  • 50
  • 1
    The accepted answer actually worsens the problem on my Samsung Galaxy S4, whose implementation of atan(Y,X) fails for small values of Y, not X. – Jonathan Lidbeck Jul 16 '15 at 17:56
12

Depending on your targeted platform, this might be a solved problem. The OpenGL spec for atan(y, x) specifies that it should work in all quadrants, leaving behavior undefined only when x and y are both 0.

So one would expect any decent implementation to be stable near all axes, as this is the whole purpose behind 2-argument atan (or atan2).

The questioner/answerer is correct in that some implementations do take shortcuts. However, the accepted solution makes the assumption that a bad implementation will always be unstable when x is near zero: on some hardware (my Galaxy S4 for example) the value is stable when x is near zero, but unstable when y is near zero.

To test your GLSL renderer's implementation of atan(y,x), here's a WebGL test pattern. Follow the link below and as long as your OpenGL implementation is decent, you should see something like this:

GLSL atan(y,x) test pattern

Test pattern using native atan(y,x): http://glslsandbox.com/e#26563.2

If all is well, you should see 8 distinct colors (ignoring the center).

The linked demo samples atan(y,x) for several values of x and y, including 0, very large, and very small values. The central box is atan(0.,0.)--undefined mathematically, and implementations vary. I've seen 0 (red), PI/2 (green), and NaN (black) on hardware I've tested.

Here's a test page for the accepted solution. Note: the host's WebGL version lacks mix(float,float,bool), so I added an implementation that matches the spec.

Test pattern using atan2(y,x) from accepted answer: http://glslsandbox.com/e#26666.0

Jonathan Lidbeck
  • 1,555
  • 1
  • 14
  • 15
  • Based on the GLSL spec, the `atan` function finds an angle over `y/x`. It's natural to see that as `x` gets closer to zero, `y/x` becomes numerically unstable. So I think Samsung S4 having instability near `y = 0` means its implementation is incompatible with the spec. It might be computing `atan(x/y)` internally before subtracting it from `pi/2` (for quadrant 1), for example. I don't have access to a Samsung device but the rendered results look perfectly fine for both implementations on Mac / iPhone as of today (2019). – HuaTham Mar 12 '19 at 07:32
  • 1
    Not exactly. The spec says atan "Returns an angle *whose tangent* is y/x". This does not require *actually computing* y/x. x/y is equally useful, but only a lousy implementation would rely on one or the other (such as the S4, which only uses x/y). – Jonathan Lidbeck Apr 04 '19 at 21:55
9

Your proposed solution still fails in the case x=y=0. Here both of the atan() functions return NaN.

Further I would not rely on mix to switch between the two cases. I am not sure how this is implemented/compiled, but IEEE float rules for x*NaN and x+NaN result again in NaN. So if your compiler really used mix/interpolation the result should be NaN for x=0 or y=0.

Here is another fix which solved the problem for me:

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

When x=0 the angle can be ±π/2. Which of the two depends on y only. If y=0 too, the angle can be arbitrary (vector has length 0). sign(y) returns 0 in that case which is just ok.

Johannes Jendersie
  • 980
  • 10
  • 16
  • 2
    Mathematically it's undefined when (x,y) = (0,0). What angle do you expect at the origin? It can be anything. Furthermore, please read my post carefully. My mix() version uses a boolean interpolator, not float. – HuaTham Dec 02 '14 at 00:47
  • 2
    I did not say that the angle is defined for (x,y)=(0,0). I just set it to zero as a valid choice. Could be any angle here but NaN bloated up my program (NaN is no valid angle even if it can be chosen arbitrary) Did not know that there is a boolean mix operation (but you are right). – Johannes Jendersie Dec 02 '14 at 14:23
5

Sometimes the best way to improve the performance of a piece of code is to avoid calling it in the first place. For example, one of the reasons you might want to determine the angle of a vector is so that you can use this angle to construct a rotation matrix using combinations of the angle's sine and cosine. However, the sine and cosine of a vector (relative to the origin) are already hidden in plain sight inside the vector itself. All you need to do is to create a normalized version of the vector by dividing each vector coordinate by the total length of the vector. Here's the two-dimensional example to calculate the sine and cosine of the angle of vector [ x y ]:

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

Once you have the sine and cosine values, you can now directly populate a rotation matrix with these values to perform a clockwise or counterclockwise rotation of arbitrary vectors by the same angle, or you can concatenate a second rotation matrix to rotate to an angle other than zero. In this case, you can think of the rotation matrix as "normalizing" the angle to zero for an arbitrary vector. This approach is extensible to the three-dimensional (or N-dimensional) case as well, although for example you will have three angles and six sin/cos pairs to calculate (one angle per plane) for 3D rotation.

In situations where you can use this approach, you get a big win by bypassing the atan calculation completely, which is possible since the only reason you wanted to determine the angle was to calculate the sine and cosine values. By skipping the conversion to angle space and back, you not only avoid worrying about division by zero, but you also improve precision for angles which are near the poles and would otherwise suffer from being multiplied/divided by large numbers. I've successfully used this approach in a GLSL program which rotates a scene to zero degrees to simplify a computation.

It can be easy to get so caught up in an immediate problem that you can lose sight of why you need this information in the first place. Not that this works in every case, but sometimes it helps to think out of the box...

0

A formula that gives an angle in the four quadrants for any value

of coordinates x and y. For x=y=0 the result is undefined.

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

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