Vue3 + Three.js实现自定义3D模型加载与交互实战全流程

 更新时间:2025年12月19日 10:33:09   作者:YAY_tyy  
Three.js是一个用于创建3D图形的JavaScript库,它基于WebGL技术,并提供了一系列的工具和API,这篇文章主要介绍了Vue3 + Three.js实现自定义3D模型加载与交互实战的相关资料,需要的朋友可以参考下

前言

在 Web 3D 开发中,Three.js 是轻量且灵活的核心框架,尤其适合自定义模型可视化场景。本文基于 Vue3 + Vite + Three.js 技术栈,从环境搭建到模型加载、交互控制逐步拆解,覆盖 GLB 模型加载、视角控制、点击高亮等核心需求,附带完整代码与避坑指南。​

一、环境搭建:Vue3 + Three.js 基础配置​

1.1 项目初始化(Vite)​

Three.js 包体积远小于 Cesium,配合 Vite 可实现秒级编译,初始化命令如下:

# 创建Vue3项目
npm create vite@latest threejs-3d-model-demo -- --template vue
cd threejs-3d-model-demo
npm install

# 安装核心依赖(Three.js + 模型加载器 + 控制器)
npm install three @tweenjs/tween.js
  • three:核心库(包含场景、相机、渲染器等基础组件)​
  • @tweenjs/tween.js:用于视角平滑过渡(替代 Three.js 原生动画)

1.2 基础工具封装(简化重复代码)​

Three.js 需手动创建场景、相机、渲染器,建议封装工具函数统一管理。在src/utils/threeHelper.js中添加:

import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' // 视角控制器

/**
 * 初始化Three.js基础组件
 * @param {HTMLElement} container - 渲染容器
 * @returns {Object} 场景、相机、渲染器、控制器实例
 */
export function initThree(container) {
  // 1. 创建场景(承载所有3D元素)
  const scene = new THREE.Scene()
  scene.background = new THREE.Color(0xf0f8ff) // 背景色(淡蓝色)

  // 2. 创建相机(透视相机,模拟人眼视角)
  const camera = new THREE.PerspectiveCamera(
    75, // 视野角度(FOV)
    container.clientWidth / container.clientHeight, // 宽高比
    0.1, // 近裁剪面(小于此距离的物体不渲染)
    1000 // 远裁剪面(大于此距离的物体不渲染)
  )
  camera.position.set(0, 5, 10) // 相机初始位置(x,y,z)

  // 3. 创建渲染器(WebGL渲染)
  const renderer = new THREE.WebGLRenderer({ antialias: true }) // 抗锯齿开启
  renderer.setSize(container.clientWidth, container.clientHeight) // 渲染尺寸
  container.appendChild(renderer.domElement) // 挂载渲染画布

  // 4. 创建控制器(支持鼠标拖动、滚轮缩放视角)
  const controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true // 阻尼效果(视角移动更平滑)
  controls.dampingFactor = 0.05 // 阻尼系数(值越小越平滑)
  controls.screenSpacePanning = false // 禁止屏幕平移(仅围绕物体旋转)
  controls.minDistance = 5 // 最小缩放距离
  controls.maxDistance = 50 // 最大缩放距离

  // 5. 监听窗口resize事件(自适应画布尺寸)
  window.addEventListener('resize', () => {
    camera.aspect = container.clientWidth / container.clientHeight
    camera.updateProjectionMatrix() // 更新相机投影矩阵
    renderer.setSize(container.clientWidth, container.clientHeight)
  })

  return { scene, camera, renderer, controls }
}

二、核心功能实现:3D 模型加载与控制​

2.1 组件初始化(挂载 Three.js 核心实例)​

在 Vue 组件中调用工具函数,初始化场景并启动渲染循环:

<template>
  <!-- Three.js渲染容器 -->
  <div id="threeContainer" class="three-container"></div>
  <!-- 模型加载进度条 -->
  <div v-if="loadProgress < 100" class="load-progress">
    加载中:{{ loadProgress }}%
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import * as THREE from 'three'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' // GLB模型加载器
import { initThree } from '@/utils/threeHelper.js'
import TWEEN from '@tweenjs/tween.js'

// 响应式数据(加载进度)
const loadProgress = ref(0)
// 全局变量(存储Three.js实例)
let scene, camera, renderer, controls, model // model为加载后的模型实例

onMounted(() => {
  // 1. 获取容器并初始化Three.js
  const container = document.getElementById('threeContainer')
  const threeInstance = initThree(container)
  scene = threeInstance.scene
  camera = threeInstance.camera
  renderer = threeInstance.renderer
  controls = threeInstance.controls

  // 2. 添加环境光(避免模型过暗)
  addLights()

  // 3. 加载3D模型
  loadGLBModel('/models/building.glb') // 模型路径(public/models目录下)

  // 4. 启动渲染循环(动画帧)
  renderLoop()
})

/**
 * 添加场景灯光(环境光+方向光)
 */
function addLights() {
  // 环境光(均匀照亮所有物体,无阴影)
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
  scene.add(ambientLight)

  // 方向光(模拟太阳光,产生阴影)
  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
  directionalLight.position.set(10, 20, 10) // 光源位置
  directionalLight.castShadow = true // 开启阴影投射
  // 优化阴影质量(降低锯齿)
  directionalLight.shadow.mapSize.set(2048, 2048)
  scene.add(directionalLight)
}

/**
 * 渲染循环(持续更新场景)
 */
function renderLoop() {
  requestAnimationFrame(renderLoop)
  controls.update() // 更新控制器状态(阻尼效果需此步骤)
  TWEEN.update() // 更新视角过渡动画
  renderer.render(scene, camera) // 渲染场景
}
</script>

<style scoped>
.three-container {
  width: 100vw;
  height: 100vh;
}
.load-progress {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 18px;
  color: #333;
  background: rgba(255, 255, 255, 0.8);
  padding: 10px 20px;
  border-radius: 4px;
  z-index: 100;
}
</style>

2.2 GLB 模型加载(带进度监听)​

Three.js 通过GLTFLoader加载模型,需处理加载进度、成功回调与错误捕获:

/**
 * 加载GLB格式3D模型
 * @param {string} modelPath - 模型路径
 */
function loadGLBModel(modelPath) {
  const loader = new GLTFLoader()

  // 1. 监听加载进度
  loader.onProgress = (xhr) => {
    loadProgress.value = Math.floor((xhr.loaded / xhr.total) * 100)
  }

  // 2. 加载成功回调
  loader.load(
    modelPath,
    (gltf) => {
      model = gltf.scene // 保存模型实例(用于后续交互)
      
      // 模型缩放与位置调整(根据实际模型大小适配)
      model.scale.set(1, 1, 1) // 缩放比例(默认1:1)
      model.position.set(0, 0, 0) // 模型初始位置(场景中心)
      
      // 允许模型投射阴影
      model.traverse((child) => {
        if (child.isMesh) {
          child.castShadow = true
          child.receiveShadow = true // 允许接收其他物体的阴影
        }
      })

      scene.add(model) // 将模型添加到场景
      loadProgress.value = 100 // 标记加载完成

      // 视角自动聚焦到模型(使用TWEEN实现平滑过渡)
      new TWEEN.Tween(camera.position)
        .to({ x: 0, y: 3, z: 8 }, 1500) // 目标位置与过渡时间(ms)
        .easing(TWEEN.Easing.Quadratic.InOut) // 缓动函数
        .start()
    },
    undefined, // onProgress已单独处理
    (error) => {
      console.error('模型加载失败:', error)
      alert(`模型加载失败,请检查路径:${modelPath}`)
    }
  )
}

2.3 模型交互:点击高亮与信息弹窗​

基于 Three.js 的Raycaster(射线检测)实现点击交互,配合 Element Plus 弹窗展示信息:

// 引入Element Plus(需提前安装:npm install element-plus)
import { ElMessageBox } from 'element-plus'
import { onMounted } from 'vue'

onMounted(() => {
  // ... 原有初始化代码 ...
  // 初始化点击交互
  initModelClick()
})

/**
 * 初始化模型点击交互
 */
function initModelClick() {
  const raycaster = new THREE.Raycaster() // 射线检测器
  const mouse = new THREE.Vector2() // 存储鼠标坐标

  // 监听鼠标点击事件
  window.addEventListener('click', (event) => {
    // 1. 将鼠标屏幕坐标转换为Three.js标准坐标(-1 ~ 1)
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1

    // 2. 更新射线方向(从相机指向鼠标点击位置)
    raycaster.setFromCamera(mouse, camera)

    // 3. 检测射线与模型的交点
    const intersects = raycaster.intersectObjects(model.children, true) // true:检测子物体

    if (intersects.length > 0) {
      const clickedMesh = intersects[0].object // 获取点击的模型网格
      
      // 4. 模型高亮(临时修改材质颜色)
      const originalMaterial = clickedMesh.material // 保存原始材质
      clickedMesh.material = new THREE.MeshBasicMaterial({ color: 0xffff00 }) // 黄色高亮

      // 5. 2秒后恢复原始材质
      setTimeout(() => {
        clickedMesh.material = originalMaterial
      }, 2000)

      // 6. 弹出模型信息(可从模型userData中获取自定义数据)
      ElMessageBox.alert(
        `<div>模型名称:${model.name || '自定义建筑模型'}</div>
         <div>位置:(${model.position.x.toFixed(2)}, ${model.position.y.toFixed(2)}, ${model.position.z.toFixed(2)})</div>
         <div>网格数量:${getMeshCount(model)}个</div>`,
        '模型信息',
        { confirmButtonText: '确定' }
      )
    }
  })
}

/**
 * 统计模型中的网格数量
 * @param {THREE.Group} object - 模型实例(Group或Scene)
 * @returns {number} 网格数量
 */
function getMeshCount(object) {
  let count = 0
  object.traverse((child) => {
    if (child.isMesh) count++
  })
  return count
}

三、Three.js 专属问题与优化方案​

3.1 模型加载后黑屏 / 不可见​

  • 相机位置问题:确保相机在模型 “可视范围内”(如模型过大时,相机需远离),可通过camera.lookAt(model.position)让相机朝向模型​
  • 灯光缺失:Three.js 默认无环境光,需手动添加AmbientLight或DirectionalLight(参考 2.1 节addLights函数)​
  • 模型缩放异常:若模型过小 / 过大,调整model.scale.set(x,y,z)(如scale.set(0.1,0.1,0.1)缩小 10 倍)​

3.2 渲染性能优化(解决卡顿)​

  • 减少面数:BlenderMeshLab简化模型(建议单模型面数 < 5 万)​
  • 材质优化:复杂场景用MeshLambertMaterial替代MeshStandardMaterial(后者计算 PBR 物理效果,性能消耗高)​
  • 开启抗锯齿权衡:若性能不足,关闭渲染器抗锯齿(new THREE.WebGLRenderer({ antialias: false })),通过renderer.setPixelRatio(window.devicePixelRatio)优化显示​

3.3 模型纹理丢失​

  • 纹理路径问题:GLB 模型若内嵌纹理,直接加载即可;若纹理单独存储,需确保纹理文件与模型在同一目录,且导出时路径正确​
  • 跨域问题:开发环境通过 Vite 配置代理,生产环境需服务器添加Access-Control-Allow-Origin响应头:
// vite.config.js 跨域配置示例
export default defineConfig({
  server: {
    proxy: {
      '/models': {
        target: 'http://your-server.com', // 模型服务器地址
        changeOrigin: true
      }
    }
  }
})

四、总结与扩展方向​

本文实现了 Vue3 + Three.js 加载 3D 模型的核心流程,相比 Cesium,Three.js 更轻量、自定义程度更高,适合非 GIS 类 3D 场景。后续可扩展的方向:​

  1. 模型动画控制:通过gltf.animations获取模型动画,用THREE.AnimationMixer实现播放 / 暂停 / 进度控制​
  2. 光影增强:添加HemisphereLight(半球光)模拟地面反光,或PointLight(点光源)模拟灯光照射效果​
  3. 粒子系统结合:用THREE.Points创建粒子云,围绕模型生成动态效果(如建筑周围的粒子流)

到此这篇关于Vue3 + Three.js实现自定义3D模型加载与交互的文章就介绍到这了,更多相关Vue3 Three.js自定义3D模型加载与交互内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • uniApp h5项目如何通过命令行打包并生成指定路径及文件名称

    uniApp h5项目如何通过命令行打包并生成指定路径及文件名称

    用uni-app来写安卓端,近日需要将程序打包为H5放到web服务器上,经过一番折腾,这里给大家分享下,这篇文章主要给大家介绍了关于uniApp h5项目如何通过命令行打包并生成指定路径及文件名称的相关资料,需要的朋友可以参考下
    2024-02-02
  • 深度解读vue-resize的具体用法

    深度解读vue-resize的具体用法

    这篇文章主要介绍了vue-resize深度解读,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • vue 子组件watch监听不到prop的解决

    vue 子组件watch监听不到prop的解决

    这篇文章主要介绍了vue 子组件watch监听不到prop的解决,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • vue-router之解决addRoutes使用遇到的坑

    vue-router之解决addRoutes使用遇到的坑

    这篇文章主要介绍了vue-router之解决addRoutes使用遇到的坑,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-07-07
  • Vue基于NUXT的SSR详解

    Vue基于NUXT的SSR详解

    这篇文章主要介绍了Vue基于NUXT的SSR详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • vue3 中ref和reactive的区别讲解

    vue3 中ref和reactive的区别讲解

    如果在template里使用的是ref类型的数据, 那么Vue会自动帮我们添加.value,如果在template里使用的是reactive类型的数据, 那么Vue不会自动帮我们添加.value,这篇文章主要介绍了vue3 中ref和reactive的区别,需要的朋友可以参考下
    2022-12-12
  • vue项目完成后如何实现项目优化的示例

    vue项目完成后如何实现项目优化的示例

    本文主要介绍了vue项目完成后如何实现项目优化的示例,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • vue项目依赖升级报错处理方式

    vue项目依赖升级报错处理方式

    这篇文章主要介绍了vue项目依赖升级报错处理方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • 关于vue3 vuex4 store的响应式取值问题解决

    关于vue3 vuex4 store的响应式取值问题解决

    这篇文章主要介绍了vue3 vuex4 store的响应式取值问题,在实际生活中遇到这样一个问题:在页面中点击按钮,数量增加,值是存在store中的,点击事件值没变,如何解决这个问题,本文给大家分享解决方法,需要的朋友可以参考下
    2022-08-08
  • 使用Element-UI的NavMenu如何隐藏自带的小箭头

    使用Element-UI的NavMenu如何隐藏自带的小箭头

    这篇文章主要介绍了使用Element-UI的NavMenu如何隐藏自带的小箭头问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07

最新评论