# Relative quaternion rotation (delta rotation) produces unstable results

Use case: I have a currentRotation quaternion and a targetRotation quaternion and need to calculate the relative rotation between them - as in: what rotation do I need to apply to transform an object with currentRotation so that it has targetRotation.

Common solutions say that the delta rotation can be calculated by qDelta = qFrom.inverse() * qTo, which I use as well and works fine for most cases.

However, I am often running into issues for some specific rotations - Codepen Example here (I'm using three.js and coffeescript).

While a relative rotation around the z-axis works fine:

qFrom = (new THREE.Quaternion()).setFromAxisAngle new THREE.Vector3(0, 0, 1), 2 * (Math.PI / 6)
qTo = (new THREE.Quaternion()).setFromAxisAngle new THREE.Vector3(0, 0, 1), 5 * (Math.PI / 6)
qExp = (new THREE.Quaternion()).setFromAxisAngle new THREE.Vector3(0, 0, 1), 3 * (Math.PI / 6)
qDelta = calculateRelativeRotation(qFrom, qTo)
qResult = qDelta.clone().multiply(qFrom)


Sane case results (delta): expected x: 0.0°, y: 0.0°, z: 90.0°, got x: 0.0°, y: 0.0°, z: 90.0°

Sane case results (result): expected x: 0.0°, y: 0.0°, z: 150.0°, got x: 0.0°, y: 0.0°, z: 150.0°

... a more complicated rotation does not work out - the combination of qFrom and qDelta does not result in the expected qTo rotation:

eFrom = new THREE.Euler(0, 170 * dToR, 10 * dToR, 'XYZ')
eTo = new THREE.Euler(0, 170 * dToR, -10 * dToR, 'XYZ')
eExp = new THREE.Euler(0, 0, -20 * dToR, 'XYZ')


Problematic case results (delta): expected x: 0.0°, y: 0.0°, z: -20.0°, got x: 0.0°, y: 0.0°, z: -20.0°

Problematic case results (result): expected x: -180.0°, y: 10.0°, z: 170.0°, got x: 176.5°, y: 9.4°, z: -149.7°

expected result quaternion (qTo): {'_x':-0.086,'_y':0.992,'_z':-0.007,'_w':0.086} *

actual result quaternion: {'_x':0.257,'_y':0.962,'_z':-0.007,'_w':0.086}

Inverting the calculated delta rotations helps for some cases, but doesn't cover all cases.

What am I missing?

• TLDR: Quaternion math is weird. I don't know what the problem is (i.e. how to fix it, whatever "it" actually is) because quaternions aren't something I understand. I can tell you that quaternion multiplication is noncommutative (i.e. order matters) but that's about it. Commented Jul 5, 2017 at 17:38
• True that. The multiplication seems to be correct, as this issue only occurs for some special cases, and generally works fine.
– AtiX
Commented Jul 6, 2017 at 9:10
• I figured you hadn't messed up that badly or nothing would work. But I still don't know why it won't always work. Commented Jul 6, 2017 at 13:06

Thanks to Neal on Mathematics Stackexchange I found out that my code to calculate relative rotations was indeed in the wrong order (I took it from Unity Anwers which seems not to be the correct way to do it within three.js) - with the tricky part being that it worked out most times. So, the correct solution to calculate a relative rotation is

qDelta = qTo * qFrom.inverse()

• As long as you deal with unit length quaternions (which you have to for 3D orientations/rotations), the conjugate is the same as the inverse. The conjugate is faster, its basically just a copy with changing the sign for the axis-part. Commented Jul 12, 2017 at 9:14
• If you're curious why the order flips, it's a matter of whether you apply the relative rotation in local or global space. If your question is "what relative rotation qRel do I need to apply to a child, so after its parent's rotation qRef is applied I get my desired result rotation qNet?" Then qNet = qRef * qRel = qRef * (qRef^-1 * qNet) ∴ qRel = qRef^-1 * qNet is correct. But if your question is "what relative rotation qRel do I need to apply on top of a starting rotation qRef" then qNet = qRel * qRef = (qNet * qRef^-1) ∴ qRel = qNet * qRef^-1. Both valid, just different cases. Commented Jul 12, 2017 at 10:01
• Oops, I left off a qRef in the second derivation above. The world space compounding case should be qNet = qRel * qRef = (qNet * qRef^-1) * qRef ∴ qRel = qNet * qRef^-1 Commented Jul 12, 2017 at 11:06