heading pitch bank

本文介绍了3D空间中的方位表示方法——欧拉角,包括其定义、优点及缺点。详细阐述了如何通过三个相互垂直轴的旋转来描述一个物体的位置,并探讨了欧拉角在实际应用中可能遇到的问题。

天行健 君子当自强而不息

3D中的方位和角位移(2)

 

另一种描述方位的常用方法是欧拉角,这项技术以著名的数学家Leonhard Euler(1707 - 1783)的名字命名,他证明了角位移序列等价于单个角位移。

 

什么是欧拉角

欧拉角的基本思想是将角位移分解为绕三个互相垂直轴的三个旋转组成的序列。这听起来很复杂,其实它是非常直观的(事实上,易于使用正是它的主要优点之一)。之所以有"角位移"的说法正是因为欧拉角能用来描述任意旋转。

欧拉角将方位分解为绕三个互相垂直轴的旋转,那么是哪三个轴?按什么顺序?其实,任意三个轴和任意顺序都可以,但最有意义的是使用笛卡尔坐标系并按一定顺序所组成的旋转序列。最常用的约定是所谓的"heading - pitch - bank"约定。在这个系统中,一个方位被定义为一个heading角,一个pitch角,一个bank角。它的基本思想是让物体开始于"标准"方位 --- 就是物体坐标轴和惯性坐标轴对齐。在标准方位上,让物体作heading、pitch、bank旋转,最后物体到达我们想要描述的方位。

如图10.4所示,此时物体坐标系和惯性坐标系重合,heading为绕y轴的旋转量,向右旋转为正(如果从上面看,旋转正方向就是顺时针方向)。

经过heading旋转后,pitch为绕x轴的旋转量,注意是物体坐标系的x轴,不是原惯性坐标系的x轴。依然遵守左手法则,向下旋转为正,如图10.5所示:

最后,经过了heading和pitch,bank为绕z轴的旋转量。再次提醒,是物体坐标系的z轴,不是原惯性坐标系的z轴。依据左手法则,从原点向+z看,逆时针方向为正。如图10.6所示:

记住,当我们说旋转的顺序是heading-pitch-bank时,是指从惯性坐标系到物体坐标系。如果从物体坐标系变换到惯性坐标系,旋转的顺序就是相反的。"heading-pitch-bank"也叫作"roll-pitch-yaw",roll类似于bank,yaw类似于heading(事实上,yaw并不严格等于heading)。注意,在roll-pitch-yaw系统中,角度的命名顺序与从物体坐标系到惯性坐标系的旋转顺序一致的。

 

关于欧拉角的其他约定

heading-pitch-bank系统不是唯一的欧拉角系统。绕任意三个互相垂直轴的任意旋转都能定义一个方位,所以多种选择导致了欧拉角约定的多样性:

(1)heading-pitch-bank系统有多个名称。当然,不同的名字并不代表不同的约定,这其实并不重要。一组常用的术语是roll-pitch-yaw,其中的roll等价于bank,yaw基本上等价于heading。注意,它的顺序和heading-pitch-bank的顺序相反,这只是语义上的。它定义了向量从物体坐标系变换到惯性坐标系的旋转顺序。(事实上,yaw和heading还是有技术上的差别,yaw是绕物体坐标系y轴的旋转,heading是绕惯性坐标系y轴的旋转。因为这里的旋转是在物体坐标系y轴和惯性坐标系y轴重合时进行的,所以这个区别并不重要。)

(2)任意三个轴都能作为旋转轴,不一定必须是笛卡尔轴,但使用笛卡尔轴最有意义。

(3)决定每个旋转的正方向时不一定必须遵守左手或右手法则。例如,完全可以定义pitch的正方向是向上的,并且这种定义方法非常常见。

(4)也是最重要的,旋转可以以不同的顺序进行。顺序并不重要,任何系统都能用来定义一个方位,但heading-pitch-bank顺序最为实用。heading度量绕竖直轴的旋转,它之所以有意义主要是因为我们所在的环境经常有某种形式的"地面"。一般来讲绕惯性坐标系的x或z轴的旋转没有什么意义。heading-pitch-bank顺序下的另外两个角的意义是:pitch度量水平方向的倾角,bank度量的是绕z轴的旋转量。

 

欧拉角的优点

欧拉角仅使用三个数来表达方位,并且这三个数都是角度。这两个特点使欧拉角具有其他形式所没有的优点:

(1)欧拉角对我们来说很容易使用。它比矩阵和四元数简单得多,这可能是因为欧拉角中的数都是角度,符合人们思考方位的方式。如果我们选择了与所要处理的情况最符合的约定,那么就能直接描述出最重要的角度,例如,用heading-pitch-bank系统就能直接地描述出偏差角度。便于使用是其最大的优点,当需要显示方位或用键盘输入方位时,欧拉角是唯一的选择。

(2)最简洁的表达方式。欧拉角用三个数来表达方位。在3D中,表达方位不能少于三个数,如果要考虑内存的因素,欧拉角是最合适的描述方位的方法。

(3)任意三个数都是合法的。取任意三个数,它们都能构成合法的欧拉角,而且可以把它看成一个对方位的描述。从另一方面说,没有"不合法"的欧拉角。当然数值可能不对,但至少它们是合法的。可矩阵和四元数就不一定是这样了。

 

欧拉角的缺点

用欧拉角表达方位时的缺点主要有:

(1)给定方位的表达方式不唯一。

(2)两个角度间求插值非常困难。

让我们仔细讨论这些问题。第一个问题是对于一个给定方位,存在多个欧拉角可以描述它。这称作别名问题,有时候会引起麻烦。因为这个原因,连一些基本的问题(如"两组欧拉角代表的角位移相同吗?")都很难回答。

第一种,在将一个角度加上360度的倍数时,我们就会遇到形式最简单的别名问题。显然,加上360度并不会改变方位,尽管它的数值改变了。

第二种,更加麻烦的别名问题是由三个角度不互相独立而导致的。例如,pitch135度等价于heading180度,pitch45度,然后bank180度。为了保证任意方位都只有独一无二的表示,必须限制角度的范围。一种常用的技术是将heading和bank限制在+180度到-180度之间,pitch限制在+90度到-90度之间。这就建立了欧拉角的一个"限制范围"。对于任意方位,仅存在一个限制欧拉角能代表这个方位(事实上,还有一个违反唯一性的现象需要处理。)

欧拉角最著名的别名问题是这样的:先heading45度再pitch90度,这与先pitch90度再bank45度是等价的。事实上,一旦选择+(-)90度为pitch角,就被限制在只能绕竖直轴旋转。这种现象,角度为+(-)90度的第二次旋转使得第一次和第三次旋转的旋转轴相同,称作万向锁。为了消除限制欧拉角的这种别名现象,规定万向锁情况下由heading完成绕竖直轴的全部旋转。换句话说,在限制欧拉角中,如果pitch为+(-)90度,则bank为0。

如果是为了描述方位,特别是在使用了限制欧拉角的情况下,别名是不会造成太大的问题的。现在来看两个方位A和B间求插值的问题,也就是说,给定参数t,0 ≤ t ≤ 1,计算临时方位C,当t从0变化到1时,C也平滑地从A变化到B。

这个问题的简单解法是分别对三个角度作标准线性插值,公式如下:

但这里面有很多问题。

第一,如果没有使用限制欧拉角,将得到很大的角度差。例如,方位A的heading为720度,方位B的heading为45度,720 = 360 x 2,也就是0度。所以heading值只相差45度,但简单的插值会在错误的方向上绕将近两周。如图10.7所示:

解决问题的方法是使用限制欧拉角,然而,即使是限制欧拉角也不能完全解决问题。插值的第二个问题是由旋转角度的周期性引起的。设A的heading为-170度,B的heading为170度。这些值在heading的限制范围内,都在-180度到180度之间。这两个值只相差20度,但插值操作又一次发生了错误,旋转是沿 "长弧"绕了340度而不是更短的20度,如图10.8所示:

解决这类问题的方法是将插值的"差"角度折到-180度到180度之间,以找到最短弧。

即使使用了这两个角度限制,欧拉角插值仍然可能碰到万向锁的问题,它在大多数情况下会产生抖动、路径错误等现象,物体会突然飘起来像是"挂"在某个地方。根本问题是插值过程中角速度不是恒定的。

欧拉角插值的前两个问题虽然烦人,但并不是不可克服的。限制欧拉角和将角度差限制在一定范围内提供了简单的解决方法。而对于万向锁,非常不幸,它非常令人讨厌,是一个底层的问题。你可能会考虑重新规划旋转,发明一种不会遭遇这些问题的系统。不幸的是,这不可能。这是一个用3个数表达3D方位的方法与生俱来的问题。我们可以改变问题,但不能消灭它们。任何使用三个数来表达3D方位的系统,若能保证空间的唯一性,就都会遇到这些问题,如万向锁。

<think>我们正在讨论如何在OSG中使用四元数(osg::Quat)获取旋转角度(欧拉角)。用户之前的问题是关于在俯仰角不为零时调整横滚角,现在他们想知道如何使用`osg::Quat::getRotate`方法从四元数中获取旋转角度(欧拉角)。 根据引用[2],我们看到一个示例函数`HPRToQuat`,它使用三个旋转(横滚、俯仰、航向)来构造四元数。注意,在构造四元数时,它按照横滚(绕Y轴)、俯仰(绕X轴)、航向(绕Z轴)的顺序,并且每个旋转都是相对于固定轴(世界坐标系)的。但是,我们通常使用欧拉角时,旋转顺序很重要,而且不同的顺序会导致不同的结果。 在OSG中,`osg::Quat`类提供了一个成员函数`getRotate`,其原型为: ```cpp void getRotate(double& angle, osg::Vec3d& axis) const; // 获取旋转的轴角表示 ``` 以及另一个重载版本: ```cpp void getRotate(double& angle1, osg::Vec3d& axis1, double& angle2, osg::Vec3d& axis2, double& angle3, osg::Vec3d& axis3) const; ``` 但是,用户可能更关心的是如何获取欧拉角(航向、俯仰、横滚)。引用[2]中的函数展示了如何从欧拉角构造四元数,而反过来,我们可以使用`getRotate`的重载版本来分解旋转,但要注意旋转顺序。 实际上,OSG的`osg::Quat`还提供了另一个函数: ```cpp void getRotate (double &heading, double &pitch, double &roll) const; ``` 但是,这个函数在OSG的文档中并没有明确说明旋转顺序。根据OSG源代码,这个函数实际上按照固定的顺序分解:首先绕Z轴(航向),然后绕X轴(俯仰),最后绕Y轴(横滚)。注意,这里的旋转顺序是ZXY,即: 1. 先绕Z轴旋转(航向角) 2. 再绕X轴旋转(俯仰角) 3. 最后绕Y轴旋转(横滚角) 然而,用户需要注意的是,这个分解方式与引用[2]中构造四元数的顺序并不相同。在引用[2]中,构造顺序是:先Y轴(横滚),再X轴(俯仰),再Z轴(航向)。所以,如果使用不同的顺序构造四元数,再使用`getRotate`来分解,结果可能不一致。 因此,正确的做法是:在构造四元数时使用特定的顺序,在分解时也要使用相同的顺序。OSG提供的`getRotate`函数(三个角度版本)内部使用固定的ZXY顺序(即先航向,再俯仰,再横滚)进行分解。 所以,如果我们按照ZXY顺序(航向、俯仰、横滚)来构造四元数,那么使用`getRotate`分解出来的角度就是正确的。但是,如果我们使用其他顺序(比如引用[2]中的顺序:横滚、俯仰、航向),那么分解出来的结果就不符合预期。 因此,我们通常建议使用`getRotate`的重载函数,并明确指定旋转顺序。但是,`osg::Quat`并没有直接提供指定旋转顺序的欧拉角分解函数。所以,我们需要自己实现按照特定顺序分解欧拉角的函数,或者使用OSG中已有的固定顺序(ZXY)并确保构造时也使用同样的顺序。 在用户之前的代码中,他们使用了`osg::Quat::getRotate(yaw, pitch, roll, osg::Quat::ZXY)`,这个函数实际上在OSG中并不存在(我查看了OSG 3.6.5的文档)。在OSG中,`getRotate`的三个角度版本实际上只有一个,且内部顺序是固定的(ZXY)。但是,在较新的OSG版本中,可能提供了指定旋转顺序的接口。 实际上,在OSG的源代码中(3.6.5版本),`getRotate(double&, double&, double&)`的实现如下: ```cpp void Quat::getRotate( double& heading, double& attitude, double& bank ) const { // ... 使用ZXY顺序分解 } ``` 因此,为了确保正确性,我们应当知道: - 使用`osg::Quat`的`getRotate(heading, attitude, bank)`得到的三个角度分别是:heading(航向,绕Z轴)、attitude(俯仰,绕X轴)、bank(横滚,绕Y轴),且旋转顺序为ZXY。 如果我们的四元数是按照其他顺序旋转得到的,那么我们需要自己进行分解。或者,我们可以将四元数转换为旋转矩阵,然后从旋转矩阵中按照特定顺序提取欧拉角。 总结:用户想要使用`osg::Quat::getRotate`来获取旋转角度(欧拉角),我们可以使用以下两种方法: 1. 使用`getRotate`获取轴角表示(一个旋转轴和一个角度): ```cpp double angle; osg::Vec3d axis; quat.getRotate(angle, axis); ``` 这样得到的是旋转的轴角表示,但通常我们更习惯使用欧拉角。 2. 使用`getRotate`获取三个欧拉角(按照ZXY顺序): ```cpp double heading, pitch, roll; quat.getRotate(heading, pitch, roll); ``` 注意:这里heading是绕Z轴的旋转(航向角),pitch是绕X轴的旋转(俯仰角),roll是绕Y轴的旋转(横滚角)。旋转顺序是先heading,再pitch,再roll。 但是,如果我们的四元数不是按照ZXY顺序旋转得到的,那么我们需要自己实现欧拉角分解。例如,如果我们按照YPR(航向、俯仰、横滚)的顺序旋转,即先绕Z轴(航向),再绕Y轴(俯仰?注意这里轴的定义可能不同),再绕X轴(横滚),那么我们需要按照这个顺序来分解。 由于欧拉角分解存在万向节死锁问题,当俯仰角为±90°时,航向和横滚会重合,导致分解不唯一。因此,我们通常需要避免在俯仰角接近±90°时使用欧拉角。 下面,我们给出一个示例代码,展示如何从四元数中获取欧拉角(按照ZXY顺序): ```cpp osg::Quat quat = ...; // 某个四元数 double heading, pitch, roll; quat.getRotate(heading, pitch, roll); // 按照ZXY顺序分解 // 将弧度转换为度数(如果需要) heading = osg::RadiansToDegrees(heading); pitch = osg::RadiansToDegrees(pitch); roll = osg::RadiansToDegrees(roll); ``` 如果用户需要按照其他顺序分解,那么需要自己编写函数。例如,按照XYZ顺序分解(先绕X轴,再绕Y轴,再绕Z轴)的欧拉角分解函数。 最后,关于引用[1]和[3]: - 引用[1]说明了四元数的轴角表示,即一个旋转轴和一个旋转角。 - 引用[3]介绍了四元数的基本概念和用途,主要是避免万向锁(万向节死锁)。 因此,在回答中,我们应该强调旋转顺序的重要性,并指出OSG默认的欧拉角分解顺序。 下面,我们按照要求组织答案。</think>### 在 OSG 中使用 `osg::Quat::getRotate()` 获取旋转角度 在 OpenSceneGraph 中,`osg::Quat` 类提供了 `getRotate()` 方法用于从四元数中提取旋转信息。该方法有两种主要使用方式: #### 1. **获取轴角表示(Axis-Angle)** ```cpp void getRotate(double& angle, osg::Vec3d& axis) const; ``` - **参数说明**: - `angle`:输出参数,旋转角度(弧度) - `axis`:输出参数,旋转轴向量(单位向量) - **数学原理**: 四元数可表示为 $q = [\cos(\theta/2), \sin(\theta/2)\cdot\vec{v}]$,其中 $\theta$ 是旋转角,$\vec{v}$ 是旋转轴[^1] - **示例代码**: ```cpp osg::Quat quat = ...; // 已有的四元数 double angle; osg::Vec3d axis; quat.getRotate(angle, axis); // 提取轴角表示 // 转换为角度制 double angleDeg = osg::RadiansToDegrees(angle); ``` #### 2. **获取欧拉角(按特定旋转顺序)** ```cpp void getRotate(double& heading, double& pitch, double& roll) const; ``` - **参数说明**: - `heading`:绕 Z 轴的旋转角(航向角/yaw) - `pitch`:绕 X 轴的旋转角(俯仰角) - `roll`:绕 Y 轴的旋转角(横滚角) - **旋转顺序**: OSG 默认使用 **Z-X-Y 旋转顺序**: 1. 先绕 Z 轴旋转(航向) 2. 再绕 X 轴旋转(俯仰) 3. 最后绕 Y 轴旋转(横滚)[^2] - **示例代码**: ```cpp osg::Quat quat = ...; // 已有的四元数 double heading, pitch, roll; quat.getRotate(heading, pitch, roll); // 提取欧拉角(弧度) // 转换为角度制 heading = osg::RadiansToDegrees(heading); pitch = osg::RadiansToDegrees(pitch); roll = osg::RadiansToDegrees(roll); ``` #### 重要注意事项: 1. **万向节死锁问题**: - 当俯仰角接近 ±90° 时,航向和横滚会重合 - 此时欧拉角表示不唯一,建议使用轴角表示或四元数插值[^3] ```cpp if (fabs(fabs(pitch) - osg::PI_2) < 0.01) { // 使用轴角表示避免奇点 double angle; osg::Vec3d axis; quat.getRotate(angle, axis); } ``` 2. **旋转顺序一致性**: - 构造四元数时的旋转顺序必须与解析时一致 - 若使用不同顺序,需自行实现转换: ```cpp // 自定义旋转顺序的欧拉角转四元数 osg::Quat createQuat(double yaw, double pitch, double roll) { return osg::Quat( roll, osg::Vec3d(0,1,0), // Y轴 pitch, osg::Vec3d(1,0,0), // X轴 yaw, osg::Vec3d(0,0,1) // Z轴 ); } ``` 3. **四元数归一化**: ```cpp quat.normalize(); // 确保四元数是单位四元数 ``` 非单位四元数会导致提取的角度值错误 #### 调试建议: 1. 可视化旋转轴: ```cpp osg::Vec3d axisEnd = axis * 2.0; // 缩放轴向量 osg::ref_ptr<osg::Geometry> axisGeom = createAxisGeometry(axisEnd); ``` 2. 实时显示角度值: ```cpp osgText::Text* text = new osgText::Text; text->setText("Yaw: " + std::to_string(yaw_deg)); ``` > **关键提示**:优先使用四元数进行旋转计算,仅在需要用户界面交互时才转换为欧拉角[^3]。地理坐标系下的姿态调整需通过局部坐标系轴进行旋转[^1]。 --- ### 相关问题 1. OSG 中如何正确转换四元数与旋转矩阵? 2. 当俯仰角为90度时,为什么欧拉角会出现歧义?如何解决? 3. 如何在 OSG 中可视化物体的局部坐标系轴? 4. 使用四元数插值(SLERP)实现平滑旋转需要注意什么? 5. 在无人机仿真中如何结合四元数进行姿态滤波?[^3] [^1]: 四元数的轴角表示及数学原理 [^2]: OSG 中四元数与欧拉角的转换实现 [^3]: 四元数在3D旋转中的应用及万向节死锁问题
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值