demo案例
PointerLockControls
是 Three.js 中用于处理鼠标锁定状态下的相机控制的类。它允许用户通过鼠标移动来控制相机的旋转方向。下面是它的详细讲解:
构造函数:
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
- 更新控制器状态。
示例:
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();
复制
示例代码解读
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); }
复制
更新逻辑
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); }
复制
完整源码
<!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>
复制
本内容来源于小豆包,想要更多内容请跳转小豆包 》