我先说一个真实的场景。一个设计师同事在群里发了份图纸,文件名叫"结构修改_最终版v3_别改了.dwg",五分钟后他又发了一版"真最终版v3.2.dwg",然后配了一句"以这版为准"。但这时候结构工程师已经基于"别改了.dwg"出了计算书,车间已经拿"v3"下了料。整个下午都在解释"你看到的不是最新版"这件事上消耗掉了。这种痛点做CAD的人应该不陌生:图纸本身不复杂,但"谁来改、改哪块、怎么让大家看到同一份状态"这件事,单机软件从来没解决好。
这不是我一时兴起的感慨。我做了十年左右CAD相关开发,从几何内核到OpenGL渲染,再到近两年参与的多人实时协作项目,最大的体会是:CAD联网这件事,远不止起个服务器、把文件发上去那么简单。它牵扯的其实是三个层次的问题——几何数据怎么同步、特征历史怎么并发编辑、渲染视图怎么保持一致。这篇是这个系列里偏"理论补全"的一篇,我会把项目中实际遇到的技术难点和踩过的坑展开讲,给想往这个方向走的人一个相对完整的地图。
1. 为什么所有试图"共享文件"的协作方式都注定低效
传统CAD是典型的单机架构,这个"单机"不是指只能在一台电脑上运行,而是指整个系统从数据模型、交互状态到渲染上下文,全都绑定在一个进程里。你打开SolidWorks、NX或者中望CAD,界面上的每一个实体、每一条约束、每一次视图旋转,都发生在本地内存和本地显卡之间。这种架构对单个用户非常友好,因为一切都是即时的,但一旦涉及到协作,它就成了最大的障碍。
1.1 单机架构的三个隐性绑定
第一个绑定是数据模型绑定。CAD的几何数据不是普通文档,它是一棵非常深的特征树。以参数化建模为例,一个零件的最终形状是一个个特征(拉伸、旋转、倒角、阵列)按顺序叠加出来的。你拖动一个草图的尺寸参数,后面整个特征序列都要重新求值(recompute),几何内核会重新构建B-rep模型。这个过程的计算量很大,而且还存在"哪一步先算、哪一步后算"的依赖顺序问题。想把这个数据结构原样搬到服务器上,首先就得让服务器也跑得起一个完整的几何内核。
第二个绑定是交互状态绑定。CAD里有一个概念叫"建模历史"(history),用户操作的每一步都会记录到历史树里。你按一次Ctrl+Z,系统要回滚的不只是视图,还有内核里的几何状态。单机环境下这个历史树是线性的,只有一个人在上面操作,没有人会来"抢"历史指针。一旦多人在线,这个历史树立刻变成一棵需要合并的分叉树,谁的操作在谁的后面、谁撤销会影响到谁的修改,全都变成难题。
第三个绑定是渲染上下文绑定。OpenGL在单机CAD里负责把模型画到屏幕上,但它的上下文(context)是跟具体窗口和显卡驱动绑在一起的。同一个模型,本地画一遍和远程画一遍,性能模型完全不同。你不可能让远程用户的每一次旋转视图都触发服务器重绘整张图然后传回来,那延迟会让人崩溃。
这三个绑定叠加在一起,造成了一个很反直觉的结论:CAD的实时协作不是"把文件分享出来"这种程度的事,而是要把整个单机计算模型改造成分布式计算模型。
1.2 那些年被包装成"协作"的伪方案
很多人会反驳说:我们用过在线CAD预览工具,直接把DWG文件拖到网页里就能看,这不就是协作吗?这里必须区分清楚"共享"和"协作"。
共享方案的本质是"一个人做完,所有人看"。比如把图纸导出成轻量化格式(JT、3D PDF或者glTF),传到网页上让别人查看。这个方案对"看图"够用,但对"一起改图"完全无效。因为轻量化模型里只有三角面片和几何外轮廓,特征树、参数、约束这些可编辑信息全部丢失了。你看到的是一具模型的"尸体",不是一个可以继续加工的"活体"。
另一个常见方案是文件锁机制。从早期的CVS、SVN到后来的各种PDM/PLM系统,核心思路都是"谁先检出,谁就锁定,别人只能等"。这个方案能防止冲突,但它把协作降级成了排队。在机械设计这种需要反复迭代的场景里,排队意味着大量的等待,而人在等待的时候往往会用微信或者邮件去"口头协调",一旦协调内容和文件状态对不上,乱子就来了。
我并不是说这些方案一无是处,它们在特定阶段确实解决了"能不能看到"的问题。但如果你想让两个工程师同时在一个装配体里工作,一个改支架、一个改螺栓,实时看到对方的进展,同时不发生数据错乱,那伪方案一个都顶不上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 几何内核的分布式改造:数据同步的核心难点
真正动手做CAD联机协作的时候,第一个绕不开的就是几何内核的处理方式。这里有个关键选择:是把完整的几何内核跑在服务器上,还是让每个客户端各持有一份内核、通过数据同步来保持一致?
两种方案在行业里都有实践,本质上是一个"中央集权"和"地方自治"的取舍。
2.1 中央集权:服务端跑一个"真正的CAD"
服务端跑完整内核的方案,最典型的做法是在服务器上部署一个无界面的几何内核实例(比如Parasolid或OpenCASCADE),客户端所有的建模操作都封装成命令,发送到服务器上执行,执行完之后把结果(几何更新、显示列表变更)推送给所有相关客户端。
这个方案的优点是:数据的一致性天然有保证,因为只有服务器这一个权威数据源,不存在多个副本之间的同步问题。对于需要严格管控数据的企业场景(经常是合规要求),这是很大一个卖点。
但它的缺点也很致命:网络延迟等于操作延迟。CAD建模操作对响应时间的容忍度极低,你拖动一个手柄,希望模型跟着动,这中间不能有超过100毫秒的感知延迟。如果每次操作都要把数据发到服务器、等服务器重算、再传回来,用户体验会非常糟糕。实测下来,在一般的企业内网条件下,一个复杂的拉伸特征重算加传输可能要一两秒,这种体验根本没法用。
2.2 地方自治:每个客户端持有完整内核,增量同步
另一个方案是每个参与的客户端都跑完整的几何内核和渲染引擎,操作在本地执行,几乎零延迟。客户端之间通过增量更新来保持状态一致——我这边拉伸了一个特征,服务器把这个"拉伸操作"广播给其他人,其他人收到后在自己的内核里重放这一次操作。
这个方案接近在线文档的协作模型,体验确实流畅。但它的问题在于几何内核是一个状态极其复杂的系统,共享操作(operation)需要非常精密的排序机制。举个例子:A把实体旋转了45度,B在同一个实体上做了个倒角,如果B的倒角操作先于A的旋转到达,B实际上是在旧位置上生成了倒角,然后A的旋转又把整个实体带到了新位置——几何结果可能跟预期完全不一样。
这就要说到CAD协作跟文档协作的一个本质区别:文档的世界里,文字和段落之间的关联很弱;CAD的世界里,几何实体之间存在强依赖的关系网络。一段文字改个字不会影响下一段文字的语义,但一个几何体的父特征一变,整个子特征链都要重新求值。
2.3 特征树合并:最让人想放弃的部分
在"地方自治"方案里,最复杂的不是几何数据本身的同步,而是特征树的合并。
特征树本质上是一个有向无环图,每个节点代表一次建模操作,节点之间有依赖关系。多个人同时编辑时,每个人都可能在新节点上挂自己的子节点,这些子节点在全局拓扑上是"并发"的。要让所有客户端最终看到同样的树,需要一个全局唯一的排序逻辑。
我们项目里用的是Lamport时间戳加向量时钟的方案。每个特征节点不仅记录操作内容,还要记录它基于哪个父节点、依赖哪些历史节点。同步时,每个客户端把自己新产生的节点连同依赖关系一起推给服务器,服务器按照全局顺序重排,再把重排结果广播回去。
这个方案能跑通,但有个问题很让人头疼:CAD模型的几何精度和特征顺序强相关。两个操作在不同顺序下执行,几何结果可能差了0.001毫米。对于一般装配可能没关系,但对于精密配合的零件,这个差异是致命的。所以合并的时候不能像在线文档那样用"文本差分"的思路,而必须做"语义合并"——判断两个操作是否真的互不影响,这个判断的复杂度比字段级冲突检测高两个数量级。
3. 并发编辑与冲突处理:给协作上保险丝
多人实时编辑模型,并发冲突是绕不开的。市面上很多号称支持"多人在线协同"的CAD工具,其实都用了很偷懒的办法:锁。
3.1 "锁"方案为什么能应付演示却经不起推敲
锁的思路很好理解:整个装配体给一个人"checkout",别人只能看不能改。这个方案实现成本极低,服务器只需要维护一个文件状态,谁锁定了谁就有写权限。但代价是:一整个装配体被一个人锁死,其他人都干不了活。
有一种改进的"细粒度锁":只锁用户正在编辑的那个零件或那个特征。这个听起来合理了,但实际用起来会发现:机械设计里很多操作是跨零件引用的。比如你要在支架上开一个孔,孔的位置参考了某个螺栓的中心轴。如果螺栓被同事锁着,你的孔位特征就创建不了;如果允许创建,假设之后螺栓位置挪了,你这个孔的参考就得跟着更新。这种"跨实体依赖"让锁的粒度变得极难划分——你根本不知道一个操作背后会引用多少个未被锁定的实体。
3.2 操作事务与撤销风暴
我们后来采用了类似"乐观并发"的策略:不锁,允许大家随便改,但每次提交操作的时候附带其依赖的基础版本号,服务器根据版本号判断有没有冲突。如果没有冲突,操作进入全局历史;如果冲突,发给提交者一个合并请求,由用户自己决定怎么处理。
这个方案看起来灵活,但它触发了一个新的问题:撤销。单机CAD里Ctrl+Z是再自然不过的操作,多人在线时却变成了一个噩梦。如果A撤销了自己在10分钟前的操作,这个撤销指令广播出去之后,B基于A那个操作做的后续修改应该怎么办?是全部级联撤销,还是保留成孤立特征?级联撤销会毁了B的工作成果,保留孤立特征又会导致模型数据的不一致。
我们最终的妥协是:只允许撤销自己尚未被他人依赖的操作,一旦你的操作被别人引用了,撤销入口关闭,只能通过"增加新的修正操作"来覆盖旧结果。 这跟Git里修改已推送的公共提交差不多。这个限制在初版上线时用户骂声不少,但确实避免了很多数据错乱的问题。
3.3 CRDT在CAD里为什么这么难落地
研究分布式协同的人会想到CRDT(无冲突复制数据类型)。CRDT在文本编辑领域已经非常成熟,但在几何建模里很难落地。原因其实很朴素:CRDT要求操作满足交换律、结合律和幂等性,而CAD建模操作几乎不满足这三条。
两个旋转操作怎么交换?旋转A再旋转B跟旋转B再旋转A的结果,在大多数情况下是不同的。那CRDT就只能用在一些简单的变换节点上,比如位移、旋转这种相对独立且可组合的操作。真正涉及布尔运算(求交、求差、求并)的特征,CRDT无能为力。目前业界的CAD协同方案里,对几何操作的处理普遍还是"顺序化",也就是把所有冲突操作排成一个全局线性顺序,按顺序逐一重放。这保证了所有客户端最终一致,代价是后操作的人要看到自己的修改结果被"重新解释"。
4. OpenGL渲染层面的协作:多人看到的是同一个世界吗
模型数据同步了,但CAD软件的"界面"还有一大半在渲染层。多人协同时,大家看到的一定是同一个场景,但每个参与者的视口、视角、高亮状态完全可以不同。这个"渲染状态"的同步,是另一个容易被低估的工程点。
4.1 两种同步思路:重绘派和推流派
在渲染协作的方案选型上,行业里分成了两派。
第一派是"状态同步重绘派"。服务器只同步模型状态和视口操作指令(比如"放大这个区域""高亮这个零件"),每个客户端用自己的OpenGL去渲染。这个方案的优点在于画质由本地显卡决定,分辨率、流畅度都跟本地一致;缺点是每个客户端都要有足够强的硬件来跑完整模型,而且如果模型非常大,弱机客户端的帧率会直接拖垮协作体验。
第二派是"帧推流派"。服务器端用离屏OpenGL渲染出图像帧,编码成视频流推给客户端。客户端的显卡只需要解码视频,不跑任何3D渲染。这个方案的优点是终端的硬件门槛极低,一个平板甚至手机都能流畅看大型装配体;缺点是交互延迟受制于视频编码和网络传输,旋转视角时能明显感觉到"拖影"和延迟,而且模型修改后重新渲染也需要计算时间。
我自己的项目是走"状态同步重绘"为主的,但部分复杂场景会降级到帧推流。这个混合方案的工程复杂度翻了一倍,因为要同时维护两条渲染管线。
4.2 无头OpenGL:让服务器"画出"看不见的图
帧推流派的核心是服务器端的离屏渲染。传统OpenGL需要窗口和显卡,服务器通常是Linux环境、无显示器甚至无GPU,怎么办?我们用的方案是EGL加Mesa软件渲染。
EGL是OpenGL ES的窗口系统接口层,它允许你创建一个"离屏表面"(pbuffer surface),不走窗口系统直接渲染到内存缓冲区。在Linux服务器上,Mesa提供了一套纯软件的渲染实现(llvmpipe/swrast),即便没有显卡,CPU也能把OpenGL指令全部软解算成像素帧。这样做的好处是环境部署非常轻,docker里就能跑;坏处是性能上限取决于CPU核数和频率。
实测下来,用一个8核的Linux服务器软渲染一个中等复杂度的装配体(约300万三角面片),每帧稳定在15-25帧左右,勉强够用。但如果模型上千万面片,软渲染直接拉胯,帧率掉到个位数。后来我们加了个"面片简化"的后备策略——推流时强制用LOD0.2的简化模型,画面细节少了,但操作能跟上。
4.3 增量渲染与拾取同步
不管重绘还是推流,都躲不开一个关键优化:增量渲染。CAD场景里,一次操作往往只影响模型的一小部分区域,但很多初版实现是全场景重绘,GPU和CPU都白白消耗。
我们用OpenGL的frustum culling(视锥剔除)加场景图脏标记,在每次模型变更时只重算受影响节点的包围盒,并标记该节点子树为"dirty",下一次draw call只遍历dirty子树。像是拉伸一个特征,实际上重建的只有那个特征的三角网格,其他零件全部走缓存。这个优化把大中型装配体的重绘代价从几十毫秒降到了个位数毫秒。
除了画得对,还要"点得对"。CAD的鼠标拾取是渲染层的重要能力,多人协同时,A高亮了一个面,B的屏幕上也要高亮同一个面。这个"拾取状态"同步不能只同步高亮颜色,还得同步拾取的对象标识。我们的做法是:所有几何实体在协作层都有全局唯一的实体ID,高亮操作广播的是实体ID而不是屏幕坐标。这样即使B当前视角跟A不同,也能在自己的屏幕上对应到位。
5. 实测中的性能瓶颈:数据量、带宽和弱网自救
理论讲完,看实际数据。我们搭了一个演示环境:一台Ubuntu服务器,一个由4个零件组成的装配体(大约8万面片),两个客户端经办公室100M局域网连接。初版同步链路走的是JSON格式的增量更新,结果一上线就卡成PPT。
5.1 序列化格式的选择:JSON是有上限的
问题出在序列化上。几何内核的变更信息,特征树节点、参数、变换矩阵、三角形索引,用JSON表达出来膨胀率几乎十倍起步。一次简单的布尔运算产生的增量数据能到2-3MB,走局域网还好,走到公网上直接卡死。
后来我们换成了二进制编码,用MessagePack定义了一套紧凑的增量协议,同样的布尔运算增量被压到400KB以内。加上对三角网格做量化压缩(把浮点坐标量化到16位整型),同样的数据进一步压到100KB以下。这一步的教训很直接:在CAD协作里,数据格式的选型优先级甚至排在算法之前,因为带宽瓶颈会决定你能否把方案铺开到公网场景。
5.2 弱网降级:从实时协作自动调整到"准实时"
公网环境无法假设人人都是千兆网络,弱网降级是必须处理的环节。我们的方案是三个档位:
第一档是"强实时"。适用于局域网或延迟低于50ms且带宽充足的环境,所有操作增量全量广播,所有客户端即时响应。
第二档是"准实时"。当网络延迟超过一定阈值(我们设的80ms),协作系统自动降低高频操作的同步频率。比如视口操作(旋转、平移、缩放)不再逐帧同步,而是200ms内取最后一帧状态进行同步。因为视口操作本来就是"给人看的临时状态",不需要每帧都保留历史。
第三档是"离线恢复"。当网络断开再恢复时,系统会做一次全量状态校验:客户端向服务器要当前模型的完整指纹(基于特征树哈希),如果跟本地不一致,就放弃本地模型副本,从服务器拉取全量数据重建。这个"拉全量重建"耗时长,但它是保证最终一致性的兜底方案。
5.3 实测中"丢脸"的几件事
这一节分享一些踩坑的真实案例。
第一件是矩阵精度引发的渲染闪烁。我们早期在协作层用float存变换矩阵,本地渲染没问题,多客户端同步后某些面片出现Z-fighting式的闪烁。排查了很久才发现是协作层把矩阵转成float传输,而本地内核是double精度,回边后浮点误差在远处视口被放大。解决方案很简单:协作协议里矩阵精度统一用double,宁肯多花一倍带宽。
第二件是高亮状态的"幽灵残留"。A在屏幕上高亮了一个零件,关闭高亮后,B的屏幕上仍然能看到高亮。原因是高亮的清除指令在B端没有被执行,因为B端那个零件当时正处于"未加载"状态(我们用到了视口范围的懒加载),高亮指令被当作"目标不存在"丢弃了,但后面零件加载出来后残留状态始终没有收到清除指令。修法是在实体加载完成时主动向服务器询问该实体是否有待同步的视图状态。
第三件是并发操作导致的"几何炸裂"。两个客户端同时对同一个零件做编辑,服务器按到达顺序把它们线性化了,但有一个客户端的本地特征树没有按这个顺序重建,导致它渲染出完全错误的形状。这个问题最后是靠"每次收到远端操作后强制校验特征树哈希,不一致就触发本地回滚重放"解决的。代价是偶尔会出现"看着模型消失又重现"的抖动,但总比错误模型留在屏幕上强。
6. 如果重新做一遍,我会在第一天就确定的几件事
这个项目做到后来,我最大的认识是:CAD联网协作不是一个"功能",而是一整套架构反思。如果从头再来,我会在第一天就做几件事。
第一,明确"协作用户要改的是什么"以及"不能改的是什么"。不是所有操作都需要实时协同。可以让绝大多数编辑操作走"即时本地+异步同步",只有那些真正影响他人工作状态的操作(比如共享坐标系、装配约束、关键特征修改)才需要走强实时链路。想清楚这两者的边界,能够省下80%的冲突处理工作。
第二,把几何内核的"命令流"和"渲染状态"分离成两个独立通道。命令流通道负责特征树的同步,走可靠传输(比如TCP或WebSocket),保证每一个操作都被所有客户端确认收到;渲染状态通道走尽力而为的传输(比如UDP类协议),允许丢帧、允许延迟,因为它本来就是临时可视信息,下一帧还会更新。
第三,前期就把"协作状态机"设计成可重放、可回溯的。我见过太多项目在后期被"状态不一致"折磨到重写核心逻辑。一套可重放的状态机意味着:任何客户端的任何异常状态,都可以通过从服务器拉取命令日志重放来恢复。这个能力在开发期调试和上线排障时都是救命稻草。
第四,渲染层的缓存策略要按"协作频率"来设计。频繁被多人同时操作的模型区域,三角网格缓存要常驻显存或内存;而那些只被单个人查看的零件,可以及时释放。我们做过一次粗糙的统计:在一个装配体协作会话里,70%的操作发生在20%的实体上。针对这20%做渲染缓存优化,收益非常明显。
工具链方面,如果团队完全从零开始,我会建议的MVP路径是:先做"单客户端 + 服务端重放"的架构验证,确认几何内核的序列化和反序列化性能;再做两个客户端的冲突处理原型,模拟真实建模操作;最后才考虑渲染层的协作(先做视口同步,再做高亮同步)。一开始就铺全量功能,大概率会让你淹没在几何内核的细节里。
最后说一句当初没想明白、现在栽过跟头之后才懂的话:CAD的协作天花板不在渲染,也不在网络传输,而在"模型变更的语义表达"。能不能用一套足够精确且紧凑的语言描述"我做了什么修改",并且让任何一台机器都能无歧义地重放这个修改,这才是整个系统的地基。地基没打好,后面不管OpenGL画得多流畅、网络传得多快,都只是在错误的世界里跑得更快而已。
如果你也在做CAD协同方向,希望这篇能帮你少踩几个坑。尤其是那个特征树哈希校验的思路,我强烈建议你从第一天就加上,别像我一样等模型"炸"了才补救。
