用Three.js r158 + Vue3 从零搭建一个可交互的3D集装箱货柜(附完整源码)
用Three.js r158 Vue3构建可交互3D集装箱货柜全流程指南在智慧物流和仓储管理领域3D可视化技术正成为提升操作效率和用户体验的关键工具。本文将带您从零开始使用Three.js最新版本(r158)与Vue3框架构建一个功能完整的可交互集装箱货柜模型。不同于基础教程我们将重点关注工程化实践包括模块化设计、性能优化以及与Vue响应式系统的深度集成。1. 环境准备与项目初始化1.1 创建Vue3项目基础结构首先确保已安装Node.js 16版本然后通过Vite快速初始化项目npm create vitelatest 3d-container-demo --template vue-ts cd 3d-container-demo npm install three0.158.0 types/three --save项目目录结构建议如下/src /assets /textures container.jpg label.png /components Container3D.vue /composables useThreeScene.ts /utils textureLoader.ts1.2 配置TypeScript支持在tsconfig.json中添加Three.js类型声明{ compilerOptions: { types: [three] } }2. 核心场景搭建2.1 模块化Three.js初始化创建useThreeScene组合式函数处理基础场景// composables/useThreeScene.ts import * as THREE from three; import { OrbitControls } from three/examples/jsm/controls/OrbitControls; export default function useThreeScene(containerRef: RefHTMLElement | null) { const scene new THREE.Scene(); scene.background new THREE.Color(0xf0f0f0); // 自适应相机设置 const camera new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.set(5, 5, 5); // 渲染器配置 const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); // 响应式调整 const updateSize () { if (!containerRef.value) return; const width containerRef.value.clientWidth; const height containerRef.value.clientHeight; renderer.setSize(width, height); camera.aspect width / height; camera.updateProjectionMatrix(); }; // 控制器添加 const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; return { scene, camera, renderer, controls, updateSize }; }2.2 光照系统最佳实践集装箱场景需要特殊的光照配置以展现材质细节function setupLights(scene: THREE.Scene) { // 环境光提供基础照明 const ambientLight new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); // 方向光模拟太阳光 const directionalLight new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 20, 10); directionalLight.castShadow true; directionalLight.shadow.mapSize.width 2048; directionalLight.shadow.mapSize.height 2048; scene.add(directionalLight); // 补充点光源增强细节 const pointLight new THREE.PointLight(0xffffff, 0.5, 100); pointLight.position.set(0, 10, 0); scene.add(pointLight); }3. 集装箱模型实现3.1 纹理加载优化方案创建纹理加载工具函数避免重复加载// utils/textureLoader.ts const textureCache new Mapstring, THREE.Texture(); export async function loadTexture( path: string, loader new THREE.TextureLoader() ): PromiseTHREE.Texture { if (textureCache.has(path)) { return textureCache.get(path)!; } return new Promise((resolve, reject) { loader.load( path, (texture) { texture.encoding THREE.sRGBEncoding; textureCache.set(path, texture); resolve(texture); }, undefined, reject ); }); }3.2 集装箱几何体构建实现带纹理的集装箱主模型async function createContainer() { const geometry new THREE.BoxGeometry(4, 2.5, 2); // 加载各面纹理 const materials await Promise.all([ loadTexture(/textures/container_side.jpg).then(texture new THREE.MeshStandardMaterial({ map: texture }) ), // 其他面材料类似加载... ]); const container new THREE.Mesh(geometry, materials); container.userData { type: container }; return container; }3.3 标签与交互元素添加创建可点击的标签平面async function createLabel() { const geometry new THREE.PlaneGeometry(0.8, 0.4); const texture await loadTexture(/textures/label.png); const material new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide }); const label new THREE.Mesh(geometry, material); label.position.set(1.5, 1, 1.01); label.userData { type: label, clickable: true }; return label; }4. Vue3组件集成4.1 响应式场景管理在Vue组件中集成Three.js场景script setup langts import { ref, onMounted, onUnmounted } from vue; import useThreeScene from /composables/useThreeScene; import { createContainer, createLabel } from /models/container; const containerRef refHTMLElement | null(null); const { scene, camera, renderer, controls, updateSize } useThreeScene(containerRef); const animate () { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); }; onMounted(async () { if (!containerRef.value) return; containerRef.value.appendChild(renderer.domElement); window.addEventListener(resize, updateSize); // 添加模型 scene.add(await createContainer()); scene.add(await createLabel()); animate(); }); onUnmounted(() { window.removeEventListener(resize, updateSize); }); /script template div refcontainerRef classcontainer-3d / /template style scoped .container-3d { width: 100%; height: 100vh; position: relative; } /style4.2 交互事件处理实现点击事件与Vue状态联动function setupInteractions( scene: THREE.Scene, camera: THREE.Camera, renderer: THREE.WebGLRenderer, emit: (event: string, ...args: any[]) void ) { const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); const onClick (event: MouseEvent) { mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(scene.children, true); for (const item of intersects) { if (item.object.userData.clickable) { emit(object-clicked, item.object.userData); break; } } }; renderer.domElement.addEventListener(click, onClick); return () { renderer.domElement.removeEventListener(click, onClick); }; }5. 高级优化技巧5.1 性能优化策略按需渲染仅在场景变化时渲染实例化渲染对相同集装箱模型使用InstancedMeshLOD管理根据距离切换模型细节级别// 实例化渲染示例 function createContainerInstances(count: number) { const geometry new THREE.BoxGeometry(4, 2.5, 2); const material new THREE.MeshStandardMaterial({ 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() * 10, 0, Math.random() * 10); mesh.setMatrixAt(i, matrix); } return mesh; }5.2 内存管理在Vue组件卸载时正确释放资源onUnmounted(() { // 清理纹理缓存 textureCache.forEach(texture texture.dispose()); textureCache.clear(); // 清理几何体和材料 scene.traverse(object { if (object instanceof THREE.Mesh) { object.geometry.dispose(); if (Array.isArray(object.material)) { object.material.forEach(m m.dispose()); } else { object.material.dispose(); } } }); });6. 工程化扩展6.1 状态管理集成将Three.js对象状态与Pinia/Vuex同步// stores/containerStore.ts import { defineStore } from pinia; export const useContainerStore defineStore(container, { state: () ({ selectedContainer: null as string | null, containers: [] as Array{ id: string; position: [number, number, number] } }), actions: { selectContainer(id: string) { this.selectedContainer id; } } });6.2 可配置化组件设计通过Props控制集装箱参数script setup langts const props defineProps({ size: { type: Object as PropType{ width: number; height: number; depth: number }, default: () ({ width: 4, height: 2.5, depth: 2 }) }, texturePaths: { type: Object as PropType{ front: string; back: string; left: string; right: string; top: string; bottom: string; }, required: true } }); /script7. 调试与问题排查7.1 常用调试工具Three.js场景检查器import { GUI } from three/examples/jsm/libs/lil-gui.module.min; function setupDebugUI(camera: THREE.Camera, lights: THREE.Light[]) { const gui new GUI(); const cameraFolder gui.addFolder(Camera); cameraFolder.add(camera.position, x, -10, 10); cameraFolder.add(camera.position, y, -10, 10); cameraFolder.add(camera.position, z, -10, 10); cameraFolder.open(); }性能监测import Stats from three/examples/jsm/libs/stats.module; const stats Stats(); document.body.appendChild(stats.dom); const animate () { requestAnimationFrame(animate); stats.update(); // ...其他渲染逻辑 };7.2 常见问题解决方案纹理不显示检查图片路径和跨域问题确认纹理加载完成后再渲染场景性能卡顿减少实时阴影计算使用BufferGeometry代替Geometry实现场景分块加载内存泄漏及时销毁不再需要的纹理和几何体移除所有事件监听器