携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
今天的例子是three.js指针锁定控制器的使用,用于第一人称漫游的实现,以下是演示gif
先写一个容器
一个放3d的容器THREE13
一个遮罩层blocker、instructions,里面显示一些提示信息,点击遮罩后,遮罩隐藏,可在3d视图上进行键盘鼠标互动。
<div id="THREE13"></div>
<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>
引入要用的指针锁定控制器
import { PointerLockControls } from "three/examples/jsm/controls/PointerLockControls.js";
然后一个mounted方法
mounted() {
this.initThreejs();
},
下面是initThreejs的代码
定义需要的变量
camera:相机
scene:场景
renderer:渲染器
controls:控制器
objects:存放场景中的物体
raycaster:光线投射,用于鼠标拾取
moveForward:前
moveBackward:后
moveLeft:左
moveRight:右
canJump:跳
performance.now():返回一个精确到毫秒的时间戳
velocity:移动的速度,每时间戳差移动的距离
direction:移动的方向
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();
init();
animate();
下面是init的代码
相机
透视相机:PerspectiveCamera(相机视锥体垂直视野角度,视锥体长宽比,视锥体近端面,视锥体远端面)
camera = new THREE.PerspectiveCamera(
75,
(window.innerWidth - 201) / 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);
光线
半球光:HemisphereLight(天空中发出光线的颜色, 地面发出光线的颜色,光照强度)
const light = new THREE.HemisphereLight(0xeeeeff, 0x777788, 0.75);
light.position.set(0.5, 1, 0.75);
scene.add(light);
指针锁定控制器
指针锁定控制器:PointerLockControls(渲染场景的相机,用于事件监听的html元素)
controls.lock():激活指针锁定
指针锁定被激活时触发事件,被锁定后instructions和blocker元素不显示
指针锁定退出时触发事件,退出后blocker和instructions显示
controls.getObject():获取相机
并将相机添加到场景
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());
键盘按键监听
w:前,a:左,s:后,d:右
const onKeyDown = function (event) {
switch (event.code) {
case "KeyW":
moveForward = true;
break;
case "KeyA":
moveLeft = true;
break;
case "KeyS":
moveBackward = true;
break;
case "KeyD":
moveRight = true;
break;
case "Space":
if (canJump === true) velocity.y += 350;
canJump = false;
break;
}
};
键盘松掉监听
const onKeyUp = function (event) {
switch (event.code) {
case "KeyW":
moveForward = false;
break;
case "KeyA":
moveLeft = false;
break;
case "KeyS":
moveBackward = false;
break;
case "KeyD":
moveRight = false;
break;
}
};
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);
光线投射
光线投射:Raycaster(光线投射的原点向量,光线投射的方向向量,返回的所有结果比near远,返回的所有结果都比far近)
raycaster = new THREE.Raycaster(
new THREE.Vector3(),
new THREE.Vector3(0, -1, 0),
0,
10
);
创建平面
平面:PlaneGeometry(宽,高,宽度分段数,高度分段数)
平面绕x轴旋转PI/2
做出地面的效果
let floorGeometry = new THREE.PlaneGeometry(2000, 2000, 100, 100);
floorGeometry.rotateX(-Math.PI / 2);
const floorMaterial = new THREE.MeshBasicMaterial({
color: new THREE.Color("green"),
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
scene.add(floor);
创建物体
用于具有镜面高光的光泽表面的材质:MeshPhongMaterial()
- specular:材质的高光颜色
- flatShading:定义材质是否使用平面着色进行渲染
创建500个正方体
const boxGeometry = new THREE.BoxGeometry(20, 20, 20).toNonIndexed();
for (let i = 0; i < 500; i++) {
const boxMaterial = new THREE.MeshPhongMaterial({
specular: 0xffffff,
flatShading: true,
color: new THREE.Color("skyblue"),
});
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 - 201, window.innerHeight);
document.getElementById("THREE13").appendChild(renderer.domElement);
下面是animate的代码:
performance.now():当前时间戳
target.copy():复制所传入属性到target上
moveForward:平行于xz平面,向前移动摄像机,有正负
moveRight:平行于xz平面,向侧面移动摄像机,有正负
function animate() {
requestAnimationFrame(animate);
const time = performance.now();
/**
* 如果指针锁定被激活
*/
if (controls.isLocked === true) {
raycaster.ray.origin.copy(controls.getObject().position); // 设置射线的原点位置为相机的位置
raycaster.ray.origin.y -= 10; // 原点的y一直变化
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;
direction.z = Number(moveForward) - Number(moveBackward); // 往前=1,往后=-1
direction.x = Number(moveRight) - Number(moveLeft); // 往右=1,往左=-1
direction.normalize(); // 确保了所有方向上的一致运动
if (moveForward || moveBackward) {
velocity.z -= direction.z * 400.0 * delta;
}
if (moveLeft || moveRight) {
velocity.x -= direction.x * 400.0 * delta;
}
velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass
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;
if (controls.getObject().position.y < 10) {
velocity.y = 0;
controls.getObject().position.y = 10;
canJump = true;
}
}
prevTime = time;
renderer.render(scene, camera);
}