原文参考我的公众号文章 threejs 之控制 3D 物体移动
实现点哪儿,物体移动到哪儿,且正面朝向点击位置。(看演示图:gif录制fps只有8,页面正常跑起来时流畅的)!
1.鼠标控制移动、转向
控制器选择
这里是基于OrbitControls
轨道控制器实现,并通过设置最大控制距离controls.minDistance = controls.maxDistance = 20;
和controls.target.copy(cube.position)
实时更新控制器位置,来实现物体视角跟随。
未选择以下两种控制器
PointerLockControls
指针锁定控制器无鼠标,主要是用于键盘操纵移动;
FirstPersonControls
第一人称控制器,适合第一人称游戏,鼠标+键盘操纵;
点击移动需要解决的两个问题
- 更新物体位置
- 更新物体朝向
通过 Raycaster 获取需要移动的目标位置,解决 1
// 鼠标事件,射线碰撞检测获取目标点空间坐标
let raycaster = new THREE.Raycaster();
let mouseVector = new THREE.Vector3();
let MTCtrl = new MoveAndTurnControl(gsap);
function onMouseDown(evt) {
// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
mouseVector.x = (evt.clientX / width) * 2 - 1; //-1,1
mouseVector.y = -((evt.clientY / height) * 2 - 1); //-1,1
mouseVector.z = 1;
// 通过摄像机和鼠标位置更新射线(将平面坐标转为世界坐标)
raycaster.setFromCamera(mouseVector, camera);
// 计算物体和射线的焦点
let raycasters = raycaster.intersectObjects(scene.children, false);
if (raycasters.length > 0) {
let intersectObj = raycasters[0];
let { point, distance } = intersectObj;
MTCtrl.go({
target: cube,
destPosition: point,
onStart: () => {
controls.enabled = false;
},
onComplete: () => {
controls.enabled = true;
},
});
}
}
window.addEventListener("mousedown", onMouseDown, false);
通过四元数解决物体朝向问题,解决 2
/**
* animate:gsap动画库
*/
class MoveAndTurnControl {
constructor(animate) {
this.animate = animate;
this.isMoving = false;
this.createTL();
}
/**
* 更新位置和朝向
* @param {*} param0
* param0.target 被控制的物体对象 <模型 | object3D | group | mesh对象>
* param0.destPosition 被控物体要移动到的三位坐标 <THREE.Vector3>
* param0.speed 移动速度
* param0.turnSpeed 物体转向速度
* param0.fixedY <Number> 设置被控物体的Y轴高度,0代表不控制,则物体的Y会等于destPosition.y + offsetY
* param0.offsetY <Number> Y轴偏移量,fixedY==0时有效
* param0.onStart 动作开始时回调
* param0.onUpdate 动作执行过程中回调
* param0.onComplete 动作结束时回调
*/
go({
target,
destPosition,
speed = 5,
turnSpeed = 0.1,
fixedY = 0,
offsetY = 0.5,
onStart,
onUpdate,
onComplete,
}) {
// 如果正在移动,结束当前动画,从新定义新的timeline
if (this.isMoving) {
this.timeline.kill();
this.createTL();
}
let targetPosition = target.position.clone();
let offsetAngle = 0; // 2 * Math.PI 等于 0 //目标移动时的朝向偏移
console.log("移动速度:", speed);
// 计算物体移动的距离
let moveDistance = Math.sqrt(
Math.pow(destPosition.x - targetPosition.x, 2) +
Math.pow(destPosition.z - targetPosition.z, 2)
);
console.log("移动距离:", moveDistance);
let t = moveDistance / speed;
console.log("移动时间:", moveDistance);
// 以下代码在多段路径时可重复执行
let mtx = new THREE.Matrix4(); //创建一个4维矩阵
mtx.lookAt(targetPosition, destPosition, target.up); // 设置朝向
mtx.multiply(
new THREE.Matrix4().makeRotationFromEuler(
new THREE.Euler(0, offsetAngle, 0)
)
);
let toTurn = new THREE.Quaternion().setFromRotationMatrix(mtx); // 计算出需要进行旋转的四元数值
// 动画:移动位置+旋转朝向
onStart && onStart();
this.isMoving = true;
this.timeline.to(target.position, {
duration: t,
x: destPosition.x,
y: fixedY == 0 ? fixedY : destPosition.y + offsetY,
z: destPosition.z,
onUpdate: () => {
// 边移动边旋转
target.quaternion.slerp(toTurn, turnSpeed);
onUpdate && onUpdate(target.position, toTurn);
},
onComplete: () => {
this.isMoving = false;
onComplete && onComplete(target);
},
});
}
createTL() {
this.timeline = this.animate.timeline();
}
}
// 使用
let MTCtrl = new MoveAndTurnControl(gsap);
MTCtrl.go({
target: cube,
destPosition: clickPoint,
onStart: () => {
controls.enabled = false;
},
onComplete: () => {
controls.enabled = true;
},
});
对于更新物体朝向个人认为是个难点,一开始想了很多方式,在完整代码中能够体现我的尝试。
最后在网上找到了一个运行起来比较完美的解决方案「Quaternion-四元数」。
四元数这个东西比较复杂,对于刚入门 threejs 的我来说暂不深入了解,只要知道它能够更好的更新物体在 3D 世界中的旋转状态,比 Eluar 角好。
最后我封装了一个控制类MoveAndTurnControl
,提供了多个可控参数和详细的参数说明,以下是完整代码。
点击移动-完整代码
<template>
<div class="container" ref="container"></div>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import gsap from "gsap";
let controls = null;
let container = ref(null);
const width = window.innerWidth;
const height = window.innerHeight;
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const cameraY = 15;
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, cameraY, 10);
camera.lookAt(0, 0, 0);
// 创建渲染器
const renderer = new THREE.WebGL1Renderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor("#262837");
// 环境光
const ambientLight = new THREE.AmbientLight("#ffffff", 0.5);
scene.add(ambientLight);
// 月光
const moonLight = new THREE.DirectionalLight("#f5f5f5", 0.5);
moonLight.position.set(2, 7, -4);
scene.add(moonLight);
// 地板
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(50, 50),
new THREE.MeshStandardMaterial({ color: "#ffffff", side: THREE.DoubleSide })
);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.01;
scene.add(floor);
// 球体
const ball = new THREE.Mesh(
new THREE.SphereGeometry(2, 32, 32),
new THREE.MeshBasicMaterial({ color: "#777777" })
);
ball.position.set(4, 5, 0);
scene.add(ball);
// 添加立方体
let backMaterial = new THREE.MeshPhysicalMaterial({
map: new THREE.TextureLoader().load("/images/texture/fty_logo.jpg"),
side: THREE.DoubleSide,
});
let plainMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.4,
});
const boxMaps = [
plainMaterial,
plainMaterial,
plainMaterial,
plainMaterial,
plainMaterial,
backMaterial,
]; // 创建纹理数组,给某个面特殊处理,方便观察移动朝向是否正确
const cube = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), boxMaps);
cube.position.set(0, 0.5, 0);
scene.add(cube);
/**
* animate:gsap动画库
*/
class MoveAndTurnControl {
constructor(animate) {
this.animate = animate;
this.isMoving = false;
this.createTL();
}
/**
* 更新位置和朝向
* @param {*} param0
* param0.target 被控制的物体对象 <模型 | object3D | group | mesh对象>
* param0.destPosition 被控物体要移动到的三位坐标 <THREE.Vector3>
* param0.speed 移动速度
* param0.turnSpeed 物体转向速度
* param0.fixedY <Number> 设置被控物体的Y轴高度,0代表不控制,则物体的Y会等于destPosition.y + offsetY
* param0.offsetY <Number> Y轴偏移量,fixedY==0时有效
* param0.onStart 动作开始时回调
* param0.onUpdate 动作执行过程中回调
* param0.onComplete 动作结束时回调
*/
go({
target,
destPosition,
speed = 5,
turnSpeed = 0.1,
fixedY = 0,
offsetY = 0.5,
onStart,
onUpdate,
onComplete,
}) {
// 如果正在移动,结束当前动画,从新定义新的timeline
if (this.isMoving) {
this.timeline.kill();
this.createTL();
}
let targetPosition = target.position.clone();
let offsetAngle = 0; // 2 * Math.PI 等于 0 //目标移动时的朝向偏移
console.log("移动速度:", speed);
// 计算物体移动的距离
let moveDistance = Math.sqrt(
Math.pow(destPosition.x - targetPosition.x, 2) +
Math.pow(destPosition.z - targetPosition.z, 2)
);
console.log("移动距离:", moveDistance);
let t = moveDistance / speed;
console.log("移动时间:", moveDistance);
let mtx = new THREE.Matrix4(); //创建一个4维矩阵
mtx.lookAt(targetPosition, destPosition, target.up); // 设置朝向
mtx.multiply(
new THREE.Matrix4().makeRotationFromEuler(
new THREE.Euler(0, offsetAngle, 0)
)
);
let toTurn = new THREE.Quaternion().setFromRotationMatrix(mtx); // 计算出需要进行旋转的四元数值
// 动画:移动位置+旋转朝向
onStart && onStart();
this.isMoving = true;
this.timeline.to(target.position, {
duration: t,
x: destPosition.x,
y: fixedY == 0 ? fixedY : destPosition.y + offsetY,
z: destPosition.z,
onUpdate: () => {
// 边移动边旋转
target.quaternion.slerp(toTurn, turnSpeed);
onUpdate && onUpdate(target.position, toTurn);
},
onComplete: () => {
this.isMoving = false;
onComplete && onComplete(target);
},
});
}
createTL() {
this.timeline = this.animate.timeline();
}
}
// 鼠标事件,射线碰撞检测获取目标点空间坐标
let raycaster = new THREE.Raycaster();
let mouseVector = new THREE.Vector3();
let MTCtrl = new MoveAndTurnControl(gsap);
function onMouseDown(evt) {
// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
mouseVector.x = (evt.clientX / width) * 2 - 1; //-1,1
mouseVector.y = -((evt.clientY / height) * 2 - 1); //-1,1
mouseVector.z = 1;
// 通过摄像机和鼠标位置更新射线(将平面坐标转为世界坐标)
raycaster.setFromCamera(mouseVector, camera);
// 计算物体和射线的焦点
let raycasters = raycaster.intersectObjects(scene.children, false);
if (raycasters.length > 0) {
let intersectObj = raycasters[0];
let { point, distance } = intersectObj;
// 方法1:controls.getAzimuthalAngle()获取控制器的水平旋转
// distance = Math.min(intersectObj.distance, 10);
// gsap.timeline().to(cube.position, {
// duration: distance/20,
// x: intersectObj.point.x,
// // y: intersectObj.point.y,
// z: intersectObj.point.z,
// onComplete: () => {
// console.log(controls.getAzimuthalAngle())
// cube.rotation.y = controls.getAzimuthalAngle()
// }
// })
// 方法2:根据物体目标点的x和z计算tan
// let turnAngle = Math.atan(point.x / point.z)+Math.PI;
// console.log('turnAngle:', turnAngle)
// gsap.timeline().to(cube.rotation, {
// duration: 0.3,
// // y: controls.getAzimuthalAngle(),
// y: turnAngle,
// onComplete: () => {
// gsap.timeline().to(cube.position, {
// duration: t,
// x: intersectObj.point.x,
// // y: intersectObj.point.y,
// z: intersectObj.point.z,
// })
// }
// })
// return
// 方法3:计算物体坐标和目标点坐标的空间向量夹角
// let v1 = new THREE.Vector3(cube.position.x, cube.position.y, cube.position.z)
// let v2 = new THREE.Vector3(point.x, point.y, point.z)
// let turnAngle = v1.cross(v2)
// console.log(turnAngle)
// gsap.timeline().to(cube.rotation, {
// duration: 0.3,
// // x: v1.x,
// y: v1.y,
// // z: v1.z,
// })
// return
// 方法4:四元数
MTCtrl.go({
target: cube,
destPosition: point,
onStart: () => {
controls.enabled = false;
},
onComplete: () => {
controls.enabled = true;
},
});
}
}
window.addEventListener("mousedown", onMouseDown, false);
function animate() {
camera.position.y = cameraY; // 视角高度锁定
controls.update(); // 更新控制器
controls.target.copy(cube.position); //(物体视角跟随)
renderer.render(scene, camera);
window.requestAnimationFrame(animate);
}
const reRender = () => {
// 修改相机的参数,宽高比
camera.aspect = window.innerWidth / window.innerHeight;
// 更新投影的变换矩阵
camera.updateProjectionMatrix();
// 重新设置渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
};
// 挂载之后获取dom
onMounted(() => {
// 添加轨道控制器,并锁定视角到被控物体上
controls = new OrbitControls(camera, container.value);
controls.enableDamping = true;
controls.enablePan = false;
controls.enableRotate = false;
controls.maxDistance = 20;
controls.minDistance = 20;
container.value.appendChild(renderer.domElement);
animate();
window.addEventListener("resize", () => reRender);
});
onUnmounted(() => {
window.removeEventListener("resize", reRender);
});
</script>
<style lang="less" scoped>
.container {
width: 100vw;
height: 100vh;
border: solid 4px hsla(160, 100%, 37%, 1);
}
</style>
2.【WASD】键盘控制移动、转向
let isUp = false,
isDown = false,
isLeft = false,
isRight = false,
isJump = false;
// 键盘事件
window.addEventListener("keydown", (e) => {
console.log(e.code);
switch (e.code) {
case "KeyW":
console.log("前进");
isUp = true;
break;
case "KeyS":
console.log("后退");
isDown = true;
break;
case "KeyA":
console.log("左转");
isLeft = true;
break;
case "KeyD":
console.log("右转");
isRight = true;
break;
case "Space":
console.log("跳跃");
cube.translateY(2);
isJump = true;
break;
}
});
window.addEventListener("keyup", (e) => {
console.log(e.code);
switch (e.code) {
case "KeyW":
isUp = false;
break;
case "KeyS":
isDown = false;
break;
case "KeyA":
isLeft = false;
break;
case "KeyD":
isRight = false;
break;
case "Space":
isJump = false;
cube.position.set(0, 0.5, 0);
// cube.rotation.set(0, 0, 0)
cube.quaternion.set(0, 0, 0, 0);
robotModel.position.set(0, 0.5, 0);
// robotModel.rotation.set(0, 0, 0)
robotModel.quaternion.set(0, 0, 0, 0);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);
break;
}
});
const objMove = () => {
if (isUp) {
cube.translateZ(-0.1); //不用.position.z,无法模拟真实效果
}
if (isDown) {
cube.translateZ(+0.1);
}
if (isLeft) {
cube.rotateY(THREE.MathUtils.degToRad(1)); //不用.position.y,无法模拟真实效果
}
if (isRight) {
cube.rotateY(-THREE.MathUtils.degToRad(1));
}
};
function animate() {
objMove();
renderer.render(scene, camera);
window.requestAnimationFrame(animate);
}
animate();
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。
在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。
本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。
除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。
在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!