Monday, February 18, 2019

xna - 360+ degree rotation skips back to 0 degrees when using Math.Atan2(y, x)


I'm new to XNA and this is my first actual project, so forgive my noobness.



I'm using


 jointAngle = System.Math.Atan2(RightStick.Y, RightStick.X);

in order to set an angle of a joint (farseer) so that the attached body points in the direction pushed with the right joystick. This all works great and i get values from negative pi to pi which correlate with the pushed direction. The problem arises when i go from 360 degrees to 361. Instead of the continued rotation beyond 360 degrees that one might expect, it jumps back to 0 degrees (or -3.14 radians), which causes a jerking motion as the joint suddenly shifts 359 degrees back instead of 1 forward.


Is there a simple way to account for this? I came up with a few solutions, but they're all pretty messy at best and completely unreliable at worst.


Please help!



Answer



atan2 is a mathematical function; it is stateless. There is nothing in it to “know” that you want an angle which is close to the angle from the previous frame, as opposed to an angle which is simply sufficient to identify the direction.


You must write your own logic to handle this case. There are several possible behaviors you could implement; here's the ones I've thought of. Note that I am not familiar with C#, XNA, or Farseer, so I have only pseudocode here, but all of these should work as long as you can retrieve the joint's current actual angle as well as the target angle.





  1. The joint should spin the smallest amount to match the direction of the joystick. This means that if the user spins the stick faster than the joint can keep up, it will start turning the other direction to make a shorter motion. This can be implemented as:


    let currentStickDirection = atan2(RightStick.Y, RightStick.X)
    let currentJointDirection = currentJointAngle mod 2π
    let difference = ((currentStickDirection - currentJointDirection + π) mod 2π) - π
    jointTargetAngle = currentJointAngle + difference

    The addition and subtraction of π (a half-turn) combined with modulus puts the difference-between-angles in the range −π…+π, which is the “smallest amount to match” property.


    Note that if you are implementing, for example, a turret, then this might be undesirable as it makes it harder to strafe a particular arc as the player needs to make sure not to lead too much.





  2. The joint should follow the motions of the stick. That is, if the user quickly makes multiple revolutions of the stick, the joint will follow and make multiple revolutions even if the user's movement is long over.


    To implement this, the logic is the same as above except that instead of using the joint's current actual angle, we use the joint's target angle (which can be simply a variable/field in your program, unrelated to the physics engine) as the state input.


    let currentStickDirection = atan2(RightStick.Y, RightStick.X)
    let currentJointDirection = jointTargetAngle mod 2π
    let difference = ((currentStickDirection - currentJointDirection + π) mod 2π) - π
    jointTargetAngle += difference


  3. Same as option 2, but the joint will never make an unnecessary full revolution. This differs from option 1 in that the joint will always turn in the same direction as the input does.



    To implement this, use the code from option 2, but after all of the above we also remove extra ±2πs from the target angle:


     let revolutions = (jointTargetAngle - currentJointAngle) / 2π
    if (revolutions >= 1)
    jointTargetAngle -= floor(revolutions) * 2π
    else if (revolutions <= -1)
    jointTargetAngle -= ceil(revolutions) * 2π




Note that if the stick is near its center, then small motions, possibly including noise/vibration rather than user motion, will cause large changes in the computed angle. If you haven't already, you will probably want to add a “dead zone” around the center which disables the angle computation. This can be a simple distance condition, like pow(RightStick.Y, 2) + pow(RightStick.X, 2) < pow(deadZoneRadius, 2).



Another interesting option would be to reduce the “motor power” of the joint (I don't know what exact options Farseer provides in this area) depending on the computed distance, so that small pushes on the stick can be used to slowly swing the joint (note that then the imprecision of direction matters less) and moving it to the limit does a fast swing. This would naturally reduce the effect of noise about the center, but unless you are using option 1 above (which does not depend on the previous target angle) and your joystick has perfect mechanical return-to-center, you will still want to have a deadzone.




Commentary: The two key principles here are:



  1. Work with differences between angles, not absolute angles.

  2. Make sure that (where appropriate for the application) you are treating any particular angle θ as equivalent to θ + 2π. This is the reason for all of the uses of modulo; the π offsets are used to control where we flip from positive to negative numbers, which affects the overall behavior of the algorithm, but not in a way which is dependent on the input range.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...