Vue3?+?Three.js實(shí)現(xiàn)自定義3D模型加載與交互實(shí)戰(zhàn)全流程
前言
在 Web 3D 開(kāi)發(fā)中,Three.js 是輕量且靈活的核心框架,尤其適合自定義模型可視化場(chǎng)景。本文基于 Vue3 + Vite + Three.js 技術(shù)棧,從環(huán)境搭建到模型加載、交互控制逐步拆解,覆蓋 GLB 模型加載、視角控制、點(diǎn)擊高亮等核心需求,附帶完整代碼與避坑指南。?
一、環(huán)境搭建:Vue3 + Three.js 基礎(chǔ)配置?
1.1 項(xiàng)目初始化(Vite)?
Three.js 包體積遠(yuǎn)小于 Cesium,配合 Vite 可實(shí)現(xiàn)秒級(jí)編譯,初始化命令如下:
# 創(chuàng)建Vue3項(xiàng)目 npm create vite@latest threejs-3d-model-demo -- --template vue cd threejs-3d-model-demo npm install # 安裝核心依賴(lài)(Three.js + 模型加載器 + 控制器) npm install three @tweenjs/tween.js
- three:核心庫(kù)(包含場(chǎng)景、相機(jī)、渲染器等基礎(chǔ)組件)?
- @tweenjs/tween.js:用于視角平滑過(guò)渡(替代 Three.js 原生動(dòng)畫(huà))
1.2 基礎(chǔ)工具封裝(簡(jiǎn)化重復(fù)代碼)?
Three.js 需手動(dòng)創(chuàng)建場(chǎng)景、相機(jī)、渲染器,建議封裝工具函數(shù)統(tǒng)一管理。在src/utils/threeHelper.js中添加:
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' // 視角控制器
/**
* 初始化Three.js基礎(chǔ)組件
* @param {HTMLElement} container - 渲染容器
* @returns {Object} 場(chǎng)景、相機(jī)、渲染器、控制器實(shí)例
*/
export function initThree(container) {
// 1. 創(chuàng)建場(chǎng)景(承載所有3D元素)
const scene = new THREE.Scene()
scene.background = new THREE.Color(0xf0f8ff) // 背景色(淡藍(lán)色)
// 2. 創(chuàng)建相機(jī)(透視相機(jī),模擬人眼視角)
const camera = new THREE.PerspectiveCamera(
75, // 視野角度(FOV)
container.clientWidth / container.clientHeight, // 寬高比
0.1, // 近裁剪面(小于此距離的物體不渲染)
1000 // 遠(yuǎn)裁剪面(大于此距離的物體不渲染)
)
camera.position.set(0, 5, 10) // 相機(jī)初始位置(x,y,z)
// 3. 創(chuàng)建渲染器(WebGL渲染)
const renderer = new THREE.WebGLRenderer({ antialias: true }) // 抗鋸齒開(kāi)啟
renderer.setSize(container.clientWidth, container.clientHeight) // 渲染尺寸
container.appendChild(renderer.domElement) // 掛載渲染畫(huà)布
// 4. 創(chuàng)建控制器(支持鼠標(biāo)拖動(dòng)、滾輪縮放視角)
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true // 阻尼效果(視角移動(dòng)更平滑)
controls.dampingFactor = 0.05 // 阻尼系數(shù)(值越小越平滑)
controls.screenSpacePanning = false // 禁止屏幕平移(僅圍繞物體旋轉(zhuǎn))
controls.minDistance = 5 // 最小縮放距離
controls.maxDistance = 50 // 最大縮放距離
// 5. 監(jiān)聽(tīng)窗口resize事件(自適應(yīng)畫(huà)布尺寸)
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight
camera.updateProjectionMatrix() // 更新相機(jī)投影矩陣
renderer.setSize(container.clientWidth, container.clientHeight)
})
return { scene, camera, renderer, controls }
}二、核心功能實(shí)現(xiàn):3D 模型加載與控制?
2.1 組件初始化(掛載 Three.js 核心實(shí)例)?
在 Vue 組件中調(diào)用工具函數(shù),初始化場(chǎng)景并啟動(dòng)渲染循環(huán):
<template>
<!-- Three.js渲染容器 -->
<div id="threeContainer" class="three-container"></div>
<!-- 模型加載進(jìn)度條 -->
<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'
// 響應(yīng)式數(shù)據(jù)(加載進(jìn)度)
const loadProgress = ref(0)
// 全局變量(存儲(chǔ)Three.js實(shí)例)
let scene, camera, renderer, controls, model // model為加載后的模型實(shí)例
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. 添加環(huán)境光(避免模型過(guò)暗)
addLights()
// 3. 加載3D模型
loadGLBModel('/models/building.glb') // 模型路徑(public/models目錄下)
// 4. 啟動(dòng)渲染循環(huán)(動(dòng)畫(huà)幀)
renderLoop()
})
/**
* 添加場(chǎng)景燈光(環(huán)境光+方向光)
*/
function addLights() {
// 環(huán)境光(均勻照亮所有物體,無(wú)陰影)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
// 方向光(模擬太陽(yáng)光,產(chǎn)生陰影)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(10, 20, 10) // 光源位置
directionalLight.castShadow = true // 開(kāi)啟陰影投射
// 優(yōu)化陰影質(zhì)量(降低鋸齒)
directionalLight.shadow.mapSize.set(2048, 2048)
scene.add(directionalLight)
}
/**
* 渲染循環(huán)(持續(xù)更新場(chǎng)景)
*/
function renderLoop() {
requestAnimationFrame(renderLoop)
controls.update() // 更新控制器狀態(tài)(阻尼效果需此步驟)
TWEEN.update() // 更新視角過(guò)渡動(dòng)畫(huà)
renderer.render(scene, camera) // 渲染場(chǎng)景
}
</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 模型加載(帶進(jìn)度監(jiān)聽(tīng))?
Three.js 通過(guò)GLTFLoader加載模型,需處理加載進(jìn)度、成功回調(diào)與錯(cuò)誤捕獲:
/**
* 加載GLB格式3D模型
* @param {string} modelPath - 模型路徑
*/
function loadGLBModel(modelPath) {
const loader = new GLTFLoader()
// 1. 監(jiān)聽(tīng)加載進(jìn)度
loader.onProgress = (xhr) => {
loadProgress.value = Math.floor((xhr.loaded / xhr.total) * 100)
}
// 2. 加載成功回調(diào)
loader.load(
modelPath,
(gltf) => {
model = gltf.scene // 保存模型實(shí)例(用于后續(xù)交互)
// 模型縮放與位置調(diào)整(根據(jù)實(shí)際模型大小適配)
model.scale.set(1, 1, 1) // 縮放比例(默認(rèn)1:1)
model.position.set(0, 0, 0) // 模型初始位置(場(chǎng)景中心)
// 允許模型投射陰影
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true
child.receiveShadow = true // 允許接收其他物體的陰影
}
})
scene.add(model) // 將模型添加到場(chǎng)景
loadProgress.value = 100 // 標(biāo)記加載完成
// 視角自動(dòng)聚焦到模型(使用TWEEN實(shí)現(xiàn)平滑過(guò)渡)
new TWEEN.Tween(camera.position)
.to({ x: 0, y: 3, z: 8 }, 1500) // 目標(biāo)位置與過(guò)渡時(shí)間(ms)
.easing(TWEEN.Easing.Quadratic.InOut) // 緩動(dòng)函數(shù)
.start()
},
undefined, // onProgress已單獨(dú)處理
(error) => {
console.error('模型加載失敗:', error)
alert(`模型加載失敗,請(qǐng)檢查路徑:${modelPath}`)
}
)
}2.3 模型交互:點(diǎn)擊高亮與信息彈窗?
基于 Three.js 的Raycaster(射線檢測(cè))實(shí)現(xiàn)點(diǎn)擊交互,配合 Element Plus 彈窗展示信息:
// 引入Element Plus(需提前安裝:npm install element-plus)
import { ElMessageBox } from 'element-plus'
import { onMounted } from 'vue'
onMounted(() => {
// ... 原有初始化代碼 ...
// 初始化點(diǎn)擊交互
initModelClick()
})
/**
* 初始化模型點(diǎn)擊交互
*/
function initModelClick() {
const raycaster = new THREE.Raycaster() // 射線檢測(cè)器
const mouse = new THREE.Vector2() // 存儲(chǔ)鼠標(biāo)坐標(biāo)
// 監(jiān)聽(tīng)鼠標(biāo)點(diǎn)擊事件
window.addEventListener('click', (event) => {
// 1. 將鼠標(biāo)屏幕坐標(biāo)轉(zhuǎn)換為T(mén)hree.js標(biāo)準(zhǔn)坐標(biāo)(-1 ~ 1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
// 2. 更新射線方向(從相機(jī)指向鼠標(biāo)點(diǎn)擊位置)
raycaster.setFromCamera(mouse, camera)
// 3. 檢測(cè)射線與模型的交點(diǎn)
const intersects = raycaster.intersectObjects(model.children, true) // true:檢測(cè)子物體
if (intersects.length > 0) {
const clickedMesh = intersects[0].object // 獲取點(diǎn)擊的模型網(wǎng)格
// 4. 模型高亮(臨時(shí)修改材質(zhì)顏色)
const originalMaterial = clickedMesh.material // 保存原始材質(zhì)
clickedMesh.material = new THREE.MeshBasicMaterial({ color: 0xffff00 }) // 黃色高亮
// 5. 2秒后恢復(fù)原始材質(zhì)
setTimeout(() => {
clickedMesh.material = originalMaterial
}, 2000)
// 6. 彈出模型信息(可從模型userData中獲取自定義數(shù)據(jù))
ElMessageBox.alert(
`<div>模型名稱(chēng):${model.name || '自定義建筑模型'}</div>
<div>位置:(${model.position.x.toFixed(2)}, ${model.position.y.toFixed(2)}, ${model.position.z.toFixed(2)})</div>
<div>網(wǎng)格數(shù)量:${getMeshCount(model)}個(gè)</div>`,
'模型信息',
{ confirmButtonText: '確定' }
)
}
})
}
/**
* 統(tǒng)計(jì)模型中的網(wǎng)格數(shù)量
* @param {THREE.Group} object - 模型實(shí)例(Group或Scene)
* @returns {number} 網(wǎng)格數(shù)量
*/
function getMeshCount(object) {
let count = 0
object.traverse((child) => {
if (child.isMesh) count++
})
return count
}三、Three.js 專(zhuān)屬問(wèn)題與優(yōu)化方案?
3.1 模型加載后黑屏 / 不可見(jiàn)?
- 相機(jī)位置問(wèn)題:確保相機(jī)在模型 “可視范圍內(nèi)”(如模型過(guò)大時(shí),相機(jī)需遠(yuǎn)離),可通過(guò)camera.lookAt(model.position)讓相機(jī)朝向模型?
- 燈光缺失:Three.js 默認(rèn)無(wú)環(huán)境光,需手動(dòng)添加AmbientLight或DirectionalLight(參考 2.1 節(jié)addLights函數(shù))?
- 模型縮放異常:若模型過(guò)小 / 過(guò)大,調(diào)整model.scale.set(x,y,z)(如scale.set(0.1,0.1,0.1)縮小 10 倍)?
3.2 渲染性能優(yōu)化(解決卡頓)?
- 材質(zhì)優(yōu)化:復(fù)雜場(chǎng)景用MeshLambertMaterial替代MeshStandardMaterial(后者計(jì)算 PBR 物理效果,性能消耗高)?
- 開(kāi)啟抗鋸齒權(quán)衡:若性能不足,關(guān)閉渲染器抗鋸齒(new THREE.WebGLRenderer({ antialias: false })),通過(guò)renderer.setPixelRatio(window.devicePixelRatio)優(yōu)化顯示?
3.3 模型紋理丟失?
- 紋理路徑問(wèn)題:GLB 模型若內(nèi)嵌紋理,直接加載即可;若紋理單獨(dú)存儲(chǔ),需確保紋理文件與模型在同一目錄,且導(dǎo)出時(shí)路徑正確?
- 跨域問(wèn)題:開(kāi)發(fā)環(huán)境通過(guò) Vite 配置代理,生產(chǎn)環(huán)境需服務(wù)器添加Access-Control-Allow-Origin響應(yīng)頭:
// vite.config.js 跨域配置示例
export default defineConfig({
server: {
proxy: {
'/models': {
target: 'http://your-server.com', // 模型服務(wù)器地址
changeOrigin: true
}
}
}
})四、總結(jié)與擴(kuò)展方向?
本文實(shí)現(xiàn)了 Vue3 + Three.js 加載 3D 模型的核心流程,相比 Cesium,Three.js 更輕量、自定義程度更高,適合非 GIS 類(lèi) 3D 場(chǎng)景。后續(xù)可擴(kuò)展的方向:?
- 模型動(dòng)畫(huà)控制:通過(guò)gltf.animations獲取模型動(dòng)畫(huà),用THREE.AnimationMixer實(shí)現(xiàn)播放 / 暫停 / 進(jìn)度控制?
- 光影增強(qiáng):添加HemisphereLight(半球光)模擬地面反光,或PointLight(點(diǎn)光源)模擬燈光照射效果?
- 粒子系統(tǒng)結(jié)合:用THREE.Points創(chuàng)建粒子云,圍繞模型生成動(dòng)態(tài)效果(如建筑周?chē)牧W恿鳎?/li>
到此這篇關(guān)于Vue3 + Three.js實(shí)現(xiàn)自定義3D模型加載與交互的文章就介紹到這了,更多相關(guān)Vue3 Three.js自定義3D模型加載與交互內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
uniApp?h5項(xiàng)目如何通過(guò)命令行打包并生成指定路徑及文件名稱(chēng)
用uni-app來(lái)寫(xiě)安卓端,近日需要將程序打包為H5放到web服務(wù)器上,經(jīng)過(guò)一番折騰,這里給大家分享下,這篇文章主要給大家介紹了關(guān)于uniApp?h5項(xiàng)目如何通過(guò)命令行打包并生成指定路徑及文件名稱(chēng)的相關(guān)資料,需要的朋友可以參考下2024-02-02
vue 子組件watch監(jiān)聽(tīng)不到prop的解決
這篇文章主要介紹了vue 子組件watch監(jiān)聽(tīng)不到prop的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08
vue項(xiàng)目完成后如何實(shí)現(xiàn)項(xiàng)目?jī)?yōu)化的示例
本文主要介紹了vue項(xiàng)目完成后如何實(shí)現(xiàn)項(xiàng)目?jī)?yōu)化的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
vue項(xiàng)目依賴(lài)升級(jí)報(bào)錯(cuò)處理方式
這篇文章主要介紹了vue項(xiàng)目依賴(lài)升級(jí)報(bào)錯(cuò)處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
關(guān)于vue3 vuex4 store的響應(yīng)式取值問(wèn)題解決
這篇文章主要介紹了vue3 vuex4 store的響應(yīng)式取值問(wèn)題,在實(shí)際生活中遇到這樣一個(gè)問(wèn)題:在頁(yè)面中點(diǎn)擊按鈕,數(shù)量增加,值是存在store中的,點(diǎn)擊事件值沒(méi)變,如何解決這個(gè)問(wèn)題,本文給大家分享解決方法,需要的朋友可以參考下2022-08-08
使用Element-UI的NavMenu如何隱藏自帶的小箭頭
這篇文章主要介紹了使用Element-UI的NavMenu如何隱藏自帶的小箭頭問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07

