三維裝配可視化界面開發筆記
項目概述
這是一個基于Vue.js和Three.js的三維裝配可視化系統,用于展示機械零部件的裝配和拆解過程。系統支持模型加載、拆解/裝配路徑生成、動畫展示和工藝流程圖生成等功能。
技術棧
- 前端框架: Vue 3 (使用組合式API)
- 構建工具: Vite 6.3.4
- 3D引擎: Three.js r152
- 狀態管理: Pinia 2.1.0
- 路由: Vue Router 4.2.0
- 模型加載: GLTFLoader, OBJLoader
- 控制器: OrbitControls
開發日志
2025-04-30
- 初始化項目,使用
npm create vite@latest
創建Vue3項目 - 安裝Three.js及相關依賴:
npm install three @types/three
- 實現了基本的Three.js場景初始化,包括場景、相機、渲染器和控制器
- 添加了模型加載功能,支持GLTF和OBJ格式
- 遇到問題:模型加載后顯示太小,調整了相機距離參數(從默認的5改為根據模型大小動態計算)
- 踩坑:Three.js的OrbitControls需要從examples中導入,不是核心包的一部分
2025-05-01
- 實現了部件選擇和拖動功能,使用Raycaster進行射線檢測
- 添加了拆解步驟記錄功能,記錄部件ID、動作類型和移動路徑
- 實現了視圖切換功能(正視圖、俯視圖、側視圖)
- 遇到問題:視圖切換后模型顯示不正確,調整了相機參數和上方向設置
- 踩坑:Three.js中相機的up向量設置對視圖方向有重要影響,特別是在俯視圖中需要設置為(0,0,-1)
2023-05-01
- 修復了模型加載問題,現在可以正確加載zhuangpeitu_asm模型
- 優化了相機控制,使模型顯示更加合理(調整了fitCameraToObject函數中的邊距系數)
- 添加了模型加載失敗時的備用方案(loadFallbackModel函數)
- 遇到問題:某些復雜模型的部件層次結構難以正確解析
- 踩坑:GLTF模型中的bin文件路徑問題,需要確保bin文件和gltf文件在同一目錄下
系統架構
整體架構
系統采用前端單頁應用架構,使用Vue3作為框架,Three.js作為3D渲染引擎。數據流向如下:
- 用戶交互 -> Vue組件 -> Pinia Store -> Three.js場景更新
- 模型加載 -> 部件提取 -> 存儲到Store -> 渲染到場景
- 部件操作 -> 記錄步驟 -> 生成工藝流程
模塊劃分
系統分為以下幾個主要模塊:
- 模型查看器模塊:負責3D場景渲染、模型加載和交互
- 工藝步驟模塊:記錄和展示裝配/拆解步驟
- 工藝流程圖模塊:可視化展示裝配流程
- 工具欄模塊:提供視圖切換、模型加載等功能
項目結構
src/
├── assets/ # 靜態資源
├── components/ # 組件
│ ├── ModelViewer/ # 3D模型查看器
│ │ └── ModelViewer.vue # 核心3D渲染組件
│ ├── ProcessChart/# 工藝流程圖
│ │ └── ProcessChart.vue # 流程圖組件
│ ├── StepList/ # 工藝步驟列表
│ │ └── StepList.vue # 步驟列表組件
│ └── ToolBar/ # 工具欄
│ └── ToolBar.vue # 工具欄組件
├── router/ # 路由配置
│ └── index.js # 路由定義
├── services/ # 服務
│ └── assemblyService.js # 裝配相關服務,包含路徑計算等
├── stores/ # 狀態管理
│ ├── modelStore.js # 模型狀態,存儲模型和部件信息
│ └── assemblyStore.js # 裝配狀態,存儲裝配步驟和播放狀態
└── views/ # 頁面視圖├── AssemblyDesignView.vue # 裝配設計頁面├── ProcessDesignView.vue # 工藝設計頁面└── StepDesignView.vue # 工步設計頁面
核心文件說明
- ModelViewer.vue: 系統核心組件,包含Three.js場景初始化、模型加載、部件交互等功能
- assemblyStore.js: 存儲裝配步驟、播放狀態等信息,提供步驟添加、播放控制等方法
- modelStore.js: 存儲模型信息、部件列表等,提供部件選擇、信息更新等方法
- assemblyService.js: 提供路徑計算、碰撞檢測等服務
關鍵功能實現與數據結構
模型加載
模型加載使用Three.js的GLTFLoader和OBJLoader實現。加載后會提取模型的部件信息,并存儲在modelStore中。
// 加載GLTF模型
const loadGLTF = (url) => {const loader = new GLTFLoader()loader.load(url,(gltf) => {// 清除現有模型clearScene()// 添加新模型到場景scene.add(gltf.scene)// 調整相機位置以適應模型fitCameraToObject(gltf.scene)// 提取部件信息const parts = extractParts(gltf.scene)modelStore.setParts(parts)// 設置動畫混合器if (gltf.animations && gltf.animations.length > 0) {animationMixer = new THREE.AnimationMixer(gltf.scene)gltf.animations.forEach((clip) => {animationMixer.clipAction(clip).play()})}},// ...錯誤處理)
}
模型加載中遇到的主要問題是bin文件路徑問題。GLTF文件通常引用外部的bin文件,需要確保這些文件在正確的相對路徑上。我們通過將所有模型文件放在public目錄下解決了這個問題。
部件提取與數據結構
部件提取是從加載的3D模型中識別和分離各個組件的過程。我們使用以下數據結構來表示部件:
// 部件數據結構
{id: String, // 部件唯一標識符name: String, // 部件名稱mesh: THREE.Mesh, // 部件的3D網格對象parentId: String // 父部件ID,用于構建層次結構
}
提取過程中,遍歷模型的所有網格對象,為每個網格創建一個部件對象:
// 從模型中提取部件信息
const extractParts = (object) => {const parts = []object.traverse((child) => {if (child.isMesh) {// 為每個網格創建一個唯一IDconst id = `part_${parts.length}`// 獲取部件名稱const name = child.name || `部件 ${parts.length + 1}`// 確定父部件IDlet parentId = nullif (child.parent && child.parent !== object) {parentId = child.parent.uuid}// 添加到部件列表parts.push({id,name,mesh: child,parentId})// 存儲原始位置child.userData.originalPosition = child.position.clone()child.userData.originalRotation = child.rotation.clone()// 添加點擊事件child.userData.partId = id}})return parts
}
部件拖拽與交互
實現了基于射線檢測的部件選擇和拖拽功能。當用戶拖動部件時,會記錄拆解步驟。
// 鼠標按下事件處理
const onMouseDown = (event) => {// 計算鼠標位置const rect = renderer.domElement.getBoundingClientRect()mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1// 設置射線raycaster.setFromCamera(mouse, camera)// 獲取與射線相交的對象const intersects = raycaster.intersectObjects(scene.children, true)if (intersects.length > 0) {// 找到第一個有partId的對象const intersectedObject = intersects.find(intersect =>intersect.object.userData && intersect.object.userData.partId)if (intersectedObject) {// 禁用軌道控制器controls.enabled = false// 設置拖拽狀態isDragging = true// 獲取選中的部件const partId = intersectedObject.object.userData.partIdselectedPart = modelStore.parts.find(part => part.id === partId)// 記錄起始位置dragStartPosition.copy(selectedPart.mesh.position)// 設置拖拽平面planeNormal.copy(camera.position).sub(controls.target).normalize()planePoint.copy(selectedPart.mesh.position)plane.setFromNormalAndCoplanarPoint(planeNormal, planePoint)}}
}
拖拽過程中,使用射線與平面的交點來確定部件的新位置:
// 鼠標移動事件處理
const onMouseMove = (event) => {if (!isDragging || !selectedPart) return// 計算鼠標位置const rect = renderer.domElement.getBoundingClientRect()mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1// 設置射線raycaster.setFromCamera(mouse, camera)// 計算射線與平面的交點const ray = raycaster.rayif (ray.intersectPlane(plane, intersectionPoint)) {// 移動部件selectedPart.mesh.position.copy(intersectionPoint)// 更新當前位置dragCurrentPosition.copy(intersectionPoint)}
}
工藝步驟記錄
當用戶拖動部件完成拆解操作時,系統會記錄這個步驟。步驟數據結構如下:
// 步驟數據結構
{partId: String, // 部件IDaction: String, // 動作類型(拆解/裝配)path: Array // 移動路徑,包含一系列位置點
}
步驟記錄過程:
// 鼠標釋放事件處理
const onMouseUp = () => {if (!isDragging || !selectedPart) return// 啟用軌道控制器controls.enabled = true// 計算移動距離const distance = dragStartPosition.distanceTo(dragCurrentPosition)// 如果移動距離足夠大,則記錄拆解步驟if (distance > 0.5) {// 計算移動路徑const path = calculateLinearPath(dragStartPosition, dragCurrentPosition, 20)// 記錄拆解步驟assemblyStore.addStep({partId: selectedPart.id,action: '拆解',path: path})} else {// 如果移動距離不夠,則恢復原位selectedPart.mesh.position.copy(dragStartPosition)}// 重置拖拽狀態isDragging = falseselectedPart = null
}
視圖切換
實現了正視圖、俯視圖和側視圖的切換功能。關鍵是設置相機位置和上方向向量:
// 改變視角
const changeView = (viewType) => {// 獲取模型的邊界框const box = new THREE.Box3().setFromObject(scene)const size = box.getSize(new THREE.Vector3())const center = box.getCenter(new THREE.Vector3())// 計算合適的距離const maxDim = Math.max(size.x, size.y, size.z)const distance = maxDim * 1.2// 根據視角類型設置相機位置switch (viewType) {case 'front':camera.position.set(center.x, center.y, center.z + distance)camera.up.set(0, 1, 0) // Y軸向上breakcase 'top':camera.position.set(center.x, center.y + distance, center.z)camera.up.set(0, 0, -1) // Z軸向下breakcase 'side':camera.position.set(center.x + distance, center.y, center.z)camera.up.set(0, 1, 0) // Y軸向上break}// 更新相機camera.lookAt(center)camera.updateProjectionMatrix()// 更新控制器controls.update()
}
工藝流程圖生成
工藝流程圖基于記錄的拆解步驟生成,使用簡單的節點和連線表示裝配關系:
// 生成工藝流程圖
const generateProcessChart = () => {const steps = assemblyStore.stepsif (steps.length === 0) return// 清除現有圖表chartContainer.innerHTML = ''// 創建SVG元素const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')svg.setAttribute('width', '100%')svg.setAttribute('height', '100%')// 為每個步驟創建節點steps.forEach((step, index) => {const part = modelStore.parts.find(p => p.id === step.partId)if (!part) return// 創建節點const node = document.createElementNS('http://www.w3.org/2000/svg', 'circle')node.setAttribute('cx', 50 + index * 100)node.setAttribute('cy', 50)node.setAttribute('r', 20)node.setAttribute('fill', '#42b883')// 創建標簽const text = document.createElementNS('http://www.w3.org/2000/svg', 'text')text.setAttribute('x', 50 + index * 100)text.setAttribute('y', 90)text.setAttribute('text-anchor', 'middle')text.textContent = part.name// 添加到SVGsvg.appendChild(node)svg.appendChild(text)// 添加連線if (index > 0) {const line = document.createElementNS('http://www.w3.org/2000/svg', 'line')line.setAttribute('x1', 50 + (index - 1) * 100)line.setAttribute('y1', 50)line.setAttribute('x2', 50 + index * 100)line.setAttribute('y2', 50)line.setAttribute('stroke', '#666')line.setAttribute('stroke-width', 2)svg.appendChild(line)}})// 添加到容器chartContainer.appendChild(svg)
}
開發流程與工作方式
開發流程
- 需求分析:確定系統功能和用戶交互方式
- 技術選型:選擇Vue3和Three.js作為主要技術棧
- 架構設計:設計系統模塊和數據流
- 組件開發:
- 先開發核心的ModelViewer組件
- 實現基本的模型加載和顯示
- 添加部件選擇和拖動功能
- 實現工藝步驟記錄
- 開發工藝流程圖生成功能
- 集成測試:測試各模塊之間的交互
- 優化改進:根據測試結果進行優化
工作方式
- 使用Git進行版本控制
- 采用組件化開發方式,每個功能模塊獨立開發
- 使用Pinia進行狀態管理,確保數據流的清晰性
- 定期進行代碼審查和重構,保持代碼質量
參考資料
- Three.js文檔: https://threejs.org/docs/
- Vue 3文檔: https://v3.vuejs.org/
- GLTF格式規范: https://github.com/KhronosGroup/glTF
- Pinia狀態管理: https://pinia.vuejs.org/
- 《3D Game Engine Design》 - David H. Eberly
- 《Learning Three.js》 - Jos Dirksen