Controls Pointerlock
Runnable three.js example: Controls Pointerlock. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
说明:点击画布锁定指针,用 WASD 在街区里第一人称行走,ESC 释放鼠标
核心逻辑:PointerLockControls 只管理朝向,位移由 moveForward / moveRight 合成
依赖:three.js 0.160.0、PointerLockControls · jsDelivr CDN(ESM + importmap)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>第一人称漫游</title>
<style>
html,body{height:100%;margin:0;background:#0a1016}
body{display:flex;flex-direction:column;overflow:hidden;
font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif;color:#e6e8ec}
.bar{flex:0 0 auto;padding:8px 12px;background:#161b22;border-bottom:1px solid #252c36;
display:flex;gap:14px;align-items:center;font-size:12px;color:#8b93a1;flex-wrap:wrap}
.bar b{color:#58a6ff;font-weight:600;font-variant-numeric:tabular-nums}
.bar input[type=range]{width:140px;accent-color:#4f9ef8}
.bar .sp{flex:1 1 auto}
#stage{flex:1 1 auto;min-height:0;position:relative;cursor:pointer}
#stage canvas{display:block}
#cross{position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-7px 0 0 -7px;
pointer-events:none;opacity:.7}
#cross::before,#cross::after{content:'';position:absolute;background:#dfe9f5}
#cross::before{left:6px;top:0;width:2px;height:14px}
#cross::after{top:6px;left:0;height:2px;width:14px}
#veil{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(8,13,18,.78);color:#cfe0f2;font-size:15px;letter-spacing:.05em;
transition:opacity .2s}
#veil.hide{opacity:0;pointer-events:none}
</style>
</head>
<body>
<div class="bar">
<span>坐标 <b id="posv">—</b></span>
<span>速度 <b id="spdv">6.0</b> 单位/秒</span>
<label>调速 <input type="range" id="spd" min="2" max="16" step="0.5" value="6"></label>
<span class="sp"></span>
<span>WASD 行走 · Shift 加速 · ESC 退出</span>
</div>
<div id="stage">
<div id="cross"></div>
<div id="veil">点击进入第一人称视角</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
const stage = document.getElementById('stage');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a1016);
scene.fog = new THREE.Fog(0x0a1016, 30, 90);
const camera = new THREE.PerspectiveCamera(70, stage.clientWidth / stage.clientHeight, 0.1, 300);
camera.position.set(0, 1.7, 26);
const controls = new PointerLockControls(camera, renderer.domElement);
scene.add(controls.getObject());
scene.add(new THREE.HemisphereLight(0xc2d9f0, 0x161d24, 1.5));
const sun = new THREE.DirectionalLight(0xffe9c8, 2.0);
sun.position.set(-16, 26, 10);
scene.add(sun);
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(200, 200),
new THREE.MeshStandardMaterial({ color: 0x1b242d, roughness: 0.95 })
);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
/* 街道:沿 X 轴一条主街,两侧排满楼 */
const roadMat = new THREE.MeshStandardMaterial({ color: 0x2b3742, roughness: 0.9 });
const road = new THREE.Mesh(new THREE.BoxGeometry(200, 0.05, 8), roadMat);
road.position.set(0, 0.03, 0);
scene.add(road);
const lineMat = new THREE.MeshBasicMaterial({ color: 0x8b9aa8 });
for (let x = -90; x <= 90; x += 6) {
const dash = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.02, 0.22), lineMat);
dash.position.set(x, 0.07, 0);
scene.add(dash);
}
function hash(n) {
const x = Math.sin(n * 33.7 + 11.3) * 43758.5453;
return x - Math.floor(x);
}
for (let i = 0; i < 64; i++) {
const side = i % 2 === 0 ? 1 : -1;
const x = -88 + (i / 2 | 0) * 5.6;
const h = 4 + Math.pow(hash(i), 1.8) * 26;
const w = 3.4 + hash(i + 4) * 1.6;
const d = 3.4 + hash(i + 8) * 1.6;
const b = new THREE.Mesh(
new THREE.BoxGeometry(w, h, d),
new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.58 - hash(i + 2) * 0.12, 0.2, 0.3 + hash(i + 6) * 0.22),
roughness: 0.7,
metalness: 0.08
})
);
b.position.set(x, h / 2, side * (6 + d / 2));
scene.add(b);
}
const keys = Object.create(null);
document.addEventListener('keydown', function (e) { keys[e.code] = true; });
document.addEventListener('keyup', function (e) { keys[e.code] = false; });
const veil = document.getElementById('veil');
veil.addEventListener('click', function () { controls.lock(); });
renderer.domElement.addEventListener('click', function () { controls.lock(); });
controls.addEventListener('lock', function () { veil.classList.add('hide'); });
controls.addEventListener('unlock', function () { veil.classList.remove('hide'); });
let speed = 6;
const spdEl = document.getElementById('spd');
const spdvEl = document.getElementById('spdv');
spdEl.addEventListener('input', function () {
speed = Number(spdEl.value);
spdvEl.textContent = speed.toFixed(1);
});
const posvEl = document.getElementById('posv');
const clock = new THREE.Clock();
renderer.setAnimationLoop(function () {
const dt = Math.min(clock.getDelta(), 0.1);
const boost = keys['ShiftLeft'] || keys['ShiftRight'] ? 2.2 : 1;
const step = speed * boost * dt;
if (controls.isLocked) {
if (keys['KeyW']) controls.moveForward(step);
if (keys['KeyS']) controls.moveForward(-step);
if (keys['KeyA']) controls.moveRight(-step);
if (keys['KeyD']) controls.moveRight(step);
/* 保持行走高度,避免镜头被抬到楼顶或钻进地面 */
controls.getObject().position.y = 1.7;
}
posvEl.textContent =
controls.getObject().position.x.toFixed(1) + ', ' +
controls.getObject().position.z.toFixed(1);
renderer.render(scene, camera);
});
window.addEventListener('resize', function () {
const w = stage.clientWidth;
const h = stage.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.