OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(4):GstarCAD / AutoCAD 客户端相关产品 ------ 深入骨髓的数据库哲学)
-
- 从个人项目到工业级软件:你还差什么?
- [方面一:GstarCAD / AutoCAD 客户端相关产品 ------ 深入骨髓的数据库哲学](#方面一:GstarCAD / AutoCAD 客户端相关产品 —— 深入骨髓的数据库哲学)
-
- [1. 对象数据库:不仅仅是"层表"和"块表"](#1. 对象数据库:不仅仅是“层表”和“块表”)
- [2. 自定义实体与协议扩展:让你的"螺栓"成为一等公民](#2. 自定义实体与协议扩展:让你的“螺栓”成为一等公民)
- [3. 海量图纸的工业化策略:GB 级图纸不卡不崩](#3. 海量图纸的工业化策略:GB 级图纸不卡不崩)
- [4. 跨平台与国产化适配:你的 CAD 不能只活在 Windows 上](#4. 跨平台与国产化适配:你的 CAD 不能只活在 Windows 上)
- [方面二:三维图纸格式与几何内核 ------ 从网格到精确数学](#方面二:三维图纸格式与几何内核 —— 从网格到精确数学)
-
- [1. 几何内核进阶:你的 CAD 需要一个"心脏"](#1. 几何内核进阶:你的 CAD 需要一个“心脏”)
- [2. 格式交换与转换引擎:打通工业生态](#2. 格式交换与转换引擎:打通工业生态)
- [3. 计算几何与数值稳定性:拒绝"漏选"和"错误干涉"](#3. 计算几何与数值稳定性:拒绝“漏选”和“错误干涉”)
- 故事尾声:站在工业巨人的肩膀上
代码仓库入口:
系列文章规划:
- (OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(1):从开发的视角看下CAD画出那些好看的图形们))
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(2):看似"老派"的 C++ 底层优化,恰恰是这些前沿领域最需要的基础设施)
- OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(3):你的 CAD 终于能画标准零件了,但用户想要"弧面"、"流线型",怎么办?)
巨人的肩膀:
- deepseek
- gemini
从个人项目到工业级软件:你还差什么?
你的 CAD 已经能画 NURBS 曲面、处理百万级面片、支持 UCS 和布局,甚至能通过 C API 供其他语言调用。
朋友看了都惊叹:"这已经是一个能用的 CAD 了!"
但你心里清楚,它离真正的商业软件(AutoCAD、浩辰CAD)还差着十万八千里。
那些软件能在全球数以万计的工程师手中稳定运行,每天处理 GB 级的图纸,支持成百上千个第三方插件,还能在 Windows、Linux 上无缝切换。
你决定,接下来要攻克的就是这些"工业化"的硬骨头。
方面一:GstarCAD / AutoCAD 客户端相关产品 ------ 深入骨髓的数据库哲学
1. 对象数据库:不仅仅是"层表"和"块表"
你早就知道 AutoCAD 内部是一个图形数据库,但你一直把它想得太简单:不就是几个表存实体嘛。
直到你第一次打开一个 2GB 的 DWG 文件,用 ARX 插件去遍历所有实体,程序瞬间卡死,然后崩溃。
你才意识到,你对数据库的理解还停留在"玩具"层面。
AcDbDatabase 的完整结构
你翻开 ObjectARX 文档,发现数据库远比你想的复杂:
- 符号表 :你熟悉的
AcDbLayerTable(层表)、AcDbBlockTable(块表),还有AcDbLinetypeTable(线型表)、AcDbTextStyleTable(文字样式表)、AcDbDimStyleTable(标注样式表)...... 它们是数据库的"系统表",存储全局定义。 - 命名对象字典 :这是一个
AcDbDictionary,可以存放任何"不是符号表也不是实体"的对象。比如图纸的自定义属性(作者、创建时间)、应用程序的扩展数据、布局(AcDbLayout)等。你想保存某个插件的配置,就把它放到命名对象字典里。 - 句柄(Handle)与对象ID(ObjectId) :每个对象(实体、符号表记录、字典条目)都有一个唯一的 句柄 (一个字符串,如
"3A2"),它在整个文件生命周期内不变。当你打开文件时,AutoCAD 会把句柄映射为临时的 对象ID (一个 64 位整数,仅在当前会话有效)。对象ID 是操作对象的"指针",但比裸指针安全得多------即使对象被删除,通过 ID 访问会返回NULL,而不是野指针。
你终于明白为什么 AutoCAD 文件关闭再打开,块引用还能找到块定义,层引用还能找到层记录:因为 DWG 文件里存的是句柄,打开时重新建立映射关系。
这就像数据库里的外键,靠的是字符串 ID 而不是内存地址。
事务机制:让 Undo/Redo 成为可能
你之前实现移动实体时,是直接修改实体的几何数据。用户说"我移错了,要撤销",你只好在代码里手动记住旧位置,写一大堆恢复逻辑。
而 AutoCAD 用 事务(Transaction) 解决了这个问题:
cpp
AcDbDatabase* pDb = ...;
AcTransaction* pTrans = actrTransactionManager->startTransaction();
// 打开实体进行写操作
AcDbEntity* pEnt = acdbOpenObject<AcDbEntity>(objId, AcDb::kForWrite);
pEnt->transformBy(...); // 移动
pEnt->close();
// 提交事务,此时 Undo 记录被自动压入栈
pTrans->end();
当你调用 startTransaction 后,所有对对象的修改都被记录下来。end 时,如果一切正常,修改生效并压入 Undo 栈;如果中途出错或用户取消,调用 abort 就能回滚。
你发现,事务不仅是 Undo/Redo 的基础,也是实现"非模态对话框"时保证数据一致性的关键------任何时候想撤销修改,只需要回滚事务,不需要自己维护状态。
2. 自定义实体与协议扩展:让你的"螺栓"成为一等公民
你之前为了画螺栓,在代码里动态生成一堆线、圆、圆柱面。但用户想要像 AutoCAD 原生对象一样操作它:选中后显示夹点(比如可以拖拽改变螺栓长度),在属性面板里直接修改直径,还能在块编辑器中编辑。
你意识到,你需要 自定义实体。
从 AcDbEntity 派生
你定义了一个类 CMyBolt,继承自 AcDbEntity,并重写关键方法:
worldDraw():负责在屏幕上绘制自己。这里你根据当前的直径、长度,用 OpenGL 或 AutoCAD 的绘图 API 生成几何图形。你不需要每次都重新计算 B-Rep,只需在属性改变时标记为"需要重绘"。subGetGripPoints()/subMoveGripPointsAt():返回夹点位置(比如螺栓头部和尾部),并处理用户拖拽夹点后的几何更新。subTransformBy():支持旋转、缩放等变换。dwgInFields()/dwgOutFields():自定义实体的序列化,让它可以被保存到 DWG 文件。
这样,你的螺栓就像原生 LINE、CIRCLE 一样,支持所有 AutoCAD 标准操作。
你甚至可以在属性面板里显示"直径"、"长度"、"螺纹类型"等自定义属性,通过实现 AcDbEntity::getGripPoints 和 AcDbEntity::getProperties 接口。
反应器:监听数据库事件
有一天,你想实现一个功能:当用户删除某个图层时,自动把该图层上的螺栓移动到另一个图层。
你不想在删除图层的代码里写硬编码逻辑,因为删除图层的操作可能来自用户、其他插件、或者脚本。
你找到了 反应器(Reactor) 。
反应器就像观察者模式,你可以注册一个对象,当特定事件发生时被通知:
cpp
class CMyLayerReactor : public AcDbLayerTableReactor {
public:
virtual void erased(const AcDbObjectId& layerId, Adesk::Boolean erasing) override {
if (erasing) {
// 图层被删除,处理螺栓迁移
}
}
};
然后把这个反应器添加到图层表。
从此,你的插件可以"静默"地响应数据库变化,而不用侵入式地修改原有逻辑。
3. 海量图纸的工业化策略:GB 级图纸不卡不崩
你从客户那里拿到一张 2.5GB 的地形测绘 DWG,里面包含几百万个三维点云和等高线。
你的 CAD 直接卡死在"打开文件"界面,内存爆了。
你痛定思痛,研究工业级 CAD 是如何处理这种"怪兽图纸"的。
按需加载
AutoCAD 并不会把整个文件读进内存。它只读取文件头、符号表、以及"页面映射",然后在用户平移/缩放时,根据当前视口范围,动态从磁盘读取需要的实体块。
你决定实现自己的 瓦片加载 :将模型空间划分为网格(比如 100m×100m 的瓦片),每个瓦片对应一个独立的实体集。打开文件时,只加载当前视口内的瓦片;当用户平移,预加载相邻瓦片,并卸载远离视口的瓦片。
你结合空间索引(BVH 或四叉树)来快速判断哪些实体属于当前瓦片。
显示与数据分离
你发现渲染线程和几何计算线程争抢 CPU,导致 UI 卡顿。
你将它们分离:
- 渲染线程:只负责 OpenGL 绘制,基于上一帧的"显示缓存"。
- 工作线程:负责加载新瓦片、更新 BVH、计算射线拾取等。
你需要用无锁队列 传递数据,用互斥锁 保护共享资源(如实体列表)。
你开始研究 std::atomic、std::shared_mutex,确保高并发下不会崩溃。
性能瓶颈定位
当 10 万个螺栓同时出现,你的系统开始掉帧。你用 Intel VTune 和 Visual Studio Profiler 进行剖析:
- 发现 70% 的时间花在
GetEntityList上,因为它遍历了所有实体来构建显示列表。 - 你用 空间索引 重构:只把视锥体内的实体传给 GPU。
- 发现单个螺栓的
worldDraw生成 500 个三角形,DrawCall 太多。 - 你引入 实例化渲染:所有相同类型的螺栓共享一个顶点缓冲区,只传变换矩阵。
- 发现内存碎片导致
new操作频繁,你改用内存池。
这些优化后,百万级实体也能流畅运行。
4. 跨平台与国产化适配:你的 CAD 不能只活在 Windows 上
浩辰和中望的用户越来越多地在 Linux 上部署(比如国产操作系统)。
你的 CAD 原本只依赖 Windows 的 MFC 和 DirectX,现在必须移植。
你开始用 CMake 管理项目,替换掉 Visual Studio 工程文件。
你将 UI 层从 MFC 替换为 Qt 或 wxWidgets ,保证在 Linux 下也能编译。
你发现 OpenGL 比 DirectX 更跨平台,于是统一用 OpenGL 渲染。
窗口系统差异:Windows 用 Win32,Linux 用 X11 或 Wayland,macOS 用 Cocoa。你封装了一个 PlatformWindow 类,在不同平台下创建 OpenGL 上下文。
你还遇到 C++ ABI 兼容性问题:不同编译器(MSVC vs GCC)生成的代码可能不兼容。你用 extern "C" 导出 C API,确保插件能跨编译器加载。
方面二:三维图纸格式与几何内核 ------ 从网格到精确数学
1. 几何内核进阶:你的 CAD 需要一个"心脏"
你之前用网格表示一切,但用户想要精确的圆柱面、球面,想要布尔运算(并、交、差)。
你决定集成一个 几何内核,而 Open CASCADE 是你最好的选择。
用 OCCT 构建精确的螺栓
你不再用一堆三角形拼凑螺栓,而是用 OCCT 的 B-Rep 构建:
cpp
// 创建圆柱体(杆部)
gp_Ax2 axis(gp_Pnt(0,0,0), gp_Dir(0,0,1));
BRepPrimAPI_MakeCylinder cylMaker(axis, radius, length);
TopoDS_Shape cylinder = cylMaker.Shape();
// 创建六角头(拉伸一个六边形)
// ... 代码略
// 创建螺纹(螺旋扫掠)
// ... 代码略
// 合并成一个实体
TopoDS_Shape bolt = BRepAlgoAPI_Fuse(cylinder, head);
bolt = BRepAlgoAPI_Fuse(bolt, thread);
现在,你的螺栓是一个精确的 B-Rep 实体,可以导出为 STEP,可以参与布尔运算。
B-Rep 拓扑访问
你需要知道螺栓的每个面、边、顶点,比如计算表面积。
OCCT 提供迭代器:
cpp
TopExp_Explorer faceExplorer(bolt, TopAbs_FACE);
for (; faceExplorer.More(); faceExplorer.Next()) {
TopoDS_Face face = TopoDS::Face(faceExplorer.Current());
// 获取面的几何(平面、圆柱面、NURBS曲面等)
Handle(Geom_Surface) surf = BRep_Tool::Surface(face);
}
布尔运算与干涉检测
你之前用 BVH 做网格碰撞,现在有了精确 B-Rep,可以直接用 OCCT 的布尔运算库做干涉检测:
cpp
BRepExtrema_DistShapeShape dist(bolt1, bolt2);
if (dist.Value() < tolerance) {
// 发生干涉
}
参数化建模引擎
你开始思考如何让用户像 SolidWorks 一样,修改一个参数(比如直径)就能更新整个模型。
你设计了一个 特征树:每个特征(拉伸、旋转、布尔)记录其输入参数,以及依赖关系。当用户修改直径,你只需重新执行受影响的特征,而不是重建整个模型。
2. 格式交换与转换引擎:打通工业生态
你的 CAD 不能只用自己的格式,必须能读写 STEP、IGES、Parasolid。
STEP 解析与生成
你学习用 OCCT 的 STEPControl_Reader 读取 STEP 文件,将其转换为内部 B-Rep:
cpp
STEPControl_Reader reader;
IFSelect_ReturnStatus status = reader.ReadFile("model.stp");
reader.TransferRoots();
TopoDS_Shape shape = reader.OneShape();
修改后,再用 STEPControl_Writer 写回。
Parasolid (.x_t)
Parasolid 是西门子的内核,许多 SolidWorks、UG 的文件基于它。
你可以用 OCCT 的 STEP 或 IGES 作为中间格式,也可以直接集成 Parasolid 的 SDK(商业授权),实现双向读写。
网格化与精度控制
你要将 NURBS 曲面转为 STL 用于 3D 打印。
OCCT 的 BRepMesh_IncrementalMesh 可以控制 弦高差(deflection):
cpp
BRepMesh_IncrementalMesh mesher(shape, deflection);
弦高差越小,网格越密,文件越大,但打印精度越高。
你需要根据场景(预览 vs 打印)动态调整。
3. 计算几何与数值稳定性:拒绝"漏选"和"错误干涉"
工业软件最不能接受的是"明明选了却选不中"、"明明没干涉却报干涉"。
这背后是浮点数精度问题。
健壮的求交算法
你实现线面求交时,不能用 == 判断相等,而是用容差:
cpp
if (fabs(distance) < tolerance) {
// 认为相交
}
求交时,你需要处理退化情况(如射线与面正好交于边界)。
OCCT 内部有复杂的容差管理,你决定尽量复用它的求交器,而不是自己写。
NURBS 基础知识
你虽然不用自己实现 NURBS,但必须理解它的数学:
- 控制点:定义曲线大致形状。
- 节点矢量:控制分段区间,决定曲线局部支撑性。
- 权重:可以精确表示圆、椭圆(非均匀有理 B 样条)。
当你通过 API 修改控制点,或者从 STEP 中读取 NURBS 曲面时,你需要知道这些参数的含义,才能正确转换和显示。
故事尾声:站在工业巨人的肩膀上
你终于明白,那些商业 CAD 软件不是一个人能写出来的,它们背后是几十年的工程沉淀:
- 数据库设计要能支撑 GB 级文件
- 自定义实体让第三方开发者能无限扩展
- 事务和反应器让交互既安全又灵活
- 跨平台让软件跑在全世界任何角落
- 几何内核让你处理真实的工业零件
- 数据交换格式让你融入整个制造业生态
你的 CAD 从一个人的玩具,变成了能交付给大客户的产品。
虽然前路还很长,但你不再畏惧------因为你知道,每一步难题背后,都有经典的设计哲学和成熟的解决方案等着你去学习和运用。
-
如果想了解一些成像系统、图像、人眼、颜色等等的小知识,快去看看视频吧 :
- 抖音:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
- 快手:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
- B站:数字图像哪些好玩的事,咱就不照课本念,轻轻松松谝闲传
- 认准一个头像,保你不迷路:

- 认准一个头像,保你不迷路:
-
您要是也想站在文章开头的巨人的肩膀啦,可以动动您发财的小指头,然后把您的想要展现的名称和公开信息发我,这些信息会跟随每篇文章,屹立在文章的顶部哦
