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