简介:基于OpenCascade 6.2.0编写的简易CAD实现程序,由作者astrosky整理发布,面向初次接触几何内核开发的图形学与CAD方向学习者。程序覆盖曲线绘制与编辑、曲面创建与编辑、实体创建与编辑等典型功能,并借助NURBS接口演示了基础造型操作,能够帮助读者快速理解OCC在参数化建模中的应用思路。资源包采用RAR压缩格式,共795个文件,主体为557个hxx头文件、102个cpp源文件和95个h头文件,对应界面框架、文件读写、命令交互等模块;另有少量lib库、vcproj与sln工程配置以及ico、bmp等界面资源,整体约2.74MB,结构清晰,便于用Visual Studio打开编译并逐步调试。目前已有5713人学习/下载。相比纯理论文档,源码形式更适合通过阅读和调试来学习,读者可以顺着BSpline曲线绘制、曲面编辑等命令入口查看对应OCC调用链,理解CAD功能在OpenCascade中的实现细节,为后续二次开发或深入研究几何建模打基础。 有段时间我一直在折腾一个基于OpenCascade的轻量CAD程序,起因很简单:公司内部需要几个定制化建模工具,商用几何内核的授权费高得离谱,算来算去,OpenCascade是唯一一个既满足B-rep建模、布尔运算、STEP数据交换,又能全平台跑的开源几何内核。项目从最开始的空白窗口做到现在能画草图、拉实体、做布尔差集、导STEP文件,前后大约两个月时间,踩的坑基本都能写成一本书。这篇文章就把整套方案的选型思路、核心实现和典型问题梳理出来,给正在做桌面端或Web端轻量CAD工具、想在C++/Qt项目里集成免费几何内核的朋友一个参考。
1. 项目定位与整体设计思路
1.1 为什么选OpenCascade当几何内核,而不是自研
先说结论:自研几何内核是一条极度漫长的路。很多团队一开始觉得“我就做个简单的CAD,画个矩形拉伸一下”,但实际做起来会发现,几何内核涉及容差处理、拓扑可靠性、非流形实体、边界表示修复等一系列深水区问题。即使是最简单的两个长方体做布尔差集,也会出现面退化、边分裂、悬挂顶点等边界情况。这些问题的解决需要极强的计算几何功底,不是几个月能搞定的。
商用内核比如ACIS、Parasolid确实稳定,但授权费用对中小团队来说相当不友好,而且会绑定商业授权协议,后续如果要开源发布产品会非常被动。
OpenCascade在这两者之间提供了一个平衡点:
- 成熟度足够高,从90年代就在工业界使用,很多CAM/CAD软件的内核就是它。
- B-rep(边界表示)体系完整,拓扑层级从Vertex、Edge、Wire、Face、Shell到Solid一应俱全,能覆盖大部分机械设计场景。
- STEP/IGES导入导出支持完善,这是做CAD工具必须具备的“与外界交互”的能力。
- LGPL协议,商用和开源项目都可以用,不会被卡脖子。
当然它的缺点也很明显:文档稀缺、API风格老派、可视化组件丑得感人、学习曲线陡峭。但这些对于有经验的C++开发来说,基本都是时间问题,不是瓶颈。
1.2 功能边界:一个“能用的CAD”需要哪些能力
项目的目标是“简单的CAD功能”,这个“简单”需要具体化。如果不定义功能边界,需求会无限膨胀,最后做出来的东西既不像CAD也不再是工具。
我给自己定的核心功能清单是这样的:
| 优先级 | 功能模块 | 具体内容 |
|---|---|---|
| P0 | 二维草图基础 | 直线、圆弧、矩形、圆,可拾取和选中 |
| P0 | 三维实体拉伸 | 基于闭合草图沿方向拉伸成实体 |
| P0 | 布尔运算 | 并集、差集、交集 |
| P0 | STEP文件导入导出 | 与外部CAD工具交换模型数据 |
| P1 | 基本变换 | 移动、旋转、删除、复制 |
| P1 | 界面集成 | Qt窗口、菜单栏、模型树、视口交互 |
| P2 | Undo/Redo | 命令级撤销、重做 |
这个功能范围对于一个技术验证型项目刚刚好。P0保证了一个“能用”的CAD雏形,P1提升可用性,P2则锻炼工程架构能力。不要一上来就做圆角、倒角、放样、扫描,这些几何算法复杂度和基础功能不在一个量级。
1.3 模块划分:从显示到几何,各层职责要清楚
整个程序我分成了三个层次,每一层只跟相邻层通信,尽量避免跨层调用。
界面层负责菜单、工具栏、模型树、快捷键和属性面板。命令控制器层负责接收界面指令,解析参数,调用几何内核完成建模,并维护命令历史。几何内核层封装OpenCascade的数据构建和显示逻辑,向上层暴露简洁的接口。
这样的分层带来的最直接好处是:当你想把桌面程序迁移到Web端时,只有界面层需要重写,命令控制器和几何内核层可以原样复用。我做这个项目时一直想着后续可能要转WebAssembly,所以几何层的接口尽量设计成与UI无关的纯数据输入、数据输出形式,函数签名基本就是参数进、TopoDS_Shape出。
2. 核心数据结构与显示链路
2.1 TopoDS_Shape:OCCT里所有几何的统一“身份证”
OpenCascade的所有几何对象最终都可以归结为一个TopoDS_Shape,这是理解OCCT的关键。你可以把它理解成一个容器,里面装的是拓扑结构:一个顶点是TopoDS_Vertex,一条边是TopoDS_Edge,一个面是TopoDS_Face,一个实体是TopoDS_Solid,多个实体放在一起是TopoDS_Compound。
这种层级结构的好处是,无论你操作的是点、线、面还是体,函数签名都可以统一用TopoDS_Shape来接收和返回。比如布尔运算的输入是两个TopoDS_Shape,输出还是TopoDS_Shape。
创建一条边非常简单:
gp_Pnt p1(0.0, 0.0, 0.0); gp_Pnt p2(100.0, 0.0, 0.0); TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(p1, p2);但要注意,MakeEdge是OCCT里的“构建器工厂”风格API。它内部会同时生成几何数据和拓扑数据,边线的几何是一条直线段,拓扑上是两个顶点通过边连接。这里有个工程上的隐藏问题:OCCT默认容差是1e-7,如果你创建一条长度为1e-8的边,算法会直接报错或产生退化边。所以设计UI交互时,最小输入尺寸要设置一个合理下限。
2.2 AIS_Shape与交互选择:显示和拾取不用自己写OpenGL
OCCT自带一套基于OpenGL的可视化模块,核心是AIS_InteractiveContext和V3d_View。AIS_Shape是AIS_InteractiveObject的子类,专门用来显示TopoDS_Shape。
初始化可视化的核心代码大致是这样:
// 初始化3D视图 Handle(V3d_Viewer) viewer = new V3d_Viewer( new V3d_GraphicDriver(graphicDriver), V3d_XposYnegZpos ); Handle(AIS_InteractiveContext) context = new AIS_InteractiveContext(viewer); // 显示一个形状 Handle(AIS_Shape) aisShape = new AIS_Shape(solidShape); context->Display(aisShape, AIS_Shaded, 0, Standard_True);这里有个新手容易忽略的点:图形驱动的初始化依赖当前窗口的Native Handle。通过Qt集成时,需要把QWidget的winId传入OpenCascade的窗口系统,二者才能正确绑定。如果你发现渲染窗口是黑的,大概率是这一步没接好。
鼠标拾取可以直接用context的MoveTo和Select:
context->MoveTo(x, y, view, Standard_True); context->Select(Standard_True);MoveTo会做射线检测,把鼠标屏幕坐标转换为三维射线,与场景中的AIS_Shape求交。拾取结果通过context->SelectedInteractive()获取,拿到的就是对应的AIS_Shape,再通过AIS_Shape::Shape()取回TopoDS_Shape。这个交互模型非常清晰,完全不需要自己写射线碰撞算法。
2.3 从鼠标点击到模型创建的命令链路
我在这套程序里实现的核心交互链路是:鼠标点击拾取输入点 -> 根据当前命令类型构造对象 -> 刷新模型树和视图。
整个链路的关键在于坐标转换。OCCT内部默认用毫米作为长度单位,而Qt鼠标事件给的是屏幕像素坐标。需要先调用convertToWorld把屏幕坐标换算成OCCT三维坐标。不同视图、不同相机角度下,同一个屏幕位置对应的世界坐标是不同的。
这里有一个实用性建议:不要让绘制逻辑直接读取鼠标事件。我封装了一个InputManager,它负责收集用户点击的多个点,攒够足够参数后触发对应的BRepBuilderAPI工厂。比如画矩形需要两个对角点,画圆需要一个圆心和一个半径点,画圆弧需要三点。参数收集完整后一次性构造几何,这样既方便Undo/Redo记录,也避免了半成品对象出现在场景里导致选择混乱。
3. 关键功能实现与核心代码
3.1 环境搭建:OCCT与Qt的编译集成
如果你只想快速跑起来,可以直接下载OpenCascade官方编译好的二进制包,Windows下有VS对应版本的预编译库。但官方二进制包有时候版本滞后,且只带release版,调试时看不到OCCT内部调用栈。我实际更推荐自己编译,虽然要花一个晚上,但可控性高得多。
CMake集成的关键配置如下:
set(OpenCASCADE_DIR "C:/OpenCascade/opencascade-7.8.0/build") find_package(OpenCASCADE REQUIRED) target_include_directories(${PROJECT_NAME} PRIVATE ${OpenCASCADE_INCLUDE_DIR} ) target_link_libraries(${PROJECT_NAME} PRIVATE TKernel TKBRep TKG3d TKGeomBase TKTopAlgo TKPrim TKBool TKSTEP TKXSBase TKService TKV3d TKOpenGl )这些库分别负责什么?简单说:TKernel是基础库,线程容器数学工具都在里面;TKBRep负责拓扑和几何数据定义;TKG3d是三维曲线曲面的数学描述;TKTopAlgo提供拓扑算法如倒角、扫掠;TKPrim是基本图元(球、圆柱、长方体);TKBool是布尔运算;TKSTEP是STEP格式读写;TKV3d和TKOpenGl是可视化相关。
编译时的三个铁律:Release/Debug库绝对不能混用,架构必须统一为x64,CMake路径里不要出现中文和空格。这三条我从一开始就严格守着,后来团队里有人没遵守,直接出现一堆莫名其妙的链接错误。
3.2 画直线、拉伸、布尔运算的代码实现
画直线的实现最基础,但它是理解OCCT建模思路的最佳入口:
TopoDS_Shape createLine(const gp_Pnt& start, const gp_Pnt& end) { return BRepBuilderAPI_MakeEdge(start, end); }拉伸操作把二维面变成三维实体,核心函数是BRepPrimAPI_MakePrism:
TopoDS_Shape extrudeFace(const TopoDS_Shape& face, const gp_Vec& direction) { BRepPrimAPI_MakePrism prism(face, direction); return prism.Shape(); }这里输入必须是面TopoDS_Face。如果你在二维草图里画了一个闭合圆,先用BRepBuilderAPI_MakeFace构造平面,再传给MakePrism。向量长度决定了拉伸高度,方向决定了拉伸方向。有个容易出问题的地方:如果草图平面法向和拉伸方向平行度不好,会生成自相交实体,拉伸前最好做一下方向矫正。
布尔差集用BRepAlgoAPI_Cut实现,这是最常用的造型操作:
TopoDS_Shape booleanCut(const TopoDS_Shape& base, const TopoDS_Shape& tool) { BRepAlgoAPI_Cut cut(base, tool); return cut.Shape(); }用这个函数实现“在长方体上打孔”的核心逻辑是:先创建一个圆柱作为工具实体,位置对准打孔点,然后用基体减去圆柱。这里有个关键经验,布尔运算对几何容差极其敏感,两个实体如果刚好“贴”在一起但不重叠,经常会产生不完整的结果。最保险的做法是让工具实体比目标位置大一小段距离,比如圆柱高度比长方体厚度多2毫米,最后多余的部分会被切除。
布尔运算的结果有时候会出现面丢失或嵌套结构,这种情况下调用BRepCheck_Analyzer检查一下形状有效性,如果返回无效,可以尝试对输入做healer修复,或者调整工具实体的位置和大小。
3.3 STEP文件的导入导出
STEP是CAD领域最通用的数据交换格式,OpenCascade对它的支持很完善。导出代码非常简洁:
void exportStep(const TopoDS_Shape& shape, const std::string& path) { STEPControl_Writer writer; writer.Transfer(shape, STEPControl_AsIs); writer.Write(path.c_str()); }导入类似:
TopoDS_Shape importStep(const std::string& path) { STEPControl_Reader reader; reader.ReadFile(path.c_str()); reader.TransferRoots(); return reader.OneShape(); }这里有个实际使用中一定会遇到的问题:中文路径。OCCT的文件读写接口内部用的是系统默认编码,在Windows中文系统下如果路径里有中文字符,写入会静默失败,返回的布尔值却是成功。排查这个问题的代价很高,我最终严格遵守“所有工程文件和数据文件一律用英文路径”这条铁律。
还有一个细节:STEP文件里可能包含多个产品实例,TransferRoots之后,ONEShape得到的可能是Compound,遍历子形状时要用TopoDS_Iterator或TopExp_Explorer,不要在没确认类型时强制转换。
4. 常见问题与排查技巧实录
4.1 OpenCascade编译期高频坑
编译OCCT的坑我基本都踩过一遍。最典型的是第三方依赖缺省会“保留”一些功能,编译时提示缺FreeType,OCCT会自动跳过字体相关模块,但如果你后续要用文本标注功能,就会瞬间瘫痪。建议编译前把以下依赖都装好:FreeType、TBB、RapidJSON、OpenGL。特别是TBB,OCCT的并行网格化算法依赖它,缺了以后大数据量模型的显示性能会差一个数量级。
另一个高频问题是运行时提示找不到TKernel.dll等动态库。这是因为OCCT的bin目录没有加入系统PATH,或者你的应用程序没有把OCCT的bin目录拷贝到可执行文件目录附近。发布程序时,用windeployqt处理完Qt依赖后,还要手动拷贝OCCT的bin目录下所有DLL,过滤掉没用的测试程序即可。
还有一次我花了整整一下午排查一个“离奇”的崩溃:程序启动后随机崩溃,而且只在配置为Release模式时才出现。最后发现是OCCT的Release库和Debug库混用了,解决方案库版本和调试库引用的库应该分开配置Build类型。
4.2 显示乱线与渲染异常的处理
这个问题的排查路径很有代表性。症状是视口内模型显示出现放射状乱线,几何数据本身并没有问题,只是显示异常。我把这张“乱线”问题拆成三类来排查:
第一类,显卡驱动问题。OpenCascade的渲染默认使用OpenGL兼容模式,部分显卡驱动在兼容模式下会抽风,尤其是一些老显卡。解决办法是切换渲染驱动,创建GraphicDriver时尝试OpenGlDriver和OpenGlHighLODDriver,或者关掉显卡驱动里的强制抗锯齿项。
第二类,离散化精度问题。OCCT在显示曲面时,默认按一定偏差做三角形网格化。如果模型的偏差系数设置得过大,网格化后的三角形看起来会有明显的棱边或裂痕,在极端角度下会被误认成乱线。调整方式是通过AIS_Shape的SetDeviationCoefficient设置曲面离散化允许偏差,一般使用物体的3D包围盒对角线的1%,太小会显著增加三角面数据量,拖慢渲染。
第三类,z-fighting问题。当两个面重合或几乎重合时,深度缓冲的精度不够会导致画面闪烁,看起来像乱线。解决办法是保证布尔运算后的模型没有重合面,或者给视图设置更高的深度缓冲精度。
4.3 交互稳定性与工程化细节
在交互层我遇到过几次比较恶性的崩溃,基本都跟AIS_InteractiveContext的生命周期有关。OCCT的Handle是引用计数指针,如果你在Qt的lamda回调里提前释放了一个AIS对象,但视图还在渲染它,程序就会崩溃。我的处理方式是把所有AIS_Shape对象统一放在一个生命周期管理器里,每次操作完成后,旧对象先标记Remove,再触发视图重绘,最后在下一个事件循环空闲时真正释放。
另一个实用性建议是顶点的拾取阈值。OCCT默认的选择精度可能偏大,你点击空白处时偶尔会选到相邻的顶点。通过SetPrecision和SetPixelTolerance可以控制拾取容差。我一般把像素容差设为10~15像素,太小的话鼠标稍微一抖就选不中。
对于Undo/Redo的实现,不要试图逆运算几何。布尔运算不是可逆的。最简单可靠的做法是记录每个命令执行前的TopoDS_Shape快照,用OCCT的Copy算法深拷贝一份,存入命令栈。快照方式虽然内存开销大,但对于轻量CAD项目来说完全足够,而且逻辑清晰,不容易出错。
我的做法是封装一个DocumentCmd接口,包含execute、undo、redo三个方法。每个命令对象内部持有原始形状快照和操作参数,undo时用快照恢复场景,redo时重新执行参数。这样代码结构统一,后续扩展镜像、阵列、圆角等新功能时,只需要新增命令类,不需要改控制器逻辑。
还有一条关于大数据模型的性能经验:当模型面数超过百万级之后,每次几何操作都做整体重建会卡顿。解决方案是引入“层级显示”策略,在操作期间用简化显示模式(线框)显示中间结果,操作完成后才切换回完整着色模式。实际测试下来,这个优化让整体交互流畅度提升了将近3倍。
模块化设计还有一个额外好处:如果团队里有人只懂Python不会C++,几何层的算法可以用pybind11暴露成Python接口,利用OCCT的Python绑定做批量处理和自动化测试。我在项目后期就是靠这套Python绑定,把几个复杂的建模流程转成了脚本,用脚本批量跑回归测试,省下了大量手动验证时间。
我个人在实际操作中最深的体会是:用OpenCascade做CAD功能,真正的工作量不在于“调用哪个API”,而在于你如何组织数据结构、如何管理显示生命周期、如何设计命令链路。几何内核只是提供了一堆好用的积木,搭成什么样的房子,完全取决于你的架构水平。希望这篇分享能让你少走一些弯路,如果后续你在集成OCCT时遇到其他稀奇古怪的问题,欢迎一起交流。
本文还有配套的精品资源,点击获取