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

THREE.js 透视相机无法跟随玩家、固定在世界原点问题求助

Three.js 第三人称相机跟随问题求助

我是 THREE.js 新手,为学习 JavaScript 和 THREE.js,我制作了一款尚在开发中的迷你游戏,玩家可以在太空中探索各类天体。
目前遇到的问题是:场景和物体渲染完成后,移动玩家操控的长方体时,相机无法跟随其移动,实际上它不会离开世界原点,仅能原地旋转看向玩家对象。我想要实现第三人称相机效果,让相机跟随这个小长方体移动,并且在物体旋转时同步调整视角。

初始实现代码

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 cameraObject, keys;
var temp = new THREE.Vector3;
var dir = new THREE.Vector3;
var a = new THREE.Vector3;
var b = new THREE.Vector3;
var distance = 0.3;
var velocity = 0.0;
var speed = 0.0;

// Camera object
const perspectiveCamera = new THREE.PerspectiveCamera( 25, window.innerWidth / window.innerHeight, 50, 1e7 )
perspectiveCamera.position.setZ(80);
cameraObject = new THREE.Object3D;
cameraObject.add(perspectiveCamera);
// perspectiveCamera.lookAt(scene.position)

// Renderer object
const renderer = new THREE.WebGLRenderer({
});
document.body.appendChild( renderer.domElement );

// Renderer
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.render( scene, perspectiveCamera )

// Light
// PointLight
const pointLight = new THREE.PointLight(0xffffff)
pointLight.position.set(20,20,20)
// AmbientLight
const ambientLight = new THREE.AmbientLight(0xffffff)
scene.add(pointLight, ambientLight)

// Helper classes
const lightHelper = new THREE.PointLightHelper(pointLight);
const axesHelper = new THREE.AxesHelper(5);
scene.add(lightHelper, axesHelper)

// 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;

let cube = new THREE.Group();
const cubeGeometry = new THREE.BoxGeometry(3,10,3)
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0x238423 })
const cube1 = new THREE.Mesh( cubeGeometry, cubeMaterial );
cube.add(cube1)
scene.add(cube)

// Objects
// 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(1500).fill().forEach(addStar)

let keyState = {};
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;
});

// Animate objects
function animate() {
  const delta = clock.getDelta();

  requestAnimationFrame( animate );
  speed = 0.0;

  if ( keys.w )
    cube.rotateX(-0.03);

  if ( keys.s )
  cube.rotateX(0.03);

  if ( keys.a )
  cube.rotateZ(0.03);

  if ( keys.d )
  cube.rotateZ(-0.03);

  if ( keys.q )
  cube.rotateY(-0.06);

  if ( keys.e )
  cube.rotateY(0.06);

  if ( keys.space )
    speed = 0.9;

  velocity += ( speed - velocity ) * .3;
  cube.translateY( velocity );

  if ( keys.shiftleft )
    speed = 0.9*5;
    velocity += ( speed - velocity ) * .3; 
    cube.translateY( velocity );

  perspectiveCamera.lookAt( cube.position ); 
  controls.update(delta);
  renderer.render( scene, perspectiveCamera );
}
animate();

尝试过的解决方案

第一次尝试

const idealOffset = new THREE.Vector3(0, 0, -80);
function animate(){
  ...
  perspectiveCamera.position.set(0, 0, -80);
  perspectiveCamera.applyQuaternion(cube.rotation);
  perspectiveCamera.position.add(cube.position);
  perspectiveCamera.lookAt(idealOffset);
  ...
}

实际效果:相机可以跟随立方体移动,但几秒后就会转回看向世界原点。

第二次尝试

// Animate objects
  requestAnimationFrame( function animate(milliseconds) {
  const delta = clock.getDelta();
  let moveDistance = 10*delta;
  let rotateAngle = Math.PI/2*delta;
  requestAnimationFrame( animate );
  speed = 0.0;

  if ( keys.w ){
    cube.rotateX(-0.03);
  }
  if ( keys.s ){
    cube.rotateX(0.03);
  }

  let rotation_matrix = new THREE.Matrix4().identity();
  if ( keys.a ){
    cube.rotateZ(rotateAngle);
  }
  if ( keys.d ){
    cube.rotateZ(-rotateAngle);
  }
  if ( keys.q ){
    cube.rotateY(-rotateAngle);
  }
  if ( keys.e ){
    cube.rotateY(rotateAngle);
  }
  if ( keys.space ){
    speed = 0.9;
    velocity += ( speed - velocity ) * .3;
    cube.translateY( velocity );
  }
  if ( keys.shiftleft ){
    speed = 0.9*5;
    velocity += ( speed - velocity ) * .3; 
    cube.translateY( velocity );
  }
  
  var relativeCameraOffset = new THREE.Vector3(0,-80,0);
  var cameraOffset = relativeCameraOffset.applyMatrix4(cube.matrixWorld);
  perspectiveCamera.position.x = cameraOffset.x;
  perspectiveCamera.position.y = cameraOffset.y;
  perspectiveCamera.position.z = cameraOffset.z;
  perspectiveCamera.lookAt(cube.position);
  renderer.render( scene, perspectiveCamera );
});

实际效果:相机确实能跟随立方体移动,但将立方体沿X轴旋转180度时相机会颠倒,且相机完全固定在玩家身上没有平滑过渡效果,只能看向立方体的背面。

解决方案

首先删除OrbitControls的所有相关代码,OrbitControls会强制接管相机的位置和旋转逻辑,和手动控制相机跟随的逻辑冲突,这是第一次尝试失败的核心原因。
针对相机颠倒、无平滑过渡的问题,用四元数球面插值实现平滑旋转,向量线性插值实现平滑位移即可,修改逻辑如下:

  1. 全局新增几个变量:
// 相机相对立方体的偏移位置,可根据需要调整距离和高度
const cameraOffset = new THREE.Vector3(0, 20, 60);
// 平滑系数,值越小跟随越顺滑,0.05左右比较合适
const smoothFactor = 0.05;
// 临时变量存储目标位置和目标旋转
const targetPosition = new THREE.Vector3();
const targetQuaternion = new THREE.Quaternion();
  1. 替换animate函数里的相机控制逻辑:
function animate() {
  const delta = clock.getDelta();
  requestAnimationFrame( animate );
  // 原有立方体移动、旋转逻辑保持不变
  speed = 0.0;
  if ( keys.w ) cube.rotateX(-0.03);
  if ( keys.s ) cube.rotateX(0.03);
  if ( keys.a ) cube.rotateZ(0.03);
  if ( keys.d ) cube.rotateZ(-0.03);
  if ( keys.q ) cube.rotateY(-0.06);
  if ( keys.e ) cube.rotateY(0.06);
  if ( keys.space ) speed = 0.9;
  velocity += ( speed - velocity ) * .3;
  cube.translateY( velocity );
  if ( keys.shiftleft ) {
    speed = 4.5;
    velocity += ( speed - velocity ) * .3; 
    cube.translateY( velocity );
  }

  // 计算相机目标位置:立方体世界坐标 + 偏移量乘以立方体的旋转
  targetPosition.copy(cameraOffset);
  targetPosition.applyQuaternion(cube.quaternion);
  targetPosition.add(cube.position);

  // 平滑移动相机
  perspectiveCamera.position.lerp(targetPosition, smoothFactor);

  // 计算相机目标旋转:看向立方体的位置,固定上方向为Y轴正方向避免相机颠倒
  targetQuaternion.setFromRotationMatrix(
    new THREE.Matrix4().lookAt(
      perspectiveCamera.position,
      cube.position,
      new THREE.Vector3(0,1,0)
    )
  );

  // 平滑旋转相机
  perspectiveCamera.quaternion.slerp(targetQuaternion, smoothFactor);

  renderer.render( scene, perspectiveCamera );
}

以上逻辑固定了相机的上方向为世界坐标Y轴正方向,就算立方体旋转180度也不会出现相机颠倒的问题,同时通过lerp和slerp实现了位移和旋转的平滑过渡,不会出现硬跟随的突兀感。如果需要支持自由调整视角,可以额外监听鼠标输入,在cameraOffset的基础上叠加旋转偏移量即可。

内容的提问来源于stack exchange,提问作者BlackBeardedTaj

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 13:36:03