大家好,我是长期关注前端图形技术的CSDN博主。在Web3D开发中,WebGL和WebGPU作为浏览器端两大图形API,经常让开发者面临技术选型和性能优化的挑战。特别是随着Three.js等框架的普及,如何在实际项目中合理运用这些技术成为关键问题。本文将围绕WebGL/WebGPU的实战案例展开,通过六个典型场景的完整实现,帮助读者掌握从基础渲染到高级优化的全流程解决方案。
无论是刚接触Web3D的新手,还是有一定经验的开发者,都能从本文找到可直接复用的代码示例和工程实践。我们将重点解决Three.js项目中的常见痛点:模型加载优化、内存管理、交互功能实现等,并提供经过验证的避坑方案。
1. WebGL与WebGPU技术背景解析
1.1 WebGL技术特点与应用场景
WebGL是基于OpenGL ES的Web图形标准,允许在浏览器中实现硬件加速的3D渲染。它通过JavaScript API直接操作GPU,为网页游戏、数据可视化、在线展览等场景提供强大的图形能力。WebGL 1.0支持基本的3D渲染功能,WebGL 2.0则引入了更多高级特性如变换反馈、实例化渲染等。
在实际项目中,WebGL的优势在于兼容性广泛,几乎所有现代浏览器都支持WebGL 1.0。但其编程模型相对底层,开发者需要手动管理着色器、缓冲区等资源,这也是Three.js等封装框架流行的主要原因。
1.2 WebGPU的技术革新与优势
WebGPU是新一代Web图形API,旨在提供更接近现代GPU架构的编程模型。与WebGL相比,WebGPU具有更好的多线程支持、更高效的资源管理和更 predictable的性能表现。它采用WGSL着色语言,支持计算着色器,为复杂的图形计算和通用GPU计算打开新局面。
目前WebGPU已在Chrome、Edge等浏览器中逐步支持,虽然兼容性不如WebGL,但在性能要求高的项目中有明显优势。特别是对于需要大量计算的任务,如物理模拟、实时光线追踪等,WebGPU能够提供数倍的性能提升。
1.3 Three.js框架的桥梁作用
Three.js作为最流行的Web3D库,在WebGL和WebGPU之间架起了桥梁。最新版本的Three.js已经支持WebGPU后端,开发者可以用相同的Three.js API同时 targeting两种底层API。这种设计让项目迁移更加平滑,也降低了学习成本。
对于大多数业务场景,建议优先使用Three.js进行开发,仅在性能瓶颈明显时考虑直接使用底层API。Three.js提供了丰富的材质系统、几何体工具和加载器,能够满足90%的Web3D需求。
2. 开发环境搭建与版本控制
2.1 基础环境配置
本文所有示例基于以下环境开发,建议读者配置相似环境以保证代码正常运行:
- 操作系统:Windows 10/11 或 macOS 12+
- 浏览器:Chrome 115+(支持WebGPU)或 Firefox 100+
- Node.js:18.0+(用于构建工具和本地服务器)
- 开发工具:VS Code 或 WebStorm
Three.js版本选择至关重要,本文使用r158版本,这是目前最稳定且功能完整的版本。避免使用过老的版本,以免缺失重要特性或存在已知bug。
2.2 项目初始化与依赖管理
创建新的Three.js项目时,推荐使用官方提供的构建工具。首先初始化npm项目:
mkdir threejs-project cd threejs-project npm init -y npm install three npm install --save-dev @types/three vite创建基础的HTML文件:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Three.js项目模板</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script type="module" src="/src/main.js"></script> </body> </html>2.3 WebGPU环境检测与回退策略
在实际项目中,需要检测浏览器对WebGPU的支持情况,并提供适当的回退方案:
// 检查WebGPU支持 async function checkWebGPUSupport() { if (!navigator.gpu) { console.warn('WebGPU不被支持,将回退到WebGL'); return false; } const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { console.warn('无法获取WebGPU适配器'); return false; } return true; } // 根据支持情况选择渲染器 async function createRenderer() { const webGPUSupported = await checkWebGPUSupport(); if (webGPUSupported) { // 使用WebGPU渲染器 const renderer = new THREE.WebGPURenderer({ antialias: true, alpha: true }); return renderer; } else { // 回退到WebGL渲染器 const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); return renderer; } }3. 模型加载与内存优化实战
3.1 模型格式选择与压缩策略
在WebGL环境下,模型加载的优化至关重要。针对网络热词中提到的压缩问题,这里给出具体解决方案:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; class OptimizedModelLoader { constructor() { this.gltfLoader = new GLTFLoader(); this.dracoLoader = new DRACOLoader(); // 配置DRACO解码器路径 this.dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/'); this.gltfLoader.setDRACOLoader(this.dracoLoader); } // 异步加载模型并返回Promise loadModel(url) { return new Promise((resolve, reject) => { this.gltfLoader.load( url, (gltf) => { // 模型加载成功后的优化处理 this.optimizeModel(gltf.scene); resolve(gltf); }, (progress) => { console.log(`加载进度: ${(progress.loaded / progress.total * 100).toFixed(2)}%`); }, (error) => { console.error('模型加载失败:', error); reject(error); } ); }); } // 模型优化处理 optimizeModel(scene) { scene.traverse((child) => { if (child.isMesh) { // 合并几何体减少draw call if (child.geometry) { child.geometry.computeVertexNormals(); } // 优化材质 if (child.material) { child.material.needsUpdate = true; } } }); } }3.2 内存管理与资源释放
WebGL应用常见的内存问题往往源于资源未及时释放。以下是完整的内存管理方案:
class MemoryManager { constructor() { this.textures = new Set(); this.geometries = new Set(); this.materials = new Set(); } // 注册资源以便统一管理 registerTexture(texture) { this.textures.add(texture); return texture; } registerGeometry(geometry) { this.geometries.add(geometry); return geometry; } registerMaterial(material) { this.materials.add(material); return material; } // 释放单个资源 disposeResource(resource) { if (resource && typeof resource.dispose === 'function') { resource.dispose(); } // 从对应的集合中移除 this.textures.delete(resource); this.geometries.delete(resource); this.materials.delete(resource); } // 批量释放资源 disposeAll() { this.textures.forEach(texture => this.disposeResource(texture)); this.geometries.forEach(geometry => this.disposeResource(geometry)); this.materials.forEach(material => this.disposeResource(material)); this.textures.clear(); this.geometries.clear(); this.materials.clear(); } // 内存使用情况监控 getMemoryUsage() { let totalMemory = 0; this.geometries.forEach(geometry => { if (geometry.attributes.position) { totalMemory += geometry.attributes.position.array.byteLength; } }); this.textures.forEach(texture => { if (texture.image) { totalMemory += texture.image.width * texture.image.height * 4; // 假设RGBA } }); return { textureCount: this.textures.size, geometryCount: this.geometries.size, materialCount: this.materials.size, estimatedMemory: totalMemory }; } }3.3 模型压缩格式实战对比
针对网络热词中提到的LZMA与LZ4压缩问题,这里给出具体测试代码:
// 测试不同压缩格式的性能影响 class CompressionTester { async testCompressionPerformance() { const testModelUrl = '/models/test-model.glb'; // 测试无压缩 console.time('无压缩加载'); await this.loadModel(testModelUrl); console.timeEnd('无压缩加载'); // 测试LZ4压缩 console.time('LZ4压缩加载'); await this.loadModel(testModelUrl + '?compression=lz4'); console.timeEnd('LZ4压缩加载'); // 内存使用对比 this.measureMemoryUsage(); } measureMemoryUsage() { if (performance.memory) { const usedJSHeapSize = performance.memory.usedJSHeapSize; const totalJSHeapSize = performance.memory.totalJSHeapSize; console.log(`内存使用: ${(usedJSHeapSize / 1024 / 1024).toFixed(2)}MB / ${(totalJSHeapSize / 1024 / 1024).toFixed(2)}MB`); } } // 实际项目中的压缩选择建议 getCompressionRecommendation() { return { recommendation: '在WebGL环境下优先使用LZ4压缩', reasons: [ 'LZ4解压速度快,内存占用低', '适合Web环境的流式解压需求', '与DRACO几何压缩配合效果更好', '避免LZMA在移动设备上的内存峰值问题' ], implementation: ` // 在构建流程中配置压缩 // webpack配置示例: module.exports = { module: { rules: [ { test: /\.(glb|gltf)$/, use: { loader: 'gltf-loader', options: { compression: 'lz4' } } } ] } } ` }; } }4. Three.js高级交互功能实现
4.1 交互式盒式剖切技术
盒式剖切是3D模型查看中的重要功能,以下是完整实现:
class BoxClippingHelper { constructor(scene, camera, renderer) { this.scene = scene; this.camera = camera; this.renderer = renderer; this.clippingPlanes = []; this.boxHelper = null; this.initBoxHelper(); this.setupEventListeners(); } initBoxHelper() { // 创建可视化剖切框 const boxGeometry = new THREE.BoxGeometry(2, 2, 2); const boxMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true, transparent: true, opacity: 0.5 }); this.boxHelper = new THREE.Mesh(boxGeometry, boxMaterial); this.scene.add(this.boxHelper); // 初始化剖切平面 this.updateClippingPlanes(); } updateClippingPlanes() { // 清空现有剖切平面 this.clippingPlanes = []; if (!this.boxHelper) return; const box = new THREE.Box3().setFromObject(this.boxHelper); const min = box.min; const max = box.max; // 创建六个剖切平面 this.clippingPlanes.push(new THREE.Plane(new THREE.Vector3(1, 0, 0), -max.x)); // 右平面 this.clippingPlanes.push(new THREE.Plane(new THREE.Vector3(-1, 0, 0), min.x)); // 左平面 this.clippingPlanes.push(new THREE.Plane(new THREE.Vector3(0, 1, 0), -max.y)); // 上平面 this.clippingPlanes.push(new THREE.Plane(new THREE.Vector3(0, -1, 0), min.y)); // 下平面 this.clippingPlanes.push(new THREE.Plane(new THREE.Vector3(0, 0, 1), -max.z)); // 前平面 this.clippingPlanes.push(new THREE.Plane(new THREE.Vector3(0, 0, -1), min.z)); // 后平面 // 应用到场景中的所有材质 this.scene.traverse((object) => { if (object.isMesh && object.material) { if (Array.isArray(object.material)) { object.material.forEach(material => { material.clippingPlanes = this.clippingPlanes; material.needsUpdate = true; }); } else { object.material.clippingPlanes = this.clippingPlanes; object.material.needsUpdate = true; } } }); // 启用渲染器的剖切功能 this.renderer.localClippingEnabled = true; } setupEventListeners() { const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); let isDragging = false; let selectedFace = null; const onMouseDown = (event) => { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, this.camera); const intersects = raycaster.intersectObject(this.boxHelper); if (intersects.length > 0) { isDragging = true; selectedFace = this.getSelectedFace(intersects[0].face); } }; const onMouseMove = (event) => { if (!isDragging || !selectedFace) return; mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // 根据选中的面更新剖切框位置 this.updateBoxPosition(mouse, selectedFace); this.updateClippingPlanes(); }; const onMouseUp = () => { isDragging = false; selectedFace = null; }; this.renderer.domElement.addEventListener('mousedown', onMouseDown); this.renderer.domElement.addEventListener('mousemove', onMouseMove); this.renderer.domElement.addEventListener('mouseup', onMouseUp); } getSelectedFace(face) { // 简化实现:根据法向量判断选中的面 const normal = face.normal; return { normal: normal.clone(), originalNormal: normal.clone() }; } updateBoxPosition(mouse, selectedFace) { // 根据鼠标位置更新剖切框 const vector = new THREE.Vector3(mouse.x, mouse.y, 0.5); vector.unproject(this.camera); const direction = vector.sub(this.camera.position).normalize(); const distance = -this.camera.position.z / direction.z; const pos = this.camera.position.clone().add(direction.multiplyScalar(distance)); // 更新剖切框位置 this.boxHelper.position.copy(pos); } }4.2 复杂模型层级处理与对象选取
针对Blender导出模型的三级空物体问题,提供专门的解决方案:
class HierarchyAwarePicker { constructor(scene, camera) { this.scene = scene; this.camera = camera; this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); // 存储原始层级关系 this.originalHierarchy = new Map(); this.setupHierarchyMapping(); } // 建立对象层级映射 setupHierarchyMapping() { this.scene.traverse((object) => { if (object.isMesh) { // 记录每个网格的完整层级路径 const path = this.getObjectPath(object); this.originalHierarchy.set(object.uuid, { object: object, path: path, parent: object.parent }); } }); } // 获取对象的完整层级路径 getObjectPath(object) { const path = []; let current = object; while (current && current !== this.scene) { path.unshift(current.name || current.type); current = current.parent; } return path.join('/'); } // 智能对象选取:考虑层级关系 intelligentPick(mouseEvent) { this.mouse.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1; this.mouse.y = -(mouseEvent.clientY / window.innerHeight) * 2 + 1; this.raycaster.setFromCamera(this.mouse, this.camera); // 获取所有相交对象 const intersects = this.raycaster.intersectObjects(this.scene.children, true); if (intersects.length === 0) return null; // 分析相交对象的层级关系 const hierarchyAnalysis = this.analyzeIntersectionHierarchy(intersects); // 根据业务逻辑选择最合适的对象 return this.selectMostRelevantObject(hierarchyAnalysis); } analyzeIntersectionHierarchy(intersects) { const analysis = { directMeshes: [], // 直接命中的网格 parentGroups: [], // 父级组 rootObjects: [], // 根级对象 hierarchyLevels: new Map() // 各层级的命中统计 }; intersects.forEach(intersect => { const object = intersect.object; analysis.directMeshes.push(object); // 分析层级关系 let current = object; let level = 0; while (current && current !== this.scene) { if (!analysis.hierarchyLevels.has(level)) { analysis.hierarchyLevels.set(level, new Set()); } analysis.hierarchyLevels.get(level).add(current); if (level === 1) { analysis.parentGroups.push(current); } if (current.parent === this.scene) { analysis.rootObjects.push(current); } current = current.parent; level++; } }); return analysis; } selectMostRelevantObject(analysis) { // 业务逻辑1:优先选择有名称的叶子节点 const namedMeshes = analysis.directMeshes.filter(mesh => mesh.name && mesh.name !== ''); if (namedMeshes.length > 0) { return namedMeshes[0]; } // 业务逻辑2:选择层级最深的组 const maxLevel = Math.max(...analysis.hierarchyLevels.keys()); if (maxLevel > 0) { const deepestObjects = Array.from(analysis.hierarchyLevels.get(maxLevel)); return deepestObjects[0]; } // 默认返回第一个命中的对象 return analysis.directMeshes[0]; } // 修复Blender导出模型的层级问题 fixBlenderHierarchy() { this.scene.traverse((object) => { // 检测空物体(只有子节点没有几何体的对象) if (object.children.length > 0 && !object.isMesh) { const hasMeshChildren = object.children.some(child => child.isMesh); if (hasMeshChildren) { // 简化层级:将网格提升一级 this.flattenHierarchy(object); } } }); } flattenHierarchy(parentObject) { const grandchildren = []; parentObject.children.forEach(child => { if (child.children.length > 0) { grandchildren.push(...child.children); child.children = []; // 清空子节点 } }); // 将孙节点直接添加到父节点 grandchildren.forEach(grandchild => { parentObject.add(grandchild); }); } }4.3 路径规划与绕路生成算法
实现Three.js中的自动绕路功能,适用于导航、游戏等场景:
class PathPlanner { constructor(scene) { this.scene = scene; this.navigationMesh = null; this.obstacles = []; this.graph = null; } // 创建导航网格 createNavigationMesh(groundGeometry, cellSize = 1) { const navMesh = new THREE.Group(); navMesh.name = 'navigationMesh'; // 将地面几何体划分为网格 const bounds = new THREE.Box3().setFromObject(groundGeometry); const width = bounds.max.x - bounds.min.x; const depth = bounds.max.z - bounds.min.z; const rows = Math.floor(depth / cellSize); const cols = Math.floor(width / cellSize); for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const x = bounds.min.x + col * cellSize + cellSize / 2; const z = bounds.min.z + row * cellSize + cellSize / 2; // 检查该网格是否可通行 if (this.isCellWalkable(x, z, cellSize)) { const cellGeometry = new THREE.PlaneGeometry(cellSize, cellSize); const cellMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.3, side: THREE.DoubleSide }); const cell = new THREE.Mesh(cellGeometry, cellMaterial); cell.position.set(x, bounds.max.y + 0.1, z); cell.rotation.x = -Math.PI / 2; navMesh.add(cell); } } } this.navigationMesh = navMesh; this.scene.add(navMesh); this.buildGraph(); return navMesh; } isCellWalkable(x, z, cellSize) { // 简化的可通行性检查 // 实际项目中需要与障碍物进行碰撞检测 const checkPoints = [ { x: x - cellSize/3, z: z - cellSize/3 }, { x: x + cellSize/3, z: z - cellSize/3 }, { x: x - cellSize/3, z: z + cellSize/3 }, { x: x + cellSize/3, z: z + cellSize/3 } ]; return checkPoints.every(point => { return !this.obstacles.some(obstacle => { const obstacleBox = new THREE.Box3().setFromObject(obstacle); return obstacleBox.containsPoint(new THREE.Vector3(point.x, 0, point.z)); }); }); } // 构建路径规划图 buildGraph() { if (!this.navigationMesh) return; this.graph = { nodes: [], edges: [] }; // 将导航网格单元格转换为图节点 this.navigationMesh.children.forEach((cell, index) => { this.graph.nodes.push({ id: index, position: cell.position.clone(), neighbors: [] }); }); // 构建邻接关系 this.graph.nodes.forEach((node, index) => { const cellSize = 1; // 假设单元格大小 const neighborPositions = [ { x: node.position.x + cellSize, z: node.position.z }, // 右 { x: node.position.x - cellSize, z: node.position.z }, // 左 { x: node.position.x, z: node.position.z + cellSize }, // 上 { x: node.position.x, z: node.position.z - cellSize }, // 下 { x: node.position.x + cellSize, z: node.position.z + cellSize }, // 右上 { x: node.position.x - cellSize, z: node.position.z + cellSize }, // 左上 { x: node.position.x + cellSize, z: node.position.z - cellSize }, // 右下 { x: node.position.x - cellSize, z: node.position.z - cellSize } // 左下 ]; neighborPositions.forEach(neighborPos => { const neighborNode = this.findNodeAtPosition(neighborPos.x, neighborPos.z); if (neighborNode && neighborNode.id !== node.id) { // 计算移动成本(距离) const distance = node.position.distanceTo(neighborNode.position); node.neighbors.push({ nodeId: neighborNode.id, cost: distance }); } }); }); } findNodeAtPosition(x, z) { return this.graph.nodes.find(node => { return Math.abs(node.position.x - x) < 0.1 && Math.abs(node.position.z - z) < 0.1; }); } // A*路径规划算法 findPath(startPos, endPos) { if (!this.graph) return null; const startNode = this.findNearestNode(startPos); const endNode = this.findNearestNode(endPos); if (!startNode || !endNode) return null; const openSet = new Set([startNode.id]); const cameFrom = new Map(); const gScore = new Map(); // 从起点到当前节点的成本 const fScore = new Map(); // gScore + 启发式估计 // 初始化分数 this.graph.nodes.forEach(node => { gScore.set(node.id, Infinity); fScore.set(node.id, Infinity); }); gScore.set(startNode.id, 0); fScore.set(startNode.id, this.heuristic(startNode, endNode)); while (openSet.size > 0) { // 选择fScore最小的节点 let currentId = null; let lowestFScore = Infinity; openSet.forEach(nodeId => { if (fScore.get(nodeId) < lowestFScore) { lowestFScore = fScore.get(nodeId); currentId = nodeId; } }); if (currentId === endNode.id) { return this.reconstructPath(cameFrom, currentId); } openSet.delete(currentId); const currentNode = this.graph.nodes.find(n => n.id === currentId); currentNode.neighbors.forEach(neighbor => { const tentativeGScore = gScore.get(currentId) + neighbor.cost; if (tentativeGScore < gScore.get(neighbor.nodeId)) { cameFrom.set(neighbor.nodeId, currentId); gScore.set(neighbor.nodeId, tentativeGScore); fScore.set(neighbor.nodeId, tentativeGScore + this.heuristic(this.graph.nodes.find(n => n.id === neighbor.nodeId), endNode)); if (!openSet.has(neighbor.nodeId)) { openSet.add(neighbor.nodeId); } } }); } return null; // 没有找到路径 } heuristic(nodeA, nodeB) { // 使用欧几里得距离作为启发式函数 return nodeA.position.distanceTo(nodeB.position); } reconstructPath(cameFrom, currentId) { const path = [this.graph.nodes.find(n => n.id === currentId).position]; while (cameFrom.has(currentId)) { currentId = cameFrom.get(currentId); path.unshift(this.graph.nodes.find(n => n.id === currentId).position); } return path; } findNearestNode(position) { let nearestNode = null; let minDistance = Infinity; this.graph.nodes.forEach(node => { const distance = node.position.distanceTo(position); if (distance < minDistance) { minDistance = distance; nearestNode = node; } }); return nearestNode; } // 可视化路径 visualizePath(path) { if (!path || path.length < 2) return; const points = path.map(point => new THREE.Vector3(point.x, point.y + 0.5, point.z)); const geometry = new THREE.BufferGeometry().setFromPoints(points); const material = new THREE.LineBasicMaterial({ color: 0xff0000 }); const line = new THREE.Line(geometry, material); this.scene.add(line); return line; } }5. Three.js与Vue3集成实战
5.1 Vue3组件化Three.js开发
将Three.js场景封装为Vue3组件,实现响应式开发:
<template> <div ref="container" class="three-container"></div> </template> <script> import { onMounted, onUnmounted, ref } from 'vue'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; export default { name: 'ThreeScene', props: { backgroundColor: { type: String, default: '#000011' }, enableControls: { type: Boolean, default: true } }, setup(props) { const container = ref(null); let scene, camera, renderer, controls; let animationId; const initScene = () => { // 创建场景 scene = new THREE.Scene(); scene.background = new THREE.Color(props.backgroundColor); // 创建相机 camera = new THREE.PerspectiveCamera( 75, container.value.clientWidth / container.value.clientHeight, 0.1, 1000 ); camera.position.z = 5; // 创建渲染器 renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(container.value.clientWidth, container.value.clientHeight); renderer.setPixelRatio(window.devicePixelRatio); container.value.appendChild(renderer.domElement); // 添加轨道控制器 if (props.enableControls) { controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; } // 添加基础灯光 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 geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshPhongMaterial({ color: 0x00ff00 }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); // 启动动画循环 animate(); }; const animate = () => { animationId = requestAnimationFrame(animate); if (controls) { controls.update(); } // 旋转立方体 const cube = scene.getObjectByName('exampleCube'); if (cube) { cube.rotation.x += 0.01; cube.rotation.y += 0.01; } renderer.render(scene, camera); }; const handleResize = () => { if (!container.value) return; camera.aspect = container.value.clientWidth / container.value.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(container.value.clientWidth, container.value.clientHeight); }; const cleanup = () => { if (animationId) { cancelAnimationFrame(animationId); } if (controls) { controls.dispose(); } if (renderer) { renderer.dispose(); } window.removeEventListener('resize', handleResize); }; onMounted(() => { initScene(); window.addEventListener('resize', handleResize); }); onUnmounted(() => { cleanup(); }); return { container }; } }; </script> <style scoped> .three-container { width: 100%; height: 100%; position: relative; } </style>5.2 Vue3响应式Three.js状态管理
使用Pinia进行Three.js场景状态管理:
// stores/threeStore.js import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; export const useThreeStore = defineStore('three', () => { // 状态 const objects = ref([]); const selectedObject = ref(null); const cameraPosition = ref({ x: 0, y: 0, z: 5 }); const sceneBackground = ref('#000011'); const animationEnabled = ref(true); // Getter const objectCount = computed(() => objects.value.length); const hasSelectedObject = computed(() => selectedObject.value !== null); const sceneInfo = computed(() => ({ objectCount: objectCount.value, background: sceneBackground.value, cameraPosition: cameraPosition.value })); // Actions const addObject = (objectData) => { const newObject = { id: Date.now().toString(), ...objectData, position: objectData.position || { x: 0, y: 0, z: 0 }, rotation: objectData.rotation || { x: 0, y: 0, z: 0 }, scale: objectData.scale || { x: 1, y: 1, z: 1 } }; objects.value.push(newObject); return newObject.id; }; const removeObject = (objectId) => { const index = objects.value.findIndex(obj => obj.id === objectId); if (index !== -1) { objects.value.splice(index, 1); if (selectedObject.value?.id === objectId) { selectedObject.value = null; } } }; const selectObject = (objectId) => { selectedObject.value = objects.value.find(obj => obj.id === objectId) || null; }; const updateObjectProperty = (objectId, property, value) => { const object = objects.value.find(obj => obj.id === objectId); if (object) { if (property in object) { object[property] = value; } else if (property in object.position) { object.position[property] = value; } else if (property in object.rotation) { object.rotation[property] = value; } else if (property in object.scale) { object.scale[property] = value; } } }; const setCameraPosition = (position) => { cameraPosition.value = { ...position }; }; const setBackground = (color) => { sceneBackground.value = color