Three.js火箭跟随相机翻转问题:如何实现角度限制?
问题
我正在开发一款基于Three.js的太空火箭飞行小游戏,已实现相机跟随火箭的功能,但当火箭向上旋转180°时,相机会出现倒置情况。请帮助我实现角度限制功能(本项目与问题旨在学习Three.js,敬请谅解我的经验不足)。
我曾尝试使用Quaternion(四元数)但未成功,偶然看到有帖子提到Spherical(球面坐标系)可能是解决方案之一。请提供使用这两者之一或其他方法实现该功能的示例代码。
以下是我的代码:
import './style.css' import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; // Scene object const scene = new THREE.Scene(); const clock = new THREE.Clock(); var keys; var velocity = 0.0; var speed; // Camera object let perspectiveCamera = new THREE.PerspectiveCamera( 25, window.innerWidth / window.innerHeight, 50, 1e7 ) // Renderer object const renderer = new THREE.WebGLRenderer({ antialias: true }); document.body.appendChild( renderer.domElement ); // Renderer renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.render( scene, perspectiveCamera ) // PointLight const pointLight = new THREE.PointLight(0xffffff) pointLight.position.set(20,20,20) // AmbientLight const ambientLight = new THREE.AmbientLight(0xffffff) scene.add(pointLight, ambientLight) // Controls const controls = new OrbitControls(perspectiveCamera, renderer.domElement); controls.movementSpeed = 1000; controls.domElement = renderer.domElement; controls.rollSpeed = Math.PI / 24; controls.autoForward = false; controls.dragToLook = false; // Rocket const geometry = new THREE.BoxGeometry( 1, 1, 3 ); const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} ); const rocket = new THREE.Mesh( geometry, material ); scene.add(rocket); // Stars function addStar() { const geometry = new THREE.SphereGeometry(0.25, 24, 24); const material = new THREE.MeshStandardMaterial( { color: 0xffffff }) const star = new THREE.Mesh( geometry, material ); const [x, y, z] = Array(3).fill().map(() => THREE.MathUtils.randFloatSpread( 500 )); star.position.set(x,y,z); scene.add(star) } Array(500).fill().forEach(addStar) keys = { a: false, s: false, d: false, w: false, q: false, e: false, space: false, shiftleft: false, }; document.addEventListener("keydown", function(e) { console.log(e.code); const key = e.code.replace('Key', '').toLowerCase(); if ( keys[ key ] !== undefined ) keys[ key ] = true; }); document.body.addEventListener( 'keyup', function(e) { const key = e.code.replace('Key', '').toLowerCase(); if ( keys[ key ] !== undefined ) keys[ key ] = false; }); // let goal = new THREE.Object3D; goal.add(perspectiveCamera); function followCamera() { //Offset from camera to player var relativeCameraOffset = new THREE.Vector3(0,0,-80); //Updating player world matrix for perfect camera follow rocket.updateMatrixWorld() //Apply offset to player matrix var cameraOffset = relativeCameraOffset.applyMatrix4( rocket.matrixWorld ); //Smooth camera movement perspectiveCamera.position.lerp(cameraOffset, 0.1); perspectiveCamera.lookAt( rocket.position ); } // Animate method function animate() { const delta = clock.getDelta(); requestAnimationFrame( animate ); speed = 0.0; if ( keys.w ) rocket.rotateX(0.02); if ( keys.s ) rocket.rotateX(-0.02); if ( keys.a ) rocket.rotateY(0.02); if ( keys.d ) rocket.rotateY(-0.02); if ( keys.q ) rocket.rotateZ(-0.05); if ( keys.e ) rocket.rotateZ(0.05); if ( keys.space ) speed = 0.8; velocity += ( speed - velocity ) * .2; rocket.translateZ( velocity ); if ( keys.shiftleft ) speed = 0.8*5; velocity += ( speed - velocity ) * .2; rocket.translateZ( velocity ); followCamera() controls.update(delta); renderer.render( scene, perspectiveCamera ); } animate();
我曾尝试复制火箭的四元数并应用到PerspectiveCamera,再进行归一化,但效果不佳。
解决方案
相机倒置的核心原因是lookAt方法会让相机完全对齐目标方向,当火箭翻转180度时,相机的up向量会被强制翻转。下面提供两种可行的解决方法:
方法一:使用Spherical坐标系限制俯仰角
通过将相机到火箭的方向向量转换为球面坐标,限制俯仰角范围,避免相机进入倒置状态:
// 新增全局变量存储球面坐标与方向向量 const cameraSpherical = new THREE.Spherical(); const cameraTargetDir = new THREE.Vector3(); function followCamera() { //Offset from camera to player const relativeCameraOffset = new THREE.Vector3(0,0,-80); //Updating player world matrix for perfect camera follow rocket.updateMatrixWorld() //Apply offset to player matrix const cameraOffset = relativeCameraOffset.applyMatrix4( rocket.matrixWorld ); //Smooth camera movement perspectiveCamera.position.lerp(cameraOffset, 0.1); // 计算相机到火箭的方向向量 cameraTargetDir.subVectors(rocket.position, perspectiveCamera.position).normalize(); // 将方向向量转为球面坐标 cameraSpherical.setFromVector3(cameraTargetDir); // 限制俯仰角(phi):范围设为0.1π到0.9π,避免相机完全倒置 // 0代表正上方,π代表正下方,此范围确保相机始终保持接近水平的正立视角 cameraSpherical.phi = THREE.MathUtils.clamp(cameraSpherical.phi, 0.1 * Math.PI, 0.9 * Math.PI); // 将限制后的球面坐标转回方向向量 cameraTargetDir.setFromSpherical(cameraSpherical); // 计算相机目标点:当前位置 + 限制后的方向向量 const target = perspectiveCamera.position.clone().add(cameraTargetDir); // 让相机看向目标点,同时保持up向量为Y轴正方向 perspectiveCamera.lookAt(target); }
关键说明
- 球面坐标转换:把相机到火箭的方向向量转为球面坐标,其中
phi对应俯仰角度(绕X轴的旋转角度)。 - 角度限制:用
THREE.MathUtils.clamp锁死phi的范围,避免相机旋转到完全倒置的角度。 - 重新计算注视目标:基于限制后的方向向量生成新的注视点,确保相机始终正立。
方法二:用四元数+虚拟对象限制旋转
创建一个虚拟对象,复制火箭的旋转但限制X轴角度,再让相机跟随这个虚拟对象:
// 新增全局虚拟对象 const cameraPivot = new THREE.Object3D(); function followCamera() { // 复制火箭的旋转,但限制X轴角度 cameraPivot.quaternion.copy(rocket.quaternion); const euler = new THREE.Euler().setFromQuaternion(cameraPivot.quaternion); // 限制X轴旋转在接近水平的范围内,避免翻转 euler.x = THREE.MathUtils.clamp(euler.x, -Math.PI/2 + 0.1, Math.PI/2 - 0.1); cameraPivot.quaternion.setFromEuler(euler); // 设置相机位置相对于虚拟对象 const relativeCameraOffset = new THREE.Vector3(0,0,-80); const cameraOffset = relativeCameraOffset.applyMatrix4(cameraPivot.matrixWorld); perspectiveCamera.position.lerp(cameraOffset, 0.1); // 让相机看向火箭 perspectiveCamera.lookAt(rocket.position); }
关键说明
通过虚拟对象中转旋转,先将四元数转为欧拉角限制X轴范围,再转回四元数,确保相机跟随的是"被约束后的火箭旋转",避免倒置。
另外注意:代码中的OrbitControls和自定义相机跟随逻辑可能存在冲突,若不需要手动控制相机,可注释掉OrbitControls相关代码,避免干扰相机跟随行为。
内容的提问来源于stack exchange,提问作者js_thin_time
相关产品推荐
相关产品推荐

