一、欧拉角
欧拉角是用于描述物体在三维空间中旋转的方法之一。它由三个连续旋转组成,分别是绕x轴旋转角度φ、绕y轴旋转角度θ和绕z轴旋转角度ψ,其旋转顺序可以是xyz、xzy、yxz、yzx、zxy、zyx等。
欧拉角的优点是直观易懂,可以通过简单的三个角度来描述旋转情况。但缺点也是很明显的,就是在某些情况下会出现万向锁问题。另外,由于欧拉角旋转的过程是连续的,一旦多次旋转累积起来,会导致误差的积累。
//欧拉角转为旋转矩阵 function euler2mat(phi, theta, psi) { const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); const cosTheta = Math.cos(theta); const sinTheta = Math.sin(theta); const cosPsi = Math.cos(psi); const sinPsi = Math.sin(psi); const mat = [ [cosTheta * cosPsi, -cosPhi * sinPsi + sinPhi * sinTheta * cosPsi, sinPhi * sinPsi + cosPhi * sinTheta * cosPsi], [cosTheta * sinPsi, cosPhi * cosPsi + sinPhi * sinTheta * sinPsi, -sinPhi * cosPsi + cosPhi * sinTheta * sinPsi], [-sinTheta, sinPhi * cosTheta, cosPhi * cosTheta] ]; return mat; }
二、四元数
四元数是一种扩展了复数的数学结构,它由一个实部和三个虚部构成。可以用于表示旋转操作和空间旋转中的旋转向量,同时解决了欧拉角的两个问题,即避免万向锁和误差积累。
四元数的乘法可以看作是两个旋转的合成,因此可以很方便地进行多次旋转的叠加。而且只需要4个数就可以表示旋转状态,比欧拉角更加紧凑和高效。
//四元数旋转矩阵转欧拉角 function mat2euler(mat) { const theta1 = -Math.asin(mat[2][0]); const cosTheta1 = Math.cos(theta1); const phi1 = Math.atan2(mat[2][1] / cosTheta1, mat[2][2] / cosTheta1); const psi1 = Math.atan2(mat[1][0] / cosTheta1, mat[0][0] / cosTheta1); return [phi1, theta1, psi1]; } //四元数类 class Quaternion { constructor(w, x, y, z) { this.w = w || 1.0; this.x = x || 0.0; this.y = y || 0.0; this.z = z || 0.0; } normalize() { const norm = Math.sqrt(this.w ** 2 + this.x ** 2 + this.y ** 2 + this.z ** 2); if (norm === 0) return; this.w /= norm; this.x /= norm; this.y /= norm; this.z /= norm; } multiply(q) { const w1 = this.w, x1 = this.x, y1 = this.y, z1 = this.z; const w2 = q.w, x2 = q.x, y2 = q.y, z2 = q.z; this.w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2; this.x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2; this.y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2; this.z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2; this.normalize(); } }
三、应用实例
在游戏开发中,我们通常会使用三维渲染引擎来展现一个三维场景。在这个场景中,有很多需要旋转的物体,比如角色、相机和物体等。下面我们以相机为例,展示欧拉角和四元数在相机旋转中的应用。
1、欧拉角控制相机旋转
在WebGL中,我们可以使用欧拉角来控制相机的旋转。假设我们有一个相机对象,可以通过鼠标控制其绕x和y轴的旋转角度,代码如下:
class Camera { constructor() { this.position = [0, 0, 5]; this.target = [0, 0, 0]; this.up = [0, 1, 0]; this.dist = Math.sqrt(this.position[0] ** 2 + this.position[1] ** 2 + this.position[2] ** 2); this.phi = Math.atan2(this.position[1], this.position[0]); this.theta = Math.acos(this.position[2] / this.dist); } rotate(deltaPhi, deltaTheta) { this.phi += deltaPhi; this.theta += deltaTheta; if (this.theta < 0.01) this.theta = 0.01; if (this.theta > Math.PI - 0.01) this.theta = Math.PI - 0.01; const x = this.dist * Math.sin(this.theta) * Math.cos(this.phi); const y = this.dist * Math.sin(this.theta) * Math.sin(this.phi); const z = this.dist * Math.cos(this.theta); this.position = [x, y, z]; } getViewMatrix() { const mat = euler2mat(this.theta, this.phi, 0); const zAxis = [mat[0][2], mat[1][2], mat[2][2]]; const xAxis = [mat[0][0], mat[1][0], mat[2][0]]; const yAxis = [mat[0][1], mat[1][1], mat[2][1]]; const tx = -xAxis[0] * this.position[0] - xAxis[1] * this.position[1] - xAxis[2] * this.position[2]; const ty = -yAxis[0] * this.position[0] - yAxis[1] * this.position[1] - yAxis[2] * this.position[2]; const tz = -zAxis[0] * this.position[0] - zAxis[1] * this.position[1] - zAxis[2] * this.position[2]; return [ xAxis[0], yAxis[0], zAxis[0], 0, xAxis[1], yAxis[1], zAxis[1], 0, xAxis[2], yAxis[2], zAxis[2], 0, tx, ty, tz, 1 ]; } }
2、四元数控制相机旋转
除了使用欧拉角之外,我们还可以使用四元数来控制相机的旋转。这里假设我们有一个相机对象和一个旋转四元数q,可以通过鼠标操作旋转四元数,然后计算出相机的旋转矩阵,代码如下:
class Camera { constructor() { this.position = [0, 0, 5]; this.target = [0, 0, 0]; this.up = [0, 1, 0]; this.dist = Math.sqrt(this.position[0] ** 2 + this.position[1] ** 2 + this.position[2] ** 2); this.phi = Math.atan2(this.position[1], this.position[0]); this.theta = Math.acos(this.position[2] / this.dist); this.quaternion = new Quaternion(); } rotate(deltaPhi, deltaTheta) { this.phi += deltaPhi; this.theta += deltaTheta; if (this.theta < 0.01) this.theta = 0.01; if (this.theta > Math.PI - 0.01) this.theta = Math.PI - 0.01; const x = this.dist * Math.sin(this.theta) * Math.cos(this.phi); const y = this.dist * Math.sin(this.theta) * Math.sin(this.phi); const z = this.dist * Math.cos(this.theta); this.position = [x, y, z]; const mat = euler2mat(this.theta, this.phi, 0); this.quaternion = new Quaternion().fromMat(mat); } getViewMatrix() { const mat = this.quaternion.toMat(); const zAxis = [mat[0][2], mat[1][2], mat[2][2]]; const xAxis = [mat[0][0], mat[1][0], mat[2][0]]; const yAxis = [mat[0][1], mat[1][1], mat[2][1]]; const tx = -xAxis[0] * this.position[0] - xAxis[1] * this.position[1] - xAxis[2] * this.position[2]; const ty = -yAxis[0] * this.position[0] - yAxis[1] * this.position[1] - yAxis[2] * this.position[2]; const tz = -zAxis[0] * this.position[0] - zAxis[1] * this.position[1] - zAxis[2] * this.position[2]; return [ xAxis[0], yAxis[0], zAxis[0], 0, xAxis[1], yAxis[1], zAxis[1], 0, xAxis[2], yAxis[2], zAxis[2], 0, tx, ty, tz, 1 ]; } } class Quaternion { constructor(w, x, y, z) { this.w = w || 1.0; this.x = x || 0.0; this.y = y || 0.0; this.z = z || 0.0; } normalize() { const norm = Math.sqrt(this.w ** 2 + this.x ** 2 + this.y ** 2 + this.z ** 2); if (norm === 0) return; this.w /= norm; this.x /= norm; this.y /= norm; this.z /= norm; } multiply(q) { const w1 = this.w, x1 = this.x, y1 = this.y, z1 = this.z; const w2 = q.w, x2 = q.x, y2 = q.y, z2 = q.z; this.w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2; this.x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2; this.y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2; this.z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2; this.normalize(); } fromMat(mat) { const trace = mat[0][0] + mat[1][1] + mat[2][2]; if (trace >= 0) { const s = Math.sqrt(trace + 1) * 2; const w = 0.25 * s; const x = (mat[2][1] - mat[1][2]) / s; const y = (mat[0][2] - mat[2][0]) / s; const z = (mat[1][0] - mat[0][1]) / s; this.w = w; this.x = x; this.y = y; this.z = z; } else if (mat[0][0] > mat[1][1] && mat[0][0] > mat[2][2]) { const s = Math.sqrt(1 + mat[0][0] - mat[1][1] - mat[2][2]) * 2; const w = (mat[2][1] - mat[1][2]) / s; const x = 0.25 * s; const y = (mat[0][1] + mat[1][0]) / s; const z = (mat[0][2] + mat[2][0]) / s; this.w = w; this.x = x; this.y = y; this.z = z; } else if