Vue 3 + Three.js r164 项目实战:5步封装可复用3D场景组件(含响应式)
Vue 3 + Three.js r164 工程化实战:构建高复用3D场景组件的5个关键步骤
在当今前端开发领域,3D可视化需求正以前所未有的速度增长。从电商产品展示到数据可视化大屏,从虚拟现实应用到游戏开发,Three.js作为WebGL的友好封装库,已经成为实现这些需求的首选工具。本文将带您深入探索如何基于Vue 3的组合式API和TypeScript,构建一个高度可复用的3D场景组件。
1. 环境准备与项目初始化
在开始之前,我们需要确保开发环境配置正确。与传统的Vue 2项目不同,现代Vue 3项目通常采用Vite作为构建工具,它能提供更快的冷启动和热更新速度。
首先创建一个基于Vite的Vue 3项目:
npm create vite@latest vue-three-project --template vue-ts cd vue-three-project npm install three @types/three项目结构建议如下:
src/ ├── components/ │ └── ThreeScene/ │ ├── ThreeScene.vue # 3D场景主组件 │ ├── composables/ # 组合式函数 │ │ ├── useRenderer.ts │ │ ├── useResize.ts │ └── types/ │ └── config.ts # 类型定义 ├── App.vue └── main.ts关键依赖版本要求:
- Vue 3.3+
- Three.js r164+
- TypeScript 5.0+
2. 核心组件架构设计
一个优秀的3D场景组件应该具备清晰的接口定义和良好的封装性。我们将使用Vue 3的defineComponent和TypeScript来确保类型安全。
首先定义组件Props类型:
// src/components/ThreeScene/types/config.ts export interface ThreeSceneProps { width?: number | string; // 画布宽度 height?: number | string; // 画布高度 backgroundColor?: string; // 背景色 cameraOptions?: { fov?: number; // 视野角度 near?: number; // 近裁剪面 far?: number; // 远裁剪面 position?: THREE.Vector3; // 相机位置 }; antialias?: boolean; // 是否开启抗锯齿 autoRotate?: boolean; // 是否自动旋转 }组件基础结构:
<!-- src/components/ThreeScene/ThreeScene.vue --> <script setup lang="ts"> import { defineProps, onMounted, onUnmounted, ref } from 'vue' import * as THREE from 'three' import type { ThreeSceneProps } from './types/config' const props = withDefaults(defineProps<ThreeSceneProps>(), { width: '100%', height: '100%', backgroundColor: '#000000', cameraOptions: () => ({ fov: 75, near: 0.1, far: 1000, position: new THREE.Vector3(0, 0, 5) }), antialias: true, autoRotate: false }) const canvasRef = ref<HTMLCanvasElement | null>(null) // 其他逻辑... </script> <template> <div class="scene-container"> <canvas ref="canvasRef"></canvas> </div> </template> <style scoped> .scene-container { position: relative; overflow: hidden; } canvas { display: block; width: v-bind('props.width'); height: v-bind('props.height'); } </style>3. 响应式场景管理
现代前端应用需要适应各种屏幕尺寸,我们的3D场景也必须具备响应式能力。以下是实现响应式场景的关键步骤:
3.1 初始化Three.js核心要素
// 在ThreeScene.vue的setup函数中 const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera( props.cameraOptions.fov, window.innerWidth / window.innerHeight, props.cameraOptions.near, props.cameraOptions.far ) camera.position.copy(props.cameraOptions.position) const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.value!, antialias: props.antialias, alpha: true }) renderer.setPixelRatio(window.devicePixelRatio) renderer.setClearColor(props.backgroundColor)3.2 实现响应式调整
const handleResize = () => { if (!canvasRef.value) return const width = canvasRef.value.clientWidth const height = canvasRef.value.clientHeight camera.aspect = width / height camera.updateProjectionMatrix() renderer.setSize(width, height, false) } onMounted(() => { window.addEventListener('resize', handleResize) handleResize() // 初始调整 }) onUnmounted(() => { window.removeEventListener('resize', handleResize) })3.3 动画循环管理
const clock = new THREE.Clock() let animationId: number const startAnimationLoop = () => { const animate = () => { animationId = requestAnimationFrame(animate) const delta = clock.getDelta() // 这里可以添加自定义动画逻辑 if (props.autoRotate) { scene.rotation.y += delta * 0.5 } renderer.render(scene, camera) } animate() } onMounted(() => { startAnimationLoop() }) onUnmounted(() => { cancelAnimationFrame(animationId) })4. 高级功能封装
4.1 轨道控制器集成
轨道控制器(OrbitControls)是Three.js中常用的交互控制工具,允许用户通过鼠标旋转、平移和缩放场景。
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' const initControls = () => { const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true controls.dampingFactor = 0.05 return { controls, update: () => controls.update() } } // 在动画循环中调用controls.update()4.2 资源加载与管理
3D场景通常需要加载各种资源(纹理、模型等),我们需要实现统一的加载和管理机制。
const loadTexture = (url: string): Promise<THREE.Texture> => { return new Promise((resolve, reject) => { const loader = new THREE.TextureLoader() loader.load( url, texture => resolve(texture), undefined, error => reject(error) ) }) } const loadModel = async (url: string): Promise<THREE.Group> => { const loader = new THREE.GLTFLoader() const gltf = await loader.loadAsync(url) return gltf.scene }4.3 性能优化技巧
| 优化技术 | 实现方式 | 适用场景 |
|---|---|---|
| InstancedMesh | 使用THREE.InstancedMesh | 大量相同几何体 |
| LOD (Level of Detail) | 创建不同细节级别的模型 | 复杂场景 |
| Frustum Culling | 启用frustumCulled属性 | 大型开放世界 |
| GPU加速 | 使用WebGLRenderer的powerPreference | 所有3D场景 |
// 实例化网格示例 const createInstancedCubes = (count: number) => { const geometry = new THREE.BoxGeometry(1, 1, 1) const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) const mesh = new THREE.InstancedMesh(geometry, material, count) const matrix = new THREE.Matrix4() for (let i = 0; i < count; i++) { matrix.setPosition( Math.random() * 20 - 10, Math.random() * 20 - 10, Math.random() * 20 - 10 ) mesh.setMatrixAt(i, matrix) } scene.add(mesh) }5. 组件化与复用实践
5.1 场景对象管理
使用Vue的响应式系统管理3D对象:
const objects = ref<THREE.Object3D[]>([]) const addObject = (obj: THREE.Object3D) => { scene.add(obj) objects.value.push(obj) } const removeObject = (obj: THREE.Object3D) => { scene.remove(obj) objects.value = objects.value.filter(o => o !== obj) } const clearScene = () => { objects.value.forEach(obj => scene.remove(obj)) objects.value = [] }5.2 组合式函数抽象
将Three.js的核心功能抽象为可复用的组合式函数:
// src/components/ThreeScene/composables/useRenderer.ts export function useRenderer(canvas: Ref<HTMLCanvasElement>, options: RendererOptions) { const renderer = shallowRef<THREE.WebGLRenderer>() onMounted(() => { renderer.value = new THREE.WebGLRenderer({ canvas: canvas.value!, antialias: options.antialias, alpha: options.alpha }) renderer.value.setPixelRatio(window.devicePixelRatio) renderer.value.setSize(canvas.value!.clientWidth, canvas.value!.clientHeight) }) onUnmounted(() => { renderer.value?.dispose() }) return { renderer } }5.3 完整组件示例
<script setup lang="ts"> import { ref, onMounted, onUnmounted, watch } from 'vue' import * as THREE from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' import { useRenderer } from './composables/useRenderer' import { useResize } from './composables/useResize' const props = defineProps({ // ...之前的props定义 }) const canvasRef = ref<HTMLCanvasElement | null>(null) const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera( props.cameraOptions.fov, window.innerWidth / window.innerHeight, props.cameraOptions.near, props.cameraOptions.far ) const { renderer } = useRenderer(canvasRef, { antialias: props.antialias, alpha: true }) const { handleResize } = useResize(canvasRef, camera, renderer) // 初始化场景内容 const initScene = () => { // 添加光源 const ambientLight = new THREE.AmbientLight(0x404040) scene.add(ambientLight) const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5) directionalLight.position.set(1, 1, 1) scene.add(directionalLight) // 添加辅助工具 const axesHelper = new THREE.AxesHelper(5) scene.add(axesHelper) } // 动画循环 const animate = () => { requestAnimationFrame(animate) renderer.value?.render(scene, camera) } onMounted(() => { initScene() animate() window.addEventListener('resize', handleResize) }) onUnmounted(() => { window.removeEventListener('resize', handleResize) // 清理资源 scene.traverse(child => { if (child instanceof THREE.Mesh) { child.geometry.dispose() if (Array.isArray(child.material)) { child.material.forEach(m => m.dispose()) } else { child.material.dispose() } } }) }) </script>进阶技巧与性能优化
内存管理与资源释放
Three.js创建的几何体、材质和纹理都需要手动释放内存:
const disposeResources = () => { scene.traverse(child => { if (child instanceof THREE.Mesh) { child.geometry.dispose() if (Array.isArray(child.material)) { child.material.forEach(m => m.dispose()) } else { child.material.dispose() } } else if (child instanceof THREE.Light) { child.dispose() } }) renderer.value?.dispose() renderer.value?.forceContextLoss() }响应式属性处理
使用watch监听props变化并更新场景:
watch(() => props.backgroundColor, (newColor) => { if (renderer.value) { renderer.value.setClearColor(newColor) } }) watch(() => props.autoRotate, (enabled) => { // 更新自动旋转状态 })性能监控
集成stats.js进行性能监控:
import Stats from 'stats.js' const stats = new Stats() stats.showPanel(0) // 0: fps, 1: ms, 2: mb onMounted(() => { document.body.appendChild(stats.dom) const animate = () => { stats.begin() // 渲染逻辑... stats.end() requestAnimationFrame(animate) } animate() }) onUnmounted(() => { document.body.removeChild(stats.dom) })实际应用案例
商品3D展示组件
<script setup lang="ts"> import { ref, onMounted } from 'vue' import { useThreeScene } from './ThreeScene/useThreeScene' const { scene, loadModel } = useThreeScene() const loading = ref(true) const error = ref(null) onMounted(async () => { try { const productModel = await loadModel('/models/product.glb') productModel.scale.set(0.5, 0.5, 0.5) scene.add(productModel) } catch (err) { error.value = err } finally { loading.value = false } }) </script> <template> <div class="product-viewer"> <ThreeScene v-if="!loading && !error" :auto-rotate="true"> <slot name="controls"></slot> </ThreeScene> <div v-if="loading" class="loading">加载中...</div> <div v-if="error" class="error">加载失败: {{ error.message }}</div> </div> </template>数据可视化大屏组件
const createBarChart = (data: number[], colors: string[]) => { const group = new THREE.Group() data.forEach((value, i) => { const geometry = new THREE.BoxGeometry(1, value, 1) const material = new THREE.MeshBasicMaterial({ color: new THREE.Color(colors[i]) }) const bar = new THREE.Mesh(geometry, material) bar.position.x = i * 2 - data.length bar.position.y = value / 2 group.add(bar) }) return group }