First-Person Controls and FPS Games in Three.js

Updated June 2026
First-person controls are the foundation of FPS games, exploration games, and any project where the player sees through a character's eyes. Building them in Three.js involves capturing the mouse with the Pointer Lock API, converting mouse movement into camera rotation, translating keyboard input into directional movement, and integrating a physics character controller for collision response. This guide walks through each component from basic mouselook to polished, game-ready controls.

Three.js includes a PointerLockControls class in its addons that handles the pointer lock request and basic camera rotation. For simple demos or walking simulators, this class might be sufficient. For a game that needs wall sliding, slope handling, jumping, crouching, and sprinting, you need to build a more complete system on top of it, or replace it with custom code that gives you full control over the input pipeline.

Step 1: Set Up Pointer Lock

The Pointer Lock API removes the mouse cursor from the screen and provides raw mouse movement data (deltas) instead of absolute positions. This is essential for first-person camera control because it lets the player rotate the view indefinitely without the cursor hitting the edge of the screen. The browser requires a user gesture (like clicking on the canvas) to enter pointer lock, and it shows a notification the first time to inform the user that the cursor has been captured.

Request pointer lock by calling canvas.requestPointerLock() in response to a click event. Listen for the pointerlockchange event on the document to detect when pointer lock is acquired or released. When locked, the mousemove event provides movementX and movementY properties containing the raw pixel deltas since the last event. When the user presses Escape, the browser releases pointer lock automatically.

Your game's UI should handle the unlocked state gracefully. Display a "Click to play" overlay when pointer lock is not active, and show a pause menu when the user presses Escape. This is standard practice for browser FPS games and helps players understand the interaction model.

Step 2: Implement Mouse Look

Convert the raw mouse deltas from the mousemove event into camera rotation. The horizontal delta (movementX) rotates the camera around the Y axis (yaw), and the vertical delta (movementY) rotates around the X axis (pitch). Multiply both deltas by a sensitivity factor, typically between 0.001 and 0.003, to convert pixel movement into radians.

Clamp the vertical rotation to prevent the camera from flipping upside down. A typical range is from -89 degrees to +89 degrees (slightly less than straight up and straight down). Without this clamp, looking too far up or down causes the camera to flip 180 degrees, which is disorienting and breaks the movement direction.

Apply the rotation by setting the camera's Euler angles directly, or by using quaternions for smoother interpolation. The order matters: apply yaw rotation to the parent object (the player body) and pitch rotation to the camera itself. This keeps vertical and horizontal rotation independent, which is the standard for FPS cameras. If you apply both rotations to the camera, horizontal movement becomes tilted when looking up or down.

Step 3: Add WASD Movement

Track which keys are currently pressed using a Set or an object that maps key codes to boolean states. Listen for keydown and keyup events and update the state accordingly. Process the key state in your game loop's update phase, not in the event handlers, to ensure movement is frame-rate independent and synchronized with the rest of the game logic.

Movement direction must be relative to the camera's facing direction, not to the world axes. When the player presses W, they should move forward in the direction the camera is looking, not along the world Z axis. Extract the camera's forward and right vectors from its world matrix, zero out the Y component (to keep movement horizontal on flat ground), normalize them, and use them to compute the movement direction.

Combine the forward/backward vector (W/S) with the left/right vector (A/D) and normalize the result to prevent diagonal movement from being faster than cardinal movement. Multiply by the movement speed and delta time to get the displacement for this frame. This displacement is what you pass to the physics character controller for collision-aware movement.

Step 4: Build a Character Controller

A physics-based character controller handles wall collision, slope climbing, step-up over small obstacles, and sliding along surfaces. Without one, the player walks through walls. Rapier provides a built-in KinematicCharacterController that handles all of these behaviors with configurable parameters.

Create a capsule-shaped collider for the player body. A capsule is the standard choice for character controllers because it slides smoothly along walls and floors without catching on edges like a box would. A typical capsule for a human character is about 0.3-0.4 units in radius and 1.7-1.8 units tall. Attach it to a kinematic rigid body positioned at the player's feet.

Each frame, pass the desired movement vector to the character controller's computeColliderMovement method. The controller calculates the actual movement after accounting for collisions, slopes, and steps. Apply the resulting movement to the rigid body, then update the Three.js camera position to match the body's position plus a height offset for the eye level (typically the capsule height minus a small amount).

Configure the controller's maxSlopeClimbAngle (typically 45-50 degrees) to prevent climbing steep surfaces, and the stepHeight (typically 0.3-0.5 units) to allow stepping over small ledges and stairs without jumping. These parameters significantly affect how the game feels to play and should be tuned through playtesting.

Step 5: Add Jumping and Gravity

Gravity in a character controller is typically handled manually rather than by the physics engine's built-in gravity, because kinematic bodies do not respond to forces automatically. Track a vertical velocity variable that increases each frame by the gravity constant (typically -9.81 meters per second squared, scaled by your game's unit system). Add this vertical velocity to the movement vector before passing it to the character controller.

Ground detection uses the character controller's computedGrounded() method after each movement computation. When the character is grounded, reset the vertical velocity to a small negative value (like -0.1) rather than zero, which ensures the controller stays in contact with the ground and detects slopes correctly. When the player presses the jump key while grounded, set the vertical velocity to a positive value (the jump impulse).

Coyote time is a common game feel enhancement where the player can still jump for a few frames after walking off a ledge. Track the number of frames since the player was last grounded, and allow jumping within a small window (typically 5-10 frames at 60fps). This makes platforming less frustrating without being visually noticeable.

Step 6: Polish the Feel

Raw first-person controls feel mechanical without polish. Head bob is a subtle vertical oscillation synchronized to movement speed that simulates the natural bounce of walking. Apply a sine wave to the camera's Y position, with the frequency tied to movement speed and the amplitude kept small (0.02-0.05 units) to avoid causing motion sickness.

Add a sprint mode activated by holding Shift that increases movement speed by 50-80% and adjusts the head bob frequency and camera FOV. A subtle FOV increase (5-10 degrees) during sprinting creates a sense of speed. Smooth the FOV change with interpolation so it transitions over a few frames rather than snapping instantly.

Crouching reduces the capsule height and camera position, slows movement speed, and can allow the player to fit through low spaces. Implement it by adjusting the collider dimensions and eye height when the crouch key is held. Check for overhead clearance before allowing the player to stand up, to prevent them from clipping into geometry above.

Audio feedback grounds the player in the world. Play footstep sounds at intervals timed to the head bob cycle. Vary the sound based on the surface material underneath the player by raycasting downward and checking the hit object's material or tag. Add subtle breathing sounds during and after sprinting. These audio cues provide feedback that makes movement feel physical and connected to the environment.

Key Takeaway

First-person controls combine Pointer Lock for mouse capture, camera rotation with pitch clamping, direction-relative WASD movement, a physics character controller for collision response, and polish elements like head bob and audio feedback. The physics integration is what separates demo-quality controls from game-ready ones.