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/three

2.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 工具与资源

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 立方体场景

  1. 创建一个 Vue 3 项目,集成 Three.js
  2. 实现一个包含旋转立方体的 3D 场景
  3. 添加光照和网格辅助线
  4. 实现基本的交互控制

7.2 进阶练习:3D 模型加载与动画

  1. 加载外部 GLTF 模型
  2. 实现模型动画控制
  3. 添加相机控制和交互
  4. 实现模型材质和纹理调整

7.3 高级练习:粒子系统与特效

  1. 创建一个粒子系统模拟星空或火焰效果
  2. 实现粒子的物理运动
  3. 添加交互控制
  4. 优化粒子系统性能

7.4 综合练习:3D 交互应用

  1. 实现一个完整的 3D 交互应用
  2. 包含模型加载、动画、交互等功能
  3. 优化性能和用户体验
  4. 添加响应式设计

8. 总结

Three.js 是一个强大的 3D 库,结合 Vue 3 的响应式系统和组件化设计,可以创建出丰富多样的 3D 应用。通过合理的架构设计和性能优化,可以实现高质量、高性能的 3D 交互体验。

在实际项目中,需要根据具体需求选择合适的技术方案,考虑性能、兼容性、用户体验等因素。同时,要注意及时更新 Three.js 版本,利用最新的特性和优化。

随着 WebGL 技术的不断发展和硬件性能的提升,浏览器中的 3D 应用将越来越普及,掌握 Vue 3 与 Three.js 的集成将为开发者打开更多的可能性,无论是游戏开发、数据可视化、产品展示还是虚拟实境,都可以利用这项技术创造出令人惊叹的效果。

« 上一篇 Vue 3与WebRTC实时通信 - 音视频应用全栈解决方案 下一篇 » Vue 3与D3.js数据可视化 - 交互式数据图表全栈解决方案