Toggle navigation
集客麦麦@谢坤
首页
随笔
首页
>>
创作中心
>>
threejs 旋转...
threejs 旋转欧拉角,四元数和万向锁问题
## 欧拉角旋转方法 ```javascript // 旋转方法1:使用欧拉角对象直接设置旋转 const euler = new THREE.Euler(Math.PI / 2, 0, 0) // 创建欧拉角,绕X轴旋转90度 cube.rotation.copy(euler) // 将欧拉角应用到立方体 // 旋转方法2:直接设置欧拉角分量 cube.rotation.set(euler.x, euler.y, euler.z) // 等效于方法1 // 旋转方法3:单独轴累加旋转(每次调用会叠加角度) cube.rotateX(Math.PI / 180 * 20) // 绕X轴旋转20度 cube.rotateX(Math.PI / 180 * 70) // 继续绕X轴叠加旋转70度,共90度 ``` ### 四元数旋转方法 ```javascript // 旋转方法4:手动计算四元数(绕X轴旋转90度) const angle = Math.PI / 180 * 90 // 90度转换为弧度 const halfAngle = angle / 2 const quaternion = new THREE.Quaternion( Math.sin(halfAngle) * 1, // X轴分量 Math.sin(halfAngle) * 0, // Y轴分量 Math.sin(halfAngle) * 0, // Z轴分量 Math.cos(halfAngle) // W分量(四元数特有) ) cube.quaternion.copy(quaternion) // 应用四元数 // 旋转方法5:使用Three.js四元数API(等效于方法4) const q = new THREE.Quaternion() q.setFromAxisAngle(new THREE.Vector3(1, 0, 0), angle) // 绕X轴旋转90度 cube.quaternion.copy(q) // 旋转方法6:从欧拉角转换为四元数 const euler = new THREE.Euler(angle, 0, 0) // 欧拉角:绕X轴90度 const quat = new THREE.Quaternion() quat.setFromEuler(euler) // 从欧拉角创建四元数 cube.quaternion.copy(quat) // 旋转方法7:组合多个四元数旋转 const q1 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI/4) // X轴45度 const q2 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI/4) // Y轴45度 const q3 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI/4) // Z轴45度 // 四元数相乘顺序影响结果:q1*q2*q3表示先绕X,再绕Y,最后绕Z const combinedQuat = q1.clone().multiply(q2).multiply(q3) // 克隆q1以避免修改原对象 cube.quaternion.copy(combinedQuat) // 旋转方法8:四元数累加(动画) const qx = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI/180) // X轴1度 const qz = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI/180) // Z轴1度 // 顺序重要:先应用qx旋转,再应用qz旋转 cube.quaternion.multiplyQuaternions(qx, cube.quaternion) // 等效于:新旋转 = qx * 当前旋转 cube.quaternion.multiplyQuaternions(qz, cube.quaternion) // 等效于:新旋转 = qz * (qx * 当前旋转) ``` ### 扩展`万向锁`复现 - 1. 代码触发条件 ```javascript cube.rotation.y = Math.PI / 2 // 先绕Y轴旋转90度(关键!) animate() function animate() { angle += 1 cube.rotation.x = (Math.PI / 180) * angle // X轴随时间旋转 cube.rotation.z = (Math.PI / 180) * angle // Z轴随时间旋转 } ``` - 2. 现象描述 当 Y 轴旋转 90 度时,X 轴和 Z 轴的旋转效果完全一致,仿佛 Z 轴旋转失效了。例如: 预期效果:X 轴旋转产生 “翻滚”,Z 轴旋转产生 “自转”。 实际效果:只有一个轴在起作用(通常是 X 轴),Z 轴旋转被忽略。 - 3. 数学原理 欧拉角使用三个角度(X,Y,Z)描述旋转,但当Y 轴旋转 90 度时,X 轴和 Z 轴会重合(数学上称为 “奇异性”),导致: 失去一个自由度(Z 轴旋转失效)。 旋转顺序混乱(无法区分 X 轴和 Z 轴旋转)。 ### 案例 ```js import * as THREE from 'three' import { OrbitControls } from 'three/addons/controls/OrbitControls.js' // 场景 const scene = new THREE.Scene() scene.background = new THREE.Color(0xeeeeee) // 相机 const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000, ) camera.position.x = 0 camera.position.y = 40 camera.position.z = 40 camera.lookAt(0, 0, 0) // 看向标签位置 // 渲染 const renderer = new THREE.WebGLRenderer() renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(renderer.domElement) // 方块 const maps = [ '../images/box/boxmap1.jpg', '../images/box/boxmap2.jpg', '../images/box/boxmap3.jpg', '../images/box/boxmap4.jpg', '../images/box/boxmap5.jpg', '../images/box/boxmap6.jpg', ] const textures = maps.map((map) => new THREE.TextureLoader().load(map)) const materials = textures.map((texture) => new THREE.MeshBasicMaterial({ map: texture })) const cube = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), materials) scene.add(cube) console.log('cube', cube.rotation) // 绕着X轴旋转 // const euler = new THREE.Euler(Math.PI / 2, 0, 0) // 绕X轴旋转90度 // 旋转方法1 // cube.rotation.copy(euler) // 设置旋转 // 旋转方法2 // cube.rotation.set(euler.x, euler.y, euler.z) // 设置旋转 // 累加旋转 // cube.rotation.set((Math.PI / 180) * 20, 0, 0) // 设置旋转20度 // cube.rotation.set((Math.PI / 180) * 70, 0, 0) // 累加旋转70度 // 旋转方法3:单独轴旋转 // cube.rotateX((Math.PI / 180) * 20) // 绕X轴旋转20度 // cube.rotateX((Math.PI / 180) * 70) // 累加绕X轴旋转70度 // 旋转方法4:使用四元数,原始计算旋转x轴90度 /* const angle = (Math.PI / 180) * 90 // 90度转换为弧度 const halfAngle = angle / 2 const sinAngle = Math.sin(halfAngle) const cosAngle = Math.cos(halfAngle) const quaternion = new THREE.Quaternion( sinAngle * 1, sinAngle * 0, sinAngle * 0, cosAngle ) // 绕X轴旋转90度 cube.quaternion.copy(quaternion) // 设置四元数 */ // 旋转方法5:使用四元数单轴旋转,原始计算旋转x轴90度 /* const angle = (Math.PI / 180) * 90 // 90度 const quaternion = new THREE.Quaternion() const axis = new THREE.Vector3(1, 0, 0) // 绕X轴旋转 quaternion.setFromAxisAngle(axis, angle) // 设置四元数 cube.quaternion.copy(quaternion) // 设置四元数 */ // 旋转方法6:使用欧拉角转换四元数旋转 /* const angle = (Math.PI / 180) * 90 // 90度 const euler = new THREE.Euler(angle, 0, 0) // 绕X轴旋转90度 const quaternion = new THREE.Quaternion() quaternion.setFromEuler(euler) // 从欧拉角转换为四元数 cube.quaternion.copy(quaternion) // 设置四元数 */ // 旋转方法7:使用四元数相乘 const angle = (Math.PI / 180) * 45 // 45度 const quaternion1 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), angle) // 绕X轴旋转45度 const quaternion2 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), angle) // 绕Y轴旋转45度 const quaternion3 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), angle) // 绕Z轴旋转45度 const combinedQuaternion = quaternion1.multiply(quaternion2).multiply(quaternion3) // 四元数相乘 cube.quaternion.copy(combinedQuaternion) // 设置四元数 // 灯光 var ambient = new THREE.AmbientLight(0xffffff) scene.add(ambient) const directionalLight = new THREE.DirectionalLight(0xffffff, 15) scene.add(directionalLight) // 控制器 const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true // 阻尼开启 controls.dampingFactor = 0.03 // 阻尼系数 controls.update() const axesHelper = new THREE.AxesHelper(50) scene.add(axesHelper) // 渲染循环 animate() function animate() { const qx = new THREE.Quaternion() qx.setFromAxisAngle(new THREE.Vector3(1, 0, 0), (Math.PI / 180) * 1) // 绕X轴旋转1度 cube.quaternion.multiplyQuaternions(qx, cube.quaternion) // 累加旋转 const qz = new THREE.Quaternion() qz.setFromAxisAngle(new THREE.Vector3(0, 0, 1), (Math.PI / 180) * 1) // 绕Z轴旋转1度 cube.quaternion.multiplyQuaternions(qz, cube.quaternion) // 累加旋转 controls.update() renderer.render(scene, camera) requestAnimationFrame(animate) } ```