Mason Fisher
Mason Fisher

Reputation: 59

How to fix ThreeJs camera controls only updating for starting view

I am making a 3d Three Js game, but I have run into a problem with my camera controls. When I look the starting direction that you look, the camera controls are all right. But if I look backward, the up and down camera controls inverse, and if I look to one of the sides, then those controls make the whole thing go sideways. I think the problem is like an old time joystick, where if you stand on the side of the claw machine or whatever it is, than forward would not be forward, it would be to the side. Here is my code so far: (Sorry, mouse lock doesn't work with stack overflow, use something like https://www.w3schools.com/html/tryit.asp?filename=tryhtml_default)

<!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</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
    let scene, camera, 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;

    const speed = 150.0;
    const jumpVelocity = 75.0;
    const gravity = 9.8 * 50.0;
    const pickUpDistance = 2.0;
    const originalCubeScale = 1;
    const heldCubeScale = 0.5;
    const friction = 0.1; // Friction coefficient
    let pitch = 0, yaw = 0;

    init();
    animate();

    function init() {
        // Scene and camera setup
        scene = new THREE.Scene();
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.y = 1.6;

        // Renderer setup
        renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

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

        // Tangible cubes
        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);
        }

        // Event listeners for movement
        document.addEventListener('keydown', onKeyDown);
        document.addEventListener('keyup', onKeyUp);

        // Lock the mouse
        document.body.addEventListener('click', () => {
            document.body.requestPointerLock();
        });
        document.addEventListener('mousemove', onMouseMove);

        window.addEventListener('resize', onWindowResize);
    }

    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.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;
        }
    }

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

            camera.rotation.y = yaw;
            camera.rotation.x = pitch;
            camera.rotation.z = 0; // Prevent rolling sideways
        }
    }

    function pickOrThrowCube() {
        if (heldCube) {
            // Throw the held cube
            const throwVelocity = new THREE.Vector3();
            camera.getWorldDirection(throwVelocity);
            throwVelocity.multiplyScalar(200); // Throw strength
            heldCube.position.add(throwVelocity.multiplyScalar(0.02));
            cubeVelocities[cubes.indexOf(heldCube)].copy(throwVelocity);
            heldCube.scale.set(originalCubeScale, originalCubeScale, originalCubeScale); // Reset cube scale
            heldCube = null;
        } else {
            // Pick up a cube if close enough and looking at it
            const raycaster = new THREE.Raycaster();
            raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
            const intersects = raycaster.intersectObjects(cubes);

            if (intersects.length > 0) {
                const intersect = intersects[0];
                if (intersect.distance < pickUpDistance) {
                    heldCube = intersect.object;
                    cubeVelocities[cubes.indexOf(heldCube)].set(0, 0, 0); // Stop the cube's movement
                    heldCube.scale.set(heldCubeScale, heldCubeScale, heldCubeScale); // Make the cube smaller
                    heldCube.position.copy(camera.position).add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(1.5));
                }
            }
        }
    }

    function animate() {
        requestAnimationFrame(animate);

        // Update time
        const time = performance.now();
        const delta = (time - prevTime) / 1000;

        // Movement logic
        direction.z = Number(moveForward) - Number(moveBackward);
        direction.x = Number(moveRight) - Number(moveLeft);
        direction.normalize();

        velocity.x -= velocity.x * 10.0 * delta;
        velocity.z -= velocity.z * 10.0 * delta;
        velocity.y -= gravity * delta;

        if (moveForward || moveBackward || moveLeft || moveRight) {
            const frontDirection = new THREE.Vector3();
            camera.getWorldDirection(frontDirection);

            const rightDirection = new THREE.Vector3();
            rightDirection.crossVectors(camera.up, frontDirection).normalize();

            frontDirection.multiplyScalar(direction.z * speed * delta);
            rightDirection.multiplyScalar(direction.x * speed * delta);

            velocity.add(frontDirection).add(rightDirection);
        }

        camera.position.addScaledVector(velocity, delta);

        // Collision detection with barriers
        camera.position.x = Math.max(-24, Math.min(24, camera.position.x));
        camera.position.z = Math.max(-24, Math.min(24, camera.position.z));

        if (camera.position.y < 1.6) {
            velocity.y = 0;
            camera.position.y = 1.6;
            canJump = true;
        }

        // Update held cube position
        if (heldCube) {
            heldCube.position.copy(camera.position).add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(1.5));
        }

        // Cube collision logic
        for (let i = 0; i < cubes.length; i++) {
            if (cubes[i] !== heldCube) {
                // Apply friction
                cubeVelocities[i].x *= (1 - friction);
                cubeVelocities[i].z *= (1 - friction);

                // Apply gravity to cubes
                cubeVelocities[i].y -= gravity * delta;
                cubes[i].position.addScaledVector(cubeVelocities[i], delta);

                // Cube collision with ground: stop bouncing
                if (cubes[i].position.y < 0.5) {
                    cubes[i].position.y = 0.5;
                    cubeVelocities[i].y = 0; // Stop upward velocity
                }

                // Simple cube interaction logic for pushing
                for (let j = 0; j < cubes.length; j++) {
                    if (i !== j) {
                        const distance = cubes[i].position.distanceTo(cubes[j].position);
                        if (distance < 1.5) { // If cubes are close enough
                            const collisionDirection = new THREE.Vector3().subVectors(cubes[i].position, cubes[j].position).normalize();
                            const relativeVelocity = new THREE.Vector3().subVectors(cubeVelocities[i], cubeVelocities[j]);
                            if (relativeVelocity.dot(collisionDirection) < 0) { // Only push if moving toward each other
                                const pushAmount = 0.02; // Adjust to control push force
                                cubeVelocities[i].add(collisionDirection.clone().multiplyScalar(pushAmount));
                                cubeVelocities[j].sub(collisionDirection.clone().multiplyScalar(pushAmount));
                            }
                        }
                    }
                }
            }
        }

        renderer.render(scene, camera);
        prevTime = time;
    }
</script>
</body>
</html>

Upvotes: 0

Views: 57

Answers (1)

A_____ A_______
A_____ A_______

Reputation: 376

you need to add

camera.rotation.order = 'YXZ' // default 'XYZ'

in your init() function

reason: rotation matrix multiplication order

you can see detalised info here: https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix
and also here: https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions

also if you add this:

const axesHelper = new THREE.AxesHelper( 15 )
//axesHelper.renderOrder = 1;
axesHelper.material.depthTest = false;
scene.add( axesHelper )

in init() it will show world axes (and will be always on top of all objects) i needed that while cheking your code so maybe you also will need it in future

Upvotes: 1

Related Questions