人物漫游
约 2044 字大约 7 分钟
2025-04-11
效果
场景中随机生成若干个立方体,人物在场景中漫游,可以通过 wasd 控制人物移动,通过鼠标控制视角。(未做碰撞检测)
知识点
3D 模型加载
使用动画混合器播放模型动画
使用三维向量计算物体的速度
使用相机向量计算物体朝向
实现步骤
封装 Threejs
由于后期需要频繁使用 Threejs
,因此封装一个 Threejs Hook
,方便后续使用。
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
//根据需要加载各种模型
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
//初始化3D
export function useThreejs(container) {
let scene, camera, renderer, controls;
const init3D = () => {
//创建场景
scene = new THREE.Scene();
//创建相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
//创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true; //开启阴影
container.appendChild(renderer.domElement);
//创建控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
//设置平行光
const light = new THREE.DirectionalLight(0xffffff, 5);
light.position.set(5, 8, 0);
light.castShadow = true; //开启阴影
scene.add(light);
//设置环境光
const ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);
//自适应窗口大小
const resize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener("resize", resize);
const clean3D = () => {
window.removeEventListener("resize", resize);
renderer.dispose();
controls.dispose();
};
return {
scene,
camera,
renderer,
clean3D,
THREE,
};
};
return init3D();
}
//加载FBX模型
export function useLoadFBX(url) {
const loader = new FBXLoader();
return new Promise((resolve, reject) => {
loader.load(url, resolve, undefined, reject);
});
}
//加载GLTF模型
export function useLoadGLTF(url) {
const loader = new GLTFLoader();
return new Promise((resolve, reject) => {
loader.load(url, resolve, undefined, reject);
});
}
创建场景
<template>
<div class="people-run" ref="container"></div>
</template>
import { useThreejs } from "@/hooks/threejs";
import { onMounted, onUnmounted, ref } from "vue";
const container = ref(null);
//生成随机数
const getRandomInt = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
onMounted(() => {
//初始化3D
let { clean3D, scene, THREE, camera, renderer } = useThreejs(container.value);
//创建场景
scene.background = new THREE.Color(0x87ceff);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);
//创建地面
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(500, 500),
new THREE.MeshLambertMaterial({ color: 0xffffff, side: THREE.DoubleSide })
);
ground.receiveShadow = true; //开启阴影
ground.rotateX(-Math.PI / 2);
scene.add(ground);
//创建立方体盒子
for (let i = 0; i < 500; i++) {
let width = getRandomInt(1, 3);
let height = getRandomInt(1, 3);
let depth = getRandomInt(1, 3);
let x = getRandomInt(-200, 200);
let z = getRandomInt(-200, 200);
let y = height / 2;
let box = new THREE.Mesh(
new THREE.BoxGeometry(width, height, depth),
new THREE.MeshLambertMaterial({ color: 0xff0000 })
);
box.position.set(x, y, z);
scene.add(box);
}
//动画渲染
const animate = () => {
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
requestAnimationFrame(animate);
onUnmounted(() => {
clean3D();
});
});
模型加载
//导入封装的加载器
import { useThreejs, useLoadGLTF } from "@/hooks/threejs";
const loadModel = async () => {
let gltf = await useLoadGLTF("/models/peopleRun.glb");
//开启投影
gltf.scene.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
}
});
scene.add(gltf.scene);
};
loadModel();
编写动作执行指令
思路:
创建动作执行相关变量
minxer
、player
是全局都需要用到的动画混合器和模型;actionObj
是动作对象,用于存储所有动作,通过name
获取对应的动作;currentAct
、previousAct
是当前动作和上一个动作;
创建键盘事件监听
创建
keypess
对象,用于储存wasd
方向的开关。监听键盘按下和松开事件,按下时将对应键值设置为true
,松开时设置为false
。创建动作切换函数
封装
switchAnimation
函数,用于切换动作。根据keyPress
对象中的键值判断当前动作,如果和上一个动作不同,则重新设置人物动作,并播放动作。创建动画渲染函数
创建
updateAnimation
函数,用于更新人物动作。根据keyPress
对象中的键值判断当前动作,并调用switchAnimation
函数切换动作。
//导入动画混合器
import { AnimationMixer } from "three";
let minxer; //动画混合器
let actionObj = {}; //动作对象
let currentAct = null; //当前动作
let previousAct = null; //上一个动作
let player; //人物
const loadModel = async () => {
let gltf = await useLoadGLTF("/models/peopleRun.glb");
//开启投影
gltf.scene.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
}
});
scene.add(gltf.scene);
player = gltf.scene;
const animations = gltf.animations; // 模型的动画
minxer = new AnimationMixer(gltf.scene); // 创建动画混合器
//创建动作对象便于控制
animations.forEach((clip) => {
actionObj[clip.name] = minxer.clipAction(clip);
});
//默认动作
currentAct = actionObj["Idle"];
//播放动作
currentAct.play();
};
loadModel();
//键盘事件
const keyPress = {};
//键盘按下
const keydown = (e) => {
const keycode = e.code.toLocaleLowerCase();
keyPress[keycode] = true;
};
window.addEventListener("keydown", keydown);
//键盘松开
const keyup = (e) => {
const keycode = e.code.toLocaleLowerCase();
keyPress[keycode] = false;
};
window.addEventListener("keyup", keyup);
//动画控制
const updateAnimation = () => {
if (
(keyPress["shiftleft"] && keyPress["keyw"]) ||
(keyPress["shiftleft"] && keyPress["keya"]) ||
(keyPress["shiftleft"] && keyPress["keyd"]) ||
(keyPress["shiftleft"] && keyPress["keys"])
) {
switchAnimation("Run");
} else if (keyPress["keyw"] || keyPress["keya"] || keyPress["keyd"] || keyPress["keys"]) {
switchAnimation("Walk");
} else {
switchAnimation("Idle");
}
};
//动画切换
const switchAnimation = (name, duration = 0.2) => {
//重新设置人物动作
previousAct = currentAct;
currentAct = actionObj[name];
//如果动作不同则切换
if (previousAct !== currentAct) {
previousAct.fadeOut(duration);
currentAct.reset().fadeIn(duration).play();
}
};
//动画渲染
const clock = new THREE.Clock();
const animate = () => {
const dt = clock.getDelta();
if (minxer) {
minxer.update(dt);
updateAnimation(); //更新模型动作
}
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
requestAnimationFrame(animate);
更新人物运动
思路:
创建相机朝向和人物朝向
创建
cameraDirector
和playerDirector
两个THREE.Vector3
对象,用于存储相机的方向和人物的方向。获取相机和人物的方向
使用
camera.getWorldDirection(cameraDirector)
和player.getWorldDirection(playerDirector)
获取相机的方向和人物的方向。判断按键状态
根据按键状态判断人物的运动方向,如果按下
w
键,则设置摄像机看向的位置,让人物看向摄像机看向的位置,并向前运动;如果按下s
键,则设置摄像机看向的位置,让人物看向摄像机看向的位置,并向后运动;如果按下a
键,则设置摄像机左方向向量,让人物看向摄像机看向的位置,并向左运动;如果按下d
键,则设置摄像机右方向向量,让人物看向摄像机看向的位置,并向右运动。更新人物位置
根据按键状态更新人物的位置,如果按下
w
键,则让人物位置加上摄像机方向向量;如果按下s
键,则让人物位置减去摄像机方向向量;如果按下a
键,则让人物位置减去摄像机左方向向量;如果按下d
键,则让人物位置加上摄像机右方向向量。
const loadModel = async () => {
let gltf = await useLoadGLTF("/models/peopleRun.glb");
//......//省略部分代码
//相机朝向
const cameraDirector = new THREE.Vector3();
camera.getWorldDirection(cameraDirector); //获取相机朝向
//人物朝向
const playerDirector = new THREE.Vector3();
player.getWorldDirection(playerDirector); //获取人物朝向
};
loadModel();
//更新人物运动
const updateLookAt = () => {
// 创建一个THREE.Vector3对象,用于存储相机的方向
const cameraDirector = new THREE.Vector3();
// 获取相机的方向,并将其存储在cameraDirector对象中
camera.getWorldDirection(cameraDirector);
// 创建一个THREE.Vector3对象,用于存储观察点的位置
const lookAtPosition = new THREE.Vector3();
// 创建一个三维向量,表示速度
const velocity = new THREE.Vector3(0, 0, 0.06);
// 获取速度向量的长度
const orignalLength = velocity.length();
//如果按下w键
if (keyPress["keyw"]) {
//设置摄像机看向的位置
lookAtPosition.set(
player.position.x - cameraDirector.x,
player.position.y,
player.position.z - cameraDirector.z
);
//让玩家看向摄像机看向的位置
player.lookAt(lookAtPosition);
//向前运动
cameraDirector.y = 0;
//归一化摄像机方向向量
cameraDirector.normalize();
//设置摄像机方向向量的长度
cameraDirector.setLength(orignalLength);
//让玩家位置加上摄像机方向向量
player.position.add(cameraDirector);
} else if (keyPress["keys"]) {
//设置摄像机看向的位置
lookAtPosition.set(
player.position.x + cameraDirector.x,
player.position.y,
player.position.z + cameraDirector.z
);
//让玩家看向摄像机看向的位置
player.lookAt(lookAtPosition);
//向后运动
cameraDirector.y = 0;
//归一化摄像机方向向量
cameraDirector.normalize();
//设置摄像机方向向量的长度
cameraDirector.setLength(orignalLength);
//让玩家位置减去摄像机方向向量
player.position.sub(cameraDirector);
} else if (keyPress["keya"]) {
//设置摄像机左方向向量
const leftDirector = new THREE.Vector3(-cameraDirector.z, 0, cameraDirector.x);
//设置摄像机看向的位置
lookAtPosition.set(
player.position.x - cameraDirector.z,
player.position.y,
player.position.z + cameraDirector.x
);
//让玩家看向摄像机看向的位置
player.lookAt(lookAtPosition);
//向左运动
//设置摄像机左方向向量的长度
leftDirector.setLength(orignalLength);
//让玩家位置减去摄像机左方向向量
player.position.sub(leftDirector);
} else if (keyPress["keyd"]) {
//设置摄像机右方向向量
const rightDirector = new THREE.Vector3(cameraDirector.z, 0, -cameraDirector.x);
//设置摄像机看向的位置
lookAtPosition.set(
player.position.x + cameraDirector.z,
player.position.y,
player.position.z - cameraDirector.x
);
//让玩家看向摄像机看向的位置
player.lookAt(lookAtPosition);
//向右运动
//设置摄像机右方向向量的长度
rightDirector.setLength(orignalLength);
//让玩家位置加上摄像机右方向向量的相反数
player.position.add(rightDirector.negate());
}
};
//动画渲染
const clock = new THREE.Clock();
const animate = () => {
//......//省略部分代码
updateLookAt(); //更新模型朝向
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
requestAnimationFrame(animate);