You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
}

关键说明

  1. 球面坐标转换:把相机到火箭的方向向量转为球面坐标,其中phi对应俯仰角度(绕X轴的旋转角度)。
  2. 角度限制:用THREE.MathUtils.clamp锁死phi的范围,避免相机旋转到完全倒置的角度。
  3. 重新计算注视目标:基于限制后的方向向量生成新的注视点,确保相机始终正立。

方法二:用四元数+虚拟对象限制旋转

创建一个虚拟对象,复制火箭的旋转但限制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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 14:37:33