当前位置: 首页 > article >正文

VUE2+THREE.JS 按照行动轨迹移动人物模型并相机视角跟随人物

按照行动轨迹移动人物模型并相机视角跟随人物

  • 1. 初始化加载模型
  • 2. 开始移动模型
  • 3. 人物模型启动
  • 4. 暂停模型移动
  • 5. 重置模型位置
  • 6. 切换区域动画
  • 7. 摄像机追踪模型
  • 8. 移动模型位置
  • 9.动画执行

人物按照上一篇博客所设定的关键点位置,匀速移动

在这里插入图片描述

1. 初始化加载模型

// 加载巡航人物模型 callback 动作完成的回调函数
initWalkPerson(callback) {
	fbxloader("walk").then((obj) => {
		obj.scale.set(2.5, 2.5, 2.5);
		obj.name = "person";
		person = obj;
		scene.add(obj);
		//有回调函数 就执行回调函数
		callback && callback();
	});
},

2. 开始移动模型

// 开始移动模型
startAnimation() {
	if (isAnimating) return this.elMessage("当前巡航已开始,请勿多次操作", "error");
	isAnimating = true;
	//说明模型已加载完成,无需重复加载,直接执行动画效果
	if (person) {
		this.personPositionStart();
	} else {
		//人物模型加载完毕后在执行
		this.initWalkPerson(() => {
			this.personPositionStart();
		});
	}
},

3. 人物模型启动

//人物动画启动
personPositionStart() {
	personMixer = new THREE.AnimationMixer(person);
	let AnimationAction = personMixer.clipAction(person.animations[0]);
	AnimationAction.play();

	person.position.set(pointArr[0]);
	scene.getObjectByName("path").material.visible = false; //隐藏行动轨迹动画
	scene.getObjectByName("person").visible = true;

	tweenHandlers = [];
	// 定义速度(单位:单位长度/秒)
	const speed = 300; // 你可以根据需要调整这个速度值
	let prevTween = null;
	let startPos = new THREE.Vector3(...pointArr[0]);

	for (let i = 1; i < pointArr.length; i++) {
		// 每次循环设置下一个目标点
		const endPos = new THREE.Vector3(...pointArr[i]);
		const newTween = this.createTween(startPos.clone(), endPos, speed);
		tweenHandlers.push(newTween);

		if (prevTween) {
			prevTween.chain(newTween);
		} else {
			// 如果是序列中的第一个tween,立即开始动画
			newTween.start();
		}

		// 将此tween存储为下一个tween的'prevTween'
		prevTween = newTween;

		// 更新起始点为当前结束点,为下一个循环准备
		startPos.copy(endPos);
	}

	// 开始第一个tween动画
	if (tweenHandlers.length > 0) {
		currentTween = tweenHandlers[0];
		currentTween.start();
		isAnimating = true;
	}
	// 在最后一个Tween结束后执行的动作
	prevTween.onComplete(() => {
		// 在动画被标记为完成时才重置位置
		this.resetPosition();
	});
},

4. 暂停模型移动

// 暂停模型移动
pauseAnimation() {
	if (!isAnimating) {
		this.elMessage("当前巡航未开始", "warning");
		return;
	}

	if (this.isPaused) {
		// 恢复摄像机状态
		camera.position.copy(savedCameraPosition);
		controls.target.copy(savedCameraTarget);
		controls.update();

		// 恢复动画
		tweenHandlers.forEach((tween) => tween.resume());
		personMixer.timeScale = 1;
		this.isPaused = false; //设置this.isPaused为false
		isAnimating = true;
		this.elMessage("动画已恢复", "success");

		this.updateCameraPosition(person, camera, new THREE.Vector3(0, 250, 200));
	} else {
		// 保存当前摄像机状态
		savedCameraPosition = camera.position.clone();
		savedCameraTarget = controls.target.clone();
		// 暂停动画
		tweenHandlers.forEach((tween) => tween.pause());
		personMixer.timeScale = 0;
		this.isPaused = true; //设置this.isPaused为true
		this.elMessage("动画已暂停", "success");
	}
},

5. 重置模型位置

// 重置模型位置
resetPosition() {
	isAnimating = false;
	this.pauseAnimation();
	// 将模型从场景中移除
	scene.getObjectByName("person").visible = false;
	// 清理动画混合器
	if (personMixer) {
		personMixer.uncacheClip(personMixer._actions[0]._clip);
		personMixer = null;
	}
	tweenHandlers.forEach((item) => item.stop());
	tweenHandlers = [];
	// 重置动画状态
	this.isPaused = false;
	this.tweenArea({ x: -5000, y: 7000, z: 16000 }, { x: 0, y: 0, z: 1 });
	//显示行动轨迹
	scene.getObjectByName("path").material.visible = true;
},

6. 切换区域动画

// 切换区域动画
tweenArea(Position, controlsTarget) {
	// 传递任意目标位置,从当前位置运动到目标位置
	const p1 = {
		// 定义相机位置是目标位置到中心点距离的2.2倍
		x: camera.position.x,
		y: camera.position.y,
		z: camera.position.z,
	};
	const p2 = {
		x: Position.x,
		y: Position.y,
		z: Position.z,
	};
	changeAreaTween = new TWEEN.Tween(p1).to(p2, 1200); // 第一段动画
	const update = function (object) {
		camera.rotation.y = (90 * Math.PI) / 180;
		camera.position.set(object.x, object.y, object.z);
		controls.target = new THREE.Vector3(controlsTarget.x, controlsTarget.y, controlsTarget.z);
		// camera.lookAt(lookAt); // 保证动画执行时,相机焦距在中心点
		controls.enabled = false;
		controls.update();
	};
	changeAreaTween.onUpdate(update);
	//  动画完成后的执行函数
	changeAreaTween.onComplete(() => {
		controls.enabled = true; // 执行完成后开启控制
	});
	changeAreaTween.easing(TWEEN.Easing.Quadratic.InOut);
	changeAreaTween.start();
},

7. 摄像机追踪模型

// 摄像机追踪模型
updateCameraPosition(model, camera, offset) {
	if (!this.isPaused && isAnimating) {
		// 添加条件判断
		const desiredPosition = new THREE.Vector3().copy(model.position).add(offset);

		camera.position.lerp(desiredPosition, 0.05);
		camera.lookAt(model.position);
	}
},

8. 移动模型位置

// 移动模型位置
createTween(startPosition, endPosition, speed) {
	// 计算起点到终点的距离
	const distance = startPosition.distanceTo(endPosition);
	// 使用距离除以速度来计算持续时间
	const duration = (distance / speed) * 1000; // 持续时间(以毫秒为单位)
	// 创建并返回一个新的Tween动画
	return new TWEEN.Tween(startPosition)
		.to({ x: endPosition.x, y: endPosition.y, z: endPosition.z }, duration)
		.easing(TWEEN.Easing.Quadratic.InOut)
		.onUpdate(() => {
			//相机的相对偏移量,z=-400 在人物模型的后面
			const relativeCameraOffset = new THREE.Vector3(0, 100, -400);
			const targetCameraPosition = relativeCameraOffset.applyMatrix4(person.matrixWorld);
			camera.position.set(targetCameraPosition.x, targetCameraPosition.y, targetCameraPosition.z);

			//更新控制器的目标为Person的位置
			const walkerPosition = person.position.clone();
			controls.target = new THREE.Vector3(walkerPosition.x, 100, walkerPosition.z);
			// 确保控制器的变更生效
			controls.update();

			// 更新模型位置
			person.position.copy(startPosition);
			person.lookAt(endPosition);
		})
		.onComplete(() => {
			// 动画完成时,确保模型位置与结束位置相匹配
			person.position.copy(endPosition);
		});
},

9.动画执行

全局定义的参数:

let personMixer = null; // 巡航混合器变量
let personClock = new THREE.Clock(); // 巡航计时工具
// 获取巡航时间差
const personDelta = personClock.getDelta();

if (personMixer && isAnimating) {
	personMixer.update(personDelta);
}
TWEEN.update();

http://www.kler.cn/a/161916.html

相关文章:

  • Ceph 中PG与PGP的概述
  • 3D编辑器教程:如何实现3D模型多材质定制效果?
  • 如何保护 Microsoft 网络免受中间人攻击
  • 【VIM】vim 常用命令
  • 淘宝代购系统;海外代购系统;代购程序,代购系统源码PHP前端源码
  • 阿里云centos7.9服务器磁盘挂载,切换服务路径
  • 智能优化算法应用:基于材料生成算法无线传感器网络(WSN)覆盖优化 - 附代码
  • Thymeleaf生成pdf表格合并单元格描边不显示
  • SpringDataJPA基础
  • Cypress:前端自动化测试的终极利器
  • Leetcode刷题笔记题解(C++):165. 比较版本号
  • 安路Anlogic FPGA下载器的驱动安装教程
  • 【mysql】下一行减去上一行数据、自增序列场景应用
  • 2023年4K投影仪怎么选?极米H6 4K高亮版怎么样?
  • Leetcode—1466.重新规划路线【中等】
  • 【PTA题目】7-7 自守数 分数 15
  • 芯知识 | 如何选择合适的单片机语音芯片?
  • 使用单例模式+观察者模式实现参数配置实时更新
  • 算术运算(这么简单?进来坐坐?)
  • 复杂gRPC之go调用go
  • C++标准模板(STL)- 类型支持 (杂项变换,确定一组类型的公共类型,std::common_type)
  • C#-using处理非托管资源
  • 我不是DBA之慢SQL诊断方式
  • 云原生之深入解析Kubernetes策略引擎对比:OPA/Gatekeeper与Kyverno
  • 【React】路由的基础使用
  • SpringAOP专栏一《使用教程篇》