深入剖析 Pascal Editor:3D 建築編輯器的核心概念與實作
嘿,台灣的開發者們!
今天要為大家介紹一個令人興奮的開源專案——Pascal Editor。這是一個基於 React Three Fiber 和 WebGPU 打造的 3D 建築編輯器。無論你是對 Web 開發、3D 圖形,還是想要探索新的專案,Pascal Editor 都提供了一個絕佳的起點。
讓我們一起來看看,這個專案是如何建構的,以及它有哪些獨特之處,幫助我們更好地理解和應用它。
什麼是 Pascal Editor?
Pascal Editor 是一個在網頁上運行的 3D 建築編輯器。它讓使用者能夠在瀏覽器中建立、編輯和檢視 3D 建築模型。 這個專案的核心技術包括:
- React: 一個用於構建使用者介面的 JavaScript 函式庫。
- React Three Fiber: React 的一個渲染器,用於將 Three.js 的功能整合到 React 程式碼中。
- Three.js: 一個基於 JavaScript 的 3D 函式庫,用於在瀏覽器中顯示 3D 物件。
- WebGPU: 一個新的網頁圖形 API,它為網頁應用程式提供了對 GPU 的更直接和高效的訪問。
Pascal Editor 旨在提供一個直觀、可擴展的 3D 建模工具,讓使用者可以輕鬆地建立和修改建築模型。
為什麼要關注 Pascal Editor?
- 學習 WebGPU 和 3D 圖形: 隨著 WebGPU 的興起,理解如何在網頁上高效地渲染 3D 圖形變得越來越重要。 Pascal Editor 提供了一個實用的案例,可以幫助你了解 WebGPU 的應用。
- React Three Fiber 實作: 學習如何使用 React Three Fiber 可以讓你更容易地在 React 專案中整合 3D 圖形。 Pascal Editor 的架構為我們提供了許多有價值的範例,展示了如何使用 React 和 Three.js 來創建互動式 3D 場景。
- 開源和可擴展性: 由於 Pascal Editor 是開源的,你可以自由地檢視程式碼、貢獻程式碼,並且根據自己的需求進行修改和擴展。
專案架構:TurboRepo Monorepo
Pascal Editor 使用 Turborepo 作為其單體倉庫(monorepo)的管理工具。這個架構有助於將專案組織成多個獨立的套件,每個套件負責特定的功能。
主要的套件包括:
- apps/editor: 這是使用 Next.js 構建的應用程式,包含了使用者介面、工具和編輯器特定的功能。
- packages/core: 核心套件,定義了場景的數據結構(Node 結構)、狀態管理(Zustand 狀態管理)、幾何生成系統和事件總線。
- packages/viewer: 負責 3D 渲染,使用 React Three Fiber 構建 3D 渲染元件,包含了預設的相機、控制和後處理效果。
這種分離關注點的架構使專案更易於維護、測試和擴展。
核心概念:資料結構和狀態管理
Pascal Editor 的核心是它的資料結構和狀態管理,這決定了 3D 場景的組織方式。
1. 節點 (Nodes)
節點是描述 3D 場景的基本資料單元。所有節點都繼承自 BaseNode,包含了以下關鍵資訊:
id: 節點的唯一識別符,通常帶有類型前綴(例如"wall_abc123")。type: 節點的類型,用於類型安全處理。parentId: 父節點的 ID,用於定義節點的層級關係。visible: 指示節點是否可見。metadata: 用於儲存任意元資料,例如節點是否是暫時性的 ({ isTransient: true })。
節點層級結構
Pascal Editor 採用了一種扁平的結構,節點儲存在一個扁平的字典 (Record<id, Node>) 中,而不是嵌套的樹狀結構。 節點之間的父子關係通過 parentId 和 children 陣列來定義。 這種結構簡化了場景的管理和渲染。
2. 場景狀態 (Zustand Store)
場景狀態由 @pascal-app/core 裡的 Zustand store 管理。 Zustand 是一個輕量級的狀態管理庫,非常適合 React 專案。
useScene.getState() 包含了以下關鍵狀態:
nodes: 儲存了所有節點的字典。rootNodeIds: 頂層節點的 ID,例如場景中的 Site 節點。dirtyNodes: 需要系統更新的節點集合。
這個 store 也包含了用於操作節點的方法,例如 createNode, updateNode 和 deleteNode。
Middleware
Pascal Editor 使用了兩個 Zustand 的 middleware:
- Persist: 將場景狀態儲存到 IndexedDB,實現了資料的持久化。
- Temporal (Zundo): 提供了撤銷 (Undo) 和重做 (Redo) 的功能,支援最多 50 個步驟的歷史記錄。
3. 場景註冊表 (Scene Registry)
場景註冊表是一個用於快速查找 Three.js 物件的工具。它將節點 ID 映射到它們對應的 Three.js 物件。這對於高效的場景更新至關重要。
sceneRegistry = {
nodes: Map<id, Object3D>, // ID → 3D object
byType: {
wall: Set<id>,
item: Set<id>,
zone: Set<id>,
// ...
}
}
渲染器使用 useRegistry hook 註冊它們的引用,使系統可以直接訪問 3D 物件。
4. 節點渲染器 (Node Renderers)
節點渲染器是 React 元件,用於為每個節點類型創建 Three.js 物件。 它們構成了 3D 場景的視覺呈現。
結構如下:
SceneRenderer
└── NodeRenderer (dispatches by type)
├── BuildingRenderer
├── LevelRenderer
├── WallRenderer
├── SlabRenderer
├── ZoneRenderer
├── ItemRenderer
└── ...
渲染器的基本流程:
- 渲染器創建一個佔位符 mesh 或 group。
- 使用
useRegistry註冊這個物件。 - 系統基於節點資料更新幾何體。
5. 系統 (Systems)
系統是 React 元件,它們在渲染迴圈 (useFrame) 中運行,用於更新幾何體和變換。
核心系統(在 @pascal-app/core 中):
WallSystem: 生成牆壁幾何體,並處理門窗的切割。SlabSystem: 從多邊形生成地板幾何體。CeilingSystem: 生成天花板幾何體。RoofSystem: 生成屋頂幾何體。ItemSystem: 在牆壁、天花板或地板上定位物品。
處理流程
系統會檢查 dirtyNodes,只更新需要重新計算的幾何體。
useFrame(() => {
for (const id of dirtyNodes) {
const obj = sceneRegistry.nodes.get(id)
const node = useScene.getState().nodes[id]
// Update geometry, transforms, etc.
updateGeometry(obj, node)
dirtyNodes.delete(id)
}
})
6. Dirty Nodes
當一個節點發生變化時,它會在 useScene.getState().dirtyNodes 中被標記為 “dirty”。這表示該節點的幾何體需要更新。
// Automatic: createNode, updateNode, deleteNode mark nodes dirty
useScene.getState().updateNode(wallId, { thickness: 0.2 })
// → wallId added to dirtyNodes
// → WallSystem regenerates geometry next frame
// → wallId removed from dirtyNodes
手動標記: 你也可以手動將節點加入 dirtyNodes。
7. 事件總線 (Event Bus)
元件之間的通訊使用一個 typed event emitter (mitt)。 這使得元件可以相互發送和接收事件,實現了高度解耦的架構。
8. 空間網格管理器 (Spatial Grid Manager)
空間網格管理器用於處理碰撞檢測和放置驗證。
spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation)
spatialGridManager.canPlaceOnWall(wallId, t, height, dimensions)
spatialGridManager.getSlabElevationAt(levelId, x, z)
它被 item 放置工具使用,以驗證位置和計算地板高度。
Editor 架構
編輯器在 viewer 的基礎上擴展了以下功能:
1. 工具 (Tools)
工具通過工具欄啟動,並處理使用者輸入以執行特定操作。
SelectTool: 選擇和操作物件。WallTool: 繪製牆壁。ZoneTool: 創建區域。ItemTool: 放置傢俱/配件。SlabTool: 創建地板。
2. 選擇管理器 (Selection Manager)
編輯器使用一個自定義的選擇管理器,具有層次結構導航:
Site → Building → Level → Zone → Items
每一層級都有自己的選擇策略,用於 hover/click 行為。
3. 編輯器專用系統 (Editor-Specific Systems)
ZoneSystem: 控制區域的顯示,基於 level 模式。- 自定義相機控制,包括節點聚焦。
資料流
了解資料如何在 Pascal Editor 中流動對於理解其工作原理至關重要。
- 使用者操作 (點擊、拖動)
- 工具處理程序
useScene.createNode() / updateNode()- 節點在 store 中被添加/更新
- 節點被標記為 dirty
- React 重新渲染 NodeRenderer
useRegistry()註冊 3D 物件- 系統檢測到 dirty 節點 (useFrame)
- 通過 sceneRegistry 更新幾何體
- 清除 dirty 標誌
實作心得與錯誤排除
在閱讀完 Pascal Editor 的程式碼和文件後,我嘗試了一些基本的實作,並遇到了一些挑戰。以下是一些我遇到的問題和解決方法,希望能幫助到你:
- 環境設定: 首先,請確保你已正確設定開發環境。這包括安裝 Node.js 和 Bun。 按照
Getting Started部分的說明,從根目錄執行bun install和bun dev。 - 套件依賴問題: 有時候,由於套件版本的關係,你可能會遇到依賴性問題。 確保你的
package.json文件中的依賴項與專案的需求相符。 可以嘗試重新安裝套件:bun install。 - TypeScript 編譯錯誤: 由於 Pascal Editor 使用 TypeScript,你可能會遇到編譯錯誤。 仔細檢查錯誤訊息,通常是由於類型定義不匹配或程式碼錯誤引起的。 嘗試使用 TypeScript 提供的類型提示和自動補全功能來解決這些問題。
- Three.js 物件未正確顯示: 如果你發現某些 Three.js 物件沒有正確顯示,可能是由於以下原因:
- 材質 (Material) 問題: 確保你為你的物件設定了正確的材質。
- 光照問題: 在 3D 場景中,光照非常重要。 確保你的場景中包含光源,例如環境光、定向光等。
- 相機位置: 檢查相機的位置和角度,確保它能看到你的物件。
- 註冊問題: 確保你使用
useRegistry註冊了你的 Three.js 物件。
- 狀態管理問題: 如果你發現場景狀態沒有正確更新,檢查你是否正確使用了
useScene.getState().updateNode()等方法。 確保你的元件被正確地連接到 Zustand store,並且能夠接收到狀態更新。
總結
Pascal Editor 是一個非常棒的專案,它展示了如何使用 React、React Three Fiber 和 WebGPU 構建一個功能豐富的 3D 建築編輯器。 透過學習它的架構和程式碼,你可以深入了解 3D 圖形、狀態管理、元件設計和現代 Web 開發技術。
無論你是新手還是經驗豐富的開發者,Pascal Editor 都是一個值得探索的專案。 希望這篇文章能幫助你更好地理解它,並激發你對 3D 圖形和 Web 開發的熱情!
如果你有任何問題或想法,歡迎在 Discord 頻道或 Twitter 上與專案貢獻者交流!
參考閱讀
https://github.com/pascalorg/editor