Search code examples
javamathgame-physics2d-games

How do you convert between polar coordinates and cartesian coordinates assuming north is zero radians?


I've been trying to make a simple physics engine for games. I am well aware that this is re-inventing the wheel but it's more of a learning exercise than anything else. I never expect it to be as complete as box2d for instance.

I'm having issues with my implementation of 2d Vectors. The issue is related to the fact that in the game world I want to represent north as being zero radians and east as being 1/2 PI Radians, or 0 and 90 degrees respectively. However in mathematics (or maybe more specifically the Math class of Java), I believe trigonometry functions like sine and cosine assume that "east" is zero radians and I think north is 1/2 PI Radians?

Anyway I've written a small version of my vector class that only demonstrates the faulty code.

public class Vector {
    private final double x;
    private final double y;

    public Vector(double xIn, double yIn) {
        x = xIn;
        y = yIn;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public double getR() {
        return Math.sqrt((x * x) + (y * y));
    }

    public double getTheta() {
        return Math.atan(y / x);
    }

    public double bearingTo(Vector target) {
        return (Math.atan2(target.getY() - y, target.getX() - x));
    }

    public static Vector fromPolar(double magnitude, double angle) {
        return new Vector(magnitude * Math.cos(angle),
                          magnitude * Math.sin(angle));
    }
}

And here is the test code to demonstrate the issue:

public class SOQuestion {
public static void main(String[] args) {
    //This works just fine
    Vector origin = new Vector(0, 0);
    Vector target = new Vector(10, 10);

    double expected = Math.PI * 0.25;
    double actual = origin.bearingTo(target);

    System.out.println("Expected: " + expected);
    System.out.println("Actual: " + actual);

    //This doesn't work
    origin = new Vector(0, 0);
    target = new Vector(10, 0);

    expected = Math.PI * 0.5; //90 degrees, or east.
    actual = origin.bearingTo(target); //Of course this is going to be zero, because in mathematics right is zero

    System.out.println("Expected: " + expected);
    System.out.println("Actual: " + actual);


    //This doesn't work either
    Vector secondTest = Vector.fromPolar(100, Math.PI * 0.5); // set the vector to the cartesian coordinates of (100,0)
    System.out.println("X: " + secondTest.getX()); //X ends up being basically zero
    System.out.println("Y: " + secondTest.getY()); //Y ends up being 100
} }

The requirements are:

  1. fromPolar(magnitude,angle) should return a vector with x and y initialized to the appropriate values assuming north is at zero radians and east is at 1/2 PI radians. for example fromPolar(10,PI) should construct a vector with x: 0 and y: -1

  2. getTheta() should return a value greater than or equal to zero and less than 2 PI. Theta is the angular component of the vector it's called on. For example a vector with x:10 and y:10 would return a value of 1/4 PI when getTheta() is called.

  3. bearingTo(target) should return a value that is greater than or equal to zero and less than 2 PI. The value represents the bearing to another vector.

The test code demonstrates that when you try to get the bearing of one point at (0,0) to another point at (10,0), it doesn't produce the intended result, it should be 90 degrees or 1/2 PI Radians.

Likewise, trying to initialize a vector from polar coordinates sets the x and y coordinate to unexpected values. I'm trying to avoid saying "incorrect values" since it' not incorrect, it just doesn't meet the requirements.

I've messed around with the code a lot, adding fractions of PI here or taking it away there, switching sine and cosine, but all of these things only fix parts of the problem and never the whole problem.

Finally I made a version of this code that can be executed online http://tpcg.io/OYVB5Q


Solution

  • Typical polar coordinates 0 points to the East and they go counter-clockwise. Your coordinates start at the North and probably go clockwise. The simplest way to fix you code is to first to the conversion between angles using this formula:

    flippedAngle = π/2 - originalAngle
    

    This formula is symmetrical in that it converts both ways between "your" and "standard" coordinates. So if you change your code to:

        public double bearingTo(Vector target) {
            return Math.PI/2 - (Math.atan2(target.getY() - y, target.getX() - x));
        }
    
        public static Vector fromPolar(double magnitude, double angle) {
            double flippedAngle = Math.PI/2 - angle;
            return new Vector(magnitude * Math.cos(flippedAngle),
                    magnitude * Math.sin(flippedAngle));
        }
    

    It starts to work as your tests suggest. You can also apply some trigonometry knowledge to not do this Math.PI/2 - angle calculation but I'm not sure if this really makes the code clearer.

    If you want your "bearing" to be in the [0, 2*π] range (i.e. always non-negative), you can use this version of bearingTo (also fixed theta):

    public class Vector {
        private final double x;
        private final double y;
    
        public Vector(double xIn, double yIn) {
            x = xIn;
            y = yIn;
        }
    
        public double getX() {
            return x;
        }
    
        public double getY() {
            return y;
        }
    
        public double getR() {
            return Math.sqrt((x * x) + (y * y));
        }
    
        public double getTheta() {
            return flippedAtan2(y, x);
        }
    
        public double bearingTo(Vector target) {
            return flippedAtan2(target.getY() - y, target.getX() - x);
        }
    
        public static Vector fromPolar(double magnitude, double angle) {
            double flippedAngle = flipAngle(angle);
            return new Vector(magnitude * Math.cos(flippedAngle),
                    magnitude * Math.sin(flippedAngle));
        }
    
        // flip the angle between 0 is the East + counter-clockwise and 0 is the North + clockwise
        // and vice versa
        private static double flipAngle(double angle) {
            return Math.PI / 2 - angle;
        }
    
        private static double flippedAtan2(double y, double x) {
            double angle = Math.atan2(y, x);
            double flippedAngle = flipAngle(angle);
            //  additionally put the angle into [0; 2*Pi) range from its [-pi; +pi] range
            return (flippedAngle >= 0) ? flippedAngle : flippedAngle + 2 * Math.PI;
        }
    }