4.5.2 四元数的概念
四元数包含⼀个标量分量和⼀个三维向量分量,四元数Q可以记作Q=[w,(x,y,z)]
在3D数学中使⽤单位四元数表⽰旋转,下⾯给出四元数的公式定义。对于三维空间中旋转轴为n,旋转⾓度为a的旋转,如果⽤四元数表⽰,则4个分量分别为
w=cos(α/2)
x=sin(α/2 )cos (βx)
y=sin(α/2 )cos (βy)
z=sin( α/2 )cos (βz)
⽤四元数表⽰旋转⼀点也不直观,4个分量w、x、y和z与绕各轴的旋转⾓度并没有直接的对应关系。在实际游戏开发中不要试图获取和修改某⼀个分量,应当只做整体处理。
前⾯提到,矩阵也可以表⽰旋转,⽽且矩阵也不存在万向节锁定问题。其实,旋转还可以⽤欧拉⾓和四元数表⽰,但是每⼀种表⽰⽅法都有其各⾃的优缺点,表4-5对这3种⽅式进⾏了对⽐。
表4-5 3种表⽰旋转的⽅法对⽐。
⼀⽅⾯,由于3种表⽰旋转的⽅法都有各⾃的优势和缺点,因此在实际中会根据具体需求进⾏考虑。另⼀⽅⾯,由于旋转的表⽰⽅法⾮常重要,因此应当尽可能在引擎层⾯进⾏统⼀,这样才能尽可能减少开发游戏时的问题。
⼩提⽰
编辑器⾥显⽰欧拉⾓是为了使⽤⽅便Unity内部旋转是⽤四元数,即结构体Quaternion表⽰的,但是在界⾯上很多地⽅会转为更直观的欧拉⾓,以便设计师直接指定旋转的⾓度。这种⽅式兼顾了准确性和便利性。
4.5.3 Quaternion结构体
下表详细介绍了Quaternion(四元数)结构体的属性(表4-6)和⽅法(表4-7)。
4.5.4 理解和运⽤四元数
在“位置与向量的关联”⼩节中,详细讲解了“位置”与“向量”的区别。接下来引出与之对应的另⼀对概念——“朝向”与“旋转”。就好⽐“位置”是⼀个具体的坐标,“朝向”也是具体的状态,如朝南、朝北、朝欧拉⾓(30, 30, 0),都是表达物体的⼀个固定⽅向。只要A物体朝向世界坐标系的后⽅,就可以确定它的欧拉⾓为(0, 180, 0)。
由于欧拉⾓允许超过360°或者是负⾓度,因此严谨地说欧拉⾓应当是(360×n, 180+360×n, 360×n),其中n为整数。“旋转”有点像前⽂提到的“向量”,指的是⼀个旋转的变化量。例如“右转90°”就是⼀个旋转,如果⾯朝南,右转90°就是朝⻄;如果⾯朝东,右转90°就是朝南。将位置、向量与朝向、旋转类⽐,可以形成清晰的概念。
前⽂提到,位置和向量都是⽤Vector3表⽰。不出意料,朝向和旋转都是⽤四元数(Quaternion)表⽰。理解了Vector3加法的意义,就可以类⽐出四元数的旋转操作,只是加法变成了乘法。表4-8说明了四元数乘法的含义。
向量加法中,向量a+b和向量b+a没有什么不同,但在旋转问题中,q1×q2与q2×q1的结果⼀般不相等。也就是说,四元数相乘不满⾜交换律,这也印证了三维空间中物体的旋转不是⼀个简单的问题。更有意思的是,四元数乘法可以直接作⽤于向量,这样就可以⽅便地直接旋转向量了,如表4-9所⽰。
注意四元数乘以向量时,必须四元数在前,向量在后。不存在“向量*四元数”的操作,代码中写“向量*四元数”会报错。下⾯补充⼀个演⽰程序,实际演⽰⼀下四元数旋转。
01 新建⼀个场景,然后在场景中新建⼀个㬵囊体,位置归0。
02 给㬵囊体新建⼀个⽴⽅体作为⼦物体,可以给⼦物体赋予有颜⾊的材质。
03 将⽴⽅体移动到㬵囊体前⽅,这是为了⽅便区分㬵囊体的⽅向,模拟⼈形⾓⾊,如图4-17所⽰。
04 新建脚本QuatTest,其内容如下。
using UnityEngine;
public class QuatTest : MonoBehaviour
{
void Update()
{
float v = Input.GetAxis("Vertical");
float h = Input.GetAxis("Horizontal");
// 将横向输⼊转化为左右旋转,将纵向输⼊转化为俯仰旋转,得到⼀个
很⼩的旋转四元数
Quaternion smallRotate = Quaternion.Euler(v, h, 0);
// 将这个⼩的旋转叠加到当前旋转位置上
if (Input.GetButton("Fire1"))
{
// 按住⿏标左键或Ctrl键时,沿世界坐标轴旋转
transform.rotation = smallRotate *
transform.rotation;
}
else
{// 不按⿏标左键和Ctrl键时,沿局部坐标轴旋转
transform.rotation = transform.rotation *
smallRotate;
}
}
}
05 将脚本挂载到㬵囊体上,进⾏测试。
运⾏游戏,按W、A、S、D键或⽅向键可以看到㬵囊体沿⾃⾝的坐标轴转动;如果按住攻击键(默认为⿏标左键或Ctrl键)再旋转,则会发现㬵囊体沿世界坐标轴转动。
仔细观察代码,会发现⽤四元数表⽰旋转很⽅便。通过transform.rotation可以获取物体当前的朝向,通过Quaternion.Euler()⽅法可以借⽤⾓度创建出任意的旋转量。未来与旋转有关的操作都可以套⽤这两种基本⽅法。
但是细⼼的读者可能会发现⼀个问题:沿世界坐标轴旋转时,⽤的是“朝向=旋转*朝向”的操作,这⼀操作不符合前⽂所说的“朝向*旋转=新的旋转”的解释。
这⾥⼜再次体现出了三维空间中旋转的复杂性:⾸先,四元数相乘不符合交换律,相乘顺序不同会影响结果;其次,也可以认为相乘顺序的不同导致“旋转轴”不同。这样⼜了解到⼀个技巧:通过交换四元数相乘的顺序,可以让物体沿不同坐标系的坐标轴进⾏旋转。
⼩提⽰
回忆⼀下前⽂⽤到的向量插值之前⽤过的Vector3.Lerp⽅法是取两个位置坐标(或者向量)之间的某个值。四元数的插值与之类似,就是在两个朝向(或者旋转)之间取⼀个值。
4.5.5 四元数的插值
前⽂通过介绍“万向节锁定”,解释了使⽤四元数的必要性。其实四元数还有⼀个“绝活”,那就是做动画与插值。物体的转向动画应该是直接的、均匀的,不能给⼈“绕远”的感觉。⼏何上,在球⾯上的两点之间移动,沿“⼤圆”的路径是最短的。⼤圆的定义是“过球⼼的平⾯与球⾯相交形成的圆”,换句话说就是能在球⾯上画出的最⼤的那⼀类圆。
以地球的经纬线为例,所有纬线中只有⾚道是⼤圆,其他纬线都不是⼤圆,⽽所有的经线都是⼤圆,如图4-18所⽰。两个朝向之间的旋转动画,当旋转的路径符合⼤圆时,旋转路径就是最短的,看起来也最舒服。
欧拉⾓在制作旋转动画时具有很⼤缺陷。如果仅沿⼀个轴旋转,例如从朝南转向朝东,则⽤欧拉⾓的⾓度的线性变化也能做出正确的动画,⽽当旋转较为复杂,x轴、y轴和z轴都有旋转时,物体则不会按照“⼤圆”的路径运动,得到的效果会很奇怪。矩阵在表现旋转动画时,也很难计算正确的插值点。⽽四元数就很擅⻓插值运算,在实际游戏开发中Unity提供了Quaternion.Slerp()⽅法,⽤于旋转的插值。它的函数定义如下。
⼩提⽰ 四元数有两种插值函数 四元数除了有Slerp⽅法,也有Lerp⽅法。 由于Lerp⽅法不是严格的球⾯插值,因此⼀般情况下使⽤Slerp ⽅法。 |
static Quaternion Slerp(Quaternion a, Quaternion b, float t); |
Quaternion的第1个参数是第1个朝向(或旋转),第2个参数是第2个朝向(或旋转),t取0到1之间的值。调⽤Quaternion.Slerp⽅法后就可以获得前⾯两者之间的⼀个四元数。
代码举例如下。
// 前⽅
Quaternion q = Quaternion.identity; // identity相当于Eular(0,
0, 0),不旋转
// 改变物体的朝向,取当前朝向与正前⽅之间10%的位置transform.rotation = Quaternion.Slerp(transform.rotation, q,
0.1f);
4.5.6 朝向与向量
朝向代表着空间中的⼀个⽅向,⽽向量也可以表⽰⼀个⽅向,因此有时也可⽤向量直接表⽰物体的朝向。例如,最常⽤的transform.forward表⽰指向物体前⽅(世界坐标系)。
不仅可以通过transform.forward属性获得指向物体前⽅的向量(已标准化,⻓度为1⽶),⽽且可以直接设置它以改变物体的朝向。例如在第2.6节的实例中,物体的转向是通过键盘控制的,这⾥可以很容易地改为⿏标控制。其步骤简单介绍如下。
01 搭建⼀个类似第2.6节的简单场景和⾓⾊。
02 为⾓⾊添加⿏标控制旋转的脚本MouseRotation。
using UnityEngine;
public class MouseRotation : MonoBehaviour
{
void Update()
{
Ray ray =
Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
transform.forward = hit.point -
transform.position;
}
}
}
这个脚本删除了所有不必要的代码,⽤最简单的⽅式实现了射线检测与专项,读者可以逐句分析。其中射线的原理可参考第3章物理系统的相关内容。
03 将脚本挂载到⾓⾊上,运⾏游戏,移动⿏标指针指向地⾯的不同位置,⾓⾊就会转向⿏标指针指向的位置,如图4-19所⽰。
这⾥想说明的是,只要获取了前⽅向量,并将transform.forward赋值,就可以实现直接转向。向量没有标准化也没关系,赋值时会⾃动将它标准化。
这段代码有⼀个⼩问题,主⾓在旋转时会低头。这是因为⾓⾊有⼀定⾼度,地⾯上的点低于⾓⾊,因此⾓⾊会向下看。解决⽅案是将结果向量的y轴设置为与⾓⾊⾼度⼀致,其代码修改如下。
if (Physics.Raycast(ray, out hit))
{
// 防⽌⾓⾊低头的代码
Vector3 v = hit.point - transform.position;
v.y = transform.y;
transform.forward = v;
}
向量可以代表朝向,四元数也可以代表朝向,那么向量也就可以转化为四元数。向量转化为四元数(朝向)的⽅法定义如下。
Quaternion Quaternion.LookRotation(Vector3 forward);
Quaternion Quaternion.LookRotation(Vector3 forward, Vector3
up);
在之前介绍的四元数使⽤⽅法中,都是借⽤物体的transform.rotation获取⼀个朝向,然后以它为基准继续旋转。Quaternion.LookRotation()则提供了直接获得朝向的⼀种⽅法。
仔细思考会发现问题——难道四元数可以⽤向量代替吗?应该不可以,如果能代替就不需要发明四元数这种复杂的表⽰⽅法了。其实向量代表朝向有很⼤的局限性,例如,⼀个⼈躺在床上,⾯朝上,他的前⽅是世界坐标系的上⽅。这时候,他可以在保持⾯朝上的情况下在床上⽔平旋转,让头部朝东或朝⻄。⽽⽆论他的头部朝向哪⾥,他的前⽅都可以保持向上。
这说明,代表某个向量⽅向的四元数不是唯⼀的。如果只⽤⼀个向量代表前⽅,理论上有⽆数种四元数都指向该⽅向,但它们的“上⽅”不同。如果仅指定前⽅,即使使⽤LookRotation()⽅法只有⼀个参数的形式,Unity也会⽤某种规则假定⼀个合适的“上⽅”;⽽如果要精确地控制,则需要⽤Quaternion的另⼀种形式,⽤两个参数分别指定前⽅向量和上⽅向量,这样就可以得到更符合要求的结果。
4.6 实例:第⼀⼈称视⾓的⾓⾊控制器
为了对前⾯所讲解的数学知识进⾏实践,本节做⼀个完整的FPS(第⼀⼈称射击)游戏⾓⾊控制器。整个过程尽可能⽤简单的脚本代码实现,避免使⽤插件,从⽽充分体现出向量、四元数等知识在实践中的运⽤⽅法。
4.6.1 搭建简单场景
01 新建⼀个场景,放置⼀个平⾯作为地板,将地板放⼤5倍。
02 创建⼀个新的材质挂载在地板上,给地板⼀个较深的颜⾊。
03 在地板上放置⼀些⽴⽅体,主要是为了在测试时作为参照物使⽤,如图4-20所⽰。
4.6.2 创建主⾓物体
这⾥依然⽤⼀个简单模型作为主⾓,能⽅便查看⽅向即可。
01 创建⼀个㬵囊体,㬵囊体默认⾼度为2,代表⼀个⾓⾊。
02 创建⼀个⽴⽅体作为㬵囊体的⼦物体,将⽴⽅体放在㬵囊体的“脸”部。给⽴⽅体挂上材质,材质要设为显眼的颜⾊。
03 将㬵囊体命名为Player。
04 重点:拖曳场景列表中的主摄像机,让主摄像机变成㬵囊体的⼦物体。
05 将主摄像机的位置归0,也就是主摄像机与主⾓重合。然后微调主摄像机的位置到⾓⾊的脸部。
这样主⾓和场景就准备完成了。第⼀⼈称射击游戏的摄像机会完全跟随主⾓移动和转动,因此设置为⼦物体是⼀种⽐较简单的做法。
4.6.3 编写控制脚本——移动部分
(1)创建脚本FPSCharacter,将其挂载于㬵囊体Player上。
(2)编写脚本内容。脚本内容是重点,下⾯分步介绍。
⾓⾊的控制分为两⼤块:⾓⾊移动和摄像机旋转。
⾓⾊移动⽅⾯,玩家可以按⽅向键进⾏前后左右平移。问题是:玩家按上⽅向键时,对应哪个⽅向;玩家按右⽅向键时,⼜对应哪个⽅向。
图4-21所⽰的⾓⾊本⾝具有前⽅向量transform.forward和右⽅向量transform.right。右⽅向量与右⽅向的移动直接对应;前⽅向量⽐较⿇烦,因为存在抬头、低头的情况。如果玩家在抬头看天时直接向前⽅⾛,就会得到向天上⻜的结果。
解决⽅案是得到前⽅向量后,略加修改,去除前⽅向量的y轴分量,这样就让前⽅向量保持⽔平了。完整代码在本章末尾,计算移动的部分代码如下。文章来源:https://www.toymoban.com/news/detail-774925.html
void Move()
{
float x = Input.GetAxis("Horizontal"); // 输⼊左右
float z = Input.GetAxis("Vertical"); // 输⼊前后
// ⽅向永远平⾏地⾯,⾓⾊不能⾛到天上去
// 获得⾓⾊前⽅向量,将y轴分量设为0
Vector3 fwd = transform.forward;
Vector3 f = new Vector3(fwd.x, 0, fwd.z).normalized;// ⾓⾊的右⽅向量与右⽅向的移动直接对应,与抬头⽆关,可以直接⽤
Vector3 r = transform.right;
// ⽤f和r作向量的基,组合成移动向量
Vector3 move = f * z + r * x;
// 直接改变玩家位置
transform.position += move * speed * Time.deltaTime;
}
这段代码中的“move=f*z+r*x”是⼀个很有⽤的技巧,f和r都是⻓度为1的⽅向向量。x是左右⽅向向量的⻓度,范围为-1~1;z是前后⽅向向量的⻓度,范围为-1~1。x控制着f的⻓度,z控制着r的⻓度。这样将前后与左右⽅向相加,就得到了移动向量。
文章来源地址https://www.toymoban.com/news/detail-774925.html
到了这里,关于Unity 3D脚本编程与游戏开发(2.6)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!