浏览器端 3D 应用正从展示型工具向专业级编辑器演进。Pascal Editor 作为开源的 3D 建筑编辑器项目,采用 React Three Fiber 与 WebGPU 技术栈,在浏览器中实现了完整的建筑建模、实时渲染与项目协作能力。本文基于其架构设计,剖析如何在复杂 3D 场景下实现高性能渲染与多用户协作。
三层分离的 Monorepo 架构
Pascal Editor 采用 Turborepo 管理的 Monorepo 结构,将功能划分为三个独立包,每层职责清晰边界明确。
@pascal-app/core 作为数据层,承载节点 Schema 定义、场景状态管理、几何生成系统与空间查询能力。所有建筑元素(墙体、楼板、门窗等)均以节点形式存在,采用扁平字典结构存储而非嵌套树,通过parentId与children数组建立层级关系。这种设计使得任意节点的增删改查时间复杂度保持 O (1),避免了深层遍历带来的性能损耗。
@pascal-app/viewer 专注 3D 渲染,基于 React Three Fiber 封装相机控制、后处理管线与默认交互行为。该层不感知业务逻辑,仅接收 core 层传递的节点数据并转换为 Three.js 场景对象,实现数据与渲染的解耦。
apps/editor 作为应用层,集成 UI 组件、编辑工具与业务特定系统。工具层(SelectTool、WallTool、ZoneTool 等)通过事件总线与 core/viewer 层通信,完成用户交互到场景变更的完整链路。
这种分层架构的优势在于:core 与 viewer 可独立发布为 npm 包供第三方集成,editor 层则专注于产品功能迭代,各层通过 Zustand Store 的订阅机制保持状态同步。
实时渲染管线:Dirty Nodes 与 Systems 模式
3D 建筑编辑器面临的核心技术挑战是:如何在用户频繁交互(拖拽墙体、调整门窗位置)时保持 60fps 的流畅渲染。Pascal Editor 采用 "脏节点标记 + 系统批量处理" 的策略解决此问题。
当用户执行updateNode(wallId, { thickness: 0.2 })操作时,core 层的 Zustand Store 会自动将该节点 ID 加入dirtyNodes集合,同时触发 React 重渲染对应的 NodeRenderer 组件。Renderer 组件通过useRegistry钩子将 Three.js 对象引用注册到 Scene Registry,建立节点 ID 与 3D 对象的映射关系。
Systems 组件在useFrame钩子中每帧检查dirtyNodes集合,仅对标记为脏的节点执行几何重建。以 WallSystem 为例,它负责生成带斜接(mitering)的墙体几何体,并执行 CSG 布尔运算在墙体上切割出门窗洞口。这种延迟计算模式避免了每帧重建整个场景,将计算开销控制在用户实际修改的节点范围内。
Scene Registry 的设计同样关键。它维护两个核心数据结构:nodes Map 存储 ID 到 Object3D 的映射,byType对象按节点类型分组存储 ID 集合。Systems 可直接通过sceneRegistry.nodes.get(id)获取 3D 对象,无需遍历 React 组件树或 Three.js 场景图,大幅提升了大规模场景下的查询性能。
状态管理:Zustand 与 Temporal 中间件
建筑编辑场景对状态管理有严苛要求:需要支持撤销重做、本地持久化、跨组件订阅,且状态变更必须精确触发最小范围的重渲染。
Pascal Editor 选择 Zustand 作为状态管理方案,并配置了三层独立的 Store。useScene管理场景数据(节点、根节点 ID、脏节点集合),通过persist中间件自动保存到 IndexedDB,通过temporal(Zundo)中间件实现 50 步历史的撤销重做。useViewer管理视图状态(当前选中的建筑 / 楼层 / 区域、楼层显示模式、相机模式)。useEditor管理编辑器状态(激活工具、图层面板状态)。
Store 的访问模式设计兼顾 React 组件与纯 JavaScript 场景。React 组件通过 Selector 订阅特定状态片段,实现细粒度重渲染控制。非 React 上下文(如事件回调、System 组件)则通过useScene.getState()直接访问当前状态,避免 Hook 调用限制。
节点 Schema 采用 Zod 进行运行时类型校验,从 API 响应到用户输入均经过 Schema 验证,在 TypeScript 静态类型安全之外增加了一层运行时防护。
空间查询与碰撞检测
建筑编辑器需要频繁执行空间查询:判断门窗能否放置在指定墙面位置、计算某点的楼板高程、检测家具摆放是否与其他物体重叠。Pascal Editor 在 core 层实现了 Spatial Grid Manager 处理此类需求。
该模块暴露canPlaceOnFloor、canPlaceOnWall、getSlabElevationAt等 API,底层基于空间哈希或 BVH(包围体层次结构)实现高效查询。与渲染管线分离的设计使得空间查询可在不触发 React 重渲染的情况下完成,工具层可在用户拖拽过程中实时调用这些 API 进行位置验证,提供即时视觉反馈。
事件总线(基于 mitt 实现)则负责跨层组件的松耦合通信。节点点击、悬停、右键菜单等事件通过类型安全的事件名分发,各层组件按需订阅感兴趣的事件类型,避免 Props 层层传递带来的组件耦合。
WebGPU 渲染器选型考量
Pascal Editor 选用 WebGPU 而非 WebGL 作为底层渲染 API,这一决策基于建筑场景的特定需求。WebGPU 提供更现代的 GPU 计算管线,支持 Compute Shader 执行大规模并行计算,这对 CSG 布尔运算、大规模实例化渲染(如重复的建筑构件)具有显著性能优势。
React Three Fiber 的 WebGPU 适配层使得开发者能以声明式 React 组件方式编写 WebGPU 渲染逻辑,同时保留 Three.js 生态的兼容性。不过需要注意,WebGPU 目前仍属新兴技术,需针对不支持的环境提供 WebGL 降级方案。
协作分享的架构预留
虽然开源版本主要聚焦单机编辑能力,但其架构为协作功能预留了扩展空间。扁平化的节点存储结构天然适合 CRDT(无冲突复制数据类型)或 OT(操作转换)算法的集成。每个updateNode操作可序列化为结构化指令,通过 WebSocket 广播给协作者。Zustand Store 的中间件机制允许插入同步层,在状态变更时自动触发网络同步。
IndexedDB 持久化层也为离线编辑与后续同步提供了基础。用户可在离线状态下持续编辑,网络恢复后通过操作日志合并实现冲突解决。
总结
Pascal Editor 的架构实践展示了浏览器端复杂 3D 应用的可行路径:通过 Monorepo 分层隔离业务复杂度,借助 Dirty Nodes+Systems 模式优化渲染性能,利用 Zustand 生态实现状态管理的工程化。对于计划开发类似工具的开发者,建议优先关注数据层与渲染层的解耦设计,以及增量更新策略的性能验证,这两点直接决定了复杂场景下的用户体验上限。
参考来源
- Pascal Editor GitHub 仓库架构文档:https://github.com/pascalorg/editor
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。