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会强制接管相机的位置和旋转逻辑,和手动控制相机跟随的逻辑冲突,这是第一次尝试失败的核心原因。
针对相机颠倒、无平滑过渡的问题,用四元数球面插值实现平滑旋转,向量线性插值实现平滑位移即可,修改逻辑如下:
- 全局新增几个变量:
// 相机相对立方体的偏移位置,可根据需要调整距离和高度 const cameraOffset = new THREE.Vector3(0, 20, 60); // 平滑系数,值越小跟随越顺滑,0.05左右比较合适 const smoothFactor = 0.05; // 临时变量存储目标位置和目标旋转 const targetPosition = new THREE.Vector3(); const targetQuaternion = new THREE.Quaternion();
- 替换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
相关产品推荐
相关产品推荐

