OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(4):GstarCAD / AutoCAD 客户端相关产品 —— 深入骨髓的数据库哲学)

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. 计算几何与数值稳定性:拒绝“漏选”和“错误干涉”)
    • 故事尾声:站在工业巨人的肩膀上

代码仓库入口:


系列文章规划:

巨人的肩膀:

  • 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::getGripPointsAcDbEntity::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::atomicstd::shared_mutex,确保高并发下不会崩溃。

性能瓶颈定位

当 10 万个螺栓同时出现,你的系统开始掉帧。你用 Intel VTuneVisual Studio Profiler 进行剖析:

  • 发现 70% 的时间花在 GetEntityList 上,因为它遍历了所有实体来构建显示列表。
  • 你用 空间索引 重构:只把视锥体内的实体传给 GPU。
  • 发现单个螺栓的 worldDraw 生成 500 个三角形,DrawCall 太多。
  • 你引入 实例化渲染:所有相同类型的螺栓共享一个顶点缓冲区,只传变换矩阵。
  • 发现内存碎片导致 new 操作频繁,你改用内存池

这些优化后,百万级实体也能流畅运行。

4. 跨平台与国产化适配:你的 CAD 不能只活在 Windows 上

浩辰和中望的用户越来越多地在 Linux 上部署(比如国产操作系统)。

你的 CAD 原本只依赖 Windows 的 MFC 和 DirectX,现在必须移植。

你开始用 CMake 管理项目,替换掉 Visual Studio 工程文件。

你将 UI 层从 MFC 替换为 QtwxWidgets ,保证在 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 的 STEPIGES 作为中间格式,也可以直接集成 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 从一个人的玩具,变成了能交付给大客户的产品。

虽然前路还很长,但你不再畏惧------因为你知道,每一步难题背后,都有经典的设计哲学和成熟的解决方案等着你去学习和运用。


相关推荐
二等饼干~za8986682 小时前
豆包GEO优化源码开发全解析:技术架构、实现逻辑与实操指南
数据库·sql·重构·架构·mybatis·音视频
青梅煮酒与君饮2 小时前
深度刨析RAG检索增强
数据库·人工智能·深度学习·语言模型·知识图谱
NineData2 小时前
MySQL到StarRocks 同步链路中的建表、DDL 跟随与数据校验
运维·数据库·starrocks·mysql·数据迁移·数据库管理工具·ninedata
fen_fen2 小时前
DB2表创建与Python插入、查询实操解析
数据库·oracle
zdl6863 小时前
MSSQL2022的一个错误:未在本地计算机上注册“Microsoft.ACE.OLEDB.16.0”提供程序
数据库·microsoft
gechunlian883 小时前
MySQL - Navicat自动备份MySQL数据
android·数据库·mysql
u86883 小时前
大模型呼叫中心助力物业报修自动化
运维·数据库·自动化
zhenxin01223 小时前
5、使用 pgAdmin4 图形化创建和管理 PostgreSQL 数据库
数据库·postgresql
keyborad pianist3 小时前
MySQl
数据库·mysql·oracle