教你Vue3+Vite+threejs 创建3D场景,实现移动跳跃碰撞检测(1)
使用threejs实现场景的创建,模型加载,移动,碰撞检测
效果:官网示例效果
第一步:初始场景的建立(一些基础的代码,没有多余的解释)
<template>
<div id="container"></div>
</template>
<script setup>
import { onMounted } from '@vue/runtime-core';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import * as THREE from 'three'
let renderer, camera, scene, container;
let clock;
onMounted(() => {
init()
animate()
})
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(10, 10, 10)
camera.rotation.order = 'YXZ'
// scene
scene = new THREE.Scene()
scene.background = new THREE.Color(0x88ccee)
scene.fog = new THREE.Fog(0x88ccee, 0, 50)
scene.add(new THREE.GridHelper(10))
// 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()
// controls
const controls = new OrbitControls(camera, renderer.domElement)
window.addEventListener("resize", onResize)
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
}
// 渲染
function animate() {
renderer.render(scene, camera)
requestAnimationFrame(animate)
}
</script>
<style>
</style>
到这里已经可以在页面上出现3D场景
第二步:场景模型的加载
这里模型是.glb格式的,直接用GLTFLoader加载即可,如果模型被压缩过,需要使用DRACOLoader同时加载模型的配置文件
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
function loadModel() {
const loader = new GLTFLoader()
loader.load('./models/collision-world.glb', gltf => {
scene.add(gltf.scene)
gltf.scene.traverse(child => {
if (child.isMesh) {
// 阴影效果
child.castShadow = true;
child.receiveShadow = true;
}
});
})
animate()
}
在 init 中调用 loadModel ,已经成功在页面上渲染出模型

第三步:Player的初始化
这里的Player其实就是相机的位置,相机的视角即是我们的第一视角。
- 先把相机位置放在原点。
- controls 我们先注释一下,暂时不需要controls 了
camera.position.set(0, 0, 0)
创建player
// new Capsule(下球心(vector3),上球心(vector3),半径)
let player = {
geometry: new Capsule(new THREE.Vector3(0, 0.35, 0), new THREE.Vector3(0, 1, 0), 0.35),
velocity: new THREE.Vector3(),
position: new THREE.Vector3()
}

第四步:添加键盘鼠标事件
添加鼠标点击移动事件
// 鼠标摁下,锁定鼠标
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
}
})
这一步完成,我们的镜头已经可以随着鼠标的移动而改变方向了

添加键盘事件
我们用keyStates来保存键盘摁下的状态,在之后的animate函数中根据keyStates的状态来处理事件
let playerOnFloor = false
let keyStates = {}
document.addEventListener('keydown', e => {
keyStates[e.code] = true
})
document.addEventListener('keyup', e => {
keyStates[e.code] = false
})
第五步:根据keyStates控制状态
function handleControls(deltaTime) {
// 如果player在地面上,速度为25
const speedDelta = deltaTime * (playerOnFloor ? 25 : 8);
if (keyStates['KeyW']) {
// 摁下W,获取当前水平向量,与这个值相乘,获得player速度改变
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 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;
}
在animate函数中调用 handleControls() 和 updatePlayer()
function animate() {
const deltaTime = Math.min(0.05, clock.getDelta())
// 控制player移动
handleControls(deltaTime)
// 更新player的位置
updatePlayer(deltaTime)
renderer.render(scene, camera)
requestAnimationFrame(animate)
}
更新player和相机的position,完成移动
function updatePlayer(deltaTime) {
let damping = Math.exp(- 4 * deltaTime) - 1;
player.velocity.addScaledVector(player.velocity, damping);
// 位移距离
const deltaPosition = player.velocity.clone().multiplyScalar(deltaTime);
player.geometry.translate(deltaPosition);
// 相机的位置,拷贝player的位置
camera.position.copy(player.geometry.end);
}
此时player可以自由移动,但是还没有碰撞检测,没有跳跃
效果
先写这么多。后续再补,完整代码
<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 * as THREE from 'three'
let renderer, camera, scene, container;
let clock;
let playerOnFloor = false
let keyStates = {}
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)
scene.add(new THREE.GridHelper(10))
// 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)
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 * 8;
if (keyStates['KeyW']) {
// 摁下W,改变水平方向的向量, multiplyScalar 乘积
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;
player.velocity.addScaledVector(player.velocity, damping);
const deltaPosition = player.velocity.clone().multiplyScalar(deltaTime);
player.geometry.translate(deltaPosition);
camera.position.copy(player.geometry.end);
}
// 获得前进方向向量
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>
转载自:https://juejin.cn/post/7181380223349293114