Unity中的欧拉旋转
欧拉角的定义
在撰写本文之前,我查阅了网上众多关于欧拉角的定义,发现大部分都引用自维基百科,在此我也引用其内容:
莱昂哈德·欧拉运用欧拉角来描述刚体在三维欧几里得空间中的取向。对于任何参考系,刚体的取向可通过从该参考系依次进行三次欧拉角旋转来确定。因此,刚体的取向能够由三个基本旋转矩阵决定。换言之,任何关于刚体旋转的旋转矩阵都是由三个基本旋转矩阵复合而成。
在三维空间的一个参考系(也称为实验室参考系,静止不动)中,任何坐标系(固定于刚体,随刚体旋转而旋转)的取向都可以用三个欧拉角表示。设定 xyz - 轴为参考系的参考轴,三个欧拉角为 (α,β,γ),蓝色的轴是 xyz - 轴,红色的轴是 XYZ - 坐标轴。xy - 平面与 XY - 平面的相交线称为交点线(绿色),用英文字母(N)代表。
zxz 顺规的欧拉角可以静态定义如下:
- α 是 x - 轴与交点线的夹角。
- β 是 z - 轴与 Z - 轴的夹角。
- γ 是交点线与 X - 轴的夹角。
遗憾的是,对于夹角的顺序和标记,以及夹角的两个轴的指定,并没有统一的常规,科学家们也未达成共识。因此,在使用欧拉角时,必须明确表示出夹角的顺序并指定其参考轴。
实际上,有多种方法可以设定两个坐标系的相对取向,欧拉角方法只是其中之一。此外,不同的作者可能会用不同组合的欧拉角来描述,或者用不同的名字表示相同的欧拉角。所以,在使用欧拉角之前,必须先做好明确的定义。
顺规
在经典力学中,常使用 zxz 顺规来设定欧拉角,按照第二个转动轴的轴名,简称为 x 顺规。此外,还有其他的欧拉角组。合法的欧拉角组中,唯一的限制是任何两个连续的旋转必须绕着不同的转动轴进行。因此,一共有 12 种顺规。例如,y 顺规(第二个转动轴是 y - 轴)常用于量子力学、核子物理学和粒子物理学;xyz 顺规则用于航空航天工程学,可参阅泰特 - 布莱恩角。
需要特别注意的是,第一段定义中使用的顺规与 Unity 中使用的顺规不同,如果不厘清这一点,很容易造成混淆,可能会误导学习 Unity 并查看欧拉角、万向节死锁等相关文章的人。
Unity 中的定义
以下内容来自 Unity 文档中 Transform.eulerAngles 的定义,它本身是一个 Vector3,即三维矢量,分别包含 x、y、z 三个参数。
文档描述:“The x, y, and z angles represent a rotation z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis (in that order).” “Only use this variable to read and set the angles to absolute values. Don't increment them, as it will fail when the angle exceeds 360 degrees. Use Transform.Rotate instead.”
这意味着 x、y、z 代表三个角度,它们定义了一组有序的旋转,即先围绕 z 轴旋转 z 度,接着围绕 x 轴旋转 x 度,最后围绕 y 轴旋转 y 度。
你应该仅用于读取或直接设置这些数值,而不要对它们进行累加操作,因为当角度超过 360 度时操作将会失败,此时应使用 Transform.Rotate 来执行旋转操作。
解释
由于 Unity 内部使用四元数进行旋转计算,不会存储欧拉角的累计值,所以说角度超过 360 度会失败是可以理解的。在使用四元数完成运算后,会更新对应的欧拉角数值,这个结果欧拉角仅代表等值的旋转变化结果,而无法代表中间过程。由于欧拉角旋转 Z 轴 361 度与 1 度的结果相同,最终只会存储 1 度,以便于观察和使用。
Unity 中的顺规
特别要注意的是,上述文档表明 Unity 使用 zxy 的顺规,这与维基百科的定义不同,因此不能用维基百科图片中的旋转方式来理解。下面我们来详细了解一下 Unity 中的欧拉旋转。
欧拉旋转的小实验
我使用 Unity 制作了一个小实验,可指定在 x +、x -、y +、y -、z +、z - 这些轴向来旋转一个箭头。
- “RotateXX” 按钮:用于旋转指定的轴向 XX。
- “Applied Angles”:代表累计的欧拉旋转角。
- “Result Angles”:代表 Unity 经过四元数计算之后输出的结果欧拉角。
- “Slider”:用于控制旋转的速度。
- 左边居中的空白框:用于显示待旋转的执行项,每次使用 “RotateXX” 按钮都会产生一个旋转待执行项。
- “Rest”:用于重置。
- “Allow Excute”:表示是否允许执行当前的待执行项。
- “Once All”:表示是按顺序执行这些可执行项还是所有执行项一起执行。
红绿蓝(RGB)三色分别代表 XYZ 轴,靠近箭头的三个轴是局部坐标轴,有圆球的一端代表轴的正向。远离箭头的上下左右前后断开的 6 个半截轴代表世界坐标轴,轴的正向与局部坐标相同。初始状态下,物体在全局坐标和局部坐标下的欧拉角都是 (0, 0, 0),即没有任何旋转。
这个小程序可以在我的 Github 中的 UnityLab/Euler 页面(https://andrewfanchina.github.io/UnityLabs/Euler/ )查看,你可以亲自尝试操作以理解这些按钮的作用。注意需要使用支持 WebGL 的浏览器打开,如 Chrome 或 Firefox(360 极速浏览器 8.7 版本以上貌似也支持)。由于文件较大,网速较慢时可能需要稍等片刻。
需要说明的是,我在编写小程序时,刻意使用了累加计算的欧拉角(小程序中的 “Applied Angles”),即每次执行的旋转都会累加到一个 Vector3 中,最后将其设置为箭头的当前欧拉角,相当于箭头复位后重新执行最新的欧拉角累计值。例如,累加计算的欧拉角 (90, 90, 90) 所对应的结果都是相同的,无论你按照何种顺序点击三个旋转按钮。这样,这个小程序可以模拟从初始状态经历当前累计的欧拉旋转。
欧拉旋转的旋转轴
之前一直未明确的一个问题是:我们在围绕哪一个轴进行旋转?Unity 的官方文档也未清晰说明。
准确来说,Unity 中每次执行欧拉旋转时,都使用 “当前轴”。例如在 Transform 的文档中有如下定义:
public void Rotate(Vector3 eulerAngles, Space relativeTo = Space.Self);
描述为:“Applies a rotation of eulerAngles.z degrees around the z axis, eulerAngles.x degrees around the x axis, and eulerAngles.y degrees around the y axis (in that order). if relativeTo is left out or set to Space.Self the rotation is applied around the transform’s local axes. (The x, y and z axes shown when selecting the object inside the Scene View.) If relativeTo is Space.World the rotation is applied around the world x, y, z axes.”
这个函数提供了一个可选的相对空间坐标系参数:
Space.Self:局部坐标系,意味着本次欧拉旋转以物体当前的局部坐标朝向为基础进行旋转。Space.World:世界坐标系,意味着本次欧拉旋转以物体当前的世界坐标朝向为基础进行旋转。
一般而言,常用的旋转是相对当前局部坐标系执行的。更为重要的是,在本次欧拉旋转过程中,其相对轴始终保持不变。
例如,我们指定一组欧拉旋转 (90, 60, 30),根据前面提到的顺规,先绕 Z 轴旋转 30 度,再绕 X 轴旋转 90 度,最后绕 Y 轴旋转 60 度。虽然有这样的顺序,但 Z 旋转后相对 X 轴、Y 轴,都是执行本组欧拉旋转前的那个轴向,并未发生变动,所以我称其为 “当前轴”。在 Unity 中的欧拉旋转就是这样定义的,不排除其他学术领域中欧拉旋转有不同的定义方式。因此,执行:
Transform.Rotate(new Vector3(90, 60, 30));
和执行:
Transform.Rotate(new Vector3(0, 0, 30));
Transform.Rotate(new Vector3(90, 0, 0));
Transform.Rotate(new Vector3(0, 60, 0));
的结果是不同的。第一种情况只执行了一组欧拉旋转,第二种情况执行了三组欧拉旋转,后两组欧拉旋转的相对轴在旋转时已经发生了变动。
使用小程序验证旋转轴
上述小程序始终只执行一组欧拉旋转,每次累计欧拉角变化后,都相当于从初始状态重新执行累计欧拉角的旋转,因此很适合用来验证相对的旋转轴向。
假设我们设定一组欧拉旋转 (90, 90, 90),其最终的旋转结果朝向如下图 A 所示。
按照 Unity 定义的顺规,先执行 Z 轴旋转 90 度,其旋转轴是初始的 +z 轴,轨迹记录了本次旋转划过的位置。接着绕 X 轴旋转 90 度,旋转轴是初始的 +X 轴。最后绕 Y 轴旋转 90 度,旋转轴是初始的 +Y 轴。最终我们得到了 (90, 90, 90) 这一组欧拉旋转的最终结果,与图 A 的结果相同。由此可见,它确实是沿着初始的固定轴向按照 Z、X、Y 的顺序进行旋转的。
总结
最后,我们总结一下 Unity 中的欧拉旋转:它是按照 Z、X、Y 顺规执行的旋转,在一组欧拉旋转过程中,相对的轴向不会发生变化。例如 Transform.Rotate(new Vector3(90, 60, 30)) 代表执行了一组欧拉旋转,它相对的是旋转前的局部坐标朝向。
正是这种顺规和轴向的定义,导致了 “万向节死锁” 的自然形成。