Shortest rotation arc between quaternions

Hi to all,

A question (that has probably come up a few times, though I couldn’t find a simple, concise answer to it on the forums) concerning the use of quaternions, to those of you familiar with them.
I have a nodepath whose rotation is stored at two different time points as a quaternion:

q1=np.getQuat()
t1=0.5

q2=np.getQuat()
t2=1.5

Now, I interpolate between these two quaternions, using this formula:

currentQuat=q1+((q2-q1)*(currentTime-t1))/(t2-t1)

To get the quaternion at the “currentTime”; “currentTime” is just a variable that I get based on the position of the thumb of a DirectSlider object. So sliding the thumb around changes the “currentTime”, which changes the “currentQuat”. I then set the “currentQuat” to the aforementioned nodepath this way:

np.setQuat(currentQuat)

That simple command is tethered to the aforementioned DirectSlider, whenever its thumb moved or its track is pressed. So now, this works properly and I can avoid the headache of euler angles, but the problem is, every time, the model takes the longest rotation arch between quarternions. So, for instance, if I just want the model to rotate a bit to the right, it ends up going towards the left and then ending up at the correct rotation at the right. Conversely, every time I want the model to rotate a bit to the left, it ends up going towards the right and then ending up at the correct rotation at the left. Each time, in the rotations I tested, it consistently takes the longest route!

So the question is: what would I do, if I explicitly wanted it to take the shortest route? I know that using this method will always make it take the longest route, (based on this empirical evidence at least :smile:), but what if I wanted it to make it always take the shortest route between these 2 quaternions?

If anything is unclear, please ask and I’ll clarify.

Thanks in advance.

What you’re probably looking for is spherical interpolation:

If you want Panda to handle the maths for you, you can look into using a LerpQuatInterval, which implements slerp (and in fact chooses the best implementation for the specific case).

Or you could look at the source code of Panda’s implementation:

Finally, you want to make sure that your quaternions are normalized before you do any kind of interpolation on them, or you’ll get skewed results.

To actually answer your question, you cannot subtract quaternions and expect to get a meaningful result. You can use q1.angleRad(q2) to get the angle between two quaternions. See the Panda source for the implementation.

Thanks for the response rdb; I had wanted to use LerpQuatInterval, but I’m not sure if “best implementation for the specific case” means that it would take the shortest route always. I made a set of visual samples to further convey what I mean:

pose1
That first image is the start-pose, where “q1” would be gotten from.

pose2
That second image is the end-pose, where “q2” would be gotten from.

This is the direction I would want the nodepath to move, as it moves from q1 to q2:
pose1DirectionRotate

However, this is the direction it ends up taking:
movingQuatLong

So as you can see, it takes the longest route, instead of just directly turning upwards, it turns downwards all the other way around. This comes from using this:

currentQuat=q1+((q2-q1)*(currentTime-t1))/(t2-t1)

The thing is, visually, by using the formula I cited, it does give me animations, just that it keeps taking the longest route, so when you say “meaningful result”, I’m not quite sure what that means.

So:

  • Were I to use LerpQuatInterval, would it always give me the shortest route, or would that end up being case-specific?

  • If not, is there any way, with some example code, that I could always make it take the shortest route no matter what? Or is that just not possible?

Please also understand that I’m not a mathematician and my knowledge of c++ is…well, not the best at this point… :grin: So if you could kindly use python, or some panda3d solution, I would extremely appreciate it. Thanks.

EDIT
If there’s any other way to achieve the same result, that does not make use of euler angles and does that not include issues arising from using them, then I am of course, open to suggestions and examples. (Maybe direct rotation matrices would be a good alternative?)

So, after some mulling, I decided to use delta-values instead of actual values. I’m writing this in case anyone else wants to achieve the same visual effect, which is: rotate a node, from one h/p/r to another h/p/r and to consistently use the longest or shortest route within an interpolation between the two sets of h/p/r without suffering from the changes that come with shifting ranges when using euler angles, e.g at times, whenever getH() is invoked, it might return -160, other times, it returns 200, making it unreliable when doing interpolations.

Firstly, the neutral hpr state of the nodepath needs to be saved; this is the hpr state before any interpolation is done on the nodepath:

neutralHpr=relevantNodePath.getHpr()

Then, whenever the h or p or r is changed, maintain a set of deltas that indicates that change. There should be a set for positive changes and a set for negative changes:

positiveHprDeltas=Point3(0,0,0)
negativeHprDeltas=Point3(0,0,0)

Then, whenever you rotate left or right by say, 90 degrees, you just update the values:

#Turned right by 90:
positiveHprDeltas.x+=90
#For reliable results, always clamp the values between 0 and 359:
if(positiveHprDeltas.x>=360):
    positiveHprDeltas.x=0
#Whenever you update the positive, you must updated the negative and vice versa:
negativeHprDeltas.x=360-positiveHprDeltas.x
if(negativeHprDeltas.x>=360):
    negativeHprDeltas.x=0

That way, at any given time, we get how many degrees the nodepath has turned from its neutral hpr state.
When it comes to interpolation, a snapshot of these deltas will be needed at different time slots. So if you want to interpolate between only two time slots for example, then you’d need two sets of both positive and negative deltas:

#For demonstration purposes, a dummy object that stores time and delta data:
exampleKeyFrameObject=keyFrameObject()
#store the information inside it:
#get the hpr deltas at the starting point:
exampleKeyFrameObject.frameTimeOne=0.1
positiveHprDOne=Point3(0,0,0)
positiveHprDOne.x=positiveHprDeltas.x
positiveHprDOne.y=positiveHprDeltas.y
positiveHprDOne.z=positiveHprDeltas.z

negativeHprDOne=Point3(0,0,0)
negativeHprDOne.x=negativeHprDeltas.x
negativeHprDOne.y=negativeHprDeltas.y
negativeHprDOne.z=negativeHprDeltas.z
exampleKeyFrameObject.deltaDataOne=[positiveHprDOne,negativeHprDOne]
#Get a snapshot of the data at the next point, where the hpr deltas have changed:
exampleKeyFrameObject.frameTimeTwo=0.8
positiveHprDTwo=Point3(0,0,0)
positiveHprDTwo.x=positiveHprDeltas.x
positiveHprDTwo.y=positiveHprDeltas.y
positiveHprDTwo.z=positiveHprDeltas.z

negativeHprDTwo=Point3(0,0,0)
negativeHprDTwo.x=negativeHprDeltas.x
negativeHprDTwo.y=negativeHprDeltas.y
negativeHprDTwo.z=negativeHprDeltas.z
exampleKeyFrameObject.deltaDataTwo=[positiveHprDTwo,negativeHprDTwo]

So, now that the data is stored, time to move between it:

#First, set the nodePath to its neutral hpr state:
relevantNodePath.setHpr(neutralHpr)
#then, find out how much time has passed since the interpolation started:
timeElapsed=sliderValue-exampleKeyFrameObject.frameTimeOne
#the rest of the calculations:
timeBetweenFrames=exampleKeyFrameObject.frameTimeTwo-exampleKeyFrameObject.frameTimeOne
#We will need to get the difference between the deltas at the different snapshots,
#Then we pick the least difference to use for turning, two sets of differences, positive and negative:
def returnDifference(deltaOne,deltaTwo):
    if(deltaTwo>=deltaOne):
        diffReturn=deltaTwo-deltaOne
    else:
        diffReturn=(360-deltaOne)+deltaTwo
    return diffReturn

diffHP=returnDifference(exampleKeyFrameObject.deltaDataOne[0].x,exampleKeyFrameObject.deltaDataTwo[0].x)
diffPP=returnDifference(exampleKeyFrameObject.deltaDataOne[0].y,exampleKeyFrameObject.deltaDataTwo[0].y)
diffRP=returnDifference(exampleKeyFrameObject.deltaDataOne[0].z,exampleKeyFrameObject.deltaDataTwo[0].z)

diffHN=returnDifference(exampleKeyFrameObject.deltaDataOne[1].x,exampleKeyFrameObject.deltaDataTwo[1].x)
diffPN=returnDifference(exampleKeyFrameObject.deltaDataOne[1].y,exampleKeyFrameObject.deltaDataTwo[1].y)
diffRN=returnDifference(exampleKeyFrameObject.deltaDataOne[1].z,exampleKeyFrameObject.deltaDataTwo[1].z)

#Now, pick the lesser delta and indicate whether it was the delta from the positive or negative value picked:
if(diffHP<diffHN):
    actDH=diffHP
    hT=1
else:
    actDH=diffHN
    hT=-1

if(diffPP<diffPN):
    actDP=diffPP
    pT=1
else:
    actDP=diffPN
    pT=-1

if(diffRP<diffRN):
    actDR=diffRP
    rT=1
else:
    actDR=diffRN
    rT=-1
#of course, if you want the long way, just flip and pick the greater delta.

#Now, we have our delta to apply, either one that results in a shorter rotation arch or a longer rotation arch, just apply it, based on the current value of the slider:
currentHDist=((timeElapsed*actDH)/timeBetweenFrames)*hT
currentPDist=((timeElapsed*actDP)/timeBetweenFrames)*pT
currentRDist=((timeElapsed*actDR)/timeBetweenFrames)*rT
#Make sure to add the delta at the first keyframe, since it's the starting point of the interpolation:
p1H=exampleKeyFrameObject.deltaDataOne[0].x
p1P=exampleKeyFrameObject.deltaDataOne[0].y
p1R=exampleKeyFrameObject.deltaDataOne[0].z

relevantNodePath.setH(relevantNodePath.getH()+p1H+currentHDist)
relevantNodePath.setP(relevantNodePath.getP()+p1P+currentPDist)
relevantNodePath.setR(relevantNodePath.getR()+p1R+currentRDist)

The slider-value referenced could just be some variable somehow provided to the interpolation method. The slider-value could be increased periodically within a task or sequence that is called at whatever rate you want it to be called. Or it could be attached to a directSlider object and manipulated by the end-user, etc.

In any case, the above is an example of how to use deltas from a neutral state to turn a nodepath and ensure it always takes either the shortest or longest rotation arch during its transform: store a neutral hpr state, maintain a positive and negative delta-variable that are updated appropriately whenever the nodepath’s hpr changes, store the values from the positive and negative delta-variables at whatever point you want involved in an interpolation, making sure to store some corresponding time value for each stored point, then, move between deltas smoothly and apply the current deltas in the manner indicated above to turn either the short way, or the long way.

I don’t know if this long post will help anyone, but I hope it does in some way. If anything I said is unclear, just tell me and I’ll try to clarify what I mean.

Although I’m a bit late to reply, I’d like to ask if you actually tried to use a LerpQuatInterval? It seems to work pretty well to me (for the most part at least; there’s one thing I’m not entirely sure of, which I’ll say more about at the end of this post).

Here’s a small example that uses a LerpQuatInterval:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *
from direct.interval.IntervalGlobal import LerpQuatInterval


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        dir_light = DirectionalLight("light")
        dir_light.set_color((1., 1., 1., .5))
        light_np = self.render.attach_new_node(dir_light)
        light_np.set_hpr(-20., -20., 0.)
        self.render.set_shader_input("light", light_np)
        self.render.set_light(light_np)
        pivot = self.render.attach_new_node("pivot")
        pivot.set_y(10.)
        smiley = self.loader.load_model("smiley")
        smiley.reparent_to(pivot)
        smiley.set_y(-3.)
        quat = Quat()
        quat.set_hpr((0., 300., .0))
        duration = 2.
        self.interval = LerpQuatInterval(pivot, duration, quat, blendType="easeInOut")
#        self.interval.loop()
        command = lambda: self.interval.set_t(self.slider["value"])

        self.slider = DirectSlider(range=(0., duration), value=0., command=command,
            pos=(0., 0., -.75), frameSize=(-1., 1., -.2, .2), pageSize=.4,
            thumb_frameSize=(-.15, .15, -.2, .2))


app = MyApp()
app.run()

The duration is just the difference between the two time points t1 and t2.
The angle of rotation has been deliberately chosen to be quite large (300 degrees); yet, the angle that the smiley actually rotates through is the smaller angle of 300 - 360 = -60 degrees. You should find that it’s indeed always the shortest path that is taken.

Looking at the source code, I think that “the specific case” refers to extreme angle values (near 0 or 180 degrees).

If I’m not mistaken, subtracting quaternions comes down to subtracting their corresponding r, i, j and k values (used in the equation of a quaternion). It’s a bit like trying to get half of an angle by dividing its sine by two; it might look like it works when the angle is quite small, but it only approximates the exact value.

The only issue I encountered when testing slerp interpolations, is that for angles larger than 90 degrees the speed is not constant, even if the blendType parameter is set to noBlend. As I don’t have that much experience with lerps, maybe I’m misunderstanding the purpose of this parameter, but even if it doesn’t guarantee a constant speed, what I’m seeing are quite extreme changes in speed for angles larger than, say, 150 degrees. See the code sample I made for this GitHub issue.

1 Like

Hi Epihaius,

Yes I did, but at times it used the shortest route, other times, it used the longest route and so it ended up being rather unreliable for my specific needs.

Thanks for the sample code, it’s exactly what I had tested earlier on using a LerpQuatInterval, but alas, at times I got strange results…

Regardless, though somewhat hacky, I did manage to circumvent these issues, by using deltas on euler angles, which work with 100% certainty all the time, consistently giving me either the shortest or longest rotation arch between two angles that a nodepath is to assume during an interpolation. So I suppose my ultimate wish has been met.