本说明结合 src/App.vue 代码,详细解释如何在 Vue3 项目中用 three.js 加载并显示 glb 模型。
1. 依赖与插件导入
import { onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import Stats from 'stats.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
- THREE:three.js 主库,包含所有核心3D功能。
- Stats:性能监控面板,显示FPS等信息。
- OrbitControls:鼠标/触摸控制相机旋转、缩放、平移。
- RoomEnvironment:生成物理渲染用的环境贴图。
- GLTFLoader/DRACOLoader:加载glb/gltf模型及解压Draco压缩网格。
2. 变量声明
let mixer: THREE.AnimationMixer | undefined
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let stats: Stats
const clock = new THREE.Clock()
- mixer:动画混合器,用于播放模型动画。
- renderer:WebGL渲染器。
- controls:轨道控制器。
- stats:性能监控。
- clock:three.js动画计时器。
3. Vue生命周期挂载
onMounted(() => { ... })
onUnmounted(() => { ... })
- onMounted:组件挂载后初始化three.js场景。
- onUnmounted:组件卸载时清理资源,防止内存泄漏。
4. three.js 场景初始化
4.1 获取容器
const container = document.getElementById('container')
- 获取用于渲染three.js的DOM节点。
4.2 性能面板
stats = new Stats()
container.appendChild(stats.dom)
- 添加FPS性能监控面板。
4.3 渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
container.appendChild(renderer.domElement)
- 创建WebGL渲染器,抗锯齿,适配高分屏,全屏显示。
4.4 场景与环境
const pmremGenerator = new THREE.PMREMGenerator(renderer)
const scene = new THREE.Scene()
scene.background = new THREE.Color(0xbfe3dd)
scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture
- 创建场景,设置背景色。
- 用RoomEnvironment生成物理渲染环境贴图,提升模型质感。
4.5 相机
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 0, 5)
- 透视相机,视野40度,近平面0.1,远平面1000,初始z=5。
4.6 轨道控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0, 0)
controls.update()
controls.enablePan = false
controls.enableDamping = true
- 允许鼠标旋转/缩放相机,目标点为原点。
- 禁止平移,启用阻尼(惯性)。
5. 加载GLB模型
5.1 Draco解码器
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/jsm/libs/draco/gltf/')
- 设置Draco解码器路径,支持压缩网格。
5.2 GLTFLoader加载模型
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
loader.load('/models/gltf/LittlestTokyo.glb', function (gltf) { ... })
- 加载glb模型,回调函数处理模型。
5.3 模型居中与相机自适应
const model = gltf.scene
const box = new THREE.Box3().setFromObject(model)
const size = new THREE.Vector3()
box.getSize(size)
const center = new THREE.Vector3()
box.getCenter(center)
model.position.sub(center)
scene.add(model)
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))
cameraZ *= 2.0 // 稍微拉远一点
camera.position.set(0, 0, cameraZ)
camera.near = cameraZ / 100
camera.far = cameraZ * 100
camera.updateProjectionMatrix()
controls.target.set(0, 0, 0)
controls.update()
- 计算模型包围盒,自动居中。
- 根据模型尺寸自动设置相机距离,保证完整显示。
5.4 动画支持
mixer = new THREE.AnimationMixer(model)
if (gltf.animations && gltf.animations.length > 0) {
mixer.clipAction(gltf.animations[0]).play()
}
- 如果模型有动画,自动播放。
5.5 启动渲染循环
renderer.setAnimationLoop(animate)
- 启动three.js渲染循环。
6. 窗口自适应
window.addEventListener('resize', onWindowResize)
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
}
- 浏览器窗口变化时,自动调整相机和渲染器。
7. 渲染循环与动画
function animate() {
const delta = clock.getDelta()
if (mixer) mixer.update(delta)
controls.update()
stats.update()
renderer.render(scene, camera)
}
- 每帧更新时间、动画、控制器、性能面板,并渲染场景。
8. 资源清理
onUnmounted(() => {
window.removeEventListener('resize', () => {})
if (renderer) renderer.dispose()
})
- 组件卸载时移除事件监听,释放WebGL资源。
9. 样式设置
html, body, #container {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
}
- 保证渲染区域全屏,无滚动条。
总结
本项目实现了three.js在Vue3中的标准集成方式,支持GLB模型加载、动画、交互、性能监控和自适应。
<template>
<div id="container" style="width: 100vw; height: 100vh;"></div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import Stats from 'stats.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
let mixer: THREE.AnimationMixer | undefined
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let stats: Stats
const clock = new THREE.Clock()
onMounted(() => {
const container = document.getElementById('container')
if (!container) return
stats = new Stats()
container.appendChild(stats.dom)
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
container.appendChild(renderer.domElement)
const pmremGenerator = new THREE.PMREMGenerator(renderer)
const scene = new THREE.Scene()
scene.background = new THREE.Color(0xbfe3dd)
scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture
// 先初始化相机在原点
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 0, 5)
controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0, 0)
controls.update()
controls.enablePan = false
controls.enableDamping = true
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/jsm/libs/draco/gltf/')
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
loader.load('/models/gltf/LittlestTokyo.glb', function (gltf) {
const model = gltf.scene
// 计算包围盒
const box = new THREE.Box3().setFromObject(model)
const size = new THREE.Vector3()
box.getSize(size)
const center = new THREE.Vector3()
box.getCenter(center)
// 居中
model.position.sub(center)
scene.add(model)
// 自动设置相机距离
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))
cameraZ *= 2.0 // 稍微拉远一点
camera.position.set(0, 0, cameraZ)
camera.near = cameraZ / 100
camera.far = cameraZ * 100
camera.updateProjectionMatrix()
controls.target.set(0, 0, 0)
controls.update()
mixer = new THREE.AnimationMixer(model)
if (gltf.animations && gltf.animations.length > 0) {
mixer.clipAction(gltf.animations[0]).play()
}
renderer.setAnimationLoop(animate)
}, undefined, function (e) {
console.error(e)
})
window.addEventListener('resize', onWindowResize)
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
}
function animate() {
const delta = clock.getDelta()
if (mixer) mixer.update(delta)
controls.update()
stats.update()
renderer.render(scene, camera)
}
})
onUnmounted(() => {
window.removeEventListener('resize', () => {})
if (renderer) renderer.dispose()
})
</script>
<style>
html, body, #container {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>