破解旋轉(zhuǎn)死鎖:Threejs 四元數(shù)魔法對(duì)抗歐拉角困局

歐拉角

歐拉角是一種表示三維空間中旋轉(zhuǎn)的方法,它由三個(gè)角度組成,通過設(shè)定物體繞 指定順序 的軸進(jìn)行旋轉(zhuǎn),可以直接對(duì)物體的 .rotation 屬性進(jìn)行操作。

rotation 屬性是一個(gè)歐拉角對(duì)象,表示物體的旋轉(zhuǎn)角度。歐拉角由三個(gè)角度組成,分別是繞 x 軸的旋轉(zhuǎn)角度、繞 y 軸的旋轉(zhuǎn)角度和繞 z 軸的旋轉(zhuǎn)角度。

// 創(chuàng)建一個(gè)立方體
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
scene.add(cube);

let euler = new THREE.Euler(Math.PI / 2, 0, 0);
cube.rotation.copy(euler);

console.log(cube.rotation);

Euler(x, y, z, order): 創(chuàng)建一個(gè)歐拉角對(duì)象,其中 x、y、z 分別表示繞 x 軸、y 軸和 z 軸的旋轉(zhuǎn)角度,order 表示旋轉(zhuǎn)順序。

換算: 歐拉角不能直接使用度數(shù),需要把度數(shù)轉(zhuǎn)換為弧度值,弧度 = (Math.PI / 180) * 度數(shù)。

簡(jiǎn)潔寫法: cube.rotation.set(x, y, z)。

注意: 歐拉角不能直接賦值,需要使用 .copy() 方法進(jìn)行賦值。

// 創(chuàng)建一個(gè)立方體
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
scene.add(cube);

let angle = 0;
function animate() {
  angle += 1;
  cube.rotation.x = (angle * Math.PI) / 180;
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

可以使用 THREE.Euler() 、 .set() 來統(tǒng)一操作 .rotation,也可以通過 .x 、 .y 、 .z 來分別操作。

euler.gif

方法

  • rotateX(angle): 繞 x 軸旋轉(zhuǎn) angle 弧度。

  • rotateY(angle): 繞 y 軸旋轉(zhuǎn) angle 弧度。

  • rotateZ(angle): 繞 z 軸旋轉(zhuǎn) angle 弧度。

區(qū)別: rotateX() 、 rotateY() 、 rotateZ() 多次使用,會(huì)疊加旋轉(zhuǎn)角度,而 set() 每次使用,都會(huì)覆蓋旋轉(zhuǎn)角度。

四元數(shù)

四元數(shù)是一種表示三維空間中旋轉(zhuǎn)的方法,它由四個(gè)部分組成,分別是 w、x、y、z。其中 w 是實(shí)部,x、y、z 是虛部。

為什么使用四元數(shù): 歐拉角在旋轉(zhuǎn)過程中會(huì)出現(xiàn)萬向節(jié)死鎖問題,而四元數(shù)不會(huì)。

萬向節(jié)死鎖

原理: 當(dāng)其中兩個(gè)旋轉(zhuǎn)軸重合時(shí),就會(huì)發(fā)生萬向節(jié)死鎖。這會(huì)致使失去一個(gè)自由度,進(jìn)而難以預(yù)測(cè)和控制物體的旋轉(zhuǎn)。在歐拉角的表示里,通常是當(dāng)繞其中一個(gè)軸旋轉(zhuǎn) ±90 度時(shí),另外兩個(gè)軸會(huì)重合,從而出現(xiàn)萬向節(jié)死鎖。

例如: 假設(shè)一個(gè)物體繞 Y 軸旋轉(zhuǎn) 90 度,此時(shí) X 軸和 Z 軸就會(huì)重合。這時(shí)候,不管是繞 X 軸旋轉(zhuǎn)還是繞 Z 軸旋轉(zhuǎn),產(chǎn)生的效果是一樣的,這就意味著失去了一個(gè)自由度,這種現(xiàn)象就是萬向節(jié)死鎖。

//萬向節(jié)死鎖示例
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
scene.add(cube);

//y軸旋轉(zhuǎn)90度
cube.rotation.y = Math.PI / 2;

let angle = 0;
function animate() {
  angle += 1;
  cube.rotation.x = (angle * Math.PI) / 180;
  cube.rotation.z = (angle * Math.PI) / 180;
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

quaternion

在四元數(shù)中是通過操作quaternion來實(shí)現(xiàn)的,其中quaternion是一個(gè)四元數(shù)對(duì)象,表示物體的旋轉(zhuǎn)。

quaternion 屬性是一個(gè)四元數(shù)對(duì)象,表示物體的旋轉(zhuǎn)。四元數(shù)由四個(gè)部分組成,分別是 w、x、y、z。其中 w 是實(shí)部,x、y、z 是虛部。

1. 創(chuàng)建四元數(shù)

const quaternion = new THREE.Quaternion(x, y, z, w);

其中 x、y、z、w 分別是四元數(shù)的分量。如果不傳入?yún)?shù),則默認(rèn)為 (0, 0, 0, 1)。

2. 四元數(shù)計(jì)算公式

q = (x, y, z, w) = (sin(θ/2) * ux, sin(θ/2) * uy, sin(θ/2) * uz, cos(θ/2))

const angle = (30 * Math.PI) / 180; // 將角度轉(zhuǎn)換為弧度
const halfAngle = angle / 2;
const sinHalfAngle = Math.sin(halfAngle);
const cosHalfAngle = Math.cos(halfAngle);
const quaternion = new THREE.Quaternion(
  sinHalfAngle * 1, //解釋
  sinHalfAngle * 0,
  sinHalfAngle * 0,
  cosHalfAngle
);

繞著哪個(gè)軸旋轉(zhuǎn),哪個(gè)軸的分量就為 1,其他軸的分量就為 0。

3. 設(shè)置四元數(shù)

cube.quaternion.copy(quaternion);

簡(jiǎn)潔寫法: setFromAxisAngle(axis, angle) 從軸和角度創(chuàng)建四元數(shù)。

1. axis: 表示旋轉(zhuǎn)軸的向量,例如 new THREE.Vector3(1, 0, 0) 表示繞 x 軸旋轉(zhuǎn)。

2. angle: 表示旋轉(zhuǎn)角度,單位是弧度。

const quaternion = new THREE.Quaternion();
const axis = new THREE.Vector3(1, 0, 0); // 繞 x 軸旋轉(zhuǎn)
const angle = (Math.PI / 180) * 90; // 旋轉(zhuǎn) 90 度
quaternion.setFromAxisAngle(axis, angle);

方法

  • setFromEuler(euler): 從歐拉角創(chuàng)建四元數(shù)。

  • multiply(quaternion): 將當(dāng)前四元數(shù)與另一個(gè)四元數(shù)相乘,會(huì)改變當(dāng)前四元數(shù)。

  • multiplyQuaternions(a, b): 將兩個(gè)四元數(shù)相乘,返回一個(gè)新的四元數(shù)。

解決死鎖

// 解決萬向節(jié)死鎖示例
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
scene.add(cube);

//y軸旋轉(zhuǎn)90度
cube.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);

function animate() {
  // 創(chuàng)建一個(gè)四元數(shù)qx
  const qx = new THREE.Quaternion();
  // 將qx設(shè)置為繞x軸旋轉(zhuǎn)0.01弧度的四元數(shù)
  qx.setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.01);
  // 將cube的旋轉(zhuǎn)四元數(shù)與qx相乘
  cube.quaternion.multiplyQuaternions(qx, cube.quaternion);
  // 創(chuàng)建一個(gè)四元數(shù)qz
  const qz = new THREE.Quaternion();
  // 將qz設(shè)置為繞z軸旋轉(zhuǎn)0.01弧度的四元數(shù)
  qz.setFromAxisAngle(new THREE.Vector3(0, 0, 1), 0.01);
  // 將cube的旋轉(zhuǎn)四元數(shù)與qz相乘
  cube.quaternion.multiplyQuaternions(qz, cube.quaternion);
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
死鎖.gif

書洞筆記

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容