前面过了遍 MathUtils 和 Euler 的 api,这节过一下剩下的 Quaternion、Matrix4 的 api
Quaternion - 四元数
Quaternion 使用四元数表示旋转
前面我们用过:
const angle = THREE.MathUtils.degToRad(30);
const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle(new THREE.Vector3(0, 0, 1), angle);
const euler = new THREE.Euler();
可以是绕某个轴旋转一定角度,然后转为 euler
它和欧拉角 eular 是可以相互转换的。

const euler = new THREE.Euler(THREE.MathUtils.degToRad(45), 0, 0);
const quaternion = new THREE.Quaternion();
quaternion.setFromEuler(euler);
console.log('From Euler:', quaternion);

那从 euler 转换成 quaternion 有什么好处呢?
主要是它可以做一些差值:
球面线性插值(Slerp)
这是四元数最重要的特性之一,可以平滑地在两个旋转之间插值:
const q1 = new THREE.Quaternion();
q1.setFromEuler(new THREE.Euler(0, 0, 0));
const q2 = new THREE.Quaternion();
q2.setFromEuler(new THREE.Euler(0, THREE.MathUtils.degToRad(90), 0));
const q = new THREE.Quaternion();
q.slerpQuaternions(q1, q2, 0.5);
console.log('Slerp result:', q);
这里第一个四元数是 0 度,第二个四元数是绕 y 旋转 90 度
那两者之间的中间角度呢?
可以通过 slerp 进行插值,拿 0.5 比例的角度值:

这样可能看不出是中间的值,我们转成 euler 然后再转角度:
const e = new THREE.Euler();
e.setFromQuaternion(q);
console.log('angle y:', THREE.MathUtils.radToDeg(e.y));

可以看到,是 45 度
这就是通过 Quaternion 计算的两个角度之间的插值。
那如果 30% 位置是什么角度呢?

改成 0.3

可以看到是 27 度
计算两个向量夹角
有了两个 Vector3 向量,计算这两个向量之间的角度,是不是一个很常见的需求?
这种就可以通过四元数 Quaternion 实现:
计算从向量 v1 到向量 v2 的旋转:
const v1 = new THREE.Vector3(1, 0, 0);
const v2 = new THREE.Vector3(0, 1, 0);
const quaternion = new THREE.Quaternion();
quaternion.setFromUnitVectors(v1, v2);
console.log('From vectors:', quaternion);

总之,Euler 用来存储角度值的,但是要计算角度的插值、向量之间的夹角等,需要转成四元数 Quaternion 来计算。
Matrix4 - 4x4 矩阵
Matrix4 用于 3D 变换(平移、旋转、缩放),是 Three.js 中最重要的矩阵类。
当然,矩阵是比较底层的运算,了解即可,一般我们都是通过 eular、quaternion 计算和设置。
直接 new 的是单位矩阵:
// 创建单位矩阵
const matrix = new THREE.Matrix4();
console.log('单位矩阵:', matrix);

可以通过 set 方法设置值:
matrix.set(
2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 2, 0,
0, 0, 0, 2
);
console.log(matrix);

也可以基于矩阵实现平移、旋转、缩放等功能。
其实我们修改 tanslate、scale、rotation 的底层都是矩阵运算:
平移、旋转、缩放
创建一个新的立方体:
创建 mesh2.js
import * as THREE from 'three';
const geometry = new THREE.BoxGeometry(100, 100, 100);
const material = new THREE.MeshPhongMaterial({
color: 'orange'
})
const mesh = new THREE.Mesh(geometry, material);
export default mesh;
引入下:


这次我们通过矩阵来对它做变换:

const translateMatrix = new THREE.Matrix4().makeTranslation(100, 0, 0);
const rotateMatrix = new THREE.Matrix4().makeRotationFromEuler(
new THREE.Euler(THREE.MathUtils.degToRad(45), 0, 0)
);
const scaleMatrix = new THREE.Matrix4().makeScale(1, 3, 1);
// 组合矩阵:注意顺序是从右到左应用
// 先缩放,再旋转,最后平移
const matrix = new THREE.Matrix4();
matrix.multiplyMatrices(translateMatrix, rotateMatrix);
matrix.multiplyMatrices(matrix, scaleMatrix);
我们先创建了平移矩阵,然后创建了旋转矩阵,之后创建了放缩矩阵。
组合矩阵的话需要用乘法,矩阵是从右往左生效,也就是最后的乘数先生效。
所以先放缩、再旋转、再位移。
看下效果:

可以看到,位移、旋转、缩放都生效了。
我们做各种变换,底层都是矩阵乘法,就是这个意思。
当然,这里你可以这样写:

translateMatrix.multiply(rotateMatrix).multiply(scaleMatrix);
mesh.applyMatrix4(translateMatrix);
连乘就行,效果一样:

这里的旋转我们是用的 Euler,那同理,换成 Quaternion 等方式也是一样的:

const q1 = new THREE.Quaternion();
q1.setFromEuler(new THREE.Euler(0, 0, 0));
const q2 = new THREE.Quaternion();
q2.setFromEuler(new THREE.Euler(0, THREE.MathUtils.degToRad(90), 0));
const q = new THREE.Quaternion();
q.slerpQuaternions(q1, q2, 0.3);
const rotateMatrix2 = new THREE.Matrix4();
rotateMatrix2.makeRotationFromQuaternion(q);
比如我们用插值计算了两个角度之间的四元数,把它设置到矩阵,用 makeRotationFromQuaternion
然后再用这个矩阵来旋转:

也同样可以生效。
或者用 makeRotationAxis 的 api,绕某个轴旋转的角度转成矩阵:

const rotateMatrix3 = new THREE.Matrix4();
const axis = new THREE.Vector3(0, 0, 1);
rotateMatrix3.makeRotationAxis(axis, Math.PI / 4);
translateMatrix.multiply(rotateMatrix3).multiply(scaleMatrix);

四元数也有这个 setFromAxisAngle 的 api,你也可以用 Quaternion 计算角度,最后转成旋转矩阵。
总之,Eular、Quarternion 等最终都会转为旋转矩阵,设置到 Matrix4 的矩阵,然后应用到物体上。
矩阵还支持除了乘法的更多运算:
const m1 = new THREE.Matrix4();
const m2 = new THREE.Matrix4();
// 矩阵乘法 m1 = m1 * m2
m1.multiply(m2);
// 预乘(右乘)m1 = m2 * m1
m1.premultiply(m2);
// 矩阵相加
m1.add(m2);
// 矩阵转置
m1.transpose();
// 求逆矩阵
m1.invert();
// 计算行列式
const determinant = m1.determinant();
这些涉及很多数学知识,就不展开了。
大家会用矩阵乘法来应用变换就行。
分解矩阵
有了 Matrix4 的变换矩阵,可不可以从中提取出位移、旋转、缩放 的矩阵呢?
可以的:
这里乘出来的矩阵还可以用 decompose 分解:

const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();
translateMatrix.decompose(position, quaternion, scale);
console.log('Position:', position);
console.log('Rotation:', quaternion);
console.log('Scale:', scale);

可以看到分解结果也是对的。
案例代码上传了小册仓库
总结
这节我们过了一遍 Quaternion 和 Matrix4 的 api。
Euler 只是存储角度,而 Quaternion 可以对角度做插值、计算两个向量夹角等。
setFromAxisAngle(axis, angle)- 从绕轴旋转角度设置setFromEuler(euler)- 从欧拉角设置setFromRotationMatrix(m)- 从旋转矩阵设置setFromUnitVectors(v1, v2)- 从两个向量夹角设置slerpQuaternions(q1, q2, t)- 角度插值
而不管是平移、旋转(Euler、Quaternion)、放缩,最终都是设置到 Matrix4,然后应用到物体
makeTranslation(x, y, z)- 创建平移矩阵makeRotationFromEuler(euler)- 从欧拉角创建旋转makeRotationFromQuaternion(q)- 从四元数创建旋转makeRotationAxis(axis, angle)- 从绕轴旋转角度设置makeScale(x, y, z)- 创建缩放矩阵multiply(m)- 矩阵乘法decompose(position, quaternion, scale)- 分解矩阵
理解了 Euler、Quaternion、Matrix4 之间的关系,再做旋转、放缩、平移等变换的时候就更灵活了。