threejs初体验——glb模型加载

前言:目前已经出差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>

glb模型下载,解压放到publi下
博客地址,欢迎访问

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值