ThreeJS游戏中相机控制随朝向异常(反转/翻滚)的问题排查与修复求助
ThreeJS游戏中相机控制随朝向异常(反转/翻滚)的问题排查与修复求助
我最近做了一个第一人称ThreeJS游戏,遇到了相机控制的奇怪问题:当我朝向初始方向时,相机操控是正常的,但一旦转头看向身后,相机控制就会反转;如果看向侧面,相机还会出现翻滚的情况。我怀疑这是因为相机的旋转只基于初始朝向的相对坐标,但自己试了好几种方法都没法修复。另外还有个小问题,这段代码在其他编辑器里运行正常,但在Stack Overflow上会报错,实在搞不懂哪里出问题了。
下面是我的游戏代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>First Person Game with Four Cameras</title> <style> body { margin: 0; } canvas { display: block; } #minimap { position: absolute; top: 10px; left: 10px; width: 200px; height: 200px; border: 1px solid #000; background-color: rgba(255, 255, 255, 0.5); pointer-events: none; } #controls { position: absolute; top: 10px; right: 10px; background: rgba(255, 255, 255, 0.8); padding: 10px; border-radius: 5px; } #controls label { display: block; margin: 10px 0; } input[type=range] { width: 100%; height: 10px; -webkit-appearance: none; appearance: none; background: #ddd; border-radius: 5px; } input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; background: #4CAF50; border-radius: 50%; cursor: pointer; } input[type=range]::-moz-range-thumb { width: 20px; height: 20px; background: #4CAF50; border: none; border-radius: 50%; cursor: pointer; } </style> </head> <body> <div id="minimap"></div> <div id="controls"> <label for="fov">Field of View: <input type="range" id="fov" min="30" max="100" value="75"></label> <label for="brightness">Brightness: <input type="range" id="brightness" min="0" max="2" step="0.1" value="1"></label> <button id="pause">Pause</button> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> let scene, renderer; let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false; let velocity = new THREE.Vector3(); let direction = new THREE.Vector3(); let canJump = false; let prevTime = performance.now(); let cubes = [], cubeVelocities = []; let heldCube = null; let aiBlock; const speed = 150.0; const jumpVelocity = 30.0; const gravity = 9.8 * 5.0; const pickUpDistance = 2.0; const originalCubeScale = 1; const heldCubeScale = 0.5; const friction = 0.05; let pitch = 0, yaw = 0; // Day/Night cycle variables let dayTime = 0; const dayDuration = 10000; let isPaused = false; // Define four cameras for North, South, East, West const cameras = { north: new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000), south: new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000), east: new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000), west: new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000), }; // Initial camera positions and rotations cameras.north.position.set(0, 1.6, -5); cameras.south.position.set(0, 1.6, 5); cameras.east.position.set(5, 1.6, 0); cameras.west.position.set(-5, 1.6, 0); cameras.north.rotation.set(0, 0, 0); cameras.south.rotation.set(0, Math.PI, 0); cameras.east.rotation.set(0, Math.PI / 2, 0); cameras.west.rotation.set(0, -Math.PI / 2, 0); // Starting camera direction let currentCamera = cameras.north; init(); animate(); function init() { scene = new THREE.Scene(); renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const groundGeometry = new THREE.PlaneGeometry(50, 50); const groundMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; scene.add(ground); const cubeGeometry = new THREE.BoxGeometry(1, 1, 1); const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0x0000ff }); for (let i = 0; i < 3; i++) { const cube = new THREE.Mesh(cubeGeometry, cubeMaterial); cube.position.set(Math.random() * 30 - 15, 1.6, Math.random() * 30 - 15); cubes.push(cube); cubeVelocities.push(new THREE.Vector3()); scene.add(cube); } const aiMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); aiBlock = new THREE.Mesh(cubeGeometry, aiMaterial); aiBlock.position.set(Math.random() * 30 - 15, 1.6, Math.random() * 30 - 15); scene.add(aiBlock); document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); document.getElementById('pause').addEventListener('click', togglePause); document.getElementById('fov').addEventListener('input', (event) => { for (let cam in cameras) { cameras[cam].fov = event.target.value; cameras[cam].updateProjectionMatrix(); } }); document.getElementById('brightness').addEventListener('input', (event) => { scene.background = new THREE.Color(`hsl(0, 0%, ${event.target.value * 100}%)`); }); document.body.addEventListener('click', () => { document.body.requestPointerLock(); }); document.addEventListener('mousemove', onMouseMove); window.addEventListener('resize', onWindowResize); } function togglePause() { isPaused = !isPaused; const controls = document.getElementById('controls'); if (isPaused) { controls.classList.add('paused'); } else { controls.classList.remove('paused'); } } function onWindowResize() { for (let cam in cameras) { cameras[cam].aspect = window.innerWidth / window.innerHeight; cameras[cam].updateProjectionMatrix(); } renderer.setSize(window.innerWidth, window.innerHeight); } function onKeyDown(event) { switch (event.code) { case 'KeyW': moveForward = true; break; case 'KeyS': moveBackward = true; break; case 'KeyA': moveLeft = true; break; case 'KeyD': moveRight = true; break; case 'Space': if (canJump) { velocity.y = jumpVelocity; canJump = false; } break; case 'KeyF': pickOrThrowCube(); break; case 'KeyP': togglePause(); break; case 'Digit1': currentCamera = cameras.north; // Switch to North camera break; case 'Digit2': currentCamera = cameras.south; // Switch to South camera break; case 'Digit3': currentCamera = cameras.east; // Switch to East camera break; case 'Digit4': currentCamera = cameras.west; // Switch to West camera break; } } function onKeyUp(event) { switch (event.code) { case 'KeyW': moveForward = false; break; case 'KeyS': moveBackward = false; break; case 'KeyA': moveLeft = false; break; case 'KeyD': moveRight = false; break; } } function onMouseMove(event) { if (document.pointerLockElement) { const sensitivity = 0.002; yaw -= event.movementX * sensitivity; pitch -= event.movementY * sensitivity; pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch)); // Apply rotation to the current camera currentCamera.rotation.y = yaw; currentCamera.rotation.x = pitch; currentCamera.rotation.z = 0; } } function pickOrThrowCube() { if (heldCube) { const throwVelocity = new THREE.Vector3(); currentCamera.getWorldDirection(throwVelocity); throwVelocity.multiplyScalar(300); heldCube.position.add(throwVelocity.multiplyScalar(0.04)); cubeVelocities[cubes.indexOf(heldCube)].copy(throwVelocity); heldCube.scale.set(originalCubeScale, originalCubeScale, originalCubeScale); heldCube = null; } else { const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(new THREE.Vector2(0, 0), currentCamera); const intersects = raycaster.intersectObjects(cubes.concat(aiBlock)); if (intersects.length > 0) { const intersect = intersects[0]; if (intersect.distance < pickUpDistance) { heldCube = intersect.object; heldCube.scale.set(heldCubeScale, heldCubeScale, heldCubeScale); } } } } function animate() { requestAnimationFrame(animate); if (isPaused) return; const time = performance.now(); const delta = (time - prevTime) / 1000; // Update day/night cycle dayTime += delta; if (dayTime > dayDuration) dayTime = 0; const nightFactor = Math.sin(dayTime / dayDuration * Math.PI * 2); const brightness = document.getElementById('brightness').value; scene.background = new THREE.Color(`hsl(0, 0%, ${(0.5 + nightFactor * 0.5) * brightness * 100}%)`); // Update camera position based on input direction.z = Number(moveForward) - Number(moveBackward); direction.x = Number(moveRight) - Number(moveLeft); direction.normalize(); // this ensures consistent movements in all directions if (moveForward || moveBackward) velocity.z -= direction.z * speed * delta; if (moveLeft || moveRight) velocity.x -= direction.x * speed * delta; // Apply friction velocity.x *= (1 - friction); velocity.z *= (1 - friction); // Apply gravity velocity.y -= gravity * delta; // Move all cameras together for (let cam in cameras) { cameras[cam].position.x += velocity.x * delta; cameras[cam].position.y += velocity.y * delta; cameras[cam].position.z += velocity.z * delta; } // Check ground collision canJump = false; if (currentCamera.position.y < 1.6) { velocity.y = 0; currentCamera.position.y = 1.6; canJump = true; } // Update cubes for (let i = 0; i < cubes.length; i++) { const cube = cubes[i]; const cubeVel = cubeVelocities[i]; if (cube !== heldCube) { cube.position.x += cubeVel.x * delta; cube.position.y += cubeVel.y * delta; cube.position.z += cubeVel.z * delta; // Apply gravity to cubes cubeVel.y -= gravity * delta; // Cube ground collision if (cube.position.y < 0.5) { cubeVel.y = 0; cube.position.y = 0.5; } } else { // Hold cube in front of camera currentCamera.getWorldDirection(direction); direction.multiplyScalar(2); heldCube.position.copy(currentCamera.position).add(direction); } } // Update AI block (simple random movement) const aiSpeed = 50; aiBlock.position.x += (Math.random() - 0.5) * aiSpeed * delta; aiBlock.position.z += (Math.random() - 0.5) * aiSpeed * delta; if (aiBlock.position.y < 1.6) { aiBlock.position.y = 1.6; } renderer.render(scene, currentCamera); prevTime = time; } </script> </body> </html>
备注:内容来源于stack exchange,提问作者Mason Fisher




