前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >欧拉角和万向节死锁

欧拉角和万向节死锁

作者头像
羽月
发布2022-10-08 13:55:16
1.1K0
发布2022-10-08 13:55:16
举报
文章被收录于专栏:羽月技术羽月技术

有很多种方式可以描述旋转,但是使用欧拉角来描述是最容易让人理解的。这篇文章将会介绍欧拉角的基础知识、欧拉角的问题和如何去解决这些问题,当然还有欧拉角无法解决的万向节死锁问题,在最后还会介绍如何将欧拉角转换成矩阵,便于程序计算。

坐标系

在介绍欧拉角之前,我们先来简单了解下坐标系。

我们知道在canvas 2d 中的画布坐标系是下图这个样子的。坐标原点在画布的左上角,X 轴正值向右,Y 轴正值向下。

WebGL 的坐标系和 canvas 2d 的不太一样,而且 WebGL 会比 canvas 2d 多一个 Z 轴。它的坐标系如下所示。

这个坐标系和数学中一样原点在中间,X 轴正值向右,Y 轴正值向上。而我们从正面看不见 Z 轴,得旋转下坐标系。

这里的 Z 轴可能有两种形式,一种是正值朝外也就是指向屏幕面前的你,另一种是正值朝内也就是指向屏幕里面。

当 Z 轴正值朝外,称为坐标系为右手坐标系,当 Z 轴正值朝内称为左手坐标系。可以伸出双手像下图一样比划下,就知道为什么称为左手坐标系和右手坐标系了。

左手坐标系和右手坐标系还有一个区别,是它们的旋转正方向。当绕 Z 轴旋转 90° 时,是顺时针还是逆时针旋转呢?

还是伸出双手,握拳,大拇指伸出,大拇指指向旋转轴的正方向,其他手指的弯曲方向就是旋转正方向。

欧拉角

我们在现实生活中向左转向右转,向上看向下看这些都是旋转,用欧拉角(Euler angles)来描述这些旋转最符合我们的常识,称作欧拉角是因为它是数学大神欧拉证明的,他证明任何一个 3D 空间的旋转,都可以拆分为沿着自身三个坐标轴的旋转,也就是任何 3D 空间旋转都是由三个基本旋转矩阵复合而成的。

我们一般称为这三个旋转为偏航-俯仰-翻滚(Yaw-Pitch-Roll 或 Heading-Pitch-Bank),也就是左右摇头-上下点头-左右歪头。这 3 个旋转的顺序是分别绕 Y 轴、X 轴和 Z 轴旋转,当然旋转的顺序也不一定非要是 YXZ,也可以 XYZ 等其他旋转顺序,比如 ThreeJS 的默认顺序就是 XYZ。

上图中使用的是上一小节介绍的右手坐标系,从轴的正值看向负值,逆时针旋转是旋转正方向。

欧拉角的三次旋转是沿着体轴旋转,而不是固定轴旋转。体轴会随着每一次旋转而旋转,固定轴则是固定不动不会跟随旋转。

我们可以先左右摇头 0 度,然后向下低头 90 度看向地面,最后按照 Z 轴旋转 90 度,此时我们还是面向地面,但是如果我们是按照固定轴旋转则此时是耳朵朝向地面。

image.png

上面图中,左边立方体是按照体轴旋转,右边立方体是安装固定轴旋转。点击该链接查看旋转动画 https://codesandbox.io/s/great-haze-grjke?file=/src/index.js 。

一个比较有意思点是,只要按照相反顺序旋转,固定轴旋转和体轴旋转一样的,比如体轴按照 YXZ 旋转,那么固定轴按照 ZXY 旋转相同角度,旋转结果是相同的!大家可以自己做下实验体会一下。

欧拉角的优点很明显,易于人类使用,我们可以轻松理解,而且欧拉角只用 3 个数字的存储定向可以节省内存。

规范欧拉角

欧拉角也有一些缺点,其中一个是定向不是唯一的,比如旋转 10 度和旋转 360 + 10 度是相同的。要解决这个问题,我们需要使用规范欧拉角,规范欧拉角将偏航和翻滚角限制在

(-180°, 180°]

,俯仰角限制在

[-90°, 90°]

,现在任何定向规范欧拉角都只有一个欧拉角三元组表示。

但是这样会有一个奇点,我们还需要规定如果俯仰角为正负 90 度时,翻滚角为 0,这在万向节死锁小节中解释。所以规范欧拉角需要满足如下规定。

代码语言:javascript
复制
-180 < Yaw <= 180
-90 <= Pitch <= 90
-180 < Roll <= 180

if (Pitch == -90 || Pitch == 90) Roll = 0

插值

欧拉角的另一个缺点是插值问题。在两个定向之间插值,给定参数 t 它的大小是 01。如果它为 0.5 我们就可以获得两个定向中间的一个定向。但是欧拉角有两个方向可以插值。

如上图所示,这两个定向之间相差 20 度,如果我们使用简单的线性插值,那么会绕一大圈旋转 340 度,而不是 20 度。要解决这个问题,我们需要将插值角度限制在

(-180°, 180°]

之间。

代码语言:javascript
复制
function wrapPi(rad) {
  if (Math.abs(rad) <= Math.PI) {
    const PI2 = Math.PI * 2
    rad -= (Math.floor((rad + Math.PI) / PI2) * PI2)
  }
  return rad
}

function lerp(rad1, rad2, t) {
  return rad1 + t * wrapPi(rad2 - rad1)
}

有了上面工具,我们可以找到两个角度之间插值的最短弧。但是这并不能完全解决问题,它还会受到万向节死锁影响。

万向节死锁

万向节死锁(Gimbal lock)是欧拉角根本性的问题,我们并不能和上面一样通过一些方法来解决这个问题。无论用什么顺序(XYZ,YZX 等)去旋转,只要第二个轴旋转角度是正负 90 度就会发生万向节死锁问题。

当第二轴旋转正负 90 度时,第一个轴和第三个轴将会重叠在一起,也就是说这时候丢失了一个自由度,只有两个旋转自由度。我们很难自己去想象这种情形,建议观看这个演示动画。

假设现在有 ZYX 顺序的旋转,其中 Y 轴旋转为 90 度。我们可以看到下图中 X 轴的旋转和 Z 轴的旋转是对相同轴的旋转!

因为欧拉角是按照体轴旋转,旋转顺序是父子关系,父轴旋转会带动子轴旋转,上图中 Y 轴旋转 90 度,带动它的子轴 X 轴旋转 90 度,使 X 轴与 Z 轴重合。

我们也可以从公式来验证这一点。

\begin{aligned} E&=R_z(b) * R_y(\frac{\pi}{2}) *R_x(a) \\ &=\begin{bmatrix} 0 & cos(b)sin(a)-cos(a)sin(b) & sin(a)sin(b)+cos(a)cos(b) \\ 0 & sin(a)sin(b)+cos(a)cos(b) & cos(a)sin(b)-cos(b)sin(a) \\ -1 & 0 & 0 \end{bmatrix} \\ &=\begin{bmatrix} 0 & sin(a-b) & cos(a-b) \\ 0 & cos(a-b) & -sin(a-b) \\ -1 & 0 & 0 \end{bmatrix} \\ &=R_y(\frac{\pi}{2}) * R_x(a-b) \end{aligned}

通过上面公式我们可以发现,绕三个轴旋转,其实最终是绕两个轴旋转(X 轴和 Y 轴),我们丢失了 Z 轴的自由度。

需要注意的是万向节死锁问题,并不是说有欧拉角无法描述的定向。而是两个定向之间的插值问题,如果看了上方视频,可以发现当第二个轴旋转 90 度时,让它再旋转到另一个定向,会发生不自然的旋转,这可能就会照成物体突然晃动等问题。如下图所示,我们期望的是第二个旋转,而不是第一个不自然的旋转。

要避免万向节死锁问题,我们可以用四元数来描述定向,这将在下一篇文章介绍。

欧拉角转矩阵

欧拉角对于人来说很容易理解,但是对于电脑来说并不是。一般由用户输入欧拉角的值,程序在内部将欧拉角转换为矩阵,然后用矩阵去使物体发生旋转并呈现给用户。

因为欧拉角是围绕三个基本坐标轴的旋转,我们可以根据三个轴的旋转矩阵去计算最终的旋转矩阵。(这里不过多介绍如果计算出 3 个轴的旋转矩阵,可以点击连接进行查看)

矩阵的一个优势就是可以将不同的变换通过矩阵乘法相乘,就可以得到一个表示最终变换的矩阵。所以这里将欧拉角转换成矩阵就是将这三个旋转矩阵结合起来。

\begin{aligned} R &= R_y(H) * R_x(P) * R_z(B) \\ &= \begin{bmatrix} cos(H) & 0 & sin(H) \\ 0 & 1 & 0 \\ -sin(H) & 0 & cos(H) \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 \\ 0 & cos(P) & -sin(P) \\ 0 & sin(P) & cos(P) \end{bmatrix} \begin{bmatrix} cos(B) & -sin(B) & 0 \\ sin(B) & cos(B) & 0 \\ 0 & 0 & 1 \end{bmatrix} \\ &= \begin{bmatrix} cos(H)cos(B)+sin(H)sin(P)sin(B) & sin(H)sin(P)cos(B)-sin(B)cos(H) & sin(H)cos(P) \\ sin(B)cos(P) & cos(P)cos(B) & -sin(P) \\ cos(H)sin(P)sin(B)-sin(H)cos(B) & sin(B)sin(H)+cos(H)sin(P)cos(B) & cos(H)cos(P) \end{bmatrix} \end{aligned}

因为我们只关注旋转所以用 3x3 的旋转矩阵就行了,旋转顺序是偏航-俯仰-翻滚,将公式转换为代码如下所示。

代码语言:javascript
复制
class Mat3 {
  static fromHPB(h, p, b, out = []) {
    const ch = Math.cos(h), cb = Math.cos(b), cp = Math.cos(p);
    const sh = Math.sin(h), sb = Math.sin(b), sp = Math.sin(p);
    out[0] = ch * cb + sh * sp * sb
    out[1] = sb * cp
    out[2] = ch * sp * sb - sh * cb
    out[3] = sh * sp * cb - sb * ch
    out[4] = cp * cb
    out[5] = sb * sh + ch * sp * cb
    out[6] = sh * cp
    out[7] = -sp
    out[8] = ch * cp
    return out
  }
}

总结

用欧拉角来描述旋转是非常容易让人理解的,但是欧拉角也会有一些问题,一些问题我们可以用一些方法去解决,但是万向节死锁问题是欧拉角最根本的问题,没有方法可以解决这个问题。万向节死锁是欧拉角的第二个轴旋转角度是正负 90 度时,将会失去一个轴的自由度,它会让两个定向之间的插值变得不自然,要解决万向节死锁问题需要用到四元数。欧拉角容易让人理解,但是对于电脑并不是,所以在程序内部一般会将欧拉角转换成矩阵。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-10-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 羽月技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 坐标系
  • 欧拉角
    • 规范欧拉角
      • 插值
      • 万向节死锁
      • 欧拉角转矩阵
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档