PointerLockControls
是 Three.js 中用于处理鼠标锁定状态下的相机控制的类。它允许用户通过鼠标移动来控制相机的旋转方向。下面是它的详细讲解:
构造函数:
javascript
PointerLockControls(object: Camera, domElement?: HTMLElement)
object
:THREE.Camera 实例,控制器将用于控制该相机。domElement
(可选):用于监听鼠标事件的 HTML 元素。如果未提供,则默认为document
。
属性:
-
enabled: boolean
- 控制器是否启用。
-
isLocked: boolean
- 当前鼠标是否被锁定。
-
minPolarAngle: number
- 极角的最小值。
-
maxPolarAngle: number
- 极角的最大值。
方法:
-
connect(): void
- 连接控制器。
-
disconnect(): void
- 断开控制器。
-
dispose(): void
- 清理控制器所占用的资源,释放内存。
-
getObject(): Camera
- 获取控制器使用的相机对象。
-
lock(): void
- 锁定鼠标。
-
unlock(): void
- 解锁鼠标。
-
update(delta: number): void
- 更新控制器状态。
示例:
javascript
import * as THREE from 'three';
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls.js';
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1, -5);
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建 PointerLockControls 实例
const controls = new PointerLockControls(camera, document.body);
// 添加 PointerLockControls 到场景中
scene.add(controls.getObject());
// 锁定鼠标
controls.lock();
// 监听鼠标锁定状态改变事件
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement === document.body) {
controls.enabled = true;
} else {
controls.enabled = false;
}
});
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update(); // 更新控制器状态
renderer.render(scene, camera);
}
animate();
示例代码解读
js
function init() {
// 创建透视相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.y = 10; // 设置相机位置
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff); // 设置场景背景颜色
scene.fog = new THREE.Fog(0xffffff, 0, 750); // 添加雾效
// 添加半球光
const light = new THREE.HemisphereLight(0xeeeeff, 0x777788, 2.5);
light.position.set(0.5, 1, 0.75);
scene.add(light);
// 创建 PointerLockControls 实例
controls = new PointerLockControls(camera, document.body);
// 获取页面元素
const blocker = document.getElementById('blocker');
const instructions = document.getElementById('instructions');
// 点击提示时锁定鼠标
instructions.addEventListener('click', function () {
controls.lock();
});
// 锁定时隐藏提示
controls.addEventListener('lock', function () {
instructions.style.display = 'none';
blocker.style.display = 'none';
});
// 解锁时显示提示
controls.addEventListener('unlock', function () {
blocker.style.display = 'block';
instructions.style.display = '';
});
// 将控制器的相机对象添加到场景中
scene.add(controls.getObject());
// 监听按键事件
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
// 创建射线投射器
raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0), 0, 10);
// 创建地面
let floorGeometry = new THREE.PlaneGeometry(2000, 2000, 100, 100);
floorGeometry.rotateX(-Math.PI / 2); // 旋转地面几何体
// 为地面顶点添加随机偏移
let position = floorGeometry.attributes.position;
for (let i = 0, l = position.count; i < l; i++) {
vertex.fromBufferAttribute(position, i);
vertex.x += Math.random() * 20 - 10;
vertex.y += Math.random() * 2;
vertex.z += Math.random() * 20 - 10;
position.setXYZ(i, vertex.x, vertex.y, vertex.z);
}
floorGeometry = floorGeometry.toNonIndexed(); // 确保每个面的顶点是唯一的
position = floorGeometry.attributes.position;
const colorsFloor = [];
// 为地面顶点设置随机颜色
for (let i = 0, l = position.count; i < l; i++) {
color.setHSL(Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace);
colorsFloor.push(color.r, color.g, color.b);
}
// 将颜色属性添加到地面几何体中
floorGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colorsFloor, 3));
// 创建地面网格
const floorMaterial = new THREE.MeshBasicMaterial({ vertexColors: true });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
scene.add(floor);
// 创建立方体对象并添加到场景中
const boxGeometry = new THREE.BoxGeometry(20, 20, 20).toNonIndexed();
position = boxGeometry.attributes.position;
const colorsBox = [];
// 为立方体顶点设置随机颜色
for (let i = 0, l = position.count; i < l; i++) {
color.setHSL(Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace);
colorsBox.push(color.r, color.g, color.b);
}
// 将颜色属性添加到立方体几何体中
boxGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colorsBox, 3));
// 创建多个立方体对象并添加到场景中
for (let i = 0; i < 500; i++) {
const boxMaterial = new THREE.MeshPhongMaterial({ specular: 0xffffff, flatShading: true, vertexColors: true });
boxMaterial.color.setHSL(Math.random() * 0.2 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace);
const box = new THREE.Mesh(boxGeometry, boxMaterial);
box.position.x = Math.floor(Math.random() * 20 - 10) * 20;
box.position.y = Math.floor(Math.random() * 20) * 20 + 10;
box.position.z = Math.floor(Math.random() * 20 - 10) * 20;
scene.add(box);
objects.push(box);
}
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 监听窗口大小变化事件
window.addEventListener('resize', onWindowResize);
}
更新逻辑
js
function animate() {
// 请求下一帧动画
requestAnimationFrame(animate);
// 获取当前时间
const time = performance.now();
// 如果控制器被锁定
if (controls.isLocked === true) {
// 更新射线投射器的起点为相机位置,并向下偏移
raycaster.ray.origin.copy(controls.getObject().position);
raycaster.ray.origin.y -= 10;
// 检测相机位置下方是否有物体
const intersections = raycaster.intersectObjects(objects, false);
const onObject = intersections.length > 0;
// 计算时间间隔
const delta = (time - prevTime) / 1000;
// 更新速度
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
// 应用重力影响
velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass
// 计算移动方向
direction.z = Number(moveForward) - Number(moveBackward);
direction.x = Number(moveRight) - Number(moveLeft);
direction.normalize(); // 保证在所有方向上的移动一致性
// 根据移动方向和按键状态更新速度
if (moveForward || moveBackward) velocity.z -= direction.z * 400.0 * delta;
if (moveLeft || moveRight) velocity.x -= direction.x * 400.0 * delta;
// 如果在物体上方,使y速度为0并允许跳跃
if (onObject === true) {
velocity.y = Math.max(0, velocity.y);
canJump = true;
}
// 更新相机位置
controls.moveRight(-velocity.x * delta);
controls.moveForward(-velocity.z * delta);
// 更新相机位置的y值
controls.getObject().position.y += (velocity.y * delta); // 新的行为
// 如果相机位置低于地面高度,使y速度为0并固定在地面上
if (controls.getObject().position.y < 10) {
velocity.y = 0;
controls.getObject().position.y = 10;
canJump = true;
}
}
// 更新时间
prevTime = time;
// 渲染场景
renderer.render(scene, camera);
}
完整源码
html
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js - pointerlock controls</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
#blocker {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
#instructions {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
font-size: 14px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="blocker">
<div id="instructions">
<p style="font-size:36px">
Click to play
</p>
<p>
Move: WASD<br/>
Jump: SPACE<br/>
Look: MOUSE
</p>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
let camera, scene, renderer, controls;
const objects = [];
let raycaster;
let moveForward = false;
let moveBackward = false;
let moveLeft = false;
let moveRight = false;
let canJump = false;
let prevTime = performance.now();
const velocity = new THREE.Vector3();
const direction = new THREE.Vector3();
const vertex = new THREE.Vector3();
const color = new THREE.Color();
init();
animate();
function init() {
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.y = 10;
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xffffff );
scene.fog = new THREE.Fog( 0xffffff, 0, 750 );
const light = new THREE.HemisphereLight( 0xeeeeff, 0x777788, 2.5 );
light.position.set( 0.5, 1, 0.75 );
scene.add( light );
controls = new PointerLockControls( camera, document.body );
const blocker = document.getElementById( 'blocker' );
const instructions = document.getElementById( 'instructions' );
instructions.addEventListener( 'click', function () {
controls.lock();
} );
controls.addEventListener( 'lock', function () {
instructions.style.display = 'none';
blocker.style.display = 'none';
} );
controls.addEventListener( 'unlock', function () {
blocker.style.display = 'block';
instructions.style.display = '';
} );
scene.add( controls.getObject() );
const onKeyDown = function ( event ) {
switch ( event.code ) {
case 'ArrowUp':
case 'KeyW':
moveForward = true;
break;
case 'ArrowLeft':
case 'KeyA':
moveLeft = true;
break;
case 'ArrowDown':
case 'KeyS':
moveBackward = true;
break;
case 'ArrowRight':
case 'KeyD':
moveRight = true;
break;
case 'Space':
if ( canJump === true ) velocity.y += 350;
canJump = false;
break;
}
};
const onKeyUp = function ( event ) {
switch ( event.code ) {
case 'ArrowUp':
case 'KeyW':
moveForward = false;
break;
case 'ArrowLeft':
case 'KeyA':
moveLeft = false;
break;
case 'ArrowDown':
case 'KeyS':
moveBackward = false;
break;
case 'ArrowRight':
case 'KeyD':
moveRight = false;
break;
}
};
document.addEventListener( 'keydown', onKeyDown );
document.addEventListener( 'keyup', onKeyUp );
raycaster = new THREE.Raycaster( new THREE.Vector3(), new THREE.Vector3( 0, - 1, 0 ), 0, 10 );
// floor
let floorGeometry = new THREE.PlaneGeometry( 2000, 2000, 100, 100 );
floorGeometry.rotateX( - Math.PI / 2 );
// vertex displacement
let position = floorGeometry.attributes.position;
for ( let i = 0, l = position.count; i < l; i ++ ) {
vertex.fromBufferAttribute( position, i );
vertex.x += Math.random() * 20 - 10;
vertex.y += Math.random() * 2;
vertex.z += Math.random() * 20 - 10;
position.setXYZ( i, vertex.x, vertex.y, vertex.z );
}
floorGeometry = floorGeometry.toNonIndexed(); // ensure each face has unique vertices
position = floorGeometry.attributes.position;
const colorsFloor = [];
for ( let i = 0, l = position.count; i < l; i ++ ) {
color.setHSL( Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace );
colorsFloor.push( color.r, color.g, color.b );
}
floorGeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colorsFloor, 3 ) );
const floorMaterial = new THREE.MeshBasicMaterial( { vertexColors: true } );
const floor = new THREE.Mesh( floorGeometry, floorMaterial );
scene.add( floor );
// objects
const boxGeometry = new THREE.BoxGeometry( 20, 20, 20 ).toNonIndexed();
position = boxGeometry.attributes.position;
const colorsBox = [];
for ( let i = 0, l = position.count; i < l; i ++ ) {
color.setHSL( Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace );
colorsBox.push( color.r, color.g, color.b );
}
boxGeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colorsBox, 3 ) );
for ( let i = 0; i < 500; i ++ ) {
const boxMaterial = new THREE.MeshPhongMaterial( { specular: 0xffffff, flatShading: true, vertexColors: true } );
boxMaterial.color.setHSL( Math.random() * 0.2 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace );
const box = new THREE.Mesh( boxGeometry, boxMaterial );
box.position.x = Math.floor( Math.random() * 20 - 10 ) * 20;
box.position.y = Math.floor( Math.random() * 20 ) * 20 + 10;
box.position.z = Math.floor( Math.random() * 20 - 10 ) * 20;
scene.add( box );
objects.push( box );
}
//
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
//
window.addEventListener( 'resize', onWindowResize );
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
const time = performance.now();
if ( controls.isLocked === true ) {
raycaster.ray.origin.copy( controls.getObject().position );
raycaster.ray.origin.y -= 10;
const intersections = raycaster.intersectObjects( objects, false );
const onObject = intersections.length > 0;
const delta = ( time - prevTime ) / 1000;
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass
direction.z = Number( moveForward ) - Number( moveBackward );
direction.x = Number( moveRight ) - Number( moveLeft );
direction.normalize(); // this ensures consistent movements in all directions
if ( moveForward || moveBackward ) velocity.z -= direction.z * 400.0 * delta;
if ( moveLeft || moveRight ) velocity.x -= direction.x * 400.0 * delta;
if ( onObject === true ) {
velocity.y = Math.max( 0, velocity.y );
canJump = true;
}
controls.moveRight( - velocity.x * delta );
controls.moveForward( - velocity.z * delta );
controls.getObject().position.y += ( velocity.y * delta ); // new behavior
if ( controls.getObject().position.y < 10 ) {
velocity.y = 0;
controls.getObject().position.y = 10;
canJump = true;
}
}
prevTime = time;
renderer.render( scene, camera );
}
</script>
</body>
</html>