一、欧拉角
1.欧拉角的算法思想是什么
陌生的你来到了成都,站在盐市口茫然四顾,想知道春熙路怎么走?这个时候你选择了去问路,得到了两种回答:
- 往东经104°04′、北纬30°40′走
- 右转后一直走
第一种回答,告诉了你春熙路的绝对坐标,可是很反人类啊!
第二种回答,告诉了你春熙路的相对坐标,很具有操作性。
欧拉角算法的思想就是采用的第二种回答的方式,优点在于很好理解。
2.具体实现步骤
有这么一副动图,清楚的表明了如何通过欧拉角来完成旋转
v2-35da80e428ca5750491ffd2770e187e1_720w.gif
image.png
图中有两组坐标:
- xyz为全局坐标,保持不动
- XYZ为局部坐标,随着物体一起运动
旋转步骤如下:
- 物体绕全局的z 轴旋转 α角
- 继续绕自己的 X轴(也就是图中的 n轴)旋转 β角
- 最后绕自己的 z轴旋转 r角
可能你感到奇怪,为什么第一步是绕着全局坐标旋转?因为要和世界保持联系,否则就和世界完全没有关
系了。
很显然,按照不同的旋转步骤,旋转的结果是不一样的。就好比刚才问路的时候,回答你,“左转再右转”,和“右转再左转”,肯定到达的地点是不一样的。
二、万向锁
参考
万向锁的理解
Unity编辑器-Scene面板里的万向锁现象
万向锁产生的根本原因是绕三个轴的旋转不是同时进行的,想象一下我们旋转矩阵的推导是不是绕三个轴的旋转矩阵乘起来得到的,这三个旋转矩阵的摆放顺序不同,最后得到的旋转矩阵也是不相同的,因此,一般的系统都会有一个规定,比如unity来说,先绕y轴旋转,然后绕x轴旋转,最后是绕z轴的旋转。
先绕y轴旋转,然后绕x轴旋转,最后绕z轴旋转,这里的xyz是模型坐标系的三个轴,当绕x轴旋转的角度为90°的时候,就会出现万向锁的问题,我们实际测试一下看看是为什么。如下图所示,绿色为y轴,红色为x轴,蓝色为z轴:
当绕x轴旋转90°的时候,会发生什么呢?
image.png
没错,正如我们想象的,蓝色的z轴转下去了,这时候绕z轴旋转任意角度就完全等价于一开始绕y轴旋转了,因为它们已经重合(相反)了。这意味着,当绕x旋转90°的时候,绕y轴旋转就等价于绕z轴旋转了,丢失了一个维度的信息,这就是万向锁。
如果还不能理解,一定要记住旋转顺序,这是理解问题的关键所在,先绕y轴旋转,再按x轴旋转,再按z轴旋转。然后先绕y旋转任意角度,然后绕x旋转90度,然后绕z旋转,观察现象即可。
三、四元数的引入
1.如何通俗地解释欧拉角?之后为何要引入四元数?
欧拉角和四元数都能表示一个旋转,同样还有转换矩阵也能表示一个旋转,它们三者之间可以互相转换。具体公式百度可查。
欧拉角是给人看的,对控制系统也好分开单独处理。
四元数是给计算机用的,避免了大量三角函数运算和死锁问题。
转换矩阵是一个矩阵,主要是便于向量的转换计算。直接一个矩阵左乘原坐标系向量就得到了新坐标系的向量。
2.为什么Unity3d旋转默认采用了有万向节死锁的欧拉角,而不用四元数?
Unity的底层是通过四元数记录物体旋转的,并通过矩阵和四元数实现物体的旋转及插值。但在上层Unity提供了,向欧拉角进行转换输出,并能够通过欧拉角进设置物体旋转的功能。
这是由于相较矩阵和四元数,欧拉角是最接近人类直观思维的一种3D旋转表达模式,我是说你应该不想通过输入矩阵或者四元数来旋转物体吧...(除非你是神仙)
在底层通过矩阵/四元数,记录完成旋转,避免万向锁,但在上层提供欧拉角的转换输出和旋转设定,这应该说是当下大多数3D架构都普遍采用的一种模式。Unity就是这样做的,所以不要再说它用了什么屑欧拉角,那是为了照顾我们这些上层开发的凡人。
3.可视化理解四元数,愿你不再掉头发
四元数的一个最主要的应用就是表示旋转,它既是紧凑的,也没有奇异性。而旋转的其他表示方法各有优劣:
- 旋转矩阵:用九个数来表示三个自由度,矩阵中的每一列表示旋转后的单位向量方向,缺点是有冗余性,不紧凑[1]。
- 旋转向量:用一个旋转轴和一个旋转角来表示旋转,但是因为周期性,任何2nπ的旋转等价于没有旋转,具有奇异性[2,3]。
- 欧拉角:将旋转分解为三个分离的转角,常用在飞行器上,但因为万向锁问题(Gimbal Lock) 而同样具有奇异性。
4.四元数——基本概念
可能四元数的由来大家都看过很多遍。很久以前,一位老者坐在大桥边上,看着过往船只,突然灵光一闪,在桥边石碑上洋洋洒洒刻上几行大字,四元数诞生了!故事大家都爱听,那么为什么我们需要四元数?一种说法是解决向量乘法,我们知道向量之间乘法有内积和外积,但这两个运算均不完美,即不满足群的条件(当然四元数诞生的时候也还没有内积外积的说法)。那向量之间是否存在这样一个非常完美的乘法,于是三维空间无法解决的问题就映射到四维空间。这便是四元数诞生的契机。
那么问题又来了,既然四元数只是为了解决矩阵乘法,那为什么我们现在要用四元数进行旋转,甚至替代了欧拉角、轴角等形式?首先,四元数并不是生来为了解决三维旋转,而是它的性质非常有利于表达旋转信息(后面会详述),所以了解四元数的性质要先于了解四元数在旋转中的应用。至于四元数替代欧拉角等形式,就需要牵扯到一些别的知识点,我先罗列一下四元数相比其他形式的优点:
- 解决万向节死锁(Gimbal Lock)问题
- 仅需存储4个浮点数,相比矩阵更加轻量
- 四元数无论是求逆、串联等操作,相比矩阵更加高效
所以综合考虑,现在主流游戏或动画引擎都会以缩放向量+旋转四元数+平移向量的形式进行存储角色的运动数据。
四、cocos 四元数与3D旋转实例!
论坛该帖图挂了,可以看这里:https://blog.csdn.net/weixin_45686592/article/details/109269154
cocos关于四元数的API可以参照:
https://docs.cocos.com/creator/3.0/api/zh/classes/core_math.quat.html
1.旋转轴和旋转角
有了旋转轴和旋转角,就可以表示旋转了,那么四元数也可以通过这个构造出来。
/**
* @zh 根据旋转轴和旋转弧度计算四元数
*/
public static fromAxisAngle<Out extends IQuatLike, VecLike
extends IVec3Like> (out: Out, axis: VecLike, rad: number) {
rad = rad * 0.5; // 为什么要除以2?因为公式推导出来的!
const s = Math.sin(rad);
out.x = s * axis.x;
out.y = s * axis.y;
out.z = s * axis.z;
out.w = Math.cos(rad);
return out;
}
2.本地坐标轴
根据该物体本地坐标轴也能确定旋转。
/**
* @zh 根据本地坐标轴朝向计算四元数,默认三向量都已归一化且相互垂直
*/
public static fromAxes<Out extends IQuatLike, VecLike
extends IVec3Like> (out: Out, xAxis: VecLike, yAxis: VecLike, zAxis: VecLike) {
Mat3.set(m3_1,
xAxis.x, xAxis.y, xAxis.z,
yAxis.x, yAxis.y, yAxis.z,
zAxis.x, zAxis.y, zAxis.z,
);
return Quat.normalize(out, Quat.fromMat3(out, m3_1));
3.视口和上方向
根据视口的前方向和上方向,先计算本地坐标轴的右向量,再算出本地坐标的上向量,最后再构造成四元数。
image.png
/**
* @zh 根据视口的前方向和上方向计算四元数
* @param view 视口面向的前方向,必须归一化
* @param up 视口的上方向,必须归一化,默认为 (0, 1, 0)
*/
public static fromViewUp<Out extends IQuatLike, VecLike
extends IVec3Like> (out: Out, view: VecLike, up?: Vec3) {
Mat3.fromViewUp(m3_1, view, up);
return Quat.normalize(out, Quat.fromMat3(out, m3_1));
}
4.两向量间的最短路径旋转
也可以用一个四元数表示两向量旋转的最短路径。
image.png
/**
* @zh 设置四元数为两向量间的最短路径旋转,默认两向量都已归一化
*/
public static rotationTo<Out extends IQuatLike, VecLike
extends IVec3Like> (out: Out, a: VecLike, b: VecLike) {
// 省略代码实现
}
5.矩阵/欧拉角
也可以通过其他表示方法转换为四元数。
/**
* @zh 根据三维矩阵信息计算四元数,默认输入矩阵不含有缩放信息
*/
public static fromMat3<Out extends IQuatLike> (out: Out, m: Mat3) {
// 省略代码实现
}
/**
* @zh 根据欧拉角信息计算四元数,旋转顺序为 YZX
*/
public static fromEuler<Out extends IQuatLike> (out: Out, x: number, y: number, z: number) {
// 省略代码实现
}
6.获取四元数相关信息
上面讲了如何去构造,相应的也可以通过四元数获取相关信息,这里不细讲了含义了,直接看看API吧。
/**
* @zh 获取四元数的旋转轴和旋转弧度
* @param outAxis 旋转轴输出
* @param q 源四元数
* @return 旋转弧度
*/
public static getAxisAngle<Out extends IQuatLike, VecLike extends IVec3Like> (outAxis: VecLike, q: Out) {
//...
}
/**
* @zh 返回定义此四元数的坐标系 X 轴向量
*/
public static toAxisX (out: IVec3Like, q: IQuatLike) {
//...
}
/**
* @zh 返回定义此四元数的坐标系 Y 轴向量
*/
public static toAxisY (out: IVec3Like, q: IQuatLike) {
//...
}
/**
* @zh 返回定义此四元数的坐标系 Z 轴向量
*/
public static toAxisZ (out: IVec3Like, q: IQuatLike) {
//...
}
/**
* @zh 根据四元数计算欧拉角,返回角度 x, y 在 [-180, 180] 区间内, z 默认在 [-90, 90] 区间内,旋转顺序为 YZX
* @param outerZ z 取值范围区间改为 [-180, -90] U [90, 180]
*/
public static toEuler (out: IVec3Like, q: IQuatLike, outerZ?: boolean) {
//...
}
7.角色朝向和平滑插值
没有实战,单纯讲API就是耍流氓!直接进入实战部分!已知当前点和下一个点,如何求出角色的朝向四元数?
- 先算出前方向
- 根据视口上方向求出四元数
const cur_p = list[index - 1]; // 当前点
const next_p = list[index]; // 最终点
const quat_end = new Quat(); // 最终旋转四元数
const dir = next_p.clone().subtract(cur_p); // 前向量
// 模型正好朝z轴方向
Quat.fromViewUp(quat_end, dir.normalize(), v3(0, 1, 0)); // 根据视口的前方向和上方向计算四元数
// 最终旋转四元数 / 视口面向的前方向 / 视口的上方向
上面给出的fromViewUp API参数:
/**
* @zh 根据视口的前方向和上方向计算四元数
* @param view 视口面向的前方向,必须归一化
* @param up 视口的上方向,必须归一化,默认为 (0, 1, 0)
*/
public static fromViewUp<Out extends IQuatLike, VecLike
extends IVec3Like> (out: Out, view: VecLike, up?: Vec3) {
Mat3.fromViewUp(m3_1, view, up);
return Quat.normalize(out, Quat.fromMat3(out, m3_1));
}
已知起始四元数和终点四元数,如何平滑旋转?
const tw = tween(this.node_bezier_role); // 使用tween动画
const quat_start = new Quat();
this.node_bezier_role.getRotation(quat_start); // 获取起始四元数
const quat_end = new Quat(); // 最终旋转四元数 假设已经算出
const quat_now = new Quat(); // 用一个中间变量
tw.to(0.2, {}, {
onUpdate: (target, ratio: number) => {
// ratio : 0~1
// 这里使用球面插值,旋转时不会出现变形
quat_now.set(quat_start).slerp(quat_end, ratio);
this.node_bezier_role.setRotation(quat_now);
},
})
tw.start();
9135d7cb5ec506c8005ce392a62c720f.gif
8.触摸旋转
50752f2465433f33ef0886fd22b621b7.gif关键是求出旋转轴,这边处理的旋转轴在 xoy 这个平面上。
image.png
// private onTouchMove(touch: Touch) {
const delta = touch.getDelta();
// 自传
// 这个物体模型‘锚点’在正中心效果比较好
// 垂直的轴,右手
//
// 旋转轴
// ↑
// ---> 触摸方向
const axis = v3(-delta.y, delta.x, 0); //旋转轴,根据相似三角形求出
const rad = delta.length() * 1e-2; //旋转角度
const quat_cur = this.node_touch_rotation_role.getRotation(); //当前的四元数
Quat.rotateAround(this.__temp_quat, quat_cur, axis.normalize(), rad); //当面的四元数绕旋转轴旋转
// 旋转后的结果 / 当前的四元数 / 旋转轴 / 旋转四元数
this.node_touch_rotation_role.setRotation(this.__temp_quat);
关于rotateAround,参考API:
/**
* @zh 绕世界空间下指定轴旋转四元数
* @param axis axis of rotation, normalized by default
* @param rad radius of rotation
*/
static rotateAround<Out extends IQuatLike, VecLike
extends IVec3Like>(out: Out, rot: Out, axis: VecLike, rad: number): Out;
这里也可以参照官方test-case-3d中cases/ui/16.coordinate/coordinate-ui-3d.scene
//rotate-around-axis.ts
const _v3_0 = new Vec3();
const _quat_0 = new Quat();
@ccclass("RotateAroundAxis")
@menu("UI/RotateAroundAxis")
export class RotateAroundAxis extends Component {
update (deltaTime: number) {
_v3_0.set(-1, 1, 0);
_v3_0.normalize();
Quat.rotateAround(_quat_0, this.node.rotation, _v3_0, Math.PI * 0.01);
this.node.setRotation(_quat_0);
}
}
可以看出,第一个参数是要返回的四元数,第二个是当前四元数,第三个是旋转轴,第四个是旋转速度。
注意第三个参数旋转轴需要归一化。
9.绕轴旋转
6e667c21ceefc4c7ebad6681a88ff5d4.gif已知旋转点、旋转轴、旋转角度,求旋转后的位置和朝向。朝向计算和触摸旋转类似,这里不详说了。这边讲讲如何计算旋转后的坐标。
- 先计算旋转点和当前位置点的向量(起始向量)
- 计算旋转四元数
- 计算起始向量旋转后的向量
- 计算旋转后的坐标点
// private onTouchMove(touch: Touch) {
const delta = touch.getDelta();
// 绕轴转
// 这里选取轴朝上
const axis2 = Vec3.UP;//旋转轴
const rad2 = 1e-2 * delta.x; //旋转角度
// 计算坐标
const point = this.node_axi.worldPosition; //旋转点
const point_now = this.node_touch_axi_role.worldPosition; // 当前点的位置
// 算出坐标点的旋转四元数
Quat.fromAxisAngle(this.__temp_quat, axis2, rad2);
// 计算旋转点和现有点的向量
Vec3.subtract(this.__temp_v3, point_now, point);
// 计算旋转后的向量
Vec3.transformQuat(this.__temp_v3, this.__temp_v3, this.__temp_quat)
// 计算旋转后的点
Vec3.add(this.__temp_v3, point, this.__temp_v3);
this.node_touch_axi_role.setWorldPosition(this.__temp_v3);
// 计算朝向
// 这么旋转会按原始的朝向一起旋转
const quat_now = this.node_touch_axi_role.worldRotation;
Quat.rotateAround(this.__temp_quat, quat_now, axis2, rad2);
Quat.normalize(this.__temp_quat, this.__temp_quat);
this.node_touch_axi_role.setWorldRotation(this.__temp_quat);
源码:https://github.com/baiyuwubing/cocos-creator-3d-examples/tree/master/1-2-x
五、3D场景中求两点间夹角后旋转,实现自动对焦
以下代码可实现物体转向,
let tempQuat: Quat = new Quat();
Quat.rotateAroundLocal(tempQuat, startRot, axis, angle);
Quat.normalize(tempQuat, tempQuat);
this.node.setRotation(tempQuat);
其中,Quat.rotateAroundLocal(tempQuat, startRot, axis, angle); 有一个angle参数
目标:3D场景中有两个物体,让一个物体转一定角度,面向另一物体.
问题:如何求两个物体的角度?
答案:直接用 destinationVec3.subtract(originVec3) 就可以得到方向向量了,然后归一化,最后使用 Quat.fromAxisAngle 来计算表示角度的 Quaternion
六、Creator 3D 在三维空间中,如何让一个物体朝向某个方形
Unity的实现方式
Vector3 dir = targetPos - this.transform.position; //获得某个方向 dir
//四元数Quaternion的LookRotation可以将一个方向转换成四元数转角
transform.rotation = Quaternion.LookRotation(dir); //物体朝向这个方向
Creator 3D的实现方式是什么?
var dir:Vec3 = targetPos.subtract(this.node.position); //获得某个方向 dir
this.node.rotation = ??
// 怎么把方向换算成角度,四元数Quat没有找到把方向转换为角度的方法
node的lookat
/**
* @zh
* 设置当前节点旋转为面向目标位置,默认前方为 -z 方向
* @param pos 目标位置
* @param up 坐标系的上方向
*/
public lookAt (pos: Vec3, up?: Vec3): void {
this.getWorldPosition(v3_a);
Vec3.subtract(v3_a, v3_a, pos);
Vec3.normalize(v3_a, v3_a);
Quat.fromViewUp(q_a, v3_a, up);
this.setWorldRotation(q_a);
}
lookAt是瞬间朝向某个点。我需要做的是从物体的当前朝向慢慢转向目标方向,有个平滑过渡的过程。比如第一人称角色慢慢转向自己的左边,总不能瞬间就转到目标方向吧,应该有个过程。
可以试试这个方法
const dir = targetPos.sub(this.node.position).normalizeSelf()
const q_tmp = new cc.Quat()
cc.Quat.rotationTo(q_tmp, cc.v3(0, -1, 0), dir)
this.node.setRotation(q_tmp)
cc.v3(0, -1, 0)这个参数指的是 node 指向的方向,比如一个子弹的图片头部是朝下的,这个参数就填 cc.v3(0, -1, 0), 如果是朝上的就是 cc.v3(0, 1, 0)
七、lookAt
继续吐槽反人类的右手坐标系,现在物体节点执行lookAt目标居然是-z轴朝向目标
lookAt如何正面相对目标点
https://docs.cocos.com/creator/3.0/manual/zh/asset/model/dcc-export-mesh.html
游戏开发过程中可能会需要用到模型的朝向,例如想要一些物体面向玩家(使用了LookAt方法),这时就需要考虑模型的初始朝向,这里提供两种方法来调整模型的初始朝向。
- Cocos Creator 3D中是以-Z轴做为正前方的朝向,而在Blender中正前方朝向为+Y轴,所以在制作模型时需要以Y轴正方向做为物体的朝向,经过导出的变换后,在Cocos Creator 3D就会是以-Z轴做为正前方的朝向。
- 如果不想在DCC工具中改变朝向,可以在场景中尝试为导入的模型增加一个父节点,然后旋转模型以使得模型的初始朝向为-Z轴,之后的各种旋转相关的操作都以父节点为操作对象。
上图中使用的Quat.rotationTo,和前面帖子中的回答类似,经过我测试,是可以使用的。就是那个v3(0, -1, 0),是哪个轴使用1或-1,需要根据实际情况试几次。
网友评论