77

What I need is a signed angle of rotation between two vectors Va and Vb lying within the same 3D plane and having the same origin knowing that:

  1. The plane contatining both vectors is an arbitrary and is not parallel to XY or any other of cardinal planes
  2. Vn - is a plane normal
  3. Both vectors along with the normal have the same origin O = { 0, 0, 0 }
  4. Va - is a reference for measuring the left handed rotation at Vn

The angle should be measured in such a way so if the plane would be XY plane the Va would stand for X axis unit vector of it.

I guess I should perform a kind of coordinate space transformation by using the Va as the X-axis and the cross product of Vb and Vn as the Y-axis and then just using some 2d method like with atan2() or something. Any ideas? Formulas?

Peter O.
  • 32,158
  • 14
  • 82
  • 96
Advanced Customer
  • 789
  • 1
  • 6
  • 5

10 Answers10

94

The solution I'm currently using seems to be missing here. Assuming that the plane normal is normalized (|Vn| == 1), the signed angle is simply:

For the right-handed rotation from Va to Vb:

atan2((Va x Vb) . Vn, Va . Vb)

For the left-handed rotation from Va to Vb:

atan2((Vb x Va) . Vn, Va . Vb)

which returns an angle in the range [-PI, +PI] (or whatever the available atan2 implementation returns).

. and x are the dot and cross product respectively.

No explicit branching and no division/vector length calculation is necessary.

Explanation for why this works: let alpha be the direct angle between the vectors (0° to 180°) and beta the angle we are looking for (0° to 360°) with beta == alpha or beta == 360° - alpha

Va . Vb == |Va| * |Vb| * cos(alpha)    (by definition) 
        == |Va| * |Vb| * cos(beta)     (cos(alpha) == cos(-alpha) == cos(360° - alpha)


Va x Vb == |Va| * |Vb| * sin(alpha) * n1  
    (by definition; n1 is a unit vector perpendicular to Va and Vb with 
     orientation matching the right-hand rule)

Therefore (again assuming Vn is normalized):
   n1 . Vn == 1 when beta < 180
   n1 . Vn == -1 when beta > 180

==>  (Va x Vb) . Vn == |Va| * |Vb| * sin(beta)

Finally

tan(beta) = sin(beta) / cos(beta) == ((Va x Vb) . Vn) / (Va . Vb)
Adrian Leonhard
  • 7,040
  • 2
  • 24
  • 38
  • 4
    Works Perfectly! The most elegant solution by far. Thank you Adrian. – Anton Holmberg May 24 '16 at 10:15
  • 1
    This is by far the best answer here. I suspect this solution is also more numerically stable – Eric May 03 '17 at 15:05
  • 3
    Am I wrong of atan2((Vb x Va) . Vn, Va . Vb) contains a typo? It should be atan2((Va x Vb) . Vn, Va . Vb) IMHO – MarcoM Dec 31 '17 at 14:32
  • 3
    @MarcoM the original question asks for the left-handed rotation from Va to Vb. For the right-handed rotation Va x Vb is indeed correct. https://en.wikipedia.org/wiki/Right-hand_rule#Rotation – Adrian Leonhard Dec 31 '17 at 16:48
  • 1
    Thanks for the clarification! – MarcoM Jan 08 '18 at 06:58
  • Thank you for the solution and the explanation! However, I do not understand the step from this: "Therefore (again assuming Vn is normalized): n1 . Vn == 1 when beta < 180 n1 . Vn == -1 when beta > 180" To that: "==> (Va x Vb) . Vn == |Va| * |Vb| * sin(beta)" – Alexko Mar 08 '21 at 22:31
  • 2
    @Alexko `sin(alpha) * n1 . Vn == sin(alpha) == sin(beta)` when `beta < 180` and `sin(alpha) * n1 . Vn == -sin(alpha) == sin(beta)` otherwise. – Adrian Leonhard Mar 09 '21 at 18:34
  • @AdrianLeonhard Thank you! I got it now. In case others might need this, I took the liberty of suggesting an edit to your answer with this added step and equation numbers, but I'm not sure it did anything (I got no feedback). So in case it didn't work, I put the suggested revised answer here: https://pastebin.com/5eGwiquc – Alexko Mar 10 '21 at 01:19
  • If there's anyone calculating Vn by Va x Vb and wondering why the result is always positive: You have to choose the direction of Vn to get the sign. If you calculate it by making the cross product the result will always be positive. (do correct me if this comment is wrong). – ThaNoob Feb 06 '22 at 20:50
79

Use cross product of the two vectors to get the normal of the plane formed by the two vectors. Then check the dotproduct between that and the original plane normal to see if they are facing the same direction.

angle = acos(dotProduct(Va.normalize(), Vb.normalize()));
cross = crossProduct(Va, Vb);
if (dotProduct(Vn, cross) < 0) { // Or > 0
  angle = -angle;
}
msell
  • 2,168
  • 13
  • 30
  • 1
    That was useful info that lead to the final solution - thanks! – Advanced Customer Mar 05 '11 at 17:39
  • 7
    @Advanced Customer If this answer is correct please tick it? Otherwise what did you change to the above? –  Jul 14 '12 at 08:12
  • 5
    Possible improvement: use `angle = angle*sgn(dotProduct(Vn,cross))` instead of the `if` statement. Not sure if it would be less/more efficient but it looks a little nicer. – NauticalMile Apr 02 '15 at 01:33
  • 1
    Even easier if you like that : angle *=sgn(dotProduct(Vn,cross)) – Tolga Birdal Jan 07 '17 at 16:50
  • 1
    Very nice answer - useful in 2D too to check if a polygon is strictly convex. I used it in a segment crossing test. – PinkTurtle Mar 10 '17 at 16:54
  • 1
    Note that when the vectors are nearly parallel, you loose precision, so if you have vector `x=array([ 1., 0., 0.], dtype=float32)` and `y=array([ 1.00000000e+00, 9.99999975e-05, 0.00000000e+00], dtype=float32)`, you have `norm(y) == 1.0f` so `y` is normalized so `dot(x,y) == 1.0` even though `norm(cross(x,y)) == 9.9999997e-05`, and `arcsin(9.9999997e-05) == 9.9999997e-05`. – Ben Mar 04 '19 at 16:09
  • 2
    In my case the imprecision of this solution reached 30 degrees. – marczellm Apr 09 '19 at 13:38
  • 3
    This solution is very imprecise when vectors are nearly parallel or nearly opposite. This is because cosine function is almost flat at these points, so acos function has very little precision. Use the solution from the answer below instead. – kaalus Mar 05 '21 at 03:29
15

You can do this in two steps:

  1. Determine the angle between the two vectors

    theta = acos(dot product of Va, Vb). Assuming Va, Vb are normalized. This will give the minimum angle between the two vectors

  2. Determine the sign of the angle

    Find vector V3 = cross product of Va, Vb. (the order is important)

    If (dot product of V3, Vn) is negative, theta is negative. Otherwise, theta is positive.

Peter O.
  • 32,158
  • 14
  • 82
  • 96
Parag
  • 159
  • 2
  • 4
    For the sign it shouldn't probably be V3.Vb - produced unstable results. In step 2 it should be: Vn . ( Va x Vb) - to check if the original normal (Vn) is facing same direction as the cross of Va and Vb. – Advanced Customer Mar 05 '11 at 17:45
  • @leetNightshade: Edited based on your comments. – Peter O. Jun 24 '15 at 19:57
  • @PeterO. Awesome. I removed my downvote. Now it's pretty similar to http://stackoverflow.com/a/5190354/353094 but in pseudo code. – leetNightshade Jun 25 '15 at 19:51
7

You can get the angle up to sign using the dot product. To get the sign of the angle, take the sign of Vn * (Va x Vb). In the special case of the XY plane, this reduces to just Va_x*Vb_y - Va_y*Vb_x.

Stephen Canon
  • 103,815
  • 19
  • 183
  • 269
  • 1
    I guess this is for a 2d vector while there is a need to make it for 3d ones. The plane where both 3d vectros belong to is not parallel to XY so using just x and y components may not work in some cases. – Advanced Customer Mar 04 '11 at 03:19
  • @Advanced Customer: You take the cross-product of Va and Vb dot-producted with Vn - the sign of that quantity is the sign of the angle. – Stephen Canon Mar 04 '11 at 04:12
  • 1
    @StephenCanon: should `dot product` be `cross product`? – Jichao Aug 28 '13 at 06:12
  • @Jichao: no; the dot product lets you compute the magnitude of the angle between two vectors. – Stephen Canon Aug 28 '13 at 11:14
  • 2
    @StephenCanon: I guess i misunderstood your meaning(`up to sign`). I mean the sign of the angle is depends on the `cross product`. – Jichao Aug 28 '13 at 12:19
  • 1
    @Jichao: There are two sentences. The first says you get the magnitude of the angle using the dot product. The second says that you get the sign of the angle using `Vn * (Va x Vb)`, which contains both a dot product **and** a cross product. The two sentences are independent. – Stephen Canon Aug 28 '13 at 12:27
3

Advanced Customer provided the following solution (originally an edit to the question):

SOLUTION:

sina = |Va x Vb| / ( |Va| * |Vb| )
cosa = (Va . Vb) / ( |Va| * |Vb| )

angle = atan2( sina, cosa )

sign = Vn . ( Va x Vb )
if(sign<0)
{
    angle=-angle
}
Peter O.
  • 32,158
  • 14
  • 82
  • 96
2

Cross one vector into the other and normalize to get the unit vector.

The sine of the angle between the two vectors equals the magnitude of the cross product divided by the magnitudes of the two vectors:

http://mathworld.wolfram.com/CrossProduct.html

duffymo
  • 305,152
  • 44
  • 369
  • 561
  • That way it works also with no sign becuase of magnitudes involved - angle is always positive as with acos(). It seems like the only proper and stable way to do so is to create a coordinate transformation matrix given Vn as Z-axis, Va as X-axis and their cross product as Y-axis so all of this would downgrade into a simple 2d case. – Advanced Customer Mar 04 '11 at 03:36
  • Actually it works but I guess Parag described it a bit more clear - so see above :) – Advanced Customer Mar 04 '11 at 10:55
  • 2
    The sign can be different because every 2D surface has two normal vectors, depending on which side you're interested in. Either one is equally valid. It's up to you to decide which one is appropriate for your problem. If I have a plane defined by two vectors for a wall in a room I can use the cross-product to get the normal vector that faces into the room or out of it, depending on my requirements and which vector comes first in the expression. Which one is correct? Both - it depends on context. – duffymo Mar 04 '11 at 12:15
  • could you please help me I am not good in math https://math.stackexchange.com/questions/2997836/create-wall-3d-math-oriented-away-from-camera – Prashant Tukadiya Nov 14 '18 at 07:29
1

Suppose Vx is the x-axis, given the normal Vn, you can get the y-axis by cross product, you can project the vector Vb to Vx and Vy (by the dot product you can get the length of the projection of Vb onto Vx and Vy), given the (x, y) coordinate on the plane, you can use atan2(y, x) to get the angle in the range [-pi, +pi]

LittleSweet
  • 534
  • 2
  • 6
  • 9
1

Let theta be the angle between the vectors. Let C = Va cross product Vb. Then

sin theta = length(C) / (length(Va) * length(Vb))

To determine if theta is positive or negative, remember that C is perpendicular to Va and Vb pointing in the direction determined by the right-hand rule. So in particular, C is parallel to Vn. In your case, if C points in the same direction as Vn, then theta is negative, since you want left-handed rotation. Probably the easiest computational way to quickly check if Vn and C point in the same direction is to just take their dot product; if it is positive they point in the same direction.

All this follows from elementary properties of the cross product.

David Norman
  • 19,396
  • 12
  • 64
  • 54
1

For those using Python, the solution provided by Adrian Leonhard is now implemented in the scikit-spatial library (the latest version will be available soon). Look for the angle_signed_3d of the Vector class.

Here are two examples:

>>> import numpy as np
>>> from skspatial.objects import Vector
>>> np.degrees(Vector([1, 0, 0]).angle_signed_3d([0, -1, 0], direction_positive=[0, 0, 2]))
-90.0

>>> np.degrees(Vector([1, 0, 0]).angle_signed_3d([0, -1, 0], direction_positive=[0, 0, -5]))
90.0
blunova
  • 2,122
  • 3
  • 9
  • 21
0

This is the Matlab code to compute the signed angle between two vectors u,v either in 2D or in 3D. The code is self explanatory. The sign convention is such that a positive +90° is output between ix and iy ([1,0,0],[0,1,0]) or iy and iz ([0,1,0],[0,0,1])

function thetaDEG = angDist2Vecs(u,v)

if length(u)==3
    %3D, can use cross to resolve sign
    uMod = sqrt(sum(u.^2));
    vMod = sqrt(sum(v.^2));
    uvPr = sum(u.*v);
    costheta = min(uvPr/uMod/vMod,1);

    thetaDEG = acos(costheta)*180/pi;

    %resolve sign
    cp=(cross(u,v));
    idxM=find(abs(cp)==max(abs(cp)));
    s=sign(cp(idxM(1)));
    if s < 0
        thetaDEG = -thetaDEG;
    end
elseif length(u)==2
    %2D use atan2
    thetaDEG = (atan2(v(2),v(1))-atan2(u(2),u(1)))*180/pi;
else
    error('u,v must be 2D or 3D vectors');
end
Massimo
  • 101
  • 2