Using rotation_difference to recover a quaternion

I'm trying to do something I think should be simple with Quaternions: Use a quaternion to rotate a vector; then use the difference between the original and new vectors to recover the quaternion. But I can't get it to work like I think it should.

bv = Vector((1,0,0))
q = Quaternion((1,1,1), radians(30)) # is our magic rotation
bvr = bv.copy()
bvr.rotate(q)
qq = bv.rotation_difference(bvr) # rotation difference must be our magic rotation, surely

qprint(qq.rotation_difference(q)) # Nope, that's not null
qprint(qq.inverted() @ q) # not this either


Results in the following (qprint is just a pretty printer for quaternions):

(<Vector (1.0000, -0.0000, -0.0000)>), 17.59
(<Vector (1.0000, -0.0000, -0.0000)>), 17.59


What am I mucking up? I woulda thought that qq would be the same rotation as q, since it is exactly the quaternion that turns bv into bvr. But that doesn't seem to be the case.

• You cannot recover a quaternion by knowing where it sends a single vector (imagine if bv is the axis of rotation for example). You can recover it if you know where it sends two linearly independent vectors though, do you need to know how? Commented Apr 19, 2022 at 22:31
• Yes, but howabout if I know the twist? to_swing_twist will return the angle of rotation around the vector. I just checked the twist on the above example and it's 17.59, suggesting that if I subtract that from the angle of my recovered vector, I'll get what I want. Commented Apr 19, 2022 at 22:43

You can't. Here's an image of your vectors:

Technically, rotation by quaternion is a many-to-one function. Given a single vector there are many different quaternions that can result in the same rotation. Since the difference function doesn't take the original rotation axis as an argument, it's free to pick any that will produce the difference.

bvx = bv.copy()
bvx.rotate(qq)
bvx - bvr
from pprint import pprint
pprint(bvx-bvr)


you'll get console output like

Vector((-5.960464477539063e-08, 0.0, 0.0))


The X value isn't 0 because of floating point roundoff, but the result of rotating by either quaternion is the same.

For the curious, the display was produced by adding this code after the calculations:

mesh = bpy.data.meshes.new("mesh")
object = bpy.data.objects.new("mesh", mesh)

verts = [(0, 0, 0), bv, bvr, q.axis, qq.axis]
edges = [(0, 1), (0, 2), (2, 3), (0, 3), (0,4)]
faces = [(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)]

mesh.from_pydata(verts, edges, faces)
mesh.update()


and removing the faces and extra edges from this output: (because I was too lazy to modify the edges and faces arrays when I realized what edges I really wanted.)

• You say 'many different' quaternions would work, but isn't it a limited set? If I have bv and bvr doesn't any quaternion that turns bv into bvr have to have an axis normal to the plane defined by bv and bvr? So really, there's just two if the axis has unit length and the angle is constrained within -360 and +360 deg, and just one if the angle has to be positive. (Thanks for the very complete answer btw, and for the cool representation.) Commented Apr 20, 2022 at 12:00

I sorta have an answer, but I'd like to know if it's working by chance or not. I extended my code to capture the twist and then use it in the reverse operation. Because I'm using the to_swing_twist() operator the initial vector is constrained to lie on an axis, but that's okay for my application.

(What I'm doing, btw, is an import/export tool for nif files. On import, if I apply a bone's transform to the unit vector I get a good representation for the bone in the Blender armature. But then I want to reverse the operation to recover the transform for export. In truth, as long as the transform is equivalent it doesn't have to be numerically identical, but for testing it's much more convenient if it is.)

from mathutils import Quaternion, Vector, Matrix

bone_vectors = {'X': Vector((1,0,0)), 'Z': Vector((0,0,1))}

def qtobone(boneq:Quaternion, axis:str):
""" Taxes a rotation and axis and applies the rotation to a unit vector
on that axis. Returns resulting vector and twist. """
s, t = boneq.to_swing_twist(axis)
v = bone_vectors[axis].copy()
v.rotate(boneq)
return v, t

def bonetoq(vec:Vector, roll:float, axis:str):
""" Takes a vector, roll angle, and axis and returns a quaternion that
rotates the unit vector on the axis to the input vector, with the roll."""

bv = bone_vectors[axis].copy()
q = bv.rotation_difference(vec)
rollq = Quaternion(bv, roll)
return q @ rollq

def qtest(q):
b1, r1 = qtobone(q, 'X')
qq = bonetoq(b1, r1, 'X')
print(f"Result \n{q}\n{qq}")


This works in every case I've tested:

qtest(Quaternion((1,0,0), radians(90)))


Output:

Result
<Quaternion (w=0.7071, x=0.7071, y=0.0000, z=0.0000)>
<Quaternion (w=0.7071, x=0.7071, y=0.0000, z=0.0000)>
Result
<Quaternion (w=0.7071, x=0.0000, y=0.7071, z=0.0000)>
<Quaternion (w=0.7071, x=0.0000, y=0.7071, z=0.0000)>
Result
<Quaternion (w=0.7071, x=0.0000, y=0.0000, z=0.7071)>
<Quaternion (w=0.7071, x=0.0000, y=0.0000, z=0.7071)>
Result
<Quaternion (w=0.9239, x=0.0000, y=0.0000, z=0.3827)>
<Quaternion (w=0.9239, x=0.0000, y=0.0000, z=0.3827)>
Result
<Quaternion (w=0.9659, x=0.1494, y=0.1494, z=0.1494)>
<Quaternion (w=0.9659, x=0.1494, y=0.1494, z=0.1494)>
Result
<Quaternion (w=0.9397, x=0.0914, y=0.1828, z=0.2742)>
<Quaternion (w=0.9397, x=0.0914, y=0.1828, z=0.2742)>
Result
<Quaternion (w=0.9397, x=-0.0914, y=-0.1828, z=-0.2742)>
<Quaternion (w=0.9397, x=-0.0914, y=-0.1828, z=-0.2742)>
Result
<Quaternion (w=0.4384, x=0.2402, y=0.4804, z=0.7206)>
<Quaternion (w=0.4384, x=0.2402, y=0.4804, z=0.7206)>