This is simple trigonometry, and won’t be a surprise to most of you. But it was a surprise to me. So, in case it helps anyone else…

Let’s say we have some sprite, that we want to rotate to face some other point. We can get the facing vector rather trivially: just subtract the two point vectors.

But we don’t want the facing vector, we want to rotate towards it. Well, we can get the angle with atan2(), and…

This, right here, is the error.

Let’s think about what we actually need to rotate a vector by an angle theta:

x' = x * cos(theta) - y * sin(theta); y' = x * sin(theta) + y * cos(theta);

or, in matrix form:

[ cos(theta) -sin(theta) ] [ sin(theta) cos(theta) ]

Note that we only ever use the angle to find its sine and cosine. We don’t really *care* what theta is, it’s not relevant to the rotation. But how can we find sine and cosine without the angle?

Well, we have a vector. Now, if we think back to the very beginning of trigonometry, you’ll probably remember a mnemonic: SOH CAH TOA.

Our vector forms a right triangle by casting a vertical line to the x-axis. Therefore:

sine = opposite / hypotenuse cosine = adjacent / hypotenuse

or:

sine = y / length cosine = x / length

If our facing vector is normalized, the length terms fall out and we can just use the x and y components as sine and cosine directly. So:

x' = x * facing_x - y * facing_y; y' = x * facing_y + y * facing_x;

As long as we know (or can easily find) the facing vector we want, no transcendentals are involved in calculating the rotation matrix. At worst, we need a square root to normalize the facing vector. In the (very rare, now) case that we *do* want to set facing with an angle, we can simply call sin/cos there. (We lose the ability to do sin/cos with 4-wide or 8-wide SIMD when batch-calculating transforms, but since we’ll be calling it a lot less that is a net win.)

So there we go. It’s easy to settle for angles in 2D, where they almost, kind of, work. But without, there are fewer transcendentals, less concern about range, everything is simpler. And no kittens murdered.