Skip to content

PointerLockControls

Tip

This course has been updated. For the newer content, please visit PointerLockControls

Video Lecture

PointerLockControls

 (Pay Per View)

You can use PayPal to purchase a one time viewing of this video for $1.49 USD.

Pay Per View Terms

  • One viewing session of this video will cost the equivalent of $1.49 USD in your currency.
  • After successful purchase, the video will automatically start playing.
  • You can pause, replay and go fullscreen as many times as needed in one single session for up to an hour.
  • Do not refresh the browser since it will invalidate the session.
  • If you want longer-term access to all videos, consider purchasing full access through Udemy or YouTube Memberships instead.
  • This Pay Per View option does not permit downloading this video for later viewing or sharing.
  • All videos are Copyright © 2019-2025 Sean Bradley, all rights reserved.

Description

The PointerLockControls implements the browsers inbuilt Pointer Lock API. It provides input methods based on the movement of the mouse over time (i.e., deltas), not just the absolute position of the mouse cursor in the viewport. It gives you access to raw mouse movement, locks the target of mouse events to a single element, eliminates limits on how far mouse movement can go in a single direction, and removes the cursor from view. It is ideal for first person 3D games, for example.

<>

Start Scripts

./dist/client/index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Three.js TypeScript Tutorials by Sean Bradley : https://sbcode.net/threejs</title>
    <style>
      body {
        overflow: hidden;
        margin: 0px;
      }

      #menuPanel {
        position: absolute;
        background-color: rgba(255, 255, 255, 0.5);
        top: 0px;
        left: 0px;
        width: 100%;
        height: 100%;
      }

      #startButton {
        height: 50px;
        width: 200px;
        margin: -25px -100px;
        position: relative;
        top: 50%;
        left: 50%;
        font-size: 32px;
      }
    </style>
  </head>

  <body>
    <div id="menuPanel">
      <button id="startButton">Click to Start</button>
    </div>
    <script type="module" src="bundle.js"></script>
  </body>
</html>

./src/client/client.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
import * as THREE from 'three'
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls'
import Stats from 'three/examples/jsm/libs/stats.module'

const scene = new THREE.Scene()
scene.add(new THREE.AxesHelper(5))

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.y = 1
camera.position.z = 2

const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)

const menuPanel = document.getElementById('menuPanel') as HTMLDivElement
// const startButton = document.getElementById('startButton') as HTMLInputElement
// startButton.addEventListener(
//     'click',
//     function () {
//         controls.lock()
//     },
//     false
// )

const controls = new PointerLockControls(camera, renderer.domElement)
// controls.addEventListener('change', () => console.log("Controls Change"))
// controls.addEventListener('lock', () => menuPanel.style.display = 'none')
// controls.addEventListener('unlock', () => menuPanel.style.display = 'block')

const planeGeometry = new THREE.PlaneGeometry(100, 100, 50, 50)
const material = new THREE.MeshBasicMaterial({
  color: 0x00ff00,
  wireframe: true,
})
const plane = new THREE.Mesh(planeGeometry, material)
plane.rotateX(-Math.PI / 2)
scene.add(plane)

const cubes: THREE.Mesh[] = []
for (let i = 0; i < 100; i++) {
  const geo = new THREE.BoxGeometry(Math.random() * 4, Math.random() * 16, Math.random() * 4)
  const mat = new THREE.MeshBasicMaterial({ wireframe: true })
  switch (i % 3) {
    case 0:
      mat.color = new THREE.Color(0xff0000)
      break
    case 1:
      mat.color = new THREE.Color(0xffff00)
      break
    case 2:
      mat.color = new THREE.Color(0x0000ff)
      break
  }
  const cube = new THREE.Mesh(geo, mat)
  cubes.push(cube)
}
cubes.forEach((c) => {
  c.position.x = Math.random() * 100 - 50
  c.position.z = Math.random() * 100 - 50
  c.geometry.computeBoundingBox()
  c.position.y = ((c.geometry.boundingBox as THREE.Box3).max.y - (c.geometry.boundingBox as THREE.Box3).min.y) / 2
  scene.add(c)
})

// const onKeyDown = function (event: KeyboardEvent) {
//     switch (event.code) {
//         case "KeyW":
//             controls.moveForward(.25)
//             break
//         case "KeyA":
//             controls.moveRight(-.25)
//             break
//         case "KeyS":
//             controls.moveForward(-.25)
//             break
//         case "KeyD":
//             controls.moveRight(.25)
//             break
//     }
// }
// document.addEventListener('keydown', onKeyDown, false)

window.addEventListener('resize', onWindowResize, false)
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
  render()
}

const stats = new Stats()
document.body.appendChild(stats.dom)

function animate() {
  requestAnimationFrame(animate)

  //controls.update()

  render()

  stats.update()
}

function render() {
  renderer.render(scene, camera)
}

animate()

Final Script

./src/client/client.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
import * as THREE from 'three'
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls'
import Stats from 'three/examples/jsm/libs/stats.module'

const scene = new THREE.Scene()
scene.add(new THREE.AxesHelper(5))

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.y = 1
camera.position.z = 2

const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)

const menuPanel = document.getElementById('menuPanel') as HTMLDivElement
const startButton = document.getElementById('startButton') as HTMLInputElement
startButton.addEventListener(
  'click',
  function () {
    controls.lock()
  },
  false
)

const controls = new PointerLockControls(camera, renderer.domElement)
//controls.addEventListener('change', () => console.log("Controls Change"))
controls.addEventListener('lock', () => (menuPanel.style.display = 'none'))
controls.addEventListener('unlock', () => (menuPanel.style.display = 'block'))

const planeGeometry = new THREE.PlaneGeometry(100, 100, 50, 50)
const material = new THREE.MeshBasicMaterial({
  color: 0x00ff00,
  wireframe: true,
})
const plane = new THREE.Mesh(planeGeometry, material)
plane.rotateX(-Math.PI / 2)
scene.add(plane)

const cubes: THREE.Mesh[] = []
for (let i = 0; i < 100; i++) {
  const geo = new THREE.BoxGeometry(Math.random() * 4, Math.random() * 16, Math.random() * 4)
  const mat = new THREE.MeshBasicMaterial({ wireframe: true })
  switch (i % 3) {
    case 0:
      mat.color = new THREE.Color(0xff0000)
      break
    case 1:
      mat.color = new THREE.Color(0xffff00)
      break
    case 2:
      mat.color = new THREE.Color(0x0000ff)
      break
  }
  const cube = new THREE.Mesh(geo, mat)
  cubes.push(cube)
}
cubes.forEach((c) => {
  c.position.x = Math.random() * 100 - 50
  c.position.z = Math.random() * 100 - 50
  c.geometry.computeBoundingBox()
  c.position.y = ((c.geometry.boundingBox as THREE.Box3).max.y - (c.geometry.boundingBox as THREE.Box3).min.y) / 2
  scene.add(c)
})

const onKeyDown = function (event: KeyboardEvent) {
  switch (event.code) {
    case 'KeyW':
      controls.moveForward(0.25)
      break
    case 'KeyA':
      controls.moveRight(-0.25)
      break
    case 'KeyS':
      controls.moveForward(-0.25)
      break
    case 'KeyD':
      controls.moveRight(0.25)
      break
  }
}
document.addEventListener('keydown', onKeyDown, false)

window.addEventListener('resize', onWindowResize, false)
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
  render()
}

const stats = new Stats()
document.body.appendChild(stats.dom)

function animate() {
  requestAnimationFrame(animate)

  //controls.update()

  render()

  stats.update()
}

function render() {
  renderer.render(scene, camera)
}

animate()

PointerLockControls (Official Documentation)

Pointer Lock API

KeyboardEvent.code