I’ve had this issue in past 2D games all the time. The ability to interpolate 2D rotations for basic animations for anyone that doesn’t understand quaternions becomes a rather laborious task. I’ve done messy if-then statements to try to catch all the cases where rotation hits the ‘360 to 0’ boundary and it’s always ugly and seemed like there had to be a more mathematically correct way to interpolate a circular value.
Well for anyone that sticks with it long enough, they undoubtedly run into quaternions. What are quaternions? I’m not about to explain it in detail, but I like to think of them as a way to express a 3D rotations and not having that annoying gimbal lock problem.
So how do quaternions help our 2D rotation needs? Well if you’re in a situation where you need to interpolate between the shortest path between 2 angles then Quaternions can help. You have a few choices to solve this problem:
- Use if-then clauses to try to catch all the cases when you hit a rotational boundary if you are rotating from, for example, 300 degrees to 2 degrees. You would want to rotate all the way through 360 instead of going backwards.
- Use quaternions, which are more applicable to 3D rotations, to interpolate between to angles using spherical interpolation (slerp for short).
- Use spinors, which are a very similar concept to quaternions, except they are more catered to 2D rotations.
You’ll eventually see that using quaternions is somewhat overdoing it as 2 axises are never used in any calculation in 2D. If you eliminate these 2 axises from the quaternion you end up with a spinor! Thanks to Frank, of Chaotic Box, who helped lead me down that direction for my own iPhone game.
To help test out the concept and make sure it’s working I ended up writing a Blitzmax quickie app using what little I know about quaternions. I’m certainly a bit iffy when it comes to dealing with complex numbers, as I don’t fully grok them as I do normal vector math, but I make do. So if anyone reads this post and sees some sort of logical error in my code, by all means leave a comment and I’ll improve the listing.
So to get started I needed a way to represent a 2D rotation as a spinor. So I created a spinor type with some basic math functions (all of them analogous to how quaternions work):
Type Spinor Field real:Float Field complex:Float Function CreateWithAngle:Spinor(angle:Float) Local s:Spinor = New Spinor s.real = Cos(angle) s.complex = Sin(angle) Return s End Function Function Create:Spinor(realPart:Float, complexpart:Float) Local s:Spinor = New Spinor s.complex = complexPart s.real = realPart Return s End Function Method GetScale:Spinor(t:Float) Return Spinor.Create(real \* t, complex \* t) End Method Method GetInvert:Spinor() Local s:Spinor = Spinor.Create(real, -complex) Return s.GetScale(s.GetLengthSquared()) End Method Method GetAdd:Spinor(other:Spinor) Return Spinor.Create(real + other.real, complex + other.complex) End Method Method GetLength:Float() Return Sqr(real \* real + complex \* complex) End Method Method GetLengthSquared:Float() Return (real \* real + complex \* complex) End Method Method GetMultiply:Spinor(other:Spinor) Return Spinor.Create(real \* other.real – complex \* other.complex, real \* other.complex + complex \* other.real) End Method Method GetNormalized:Spinor() Local length:Float = GetLength() Return Spinor.Create(real / length, complex / length) End Method Method GetAngle:Float() Return ATan2(complex, real) \* 2 End Method Function Lerp:Spinor(startVal:Spinor, endVal:Spinor, t:Float) Return startVal.GetScale(1 – t).GetAdd(endVal.GetScale(t)).GetNormalized() End Function Function Slerp:Spinor(from:Spinor, dest:Spinor, t:Float) Local tr:Float Local tc:Float Local omega:Float, cosom:Float, sinom:Float, scale0:Float, scale1:Float ‘calc cosine cosom = from.real \* dest.real + from.complex \* dest.complex ‘adjust signs If (cosom < 0) Then cosom = -cosom tc = -dest.complex tr = -dest.real Else tc = dest.complex tr = dest.real End If ' coefficients If (1 - cosom) > 0.001 Then ‘threshold, use linear interp if too close omega = ACos(cosom) sinom = Sin(omega) scale0 = Sin((1 – t) _ omega) / sinom scale1 = Sin(t _ omega) / sinom Else scale0 = 1 – t scale1 = t End If ‘ calc final Local res:Spinor = Spinor.Create(0, 0) res.complex = scale0 \* from.complex + scale1 \* tc res.real = scale0 \* from.real + scale1 \* tr Return res End Function End Type
One caveat in the above code is that any angle from 0-360 is mapped to 0-180 in the Spinor. What does this mean? it means if I want to represent the angle 270 I need to divide it by 2, creating the spinor with the angle 135. Now when I call GetAngle() it will return 270. This allows us to smoothly rotate the whole 360 degrees correctly. This is explained here in more detail.
So now I want to spherically interpolate (slerp) between 2 2D angles. Well, if I call Slerp() on the spinor type it’ll do just that, giving me another spinor that sits in between the 2 angles. I’ve read a couple of articles in the past on slerp and how to implement it, but in order to get things done I just used a publically listed code snippet and ported it to blitzmax. That whole article is worth the read and recommended, even if you’re just using 2D.
Now that the concept of spinors is encapsulated in the Spinor type I wanted just a basic convenience function that I can feed 2 angles and get another angle out:
Function Slerp2D:Float(fromAngle:Float, toAngle:Float, t:Float) Local from:Spinor = Spinor.Create(Cos(fromangle / 2), Sin(fromangle / 2)) Local toSpinor:Spinor = Spinor.Create(Cos(toAngle / 2), Sin(toAngle / 2)) Return Spinor.Slerp(from, toSpinor, t).GetAngle() End Function
the ‘t’ variable is the time variable from 0 to 1 that determines how far to interpolate between the 2 angles. Giving it a value of 1 means you will get toAngle out, and a value of 0 is equal to fromAngle and anything in-between is just that…in-between.
Now I can call Slerp2D(300, 10, 0.5) And get the correct angle without worrying if I’m interpolating in the right direction :).
Also, I want to point out that the above code was just used in a quick proof of concept app. It’s somewhat sloppy and to be honest, I’m not sure I named the portions of the spinor correctly (real,complex), but it works. For clarity and to save time I didn’t inline the Slerp2D functions or the Spinor type methods, so it generates a lot of garbage. You would need to optimize this to use it in a tight loop somewhere. Any suggestions are welcomed.
As for references, I did some searching and got most of the ‘missing link’ materials I needed to bridge the gap between quaternions and spinors from:
- Frank’s thread in iDevGames about 2D interpolation
- Euclidean Space’s explanations on rotations using complex numbers.
- The gamasutra article showing a simple implementation of slerp using quaternions.