Preface: this answer assumes some knowledge of linear algebra, trigonometry, and rotations/transformations.
Your problem stems from the usage of rotation angles. Due to the discontinuous nature of the inverse trigonometric functions, it is quite difficult (if not outright impossible) to eliminate "jumps" in the value of the functions for relatively close inputs. Specifically, when x < 0
, atan2(+0, x) = +pi
(where +0
is a positive number very close to zero), but atan2(-0, x) = -pi
. This is exactly why you experience the difference of 2 * pi
which causes your problem.
Because of this, it is often better to work directly with vectors, rotation matrices and/or quaternions. They use angles as arguments to trigonometric functions, which are continuous and eliminate any discontinuities. In our case, spherical linear interpolation (slerp) should do the trick.
Since your code measures the angle formed by the relative position of the mouse to the absolute rotation of the object, our goal boils down to rotating the object such that the local axis (1, 0)
(= (cos rotateRad, sin rotateRad)
in world space) points towards the mouse. In effect, we have to rotate the object such that (cos p.rotateRad, sin p.rotateRad)
equals (win.MousePosition().Y - p.pos.Y, win.MousePosition().X - p.pos.X).normalized
.
How does slerp come into play here? Considering the above statement, we simply have to slerp geometrically from (cos p.rotateRad, sin p.rotateRad)
(represented by current
) to (win.MousePosition().Y - p.pos.Y, win.MousePosition().X - p.pos.X).normalized
(represented by target
) by an appropriate parameter which will be determined by the rotation speed.
Now that we have laid out the groundwork, we can move on to actually calculating the new rotation. According to the slerp formula,
slerp(p0, p1; t) = p0 * sin(A * (1-t)) / sin A + p1 * sin (A * t) / sin A
Where A
is the angle between unit vectors p0
and p1
, or cos A = dot(p0, p1)
.
In our case, p0 == current
and p1 == target
. The only thing that remains is the calculation of the parameter t
, which can also be considered as the fraction of the angle to slerp through. Since we know that we are going to rotate by an angle p.turnSpeed * dt
at every time step, t = p.turnSpeed * dt / A
. After substituting the value of t
, our slerp formula becomes
p0 * sin(A - p.turnSpeed * dt) / sin A + p1 * sin (p.turnSpeed * dt) / sin A
To avoid having to calculate A
using acos
, we can use the compound angle formula for sin
to simplify this further. Note that the result of the slerp operation is stored in result
.
result = p0 * (cos(p.turnSpeed * dt) - sin(p.turnSpeed * dt) * cos A / sin A) + p1 * sin(p.turnSpeed * dt) / sin A
We now have everything we need to calculate result
. As noted before, cos A = dot(p0, p1)
. Similarly, sin A = abs(cross(p0, p1))
, where cross(a, b) = a.X * b.Y - a.Y * b.X
.
Now comes the problem of actually finding the rotation from result
. Note that result = (cos newRotation, sin newRotation)
. There are two possibilities:
- Directly calculate
rotateRad
by p.rotateRad = atan2(result.Y, result.X)
, or
If you have access to the 2D rotation matrix, simply replace the rotation matrix with the matrix
|result.X -result.Y|
|result.Y result.X|