PyMEL: Vector rotation off by 180 degrees


#1

I’m trying to calculate the angle between two vectors and get it in degrees, and then rotate an object by this angle. Problem is that in some cases, I get an angle that is off by exactly 180 degrees.

# Calculate vectors
vectorA = pm.datatypes.Vector(targetP0[0] - targetP1[0], targetP0[1] - targetP1[1], 0.0)
vectorB = pm.datatypes.Vector(sourceP1[0] - sourceP0[0], sourceP1[1] - sourceP0[1], 0.0)

# Returned unsigned angle in degrees
angle = math.degrees(vectorB.angle(vectorA)) 

# Calculate dot product
dot = (vectorB.x * vectorA.y) - (vectorB.y * vectorA.x)

if dot > 0: # Same
    sign = 1
elif dot < 0: # Opposite
    sign = -1
else: # Perpendicular
    sign = 0
    
# Signed angle
angle = (angle * sign)

Gives correct rotation
targetP0: [0.674, 0.825]
targetP1: [0.557, 0.8463]
sourceP0: [0.514, 0.901]
sourceP1: [0.372, 0.901]

Does not give correct rotation (off by precisely 180 degrees)
targetP0: [0.514, 0.901]
targetP1: [0.372, 0.901]
sourceP0: [0.674, 0.825]
sourceP1: [0.557, 0.846]

It’s probably something stupid and obvious that I’m missing - but I don’t see what that would be.


#2

I’m assuming you made some typos in the example data you provided since there is no targetP1 or sourceP0. I also wasnt sure if the code was intentionally reversing the logic in the vector construction to prove a point. But I set up my own test with 4 locators, S0, S1, T0, T1, acting as my point sources, and ran the following code while dragging them around. All the results seemed correct to me. I saw no cases where they returned opposite results to what I would expect.

pn = pm.PyNode
sourceP0 = pn('S0').worldPosition.get()
sourceP1 = pn('S1').worldPosition.get()

targetP0 = pn('T0').worldPosition.get()
targetP1 = pn('T1').worldPosition.get()


# Calculate vectors
vectorA = pm.datatypes.Vector(targetP0[0] - targetP1[0], targetP0[1] - targetP1[1], 0.0)
vectorB = pm.datatypes.Vector(sourceP0[0] - sourceP1[0], sourceP0[1] - sourceP1[1], 0.0)

# Returned unsigned angle in degrees
angle = math.degrees(vectorB.angle(vectorA)) 

# Calculate dot product
dot = (vectorB.x * vectorA.y) - (vectorB.y * vectorA.x)

if dot > 0: # Same
    sign = 1
elif dot < 0: # Opposite
    sign = -1
else: # Perpendicular
    sign = 0
    
# Signed angle
angle = (angle * sign)
print angle

David


#3

Omg I don’t know how I could do such a clumsy error in the original post (I forgot to include the y-values in the example for the points).

Either way, I still have a problem with this. My current example code:

# targetP0 - target vector point 0, in the form of pm.datatypes.Point()
# sourceP1 - source vector point 1...
# etc...

# Create vectors
sourceVector = pm.datatypes.Vector(sourceP1[0] - sourceP0[0], sourceP1[1] - sourceP0[1], 0.0)
targetVector = pm.datatypes.Vector(targetP0[0] - targetP1[0], targetP0[1] - targetP1[1], 0.0)

# Calculate dot product and get sign for the angle
temp = sourceVector.dot(targetVector)
if temp > 0: # Same
    sign = 1
elif temp < 0: # Opposite
    sign = -1
    sourceVector = -sourceVector

else: # Perpendicular
    sign = 0

# Calculate angle - returns unsigned angle as degrees
angle = math.degrees(sourceVector.angle(targetVector))

# Set correct sign
angle = angle * sign

# Rotate by the value in angle

What end result I get is dependant on the directions of the targetVector and sourceVector. If I rotate one of them juuust enough, everything turns out all right. But if their directions happen to be “wrong” somehow, I get an error of either 180 degrees minus the angle, or the angle but with an inverted sign.


#4

Please provide a full code that includes a specific example, its output, and the output you expects.


#5
import pymel.core as pm

## Data

# Case 1
targetP0 = pm.datatypes.Point([0.6341235637664795, 0.0872100293636322])
targetP1 = pm.datatypes.Point([0.5, 0.06596691161394119])
sourceP0 = pm.datatypes.Point([0.4916876554489136, -0.049265921115875244])
sourceP1 = pm.datatypes.Point([0.6320921778678894, 0.04852628707885742])
# Result - Rotation in the other direction.
# Rotated 25.8573239132 - should've rotated -25.8573239132


# Case 2
targetP0 = pm.datatypes.Point([0.755118191242218, 0.1488598734140396])
targetP1 = pm.datatypes.Point([0.6341235637664795, 0.0872100293636322])
sourceP0 = pm.datatypes.Point([0.6881994009017944, 0.053124457597732544])
sourceP1 = pm.datatypes.Point([0.8034881353378296, 0.048990845680236816])
# Result - Rotation in the correct direction.
# Rotated 29.0534287144 - which is correct

## Function

# Create vectors
sourceVector = pm.datatypes.Vector(sourceP1[0] - sourceP0[0], sourceP1[1] - sourceP0[1], 0.0)
targetVector = pm.datatypes.Vector(targetP0[0] - targetP1[0], targetP0[1] - targetP1[1], 0.0)

# Calculate dot product and get sign for the angle
temp = sourceVector.dot(targetVector)
if temp > 0: # Same
    sign = 1
elif temp < 0: # Opposite
    sign = -1
    sourceVector = -sourceVector

else: # Perpendicular
    sign = 0

# Calculate angle - returns unsigned angle as degrees
angle = math.degrees(sourceVector.angle(targetVector))

# Set correct sign
angle = angle * sign

# Rotate source vector by value in angle

#6

You say that there’s an error in case 1, but I don’t see any error. The two normalized vectors, source and target respectively, are:

s = [0.8206    0.5715         0]
t = [0.9877    0.1564         0]

The angle between them is 25.85 < 90 and sign=1. I don’t know why you expect a negative sign.


#7

By the way the angle between vectors is calculated from the dot product which is a commutative operation. This means that while the angle between them is 25 degrees, you don’t know if to rotate s cw or ccw. For that, you’ll need to get the sign from the cross product (not from the dot product). Also, if you are just looking to rotate one vector to another, just use quaternions.


#8

It’s possible that I simply mixed them up (CW <–> CWW). Sorry for the confusion.

Alright, I tried using the cross product instead and also checked this against the axis() function for the Vector class - it gives the same signs so this is correct. I still get problems with my rotations though - code and case data below:

# Case 3 - Points will give two vectors pointing in different direction.
targetP0 = pm.datatypes.Point([0.8511400818824768, 0.2448817789554596])
targetP1 = pm.datatypes.Point([0.755118191242218, 0.1488598734140396])
sourceP0 = pm.datatypes.Point([0.8378584384918213, 0.1720251441001892])
sourceP1 = pm.datatypes.Point([0.8584747314453125, 0.05453154444694519])


## Function

# Create vectors
sourceVector = pm.datatypes.Vector(sourceP1[0] - sourceP0[0], sourceP1[1] - sourceP0[1], 0.0)
targetVector = pm.datatypes.Vector(targetP0[0] - targetP1[0], targetP0[1] - targetP1[1], 0.0)

# Calculate cross product and get sign for the angle
temp = sourceVector.cross(targetVector)

if temp[2] > 0: # Same
    sign = 1
elif temp[2] < 0: # Opposite
    sign = -1
    sourceVector = -sourceVector     

else: # Perpendicular
    sign = 0

# Calculate angle - returns unsigned angle as degrees
angle = math.degrees(sourceVector.angle(targetVector))

# Set correct sign
angle = angle * sign

# Rotate source vector by value in angle.

With the data in case 3, I get a resulting angle of 125.047778377 and a sign of -1. I get this both from the cross product function cross() and also from the function axis() which according to the documentation, can be used to determine the sign.
However, the correct result here would be (180 - angle) * sign = -54,95222 (ie, a rotation that is clockwise)


#9

For case 3, the sign is 1, and the final angle is 125 (rotation is always ccw). I drew the vectors and it seems fine.

Also, check the alternative Vector.rotateTo:

http://download.autodesk.com/global/docs/maya2012/zh_cn/PyMel/generated/classes/pymel.core.datatypes/pymel.core.datatypes.Vector.html

It would return the quaternion that rotates one vector to another, which in turn you can supply to rotateBy.


#10

Alright so I did find out the issue here.
In some cases I actually do NOT want the vectors to be aligned - but in precisely opposite directions. So the math itself is right, I just need to add another snippet of code for handling these exception cases.
Thanks for pointing me in the right direction.