Vue 3 + Three.js r164 项目实战:5步封装可复用3D场景组件(含响应式)

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 vitelatest 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.3Three.js r164TypeScript 5.02. 核心组件架构设计一个优秀的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 langts import { defineProps, onMounted, onUnmounted, ref } from vue import * as THREE from three import type { ThreeSceneProps } from ./types/config const props withDefaults(definePropsThreeSceneProps(), { 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 refHTMLCanvasElement | null(null) // 其他逻辑... /script template div classscene-container canvas refcanvasRef/canvas /div /template style scoped .scene-container { position: relative; overflow: hidden; } canvas { display: block; width: v-bind(props.width); height: v-bind(props.height); } /style3. 响应式场景管理现代前端应用需要适应各种屏幕尺寸我们的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): PromiseTHREE.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): PromiseTHREE.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 refTHREE.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: RefHTMLCanvasElement, options: RendererOptions) { const renderer shallowRefTHREE.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 langts 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 refHTMLCanvasElement | 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 langts 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 classproduct-viewer ThreeScene v-if!loading !error :auto-rotatetrue slot namecontrols/slot /ThreeScene div v-ifloading classloading加载中.../div div v-iferror classerror加载失败: {{ 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 }