两个在同一平面内、有相同起点的三维向量之间的有向夹角

80
我需要的是两个位于同一3D平面且拥有相同起点的向量Va和Vb之间旋转角度的有符号值,已知以下条件:
  1. 包含两个向量的平面是任意的,不与XY或其他基本平面平行
  2. Vn-是一个平面法线
  3. 两个向量以及法线都具有相同的起点O = {0, 0, 0}
  4. Va-是在Vn处测量左手旋转的参考

该角度应该按照这样的方式测量,即如果平面是XY平面,则Va将代表其X轴单位向量。

我猜我应该使用Va作为X轴和Vb和Vn的叉积作为Y轴进行一种坐标空间转换,然后只需使用类似于atan2()的2d方法。 有什么想法?公式?


4
是的,我知道"acos(Va.Vb)"的方法,但由于余弦函数的性质总是给出正结果。 - Advanced Customer
2
你能解释一下Va吗?它是否与Vn并行? - David Norman
1
Vn 是飞机的法向量,因此它垂直于 Va 和 Vb - 并且最初已知。 - Advanced Customer
1
问题中的任务已经被简化。在这种情况下,Vn是最初已知的唯一向量,以及旋转矩阵R。然后通过将Vn与基向量之一进行叉积来计算Va:Va = normalize( Vn x {0,1,0} )。 - Advanced Customer
4
在使用 atan2 函数时,不需要除以 (|Va||Vb|) 来计算 sincos。因为 atan2 函数的工作原理会消去分母。 - John Alexiou
显示剩余3条评论
10个回答

98

我目前正在使用的解决方案似乎在此处缺失。 假设平面法线已经归一化(|Vn| == 1),有向角度简单地表示为:

从Va到Vb的右手旋转:

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

从Va到Vb的左手旋转:

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

它返回一个范围为[-PI,+PI]的角度(或任何可用的atan2实现返回的角度)。

“.”"x"分别表示点乘和叉乘。

不需要显式分支或除法/向量长度计算。

为什么这有效的解释:假设alpha是向量之间的直接角度(0°至180°),beta是我们要查找的角度(0°至360°),并且具有beta == alphabeta == 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)

最后

tan(beta) = sin(beta) / cos(beta) == ((Va x Vb) . Vn) / (Va . Vb)

4
完美运行!迄今为止最优雅的解决方案。谢谢Adrian。 - Anton Holmberg
1
这绝对是这里最好的答案。我怀疑这个解决方案也更加数值稳定。 - Eric
3
我认为atan2((Vb x Va) . Vn, Va . Vb)中有一个错别字,是不是我错了?应该是atan2((Va x Vb) . Vn, Va . Vb)。 - MarcoM
3
原问题要求从Va到Vb的左手旋转。对于右手旋转来说,Va x Vb是正确的。参见https://en.wikipedia.org/wiki/Right-hand_rule#Rotation。 - Adrian Leonhard
2
beta < 180 时,sin(alpha) * n1 . Vn == sin(alpha) == sin(beta) ,否则 sin(alpha) * n1 . Vn == -sin(alpha) == sin(beta) - Adrian Leonhard
显示剩余4条评论

80
使用两个向量的叉积来获取由这两个向量组成的平面的法向量。然后检查该法向量与原始平面法向量之间的点积,以确定它们是否面向相同的方向。
angle = acos(dotProduct(Va.normalize(), Vb.normalize()));
cross = crossProduct(Va, Vb);
if (dotProduct(Vn, cross) < 0) { // Or > 0
  angle = -angle;
}

7
如果这个答案正确,请打勾吗?否则,您对上述内容做了哪些更改? - user234736
5
可能的改进:使用 angle = angle*sgn(dotProduct(Vn,cross)) 替代 if 语句。不确定其效率是否更高,但看起来更简洁美观。 - NauticalMile
1
非常好的答案 - 在2D中也很有用,可以检查多边形是否严格凸。我在线段交叉测试中使用了它。 - PinkTurtle
2
在我的情况下,这个解决方案的不精确度达到了30度。 - marczellm
3
当向量几乎平行或几乎相反时,此解决方案非常不精确。这是因为余弦函数在这些点上几乎是平的,所以反余弦函数的精度非常低。请改用下面回答中的解决方案。 - kaalus
显示剩余3条评论

15

你可以分两步来完成这个操作:

  1. 计算出两个向量之间的夹角

    theta = acos(Va和Vb的点积)。假设Va和Vb已经被标准化了,这将给出两个向量之间的最小夹角。

  2. 确定夹角的正负号

    找到向量V3 = Va和Vb的叉积。(顺序很重要)

    如果(V3和垂直于它们平面的单位法向量的点积)是负数,则theta是负的。否则,theta为正。


4
对于该符号,可能不应该是V3.Vb-会产生不稳定的结果。在步骤2中应该是:Vn.(Va x Vb)-用来检查原始法线(Vn)是否与Va和Vb的叉积方向相同。 - Advanced Customer
@leetNightshade:根据您的评论进行了编辑。 - Peter O.
@PeterO。太棒了。我取消了我的反对票。现在它与https://dev59.com/rG435IYBdhLWcg3w1juV#5190354非常相似,但是使用伪代码。 - leetNightshade

7

使用点积可以得到角度的正负。 要获取角度的符号,请使用Vn *(Va x Vb)的符号。在XY平面的特殊情况下,这仅缩减为Va_x * Vb_y-Va_y * Vb_x


1
我猜这是为了2D向量,而现在需要将其改为3D向量。两个3D向量所属的平面不与XY平面平行,因此仅使用x和y分量在某些情况下可能无法正常工作。 - Advanced Customer
@高级客户:您需要对Va和Vb的叉积进行点积,再与Vn相乘-该数量的符号即为角度的符号。 - Stephen Canon
1
@StephenCanon:应该是“点积”还是“叉积”? - Jichao
@Jichao:不是的;点积可以让你计算出两个向量之间的夹角大小。 - Stephen Canon
2
@StephenCanon:我想我误解了你的意思(up to sign)。我的意思是角度的符号取决于叉积 - Jichao
1
@Jichao:这里有两个句子。第一个句子表示你可以使用点积来获取角度的大小。第二个句子表示你可以使用 Vn * (Va x Vb) 来获取角度的符号,其中包含了点积和叉积。这两个句子是独立的。 - Stephen Canon

3
高级客户提供了以下解决方案(最初是对问题的编辑):
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
}

2

这样做的好处是,由于涉及到大小,即使没有符号,它也能正常工作 - 角度始终为正,就像acos()一样。似乎唯一正确和稳定的方法是创建一个坐标变换矩阵,以Vn作为Z轴,Va作为X轴,它们的叉积作为Y轴,这样所有这些都会降级为简单的2D情况。 - Advanced Customer
实际上它是可以工作的,但我想Parag描述得更清楚一些 - 所以请参见上面 :) - Advanced Customer
2
符号可能不同,因为每个二维表面都有两个法向量,取决于您感兴趣的是哪一侧。 任何一个都是有效的。由您决定哪个适合您的问题。如果我有一个由两个向量定义的平面作为房间中的墙壁,我可以使用叉积来获得法向量,该法向量面向房间内部或外部,具体取决于我的要求和在表达式中先出现的向量。哪个是正确的?两个都是 - 这取决于上下文。 - duffymo
你能帮我吗?我数学不好。 https://math.stackexchange.com/questions/2997836/create-wall-3d-math-oriented-away-from-camera - Prashant Tukadiya

1

设θ为向量之间的夹角。令C = Va叉乘Vb。则

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

要确定θ是正数还是负数,请记住C垂直于Va和Vb,指向由right-hand rule确定的方向。因此,特别地,C与Vn平行。在您的情况下,如果C指向与Vn相同的方向,则θ为负,因为您想要左旋转。可能最简单的计算方法是快速检查Vn和C是否指向相同的方向,只需取它们的点积;如果它是正的,则它们指向相同的方向。

所有这些都遵循cross product的基本属性。


2
这在有符号性方面是不现实的,因为所有的量级都是2的幂的乘积 - 因此始终是正数。 - Advanced Customer
1
符号来自于C和Vn的点积,如果它们指向相反的方向,则为负。 - David Norman
2
从问题“Vn-是一个平面法线”开始。 - David Norman
你能帮我吗?我不擅长数学。https://math.stackexchange.com/questions/2997836/create-wall-3d-math-oriented-away-from-camera - Prashant Tukadiya

1
假设Vx是x轴,给定法向量Vn,通过叉积可以得到y轴,你可以将向量Vb投影到Vx和Vy上(通过点积可以得到Vb在Vx和Vy上的投影长度),给定平面上的(x, y)坐标,你可以使用atan2(y, x)来获取范围在[-pi, +pi]内的角度。

1
对于使用Python的人,Adrian Leonhard 提供的解决方案现在已经在 scikit-spatial 库中实现了(最新版本即将发布)。请查找 Vector classangle_signed_3d
以下是两个示例:
>>> 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

0
这是用于计算二维或三维中两个向量u和v之间有符号角度的Matlab代码。代码本身很容易理解。符号约定是,当ix和iy([1,0,0],[0,1,0])或iy和iz([0,1,0],[0,0,1])之间输出正的+90°。
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

最好编辑帖子并在那里写下那些信息。 - illright
我在这里提供了MATLAB代码下载链接:https://www.mathworks.com/matlabcentral/fileexchange/78300-signedangletwovectors - brethvoice

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接