Search code examples
c++glslcoordinatesatan2numerical-stability

Robust atan(y,x) on GLSL for converting XY coordinate to angle


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?


Solution

  • 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