35

NB: I'll present this question in degrees purely for simplicity, radians, degrees, different zero-bearing, the problem is essentially the same.

Does anyone have any ideas on the code behind rotational interpolation? Given a linear interpolation function: Lerp(from, to, amount), where amount is 0...1 which returns a value between from and to, by amount. How could I apply this same function to a rotational interpolation between 0 and 360 degrees? Given that degrees should not be returned outside 0 and 360.

Given this unit circle for degrees:

Unit Circle

where from = 45 and to = 315, the algorithm should take the shortest path to the angle, i.e. it should go through zero, to 360 and then to 315 - and not all the way round 90, 180, 270 to 315.

Is there a nice way to achieve this? Or is it going to just be a horrid mess of if() blocks? Am I missing some well understood standard way of doing this? Any help would be appreciated.

Rob
  • 1,687
  • 3
  • 22
  • 34

10 Answers10

47

I know this is 2 years old, but I've recently been looking around for the same problem and I don't see an elegant solution without ifs posted in here, so here it goes:

    shortest_angle=((((end - start) % 360) + 540) % 360) - 180;
    return shortest_angle * amount;

that's it

ps: of course, % is meaning modulo and shortest_angle is the variable that holds the whole interpolation angle

user151496
  • 1,849
  • 24
  • 38
  • 4
    [Not all modulos behave the same](http://stackoverflow.com/q/1907565/190597), but in Python, this can be simplified to `shortest_angle = ((end-start) + 180) % 360 - 180`. – unutbu Apr 15 '15 at 18:36
  • 1
    Can you explain why the values? So it can be converted to radians instead of degrees. – Ismail Jun 29 '16 at 15:48
  • 1
    do `N/180*Pi` for every number i guess :) – user151496 Jun 30 '16 at 17:06
  • @Ismail I've asked for clarification here: http://math.stackexchange.com/questions/2144234/interpolating-between-2-angles – Jonathan Mee Feb 14 '17 at 16:30
  • @Ismail OK I've proved this answer works for all `end`-`start` combinations here: http://math.stackexchange.com/a/2144499/194115 – Jonathan Mee Feb 14 '17 at 19:53
  • 1
    @JonathanMee Thanks for your response! It's been a while! I have already made a solution in JavaScript, if you want you can use it: http://pastebin.com/wp15rK3v The function accepts a boolean determining if it is an angle or just an integer value. example: ```var tankRotation = new AnimatedValue(1,true); tankRotation.set(6.2,100);//6.2 = radians, 100 = 100ms time interpolation tankRotation.get(); // returns a value that is going to 0, and after reaching 0 it continues at PI*2``` – Ismail Feb 14 '17 at 21:20
  • @Ismail Nice solution! I'm working in [tag:c++] so I won't be able to take advantage of your work :( Just wanted you to know that I shared and appreciated your concern that this function works as advertised; and since it does I guess we can all go home happy! – Jonathan Mee Feb 14 '17 at 21:34
  • Indeed! Porting the code to C++ wouldn't be that hard, the solution is there only converting it to your prefered language. Make it like christmas time:) – Ismail Feb 14 '17 at 21:39
  • 2
    A previous editor had broken this answer. If you look at the previous version of this post, when `amount = 0`, the given answer would always return 0, instead of the `start` angle. I've changed it back to a working version. – frodo2975 Feb 26 '19 at 07:36
  • Should the last line read `(start + shortest_angle * amount) % 360` ? – Peter Nov 28 '19 at 00:51
  • i have edited the answer back to the good answer. would people kindly stop editing the answer and breaking the algorithm? – user151496 Mar 10 '21 at 22:06
  • This fixed my PID controller... but I had to look at the revision history and go with the one that was removed that mentioned the adding start to it. That version needs to be restored as the real one, seriously. – Jimmio92 Jun 19 '22 at 15:07
  • 1
    @Jimmio92 i think people misunderstand the answer. the function returns the amount of angle you have to change. of course, if you want to perform the rotation from the point you add its starting point to it – user151496 Jun 20 '22 at 10:14
13

Sorry, that was a bit convoluted, here's a more concise version:

    public static float LerpDegrees(float start, float end, float amount)
    {
        float difference = Math.Abs(end - start);
        if (difference > 180)
        {
            // We need to add on to one of the values.
            if (end > start)
            {
                // We'll add it on to start...
                start += 360;
            }
            else
            {
                // Add it on to end.
                end += 360;
            }
        }

        // Interpolate it.
        float value = (start + ((end - start) * amount));

        // Wrap it..
        float rangeZero = 360;

        if (value >= 0 && value <= 360)
            return value;

        return (value % rangeZero);
    }

Anyone got a more optimised version?

waterproof
  • 4,943
  • 5
  • 30
  • 28
Rob
  • 1,687
  • 3
  • 22
  • 34
8

I think a better approach is to interpolate sin and cos since they don't suffer form being multiply defined. Let w = "amount" so that w = 0 is angle A and w = 1 is angle B. Then

CS = (1-w)*cos(A) + w*cos(B);
SN = (1-w)*sin(A) + w*sin(B);
C = atan2(SN,CS);

One has to convert to radians and degrees as needed. One also has to adjust the branch. For atan2 C comes back in the range -pi to pi. If you want 0 to 2pi then just add pi to C.

Paul Colby
  • 89
  • 1
  • 1
  • Gven the question is old and has many answers, could you elaborate why this is better? – namezero May 08 '15 at 17:13
  • 1
    Better (at least for me) is I'm more likely to code it correctly the first time. The problem with most of the answers given is they take multiple arithmetic test with associated branches. Edge conditions on these tests also add a level of complexity I have screwed up on more than once. The initial question of "which way do I go?" to interpolate or which angle branch I'm on is answered uniquely from the get go. – Paul Colby May 20 '15 at 20:35
  • This is the only answer that worked **flawlessly**. Other answers seem to stutter some times in the occasional odd case. – Zoyt Jul 30 '15 at 11:45
  • 3
    FYI this doesn't work when a and b are very far apart (almost 180 degrees), and is not a true linear interpolation. – mklingen Feb 09 '16 at 12:01
  • Two questions to consider are 1) Is there a unique answer for the -180 case that makes any useful physical sense? 2) Why is pure linear interpolation to be preferred over the non-linear one suggested above? – Paul Colby Feb 10 '16 at 15:24
3

NB: using C# code

After some crazy rummaging around in my brain, here's what I've come up with. Basically the premise is to perform the 0-360 wrapping at the last minute. Deal internally with values outside 0-360 and then wrap them inside 0-360 at the point a value is requested from the function.

At the point where you pick a start an an end point, you perform the following:

float difference = Math.Abs(end - start);
if (difference > 180)
{
    // We need to add on to one of the values.
    if (end > start)
    {
        // We'll add it on to start...
        start += 360;
    }
    else
    {
        // Add it on to end.
        end += 360;
    }
}

This gives you the actual start and end values, which may be outside 0-360...

We have a wrap function to ensure a value is between 0 and 360...

public static float Wrap(float value, float lower, float upper)
{
    float rangeZero = upper - lower;

    if (value >= lower && value <= upper)
        return value;

    return (value % rangeZero) + lower;
}

Then at the point you request the current value from the function:

return Wrap(Lerp(start, end, amount), 0, 360);

This is almost certainly not the most optimal solution to the problem, however it does appear to work consistently. If anyone has any more optimal way to do this that would be great.

waterproof
  • 4,943
  • 5
  • 30
  • 28
Rob
  • 1,687
  • 3
  • 22
  • 34
2

I wanted to rewrite my answer to better explain answer the question. I'm using EXCEL for my formulas, and degrees for my units.

For simplicity, B is the larger of the two values, and A is the smaller of the two values. You can use MAX() and MIN() respectively in your solution later.

PART 1 - WHICH WAY TO GO?

What we want to do first is work out in which direction we want to perform the calculation, clockwise or anticlockwise. We use an IF() Statement for that:

IF( (B-A)<=180, (Clockwise_Formula), (AntiClockwise_Formula) )

The above formula checks if going anticlockwise from B to A (which is the same as going clockwise from A to B) is less than or equal to 180 degrees. If not, it's going to be shorter to go the other direction.

To check this works: 90 - 45 = 45 (which is less than or equal to 180) makes the IF statement TRUE, so the clockwise direction is shorter, but 315 - 45 = 270 (which is larger than 180) makes the if statement FALSE, so the anticlockwise formula would be shorter.

PART 2 - CLOCKWISE FORMULA

Now you want to interpolate N times between A and B, either clockwise or anticlockwise. The clockwise formula is relatively simple.

Clockwise_Formula: ((B-A)/N*S)+A

Where S is a count of the number of interpolations, starting at 1 and finishing at N-1 (If S = N, your answer will be B)

Example: A = 90, B = 270, N = 4

S=1:     ((270-90)/4*1)+90 = 135
S=2:     ((270-90)/4*2)+90 = 180
S=3:     ((270-90)/4*3)+90 = 225

PART 3 - ANITCLOCKWISE FORMULA

The anitclockwise formula is going to be a little more complex, because we'll need to cross anticlockwise over the 360 degree angle. The easiest method I can think of is to add 360 to A, then Modulate the answer by 360 using the MOD(FORMULA,VALUE) function.

You'll also have to swap A and B around in the formula because B is now the smallest number. (That might sound a bit confusing, but it works!)

(Unmodulated) AntiClockwise_Formula: (((A+360)-B)/N*S)+B

Example: A = 60, B = 300, N = 4

S=1:     (((60+360)-300)/4*1)+300 = 330
S=2:     (((60+360)-300)/4*2)+300 = 360
S=3:     (((60+360)-300)/4*3)+300 = 390

PART 4 - RESTRICTING ANSWERS TO BETWEEN 0 AND 360

See how sometimes (but not always) the answers are going to be greater than 360? This is where the wrapping your Anticlockwise_formula in a MOD() function comes in:

AntiClockwise_Formula: MOD((((A+360)-B)/N*S)+B,360)

Modulating the example used in Part 3 will give you:

S=1:     330
S=2:     0
S=3:     30

PART 5 - PUTTING IT ALL TOGETHER

Combining all of the elements from Parts 1-4 together, the answer is:

IF((B-A)<=180,((B-A)/N*S)+A,MOD((((A+360)-B)/N*S)+B,360))

Where:

A = The smaller of the two values (you can replace A with MIN())

B = The larger of the two values (you can replace B with MAX())

N = The number of interpolations you want to do (e.g. 2 is a half, 3 is into thirds etc)

S = An incrimental count to a max of N-1 (see Part 2 for explanation)

Adam
  • 27
  • 1
  • 7
Adam
  • 21
  • 1
0

My preferred way to deal with angle is to use units that are a power of 2 per revolution. For exanple, it you use 16 bit signed integers to represent -180 to +180 degrees, you can simply take (from-to)/num_steps to do your interpolation. Adding and subtracting angles always works, as the binary values overflow right at the point where you go from 360 to 0.

What you probably want to do in your case is math modulo 360. So angle differences are computed as (from-to)%360. There are still some sign issues with that which have been addressed in other SO questions.

phkahler
  • 5,687
  • 1
  • 23
  • 31
  • This answer does not address the main question. With `from` 10 degrees and `to` 350 degrees, your interpolation still has 180 degrees as midway point, whereas interpolation via the shortest route will have 0 degrees as midway point. – CvR Jul 23 '21 at 22:10
0

My solution to slerp of degrees. In my VarTracker class

    @classmethod
def shortest_angle(cls, start: float, end: float, amount: float):
    """ Find shortest angle change around circle from start to end, the return
        fractional part by amount.
    VarTracker.shortest_angle(10, 30, 0.1) --> 2.0
    VarTracker.shortest_angle(30, 10, 0.1) --> -2.0
    VarTracker.shortest_angle(350, 30, 0.1) --> 4.0
    VarTracker.shortest_angle(350, 30, 0.8) --> 32.0
    VarTracker.shortest_angle(30, 350, 0.5) --> -20.0
    VarTracker.shortest_angle(170, 190, 0.1) --> 2.0
    VarTracker.shortest_angle(10, 310, 0.5) --> -30.0
    """
    sa = ((((end - start) % 360) + 540) % 360) - 180;
    return sa * amount;

@classmethod
def slerp(cls, current: float, target: float, amount: float):
    """ Return the new value if spherical linear interpolation from current toward target, by amount, all in degrees.
    This method uses abs(amount) so sign of amount is ignored.
    current and target determine the direction of the lerp.
    Wraps around 360 to 0 correctly.

    Lerp from 10 degrees toward 30 degrees by 3 degrees
    VarTracker.slerp(10, 30, 3.0) --> 13.0
    Ignores sign of amount
    VarTracker.slerp(10, 30, -3.0) --> 13.0
    VarTracker.slerp(30, 10, 3.0) --> 27.0
    Wraps around 360 correctly
    VarTracker.slerp(350, 30, 6) --> 356.0
    VarTracker.slerp(350, 30, 12) --> 2.0
    VarTracker.slerp(30, 350, -35) --> 355.0
    a = VarTracker.slerp(30, 3140, -35) --> 355.0
    VarTracker.slerp(170, 190, 2) --> 172.0
    VarTracker.slerp(10, 310, 12) --> 358.0
    Wraps over 0 degrees correctly
    VarTracker.slerp(-10, 10, 3) --> 353.0
    VarTracker.slerp(10, -10, 12) --> 358
    """
    a = VarTracker.shortest_angle(current, target, 1.0)
    diff = target - current
    if np.abs(amount) > np.abs(diff):
        amount = diff
    if a < 0:
        amount = -np.abs(amount)
    else:
        amount = np.abs(amount)
    ret = current + amount
    while ret < 0:
        ret = ret + 360
    ret = ret % 360
    return ret
Richard Keene
  • 398
  • 3
  • 14
0

Modification of user151496 's answer (the original was in degrees and also giving me a wrong output):

 def interp_angle(theta_1, theta_2, ratio):
    shortest_angle = ((((theta_2 - theta_1) % (np.pi*2)) + np.pi) % (np.pi*2)) - np.pi
    return (theta_1 + shortest_angle * ratio) % (np.pi*2)

Tests: Running with

theta1, theta2 = 0, 0.5
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
theta1, theta2 = 0, 0.99
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
theta1, theta2 = 0, 1.01
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
theta1, theta2 = 0.1, -0.1
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))
theta1, theta2 = 0.1, 2-0.1
print('Average of {:.4g}pi rad and {:.4g}pi rad = {:.4g}pi rad'.format(theta1, theta2, interp_angle(theta1*np.pi, theta2*np.pi, 0.5)/np.pi))

Gives me:

Average of 0pi rad and 0.5pi rad = 0.25pi rad
Average of 0pi rad and 0.99pi rad = 0.495pi rad
Average of 0pi rad and 1.01pi rad = 1.505pi rad
Average of 0.1pi rad and -0.1pi rad = 0pi rad
Average of 0.1pi rad and 1.9pi rad = 0pi rad
Peter
  • 12,274
  • 9
  • 71
  • 86
0

For this problem, if you have angles in the range +-pi use this: ((end - start + pi)%tau + tau)%tau - pi

0

My personal recommendation?: Don't! Similar to 3d rotation with Euler angles , I find that using a higher dimension abstraction far less error prone and much easier to implement. In this case, instead of Quaternions, just use a simple 2 dimensional vector, perform the linear interpolation on the vector ( a trivial and unambiguous operation), and then use atan2 to get the angle! Something like this:

     Vector2 interop=lerp(v1,v2);
     float angle=atan2(interop.x,interop.y);

Where v1, v2 are two vectors pointing to different points on a unit circle and lerp() is just your average linear interpolation function. Depending upon your environment, you may or may not have access to vector classes, but assuming you have even a rudimentary background in math, the basics are very trivial to implement (and there are tons of libraries if you can't be bothered!). As an added bonus, you can trivially change the type of interpolation without messing with any extra conditions etc...

P.S. I'm reasonably new to answering questions on SO , so I'm not sure if it's acceptable to answer a question by directing someone to a completely different method. I've seen it done, but it sometimes receive opposition...

kaifas
  • 101
  • 1
  • 2
  • And if for some reason the input must be an angle measure, you can easily convert that to a unit vector, do the lerping, and then convert it back (similar again to Quaternions). – kaifas May 16 '21 at 07:29