You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动