How to Use PointerLockControls in Three.js

This article explains how to implement the PointerLockControls class in Three.js to capture the user’s mouse cursor, enabling a classic first-person shooter (FPS) camera control style. We will cover importing the controls, initializing them with a camera, handling the pointer lock state transitions, and integrating basic keyboard movement to create an immersive 3D navigation experience.

Understanding PointerLockControls

The PointerLockControls class utilizes the browser’s native Pointer Lock API. When activated, it hides the mouse cursor and provides raw mouse movement data to rotate the Three.js camera. This allows users to look around a 3D scene infinitely without the cursor leaving the browser window.

Step 1: Import the Module

To use PointerLockControls, you must import it alongside standard Three.js. It is located in the examples folder of the Three.js package:

import * as THREE from 'three';
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls.js';

Step 2: Initialize the Controls

Create your camera and scene, then instantiate PointerLockControls. You must pass the camera and the HTML element (typically document.body) that will listen for mouse events.

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const scene = new THREE.Scene();

const controls = new PointerLockControls(camera, document.body);

// Add the controls' target object to the scene
scene.add(controls.getObject());

Adding controls.getObject() to the scene is crucial because the controls wrap the camera inside a utility object to manage position and rotation offsets.

Step 3: Trigger Pointer Lock

For security reasons, web browsers require a direct user interaction (like a mouse click) to lock the cursor. You can bind the lock trigger to a landing screen, button, or the entire document body:

const instructions = document.getElementById('instructions');

instructions.addEventListener('click', () => {
    controls.lock();
});

To display instructions when the user enters or exits the pointer lock mode, listen to the lock and unlock events:

controls.addEventListener('lock', () => {
    instructions.style.display = 'none'; // Hide UI overlay
});

controls.addEventListener('unlock', () => {
    instructions.style.display = 'block'; // Show pause/instructions UI
});

Step 4: Implement First-Person Keyboard Movement

Once the mouse is locked to control the camera’s orientation, you need to set up keyboard listeners to move the camera forward, backward, and sideways.

First, track key states:

const moveState = {
    forward: false,
    backward: false,
    left: false,
    right: false
};

const onKeyDown = (event) => {
    switch (event.code) {
        case 'KeyW':
        case 'ArrowUp':
            moveState.forward = true;
            break;
        case 'KeyS':
        case 'ArrowDown':
            moveState.backward = true;
            break;
        case 'KeyA':
        case 'ArrowLeft':
            moveState.left = true;
            break;
        case 'KeyD':
        case 'ArrowRight':
            moveState.right = true;
            break;
    }
};

const onKeyUp = (event) => {
    switch (event.code) {
        case 'KeyW':
        case 'ArrowUp':
            moveState.forward = false;
            break;
        case 'KeyS':
        case 'ArrowDown':
            moveState.backward = false;
            break;
        case 'KeyA':
        case 'ArrowLeft':
            moveState.left = false;
            break;
        case 'KeyD':
        case 'ArrowRight':
            moveState.right = false;
            break;
    }
};

document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);

Next, update the camera position inside your animation loop using the built-in movement methods of PointerLockControls. These methods automatically account for the camera’s current rotation.

const clock = new THREE.Clock();
const moveSpeed = 50.0; // Units per second

function animate() {
    requestAnimationFrame(animate);

    if (controls.isLocked) {
        const delta = clock.getDelta(); // Seconds passed since last frame
        const actualSpeed = moveSpeed * delta;

        if (moveState.forward) controls.moveForward(actualSpeed);
        if (moveState.backward) controls.moveForward(-actualSpeed);
        if (moveState.right) controls.moveRight(actualSpeed);
        if (moveState.left) controls.moveRight(-actualSpeed);
    }

    renderer.render(scene, camera);
}

animate();