前言:目前已经出差1个多月,真累。趁着空闲研究了一下threejs的使用,特此记录一下
<template>
<div ref="canvasContainer" style="width: 1080px; height: 680px"></div>
<button @click="toggleModel">切换模型</button>
</template>
<script setup>
import { onMounted, ref, onBeforeUnmount } from "vue";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
// 定义组件的 props
const props = defineProps({
scaleFactor: {
type: Number,
default: 1,
validator: (value) => value > 0 && value <= 10, // 验证 scaleFactor 的值
},
});
// 声明全局变量
let mixer; // 动画混合器
let isAnimating = false; // 动画状态标志
const canvasContainer = ref(null); // 画布容器引用
const models = ["/Castanea.glb", "/Bee.glb", "/Public Bus.glb"]; // 模型路径数组
let currentIndex = 0; // 当前模型索引
let scene, camera, renderer, animations; // 3D 场景、相机、渲染器和动画
// 组件挂载时初始化 Three.js
onMounted(() => {
initThreeJS(); // 初始化 Three.js
loadModel(); // 加载初始模型
});
// 切换模型的函数
function toggleModel() {
loadModel(); // 加载下一个模型
}
// 初始化 Three.js 的基本设置
function initThreeJS() {
scene = new THREE.Scene(); // 创建场景
camera = new THREE.PerspectiveCamera(75, 1080 / 680, 0.1, 1000); // 创建透视相机
renderer = new THREE.WebGLRenderer({ antialias: true }); // 创建 WebGL 渲染器
renderer.setSize(1080, 680); // 设置渲染器大小
renderer.setClearColor(0xaaaaaa); // 设置背景颜色
if (canvasContainer.value) {
canvasContainer.value.appendChild(renderer.domElement); // 将渲染器的 DOM 元素添加到容器
}
// 添加环境光和方向光
const ambientLight = new THREE.AmbientLight(0xffffff, 1); // 创建环境光
scene.add(ambientLight); // 将环境光添加到场景
const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // 创建方向光
directionalLight.position.set(5, 5, 5).normalize(); // 设置方向光的位置
scene.add(directionalLight); // 将方向光添加到场景
window.addEventListener("resize", onWindowResize); // 监听窗口大小变化
}
// 创建 Raycaster 和鼠标位置向量
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// 加载模型的函数
function loadModel() {
clearPreviousModel(); // 清除上一个模型
currentIndex = (currentIndex + 1) % models.length; // 更新当前模型索引
const loader = new GLTFLoader(); // 创建 GLTF 加载器
loader.load(
models[currentIndex], // 加载当前模型
(gltf) => {
const model = gltf.scene; // 获取模型场景
animations = gltf.animations; // 获取模型动画
mixer = new THREE.AnimationMixer(model); // 创建动画混合器
model.traverse((child) => {
if (child.isMesh) {
child.userData.clickable = true; // 标记可点击的网格
child.material.transparent = true; // 确保材质透明度设置正确
}
});
model.scale.set(props.scaleFactor, props.scaleFactor, props.scaleFactor); // 设置模型缩放
scene.add(model); // 将模型添加到场景
setCameraPosition(model); // 设置相机位置
addOrbitControls(model); // 添加轨道控制器
animate(); // 启动动画循环
},
undefined,
(error) => {
console.error("Error loading GLB file:", error); // 错误处理
}
);
}
// 清除上一个模型
function clearPreviousModel() {
scene.children.forEach((child) => {
if (child.type === "Group") {
scene.remove(child); // 移除上一个模型
}
});
}
// 设置相机位置的函数
function setCameraPosition(model) {
const box = new THREE.Box3().setFromObject(model); // 获取模型的包围盒
const center = box.getCenter(new THREE.Vector3()); // 获取包围盒中心
const size = box.getSize(new THREE.Vector3()); // 获取包围盒大小
const maxDim = Math.max(size.x, size.y, size.z); // 获取最大维度
const fov = camera.fov * (Math.PI / 180); // 将相机视角转换为弧度
let cameraZ = Math.abs((maxDim / 2) * Math.tan(fov * 2)); // 计算相机 Z 位置
cameraZ *= 6; // 增加距离以确保模型完全显示
camera.position.set(center.x - 2, center.y - 2, cameraZ - 5); // 设置相机位置
camera.lookAt(center); // 让相机看向模型中心
}
// 添加轨道控制器的函数
function addOrbitControls(model) {
const controls = new OrbitControls(camera, renderer.domElement); // 创建轨道控制器
controls.target.copy(model.position); // 设置控制目标为模型位置
controls.update(); // 更新控制器
}
// 处理鼠标点击事件的函数
function onMouseClick(event) {
// 计算鼠标在归一化设备坐标 (-1到+1) 中的坐标
mouse.x = (event.clientX / 1080) * 2 - 1;
mouse.y = -(event.clientY / 680) * 2 + 1;
// 更新 Raycaster
raycaster.setFromCamera(mouse, camera);
// 计算物体与射线的交叉
const intersects = raycaster.intersectObjects(scene.children, true);
console.log("intersects===>", intersects);
if (intersects.length > 0) {
const clickedObject = intersects[0].object; // 获取点击的对象
if (clickedObject.userData.clickable) {
// 检查对象是否可点击
triggerAnimation(); // 触发动画
}
}
}
// 触发动画的函数
function triggerAnimation() {
if (!isAnimating) {
// 检查动画是否正在播放
isAnimating = true; // 设置为正在动画状态
animations.forEach((clip) => {
const action = mixer.clipAction(clip); // 获取动画动作
action.loop = THREE.LoopOnce; // 设置为单次循环
action.reset().play(); // 播放动画
// 使用一个定时器检查动画是否完成
const checkAnimationFinished = () => {
if (action.isRunning()) {
requestAnimationFrame(checkAnimationFinished); // 继续检查
} else {
isAnimating = false; // 动画结束后重置状态
action.stop(); // 停止动画
action.reset(); // 重置动画状态
}
};
checkAnimationFinished(); // 开始检查动画状态
});
}
}
// 添加鼠标点击事件监听器
window.addEventListener("click", onMouseClick, false);
// 动画循环的函数
function animate() {
requestAnimationFrame(animate); // 请求下一帧动画
if (mixer) {
mixer.update(0.02); // 更新动画
}
renderer.render(scene, camera); // 渲染场景
}
// 处理窗口大小变化的函数
function onWindowResize() {
camera.aspect = 1080 / 680; // 更新相机宽高比
camera.updateProjectionMatrix(); // 更新相机投影矩阵
renderer.setSize(1080, 680); // 更新渲染器大小
}
// 组件卸载时移除事件监听器
onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowResize); // 移除窗口大小变化监听器
});
</script>