Vue 3 与 Three.js 3D 应用
1. 核心概念与概述
1.1 Three.js 简介
Three.js 是一个基于 WebGL 的 JavaScript 3D 库,它提供了创建和展示 3D 图形的高级 API,使开发者能够在浏览器中轻松创建复杂的 3D 场景和交互效果,而无需直接编写底层的 WebGL 代码。
1.2 Three.js 主要特性
- 跨浏览器兼容:支持所有现代浏览器
- 丰富的几何体库:内置多种 3D 几何体
- 材质系统:支持多种材质和纹理
- 光照系统:支持环境光、方向光、点光源等多种光源
- 相机系统:支持透视相机和正交相机
- 动画系统:支持关键帧动画和骨骼动画
- 物理引擎集成:可与 Cannon.js、ammo.js 等物理引擎集成
- VR/AR 支持:支持 WebXR API
1.3 Vue 3 与 Three.js 集成优势
- 响应式数据:Vue 3 的响应式系统可轻松管理 3D 场景状态
- 组合式 API:便于封装 Three.js 逻辑为可复用的组合式函数
- 组件化设计:适合构建复杂的 3D 交互界面
- TypeScript 支持:提供更好的类型安全性
- 生态系统集成:可与 Vue 生态中的其他库无缝集成
2. 核心知识与实现
2.1 Three.js 基础架构
Three.js 场景由以下核心组件组成:
- 场景(Scene):3D 空间的容器
- 相机(Camera):决定观察者的视角
- 渲染器(Renderer):将 3D 场景渲染到 2D 画布
- 几何体(Geometry):定义 3D 对象的形状
- 材质(Material):定义 3D 对象的外观
- 网格(Mesh):几何体和材质的组合
- 光源(Light):照亮场景中的对象
- 控制器(Controls):提供交互控制
2.2 Vue 3 项目中集成 Three.js
2.2.1 项目初始化
npm create vite@latest threejs-demo -- --template vue-ts
cd threejs-demo
npm install three @types/three2.2.2 实现基本的 3D 场景组件
<template>
<div class="threejs-container">
<div ref="canvasContainer" class="canvas-container"></div>
<div class="controls">
<button @click="toggleRotation">
{{ isRotating ? '停止旋转' : '开始旋转' }}
</button>
<button @click="changeColor">改变颜色</button>
<input
type="range"
min="0"
max="10"
step="0.1"
v-model.number="scale"
@input="updateScale"
/>
<span>缩放: {{ scale.toFixed(1) }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
const canvasContainer = ref<HTMLElement | null>(null)
const isRotating = ref(true)
const scale = ref(1)
// Three.js 核心对象
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let cube: THREE.Mesh
let animationId: number
// 初始化 Three.js 场景
const initScene = () => {
if (!canvasContainer.value) return
// 创建场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0xf0f0f0)
// 创建相机
camera = new THREE.PerspectiveCamera(
75,
canvasContainer.value.clientWidth / canvasContainer.value.clientHeight,
0.1,
1000
)
camera.position.z = 5
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(canvasContainer.value.clientWidth, canvasContainer.value.clientHeight)
canvasContainer.value.appendChild(renderer.domElement)
// 添加轨道控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(1, 1, 1)
scene.add(directionalLight)
// 创建几何体
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({ color: 0x42b983 })
cube = new THREE.Mesh(geometry, material)
scene.add(cube)
// 添加网格辅助线
const gridHelper = new THREE.GridHelper(10, 10)
scene.add(gridHelper)
// 添加坐标轴辅助线
const axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)
// 开始动画循环
animate()
// 监听窗口大小变化
window.addEventListener('resize', onWindowResize)
}
// 动画循环
const animate = () => {
animationId = requestAnimationFrame(animate)
// 旋转立方体
if (isRotating.value) {
cube.rotation.x += 0.01
cube.rotation.y += 0.01
}
// 更新控制器
controls.update()
// 渲染场景
renderer.render(scene, camera)
}
// 窗口大小变化处理
const onWindowResize = () => {
if (!canvasContainer.value) return
camera.aspect = canvasContainer.value.clientWidth / canvasContainer.value.clientHeight
camera.updateProjectionMatrix()
renderer.setSize(canvasContainer.value.clientWidth, canvasContainer.value.clientHeight)
}
// 切换旋转状态
const toggleRotation = () => {
isRotating.value = !isRotating.value
}
// 改变立方体颜色
const changeColor = () => {
const material = cube.material as THREE.MeshStandardMaterial
material.color.set(Math.random() * 0xffffff)
}
// 更新缩放
const updateScale = () => {
cube.scale.set(scale.value, scale.value, scale.value)
}
// 组件挂载时初始化
onMounted(() => {
initScene()
})
// 组件卸载时清理资源
onUnmounted(() => {
cancelAnimationFrame(animationId)
window.removeEventListener('resize', onWindowResize)
renderer.dispose()
if (canvasContainer.value && renderer.domElement) {
canvasContainer.value.removeChild(renderer.domElement)
}
})
</script>
<style scoped>
.threejs-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
.canvas-container {
flex: 1;
width: 100%;
overflow: hidden;
}
.controls {
display: flex;
gap: 15px;
padding: 20px;
background: #f5f5f5;
align-items: center;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background: #42b983;
color: white;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
button:hover {
background: #35495e;
}
input[type="range"] {
width: 200px;
}
</style>2.3 加载外部 3D 模型
Three.js 支持加载多种格式的 3D 模型,如 GLTF、OBJ、FBX 等。以下是加载 GLTF 模型的示例:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
// 加载 GLTF 模型
const loader = new GLTFLoader()
loader.load(
'/models/model.gltf',
(gltf) => {
const model = gltf.scene
scene.add(model)
// 对模型进行操作
},
(xhr) => {
console.log((xhr.loaded / xhr.total * 100) + '% loaded')
},
(error) => {
console.error('Error loading model:', error)
}
)2.4 使用组合式函数封装 Three.js 逻辑
将 Three.js 逻辑封装为可复用的组合式函数:
// composables/useThreeScene.ts
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
export function useThreeScene(containerRef: any) {
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let animationId: number
const init = () => {
if (!containerRef.value) return
// 创建场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0xf0f0f0)
// 创建相机
camera = new THREE.PerspectiveCamera(
75,
containerRef.value.clientWidth / containerRef.value.clientHeight,
0.1,
1000
)
camera.position.z = 5
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
containerRef.value.appendChild(renderer.domElement)
// 窗口大小变化处理
const onWindowResize = () => {
if (!containerRef.value) return
camera.aspect = containerRef.value.clientWidth / containerRef.value.clientHeight
camera.updateProjectionMatrix()
renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
}
window.addEventListener('resize', onWindowResize)
// 动画循环
const animate = () => {
animationId = requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
}
const addObject = (object: THREE.Object3D) => {
scene.add(object)
}
const removeObject = (object: THREE.Object3D) => {
scene.remove(object)
}
const cleanup = () => {
cancelAnimationFrame(animationId)
renderer.dispose()
if (containerRef.value && renderer.domElement) {
containerRef.value.removeChild(renderer.domElement)
}
}
onMounted(() => {
init()
})
onUnmounted(() => {
cleanup()
})
return {
scene,
camera,
renderer,
addObject,
removeObject
}
}2.5 实现粒子系统
Three.js 支持创建复杂的粒子系统,用于实现烟雾、火焰、星空等效果:
// 创建粒子系统
const createParticleSystem = () => {
const particleCount = 1000
const geometry = new THREE.BufferGeometry()
const positions = new Float32Array(particleCount * 3)
const colors = new Float32Array(particleCount * 3)
// 初始化粒子位置和颜色
for (let i = 0; i < particleCount * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 10
positions[i + 1] = (Math.random() - 0.5) * 10
positions[i + 2] = (Math.random() - 0.5) * 10
colors[i] = Math.random()
colors[i + 1] = Math.random()
colors[i + 2] = Math.random()
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))
const material = new THREE.PointsMaterial({
size: 0.1,
vertexColors: true,
transparent: true,
opacity: 0.8
})
const particles = new THREE.Points(geometry, material)
scene.add(particles)
return particles
}3. 最佳实践
3.1 性能优化
- 使用 BufferGeometry:相比 Geometry,BufferGeometry 性能更优
- 减少绘制调用:合并多个网格为一个,使用 InstancedMesh 渲染大量相同对象
- 合理设置材质:避免过度使用透明材质和复杂着色器
- 优化纹理:使用压缩纹理,合理设置纹理大小
- 使用 LOD(Level of Detail):根据距离显示不同细节的模型
- 实现视锥体剔除:只渲染相机可见范围内的对象
3.2 内存管理
- 及时释放资源:使用
dispose()方法释放不再使用的几何体、材质、纹理等 - 避免内存泄漏:在组件卸载时清理 Three.js 资源
- 使用对象池:对于频繁创建和销毁的对象,使用对象池复用
3.3 交互设计
- 使用 OrbitControls:提供直观的 3D 场景导航
- 实现射线检测:用于对象选择和交互
- 提供清晰的控制界面:便于用户操作 3D 场景
- 优化移动端体验:考虑触摸设备的交互特点
3.4 代码组织
- 模块化设计:将 Three.js 逻辑封装为独立的模块或组合式函数
- 类型安全:使用 TypeScript 提供更好的类型检查
- 注释文档:为复杂的 3D 逻辑添加注释
- 代码复用:抽象通用的 3D 功能为可复用组件
4. 常见问题与解决方案
4.1 性能问题
问题:3D 场景渲染卡顿
解决方案:
- 减少场景中的对象数量
- 优化几何体和材质
- 使用 WebGLRenderer 的性能监控
- 实现按需加载和 LOD
4.2 内存泄漏
问题:长时间运行后内存占用过高
解决方案:
- 及时释放不再使用的资源
- 在组件卸载时清理 Three.js 实例
- 使用浏览器的开发者工具进行内存分析
4.3 跨域问题
问题:加载外部模型或纹理时出现跨域错误
解决方案:
- 配置服务器支持 CORS
- 使用本地开发服务器
- 将资源转换为 Data URL
4.4 移动端兼容性
问题:在移动端浏览器上无法正常显示或性能不佳
解决方案:
- 优化场景复杂度
- 使用 WebGL 2.0 特性(如果支持)
- 实现自适应分辨率
- 考虑使用预渲染或混合渲染方案
5. 进一步学习资源
5.1 官方文档
5.2 学习教程
5.3 开源项目
5.4 工具与资源
- Blender:3D 建模软件
- Sketchfab:3D 模型资源平台
- Poly Haven:免费纹理和 HDRI 资源
- GLTF Validator:GLTF 模型验证工具
6. 代码优化与性能提升
6.1 使用 InstancedMesh 渲染大量相同对象
对于需要渲染大量相同对象的场景,使用 InstancedMesh 可以显著提高性能:
// 创建 InstancedMesh
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1)
const material = new THREE.MeshStandardMaterial({ color: 0x42b983 })
const instanceCount = 1000
const instancedMesh = new THREE.InstancedMesh(geometry, material, instanceCount)
// 设置实例矩阵
const matrix = new THREE.Matrix4()
for (let i = 0; i < instanceCount; i++) {
matrix.setPosition(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10
)
instancedMesh.setMatrixAt(i, matrix)
}
scene.add(instancedMesh)6.2 使用 Web Workers 处理复杂计算
将复杂的 3D 计算放在 Web Workers 中,避免阻塞主线程:
// worker.js
self.onmessage = (event) => {
// 处理复杂计算
const result = performComplexCalculation(event.data)
self.postMessage(result)
}
// 主线程中使用
const worker = new Worker('/worker.js')
worker.onmessage = (event) => {
// 使用计算结果更新 3D 场景
updateScene(event.data)
}
worker.postMessage({ /* 计算参数 */ })6.3 实现按需加载
对于大型 3D 模型和纹理,实现按需加载可以提高初始加载速度:
// 使用动态导入加载 Three.js 组件
const loadModel = async () => {
const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js')
const loader = new GLTFLoader()
// 加载模型
const gltf = await new Promise((resolve, reject) => {
loader.load(
'/models/large-model.gltf',
resolve,
undefined,
reject
)
})
scene.add(gltf.scene)
}6.4 使用 GPU 加速的后处理效果
Three.js 提供了后处理库,可以实现各种视觉效果:
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { BloomEffect, EffectPass } from 'three/examples/jsm/postprocessing/EffectComposer.js'
// 创建后处理渲染器
const composer = new EffectComposer(renderer)
// 添加渲染通道
const renderPass = new RenderPass(scene, camera)
composer.addPass(renderPass)
// 添加 bloom 效果通道
const bloomEffect = new BloomEffect()
const effectPass = new EffectPass(camera, bloomEffect)
composer.addPass(effectPass)
// 在动画循环中使用 composer 渲染
const animate = () => {
animationId = requestAnimationFrame(animate)
composer.render()
}7. 实践练习
7.1 基础练习:3D 立方体场景
- 创建一个 Vue 3 项目,集成 Three.js
- 实现一个包含旋转立方体的 3D 场景
- 添加光照和网格辅助线
- 实现基本的交互控制
7.2 进阶练习:3D 模型加载与动画
- 加载外部 GLTF 模型
- 实现模型动画控制
- 添加相机控制和交互
- 实现模型材质和纹理调整
7.3 高级练习:粒子系统与特效
- 创建一个粒子系统模拟星空或火焰效果
- 实现粒子的物理运动
- 添加交互控制
- 优化粒子系统性能
7.4 综合练习:3D 交互应用
- 实现一个完整的 3D 交互应用
- 包含模型加载、动画、交互等功能
- 优化性能和用户体验
- 添加响应式设计
8. 总结
Three.js 是一个强大的 3D 库,结合 Vue 3 的响应式系统和组件化设计,可以创建出丰富多样的 3D 应用。通过合理的架构设计和性能优化,可以实现高质量、高性能的 3D 交互体验。
在实际项目中,需要根据具体需求选择合适的技术方案,考虑性能、兼容性、用户体验等因素。同时,要注意及时更新 Three.js 版本,利用最新的特性和优化。
随着 WebGL 技术的不断发展和硬件性能的提升,浏览器中的 3D 应用将越来越普及,掌握 Vue 3 与 Three.js 的集成将为开发者打开更多的可能性,无论是游戏开发、数据可视化、产品展示还是虚拟实境,都可以利用这项技术创造出令人惊叹的效果。