Cg 编程/Unity/旋转
本教程讨论了 Unity 中局部旋转的不同表示方法。它基于“顶点变换”章节。
如果创建一个游戏对象并选中它,“检查器窗口”将在“变换 > 旋转”下显示三个值 X、Y、Z。这些是三个以度为单位测量的欧拉角。(更准确地说,它们是一组可能的三个泰特-布莱恩角,或航海角或卡尔丹角,因为它们描述了绕三个不同轴的旋转,而“正确的欧拉角”将描述一组三个角度,这些角度描述了三个旋转,其中两个旋转是绕同一轴的。)
在 C# 中,您可以将这三个角度访问为 Vector3
变量 Transform.localEulerAngles
。文档指出,这些角度按以下顺序表示——绕 z 轴旋转 Z 度,绕 x 轴旋转 X 度,以及绕 y 轴旋转 Y 度。更准确地说,这些是绕父对象的固定(!)轴的旋转(如果没有父对象,则为世界轴)。由于这些旋转使用固定轴,因此它们也被称为“外旋”。
这三个角度可以描述物体在三维空间中的任何旋转。在 Transform.localEulerAngles
的情况下,它们实际上描述了物体相对于父坐标系(如果没有父对象,则为世界坐标系)的方向。旋转(作为旋转运动的意义)将在下面讨论。
在航空中,当飞机的方向相对于地面的固定轴(例如机场的塔台)指定时,会使用欧拉角,如上图所示。在这种情况下,它们被称为“滚转”(对应于 Z)、“俯仰”(对应于 X)和“航向”(对应于 Y)。以下名为“Extrinsic_Rotations”的 C# 脚本可以附加到对象上以根据这些名称设置欧拉角。(在“项目窗口”中选择“创建 > C# 脚本”,将其重命名为“Extrinsic_Rotations”,双击打开它,将下面的代码复制粘贴到脚本中,然后将脚本从“项目窗口”拖到“层次结构窗口”中的游戏对象上,然后选择游戏对象并在“检查器窗口”中找到脚本的公共变量。)
using UnityEngine;
[ExecuteInEditMode]
public class Extrinsic_Rotations : MonoBehaviour {
public float bankZ, elevationX, headingY;
void Update () {
transform.localEulerAngles =
new Vector3(elevationX, headingY, bankZ);
}
}
与“检查器”中的三个变量交互,以熟悉它们的含义。您可以从所有三个角度都设置为 0 开始。然后依次更改滚转、俯仰和航向,并观察当更改滚转时对象如何绕父(或世界)的 z 轴(Unity 中的蓝色轴)旋转,当更改俯仰时如何绕父的 x 轴(红色)旋转,以及当更改航向时如何绕父的 y 轴(绿色)旋转。
为了建立与“顶点变换”章节中的基本模型矩阵的联系,本小节介绍如何根据三个角度 X、Y、Z(或 θ、ψ 和 φ)计算旋转矩阵。
绕 z 轴旋转 φ(= Z)角度的 4×4 旋转矩阵 为
最左侧列的前两个坐标可以理解为向量 (1,0) 绕角度 旋转的结果,旋转后的向量为 。(图示中使用角度 t;因此,结果为 。)类似地,下一列的前两个分量可以理解为向量 (0,1) 绕角度 旋转的结果,结果为 。
类似地,绕 x 轴旋转 θ(= X)角度的 4×4 旋转矩阵 为
绕 y 轴旋转 ψ(= Y)角度的 4×4 旋转矩阵 为
请注意,正弦项的符号取决于某些约定。这里我们使用 Unity 的约定。
这三个矩阵必须组合成一个矩阵乘积以形成总旋转。由于(列)向量从右侧乘以变换矩阵,因此第一个旋转 必须位于最右侧,最后一个旋转 必须位于最左侧。因此,正确排序的矩阵乘积为
然后,矩阵 可以与其他基本变换矩阵相乘,以形成模型矩阵,如 “顶点变换”部分 中所述。
在 Unity 中,您可以通过以下方式为欧拉角 X、Y、Z
计算一个 4×4 矩阵 m
(下面将更详细地讨论四元数)
Matrix4x4 m = Matrix4x4.TRS(Vector3.zero,
Quaternion.Euler(X, Y, Z), Vector3.one);
在某些情况下,例如机器人学,使用移动旋转轴(即内禀旋转)而不是迄今为止讨论的固定旋转轴(即外禀旋转)更有趣。通常,相对于机器人底座的旋转被称为“偏航”(yaw),手臂的上下旋转被称为“俯仰”(pitch),下一个旋转被称为“滚转”(roll)。由于每个旋转都会导致关节旋转,因此需要对具有移动旋转轴的旋转进行描述。
幸运的是,存在一个简单的等价关系:由 Z、X 和 Y 指定的外禀旋转对应于以下三个内禀旋转(按此顺序):绕 y 轴旋转 Y (“偏航”),绕旋转后的 x 轴旋转 X (“俯仰”),以及绕旋转后的 z 轴旋转 Z (“滚转”)。换句话说,如果以相反的顺序应用移动旋转轴的旋转角(Y、X、Z 而不是 Z、X、Y),则它们与固定旋转轴的旋转角相同。对于大多数人来说,这种等价关系并不直观;但是,通过使用以下允许您指定偏航、俯仰和滚转的 C# 脚本进行练习,您可能会获得更好的理解。
using UnityEngine;
[ExecuteInEditMode]
public class Intrinsic_Rotations : MonoBehaviour {
public float yawY, pitchX, rollZ;
void Update() {
transform.localEulerAngles =
new Vector3(pitchX, yawY, rollZ);
}
}
在数学上,可以通过计算内禀旋转的旋转矩阵来证明这种等价关系。我们从绕 y 轴的旋转开始,即矩阵。如果我们将此矩阵视为从局部坐标到全局坐标的变换,那么很明显,任何在局部坐标中的额外变换都应该从右边相乘(其中向量在局部坐标中指定),而任何在全局坐标中的额外变换都应该从左边相乘(其中结果向量在全局坐标中)。因此,绕局部(即旋转后的)x 轴的旋转应该从右边相乘,并且积变为。使用相同的逻辑,绕局部(即旋转后的)z 轴的后续旋转也应该从右边相乘,并且完整的积变为,这与以相反顺序进行外禀旋转的结果相同,即内禀旋转等价于以相反顺序进行的外禀旋转。
欧拉角的一个有趣特征(以及 Unity 和大多数其他图形应用程序在内部不使用它们的原因之一)是它们可能导致一种称为“万向节锁”的情况。在这种情况下,两个旋转轴平行,因此只使用了两个不同的旋转轴。换句话说,一个自由度丢失了。如果第一个旋转(绕 x 轴的仰角)为 X = ±90°,则 Unity 的欧拉角会出现这种情况。在这种情况下,z 轴旋转到 y 轴上;因此,(旋转后的)第一个旋转轴与(固定的)第三个旋转轴相同。
如果您将仰角设置为 90° 或 -90°,则可以使用上面的脚本轻松构建这种情况。
滚转、俯仰和偏航也用于描述车辆的方向;请参阅“轴约定”的维基百科文章。此外,飞机的主轴也称为滚转(Unity 中的 z 轴)、俯仰(Unity 中的 x 轴)和偏航(Unity 中的 y 轴)。这些轴始终固定在飞机上;因此,滚转、俯仰和偏航始终指的是围绕这些飞机主轴的旋转,而不管飞机的方向如何。因此,这些旋转的数学方法与同名的欧拉角不同。事实上,围绕飞机主轴的旋转更类似于接下来讨论的围绕任意轴的旋转。
如“顶点变换”章节中所述,绕归一化轴旋转 α 角的旋转矩阵为
幸运的是,在使用 Unity 时,几乎永远不需要这个矩阵。相反,可以通过以下方式将对象的局部“旋转”设置为围绕axis
旋转angle
度:
using UnityEngine;
[ExecuteInEditMode]
public class Angle_Axis_Rotations : MonoBehaviour {
public float angle;
public Vector3 axis;
void Update() {
transform.localRotation =
Quaternion.AngleAxis(angle, axis);
}
}
实际上,这会设置对象相对于其父对象(如果没有父对象,则相对于世界)的方向。为了围绕给定轴以给定的角速度(度/秒)旋转,可以使用函数Transform.Rotate
using UnityEngine;
[ExecuteInEditMode]
public class Angle_Axis_Rotating : MonoBehaviour {
public float degreesPerSecond;
public Vector3 axis;
void Update() {
transform.Rotate(axis,
degreesPerSecond * Time.deltaTime,
Space.Self);
}
}
degreesPerSecond
是角速度,它指定对象旋转的速度。但是,Transform.Rotate
需要以度为单位的旋转角度。要计算此角度,必须将角速度乘以自上次调用Update
以来经过的时间(以秒为单位),即Time.deltaTime
。
此外,Transform.Rotate
接受第三个参数,该参数指定旋转轴所指定的坐标系:Space.Self
表示对象的局部坐标系,Space.World
表示世界坐标系。使用Space.Self
,我们可以通过指定俯仰轴(1,0,0)、偏航轴(0,1,0)和滚转轴(0,0,1)轻松模拟滚转、俯仰和偏航。
您可能已经在上面的代码中注意到,Unity 使用四元数(实际上是归一化的四元数)来表示旋转。例如,变量Transform.localRotation
的类型为Quaternion
。
从某种意义上说,四元数只是具有某些特殊函数的四维向量。长度为 1 的归一化四元数对应于旋转,并且当您知道归一化的旋转轴(x、y、z)和旋转角 α 时,很容易构建。相应的四元数 q 只是
但是,在 Unity 中,您可以使用构造函数Quaternion.AngleAxis(alpha, new Vector3(x, y, z))
(其中 alpha 以度为单位)来构造归一化四元数。(请参阅上一小节中的示例。)
反之,具有分量的归一化四元数 q 对应于角度的旋转。可以通过归一化 3D 向量来确定旋转轴的方向。在 Unity 中,您可以使用函数Quaternion.ToAngleAxis
来计算相应的轴和角度。
由于归一化四元数对应于旋转,因此它们也对应于旋转矩阵。实际上,两个归一化四元数的乘积对应于以相同顺序对应的旋转矩阵的乘积。计算两个四元数的乘积实际上有点棘手,但在 Unity 中,您可以只使用*
运算符。因此,在四元数乘积(以及逆四元数)的情况下,将四元数视为“类似于旋转矩阵”而不是四维向量是有用的。
增量旋转在实时图形中非常常见。关于某个轴**在物体局部坐标系中**进行给定角速度旋转的代码需要将先前的旋转矩阵(指定先前的方向)与新的增量旋转矩阵组合。如果增量旋转矩阵在局部坐标中应用,则应首先应用它(当点仍在局部坐标中时,即在任何其他变换之前)。由于矩阵乘积应从右到左读取,这意味着我们必须按此顺序将先前的旋转矩阵与增量旋转矩阵相乘。先前方向的四元数为transform.localRotation
,此代码中新的增量四元数称为q
using UnityEngine;
[ExecuteInEditMode]
public class Quaternion_Rotating : MonoBehaviour {
public float degreesPerSecond; // angular speed
public Vector3 axis;
void Update() {
Quaternion q = Quaternion.AngleAxis(
degreesPerSecond * Time.deltaTime, axis);
transform.localRotation = transform.localRotation * q;
}
}
如果最后一行中的四元数乘积更改为
transform.localRotation =
q * transform.localRotation;
它对应于在**父坐标系**中应用增量旋转(如果不存在父级,则为世界坐标系),就像在将顶点变换到父坐标系后应用旋转矩阵一样。(始终记住矩阵乘积和四元数乘积应从右到左读取,因为列向量是从右侧乘以的。)
在许多情况下,将四元数视为旋转矩阵,然后将旋转矩阵视为坐标系之间的(被动)变换是有用的。例如,从物体到世界坐标的变换矩阵可以认为是从父坐标系到世界坐标的变换矩阵乘以从物体坐标到父坐标系的变换矩阵的乘积(记住从右到左读取矩阵乘积),即
物体的全局和局部旋转以及其父级的旋转的对应四元数按相同的顺序相乘
我们可以针对任何一个矩阵或四元数求解这些方程。例如,我们可能有一个全局旋转(例如,来自Quaternion.LookRotation),但我们需要知道局部旋转,因为某些函数需要它,例如Animator.SetBoneLocalRotation
。为此,我们可以通过从左侧乘以的逆矩阵并将左侧与右侧交换来求解矩阵方程以获得
同样,将这些矩阵视为四元数的表示,我们可以为相应的四元数编写一个方程
因此,如果我们有一个四元数 qGlobal
表示游戏对象 gameObject
的全局旋转,那么我们可以使用以下代码计算局部旋转 qLocal
Quaternion qLocal =
Quaternion.Inverse(gameObject.transform.parent.rotation)
* qGlobal;
在虚拟现实 (VR) 应用程序中,通常需要根据跟踪器的方向设置对象的方位。例如,可能需要根据连接到用户脚部的跟踪器方向设置化身虚拟脚部的方位。或者可能需要根据头戴式显示器的方向设置虚拟摄像机的方位。
在这些情况下,跟踪器的初始方向通常是未知的,并且不应该重要。因此,通常使用校准,即要求用户假设某个姿势(校准姿势),并记录校准姿势下跟踪器的方向。校准后,使用跟踪器的当前方向来确定对象的方向。如果跟踪器的当前方向与其校准姿势的方向相同,则对象的方向应对应于校准姿势。否则,使用跟踪器的当前方向与其校准姿势的方向之间的偏差来旋转校准姿势。
此操作的数学原理如下:我们用四元数 表示校准时跟踪器的方向,用四元数 表示跟踪器的当前方向;两者都相对于世界坐标系。当前跟踪器方向与校准时跟踪器方向之间的偏差由乘积 给出。从右到左读取乘积:我们撤消校准时跟踪器的记录旋转(因此是逆运算),然后根据当前跟踪器方向旋转。(由于 是相对于世界坐标系的旋转,因此必须从左侧进行乘法。)如果这两个四元数相等,则两个方向之间没有偏差。在这种情况下,四元数与其逆的乘积抵消了任何旋转,得到的乘积表示 0 度的旋转,即没有旋转,这与两个方向之间没有偏差时的情况一致。
对于对象的实际方向,我们需要用此偏差旋转校准姿势。由于此偏差以世界坐标给出,因此我们只需将此偏差从左侧乘以表示校准姿势的四元数
.
此乘积表示基于校准跟踪器的对象的最终方向。
至少在计算机图形学中,(归一化)四元数相当无害,并且永远不会比旋转矩阵或轴角表示更复杂(取决于上下文)。它们的具体优势在于它们没有万向节死锁(与欧拉角相反),它们可以通过乘法轻松组合(与欧拉角和旋转的轴角表示相反),并且它们易于归一化(与旋转矩阵的正交化相反)。因此,大多数图形应用程序在内部使用四元数来表示旋转——即使在用户界面中使用欧拉角。
在这个相当理论的教程中,我们已经了解了
- Unity 用于外旋的欧拉角(即俯仰、航向和横滚)。
- Unity 用于内旋的欧拉角(即俯仰、偏航和滚转)。
- 旋转的矩阵表示。
- 旋转的轴角表示。
- 旋转的四元数表示。
如果您想了解更多信息
- 关于模型矩阵,您可以阅读 “顶点变换” 部分的描述。
- 关于欧拉角,您可以阅读 关于欧拉角的维基百科文章。
- 关于使用欧拉角指定车辆方向,您可以阅读 关于坐标系约定的维基百科文章。
- 关于 Unity 中的相关类,您应该阅读
Transform
、Quaternion
和Matrix4x4
的官方文档。