使用threejs实现的场景的创建,模型加载,移动,碰撞检测
为了让大家更容易看明白,这篇文章尽量更多的文字说明
上篇文章地址:(教你Vue3+Vite+threejs 创建3D场景,实现移动跳跃碰撞检测(1) #掘金文章# juejin.cn/post/718138…)
上篇实现了场景模型的加载,以及移动
实现方法:通过计算player的速度(vector3),以及时间片段,计算在每个时间片段的位移距离,来控制player在每一帧的位移,同时控制摄像机的位置与player位置同步,
// 更新位置
function updatePlayer(deltaTime) {
let damping = Math.exp(- 4 * deltaTime) - 1;
player.velocity.addScaledVector(player.velocity, damping);
// console.log(player.velocity);
const deltaPosition = player.velocity.clone().multiplyScalar(deltaTime);
player.geometry.translate(deltaPosition);
camera.position.copy(player.geometry.end);
}
这篇主要实现碰撞检测
第一步:跳跃的实现
我们需要一个变量playerOnFloor,来判断player是否在地面上 我们需要一个常量GEAVITY,描述引力大小
let playerOnFloor = false
let GRAVITY = 30
在函数handleControls里面增加空格事件
速度根据playerOnFloor重新判断
以及,增加空格跳跃事件,如果在地上,则摁下空格添加一个Y轴初速度
// 根据摁下键盘控制
const speedDelta = deltaTime * (playerOnFloor ? 25 : 8);
if (playerOnFloor) {
if (keyStates['Space']) {
player.velocity.y = 15;
}
}
这里结束已经可以实现跳跃,可以把playerOnFloor改成true测试一下,发现是有效的,但是跳起后没有落地,因为我们还没有实现重力效果。 看完效果我们把playerOnFloor改成false
第二步:重力效果的实现
我们在函数updatePlayer中实现重力效果
如果 playerOnFloor == false,给player在Y轴的速度递减
if (!playerOnFloor) {
player.velocity.y -= GRAVITY*deltaTime
damping *= 0.1
}
至此,重力效果也实现了,是不是很简单
第三步:碰撞检测的实现
碰撞检测算是一个难点,这里使用的是octree(八叉树)来检测碰撞,所以我们需要在全局引入Octree。并且在模型加载完成之后我们就可以调用worldOctree.fromGraphNode(gltf.scene) 并且添加OctreeHeiper
至于Octree是如何做碰撞检测的,为什么可以,原理是什么? 我也不懂,百度上的文章少之又少,只知道是一种数据结构。
- 引入模型的时候,我们就可以调用八叉树
import { Octree } from 'three/examples/jsm/math/Octree';
import { OctreeHelper } from 'three/examples/jsm//helpers/OctreeHelper.js';
// 八叉树
const worldOctree = new Octree()
function loadModel() {
const loader = new GLTFLoader()
loader.load('./models/collision-world.glb', gltf => {
scene.add(gltf.scene)
// Octree
worldOctree.fromGraphNode(gltf.scene)
// OctreeHelper
const helper = new OctreeHelper(worldOctree);
helper.visible = true;
scene.add(helper);
gltf.scene.traverse(child => {
if (child.isMesh) {
// 阴影效果
child.castShadow = true;
child.receiveShadow = true;
}
});
})
我们可以通过OctreeHelper 看到效果 。
- 在 函数updatePlayer 中做碰撞检测
function updatePlayer(deltaTime) {
let damping = Math.exp(- 4 * deltaTime) - 1;
// 如果人物不在地面上,人物Y轴速度根据GRAVITY来递减
if (!playerOnFloor) {
playerVelocity.y -= GRAVITY * deltaTime;
// small air resistance
damping *= 0.1;
}
playerVelocity.addScaledVector(playerVelocity, damping);
const deltaPosition = playerVelocity.clone().multiplyScalar(deltaTime);
playerCollider.translate(deltaPosition);
playerCollisions(); // 碰撞检测的位置也很重要,一定要在player移动之后检查
persCamera.position.copy(playerCollider.end);
}
playerCollisions()
const result = worldOctree.capsuleIntersect(playerCollider)
如果没有碰撞,函数返回false , 如果碰撞了,函数返回一个对象,包含碰撞点信息,根据信息重新计算player的速度
function playerCollision() {
// 碰撞检测
const result = worldOctree.capsuleIntersect(player.geometry)
playerOnFloor = false
// 有碰撞,result对象包含了碰撞点的信息
if (result) {
playerOnFloor = result.normal.y > 0
if (!playerOnFloor) {
// 重新计算 player的速度向量
player.velocity.addScaledVector(result.normal, -result.normal.dot(player.velocity))
}
player.geometry.translate(result.normal.multiplyScalar(result.depth));
}
}
最后,我们终于实现了官方文档的部分效果,模型加载,移动,跳跃,碰撞检测
完整代码
<template>
<div id="container"></div>
</template>
<script setup>
import { onMounted } from '@vue/runtime-core';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { Capsule } from 'three/examples/jsm/math/Capsule'
import { Octree } from 'three/examples/jsm/math/Octree';
import { OctreeHelper } from 'three/examples/jsm//helpers/OctreeHelper.js';
import * as THREE from 'three'
let renderer, camera, scene, container;
let clock;
let keyStates = {}
let GRAVITY = 30
let playerOnFloor = true
const worldOctree = new Octree()
let player = {
geometry: new Capsule(new THREE.Vector3(0, 0.35, 0), new THREE.Vector3(0, 1, 0), 0.35),
velocity: new THREE.Vector3(),
direction: new THREE.Vector3()
}
onMounted(() => {
init()
})
function init() {
// renderer
container = document.getElementById('container');
renderer = new THREE.WebGLRenderer({ antialias: true }) // antialias:true 开启抗锯齿
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.shadowMap.enable = true
renderer.shadowMap.type = THREE.VSMShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
container.appendChild(renderer.domElement);
// camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 0, 0)
camera.rotation.order = 'YXZ'
// scene
scene = new THREE.Scene()
scene.background = new THREE.Color(0x88ccee)
scene.fog = new THREE.Fog(0x88ccee, 0, 50)
// light
const fillLight1 = new THREE.HemisphereLight(0x4488bb, 0x002244, 0.5);
fillLight1.position.set(2, 1, 1);
scene.add(fillLight1);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(- 5, 25, - 1);
directionalLight.castShadow = true;
directionalLight.shadow.camera.near = 0.01;
directionalLight.shadow.camera.far = 500;
directionalLight.shadow.camera.right = 30;
directionalLight.shadow.camera.left = - 30;
directionalLight.shadow.camera.top = 30;
directionalLight.shadow.camera.bottom = - 30;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.shadow.radius = 4;
directionalLight.shadow.bias = - 0.00006;
scene.add(directionalLight);
// clock
clock = new THREE.Clock()
window.addEventListener("resize", onResize)
loadModel()
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
}
// 加载模型
function loadModel() {
const loader = new GLTFLoader()
loader.load('./models/collision-world.glb', gltf => {
scene.add(gltf.scene)
// Octree
worldOctree.fromGraphNode(gltf.scene)
// OctreeHelper
const helper = new OctreeHelper(worldOctree);
helper.visible = false;
scene.add(helper);
gltf.scene.traverse(child => {
if (child.isMesh) {
// 阴影效果
child.castShadow = true;
child.receiveShadow = true;
}
});
})
animate()
}
// 渲染帧
function animate() {
const deltaTime = Math.min(0.05, clock.getDelta())
// 在控制之后,需要更新player的位置
handleControls(deltaTime)
updatePlayer(deltaTime)
renderer.render(scene, camera)
requestAnimationFrame(animate)
}
// 根据摁下键盘控制
function handleControls(deltaTime) {
const speedDelta = deltaTime * (playerOnFloor ? 25 : 8);
if (keyStates['KeyW']) {
// 摁下W,改变水平方向的速度
player.velocity.add(getForwardVector().multiplyScalar(speedDelta));
}
if (keyStates['KeyS']) {
player.velocity.add(getForwardVector().multiplyScalar(- speedDelta));
}
if (keyStates['KeyA']) {
player.velocity.add(getSideVector().multiplyScalar(- speedDelta));
}
if (keyStates['KeyD']) {
player.velocity.add(getSideVector().multiplyScalar(speedDelta));
}
if (playerOnFloor) {
if (keyStates['Space']) {
player.velocity.y = 15;
}
}
}
// 更新位置
function updatePlayer(deltaTime) {
let damping = Math.exp(- 4 * deltaTime) - 1;
if (!playerOnFloor) {
player.velocity.y -= GRAVITY * deltaTime
damping *= 0.1
}
player.velocity.addScaledVector(player.velocity, damping);
// console.log(player.velocity);
const deltaPosition = player.velocity.clone().multiplyScalar(deltaTime);
player.geometry.translate(deltaPosition);
playerCollision()
camera.position.copy(player.geometry.end);
}
// 碰撞检测
function playerCollision() {
const result = worldOctree.capsuleIntersect(player.geometry)
playerOnFloor = false
if (result) {
playerOnFloor = result.normal.y > 0
if (!playerOnFloor) {
player.velocity.addScaledVector(result.normal, -result.normal.dot(player.velocity))
}
player.geometry.translate(result.normal.multiplyScalar(result.depth));
}
}
// 获得前进方向向量
function getForwardVector() {
camera.getWorldDirection(player.direction);
player.direction.y = 0;
// 转化为单位向量
player.direction.normalize();
return player.direction;
}
// 获得左右方向向量
function getSideVector() {
// Camera.getWorldDirection ( target : Vector3 ) : Vector3 调用该函数的结果将赋值给该Vector3对象。
camera.getWorldDirection(player.direction);
player.direction.y = 0;
// 将该向量转换为单位向量(unit vector), 也就是说,将该向量的方向设置为和原向量相同,但是其长度(length)为1。
player.direction.normalize();
player.direction.cross(camera.up);
return player.direction;
}
document.addEventListener('mousedown', e => {
document.body.requestPointerLock()
})
document.addEventListener('mousemove', e => {
// 当鼠标在锁定状态时
if (document.pointerLockElement === document.body) {
camera.rotation.y -= e.movementX / 500
camera.rotation.x -= e.movementY / 500
}
})
document.addEventListener('keydown', e => {
keyStates[e.code] = true
})
document.addEventListener('keyup', e => {
keyStates[e.code] = false
})
</script>
<style>
</style>