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.clientHeightcamera.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.jsconst container = document.getElementById('threeContainer')const threeInstance = initThree(container)scene = threeInstance.scenecamera = threeInstance.camerarenderer = threeInstance.renderercontrols = 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 = truechild.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 - 1mouse.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 = 0object.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 渲染性能优化(解决卡顿)
- 减少面数:用Blender或MeshLab简化模型(建议单模型面数 < 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 场景。后续可扩展的方向:
- 模型动画控制:通过gltf.animations获取模型动画,用THREE.AnimationMixer实现播放 / 暂停 / 进度控制
- 光影增强:添加HemisphereLight(半球光)模拟地面反光,或PointLight(点光源)模拟灯光照射效果
- 粒子系统结合:用THREE.Points创建粒子云,围绕模型生成动态效果(如建筑周围的粒子流)