OCCT OCAF 实战

OCCT OCAF 实战:从"看不见的数据"到数据驱动的 3D 显示

前段时间在学 OpenCASCADE(OCCA Technology,下称 OCCT)的 OCAF 模块,踩了不少坑,也终于把"数据驱动 3D 显示"这条链路跑通了。这篇文章记录完整过程:从 OCAF 是什么、怎么搭,到自定义属性、撤销重做、持久化,最后用 TPrsStd 把数据接到 3D 视图,并解决"改了参数 viewer 没反应"的经典问题。

环境:OCCT 7.9.3(Homebrew 安装)/ macOS + Apple M2 / CMake + GLFW / C++17

最终效果:两个零件来自同一个 OCAF 文档,各自带着不同的变换属性------一个绕斜轴旋转 45°,一个平移到远处

一、OCAF 到底是干什么的?

先说清一个容易混淆的点:几何内核和 OCAF 是两层东西。

  • 几何内核(TKBRep/TKPrim...)管的是"形状是什么 "------TopoDS_Shape 就是一个纯几何对象,程序一关啥都不剩
  • OCAF(Open CASCADE Application Framework)管的是 CAD 应用的"其他一切":
CAD 软件需要 OCAF 提供
模型层次结构(装配→零件→特征) 标签树(TDF_Label)
参数、名字、注释、引用 标准属性(TDataStd_*)
自定义业务数据 自定义属性(继承 TDF_Attribute)
Ctrl+Z / Ctrl+Y 事务 + TDF_Delta 自动生成
形状的参数化演化历史 TNaming(拓扑命名)
文档保存/打开 持久化驱动框架

一句话:几何内核是"画图的",OCAF 是让你做出"CAD 软件"的。

二、工程搭建:两个链接坑

用 CMake 链 OCAF 库时踩了两个坑(7.9.3):

cmake 复制代码
target_link_libraries(ocaf_tutorial PRIVATE
  TKernel TKMath          # 基础 / gp 数学
  TKCDF TKLCAF            # 文档基座(CDM,CDF,TDF) / 标准属性+文档
  TKCAF                   # TNaming 拓扑命名 ← 坑2
  TKBin                   # 完整 OCAF 二进制持久化驱动 ← 坑1相关
  TKBRep TKPrim TKTopAlgo
)
  • 坑 1 :网上老教程让链接 TKTDF------7.9 里这个库不存在,TDF 已并入 TKLCAF
  • 坑 2 :TNaming_NamedShape 符号在 TKCAF 里,不在 TKLCAF,漏链就是一堆 undefined symbol

三、第 1-2 课:应用、文档、标签树

OCAF 的入口是"应用"和"文档"。老教程会教你手写 Plugin/Resources 文件 + 设环境变量------7.9 已经不用了 ,一行 DefineFormat 搞定:

cpp 复制代码
Handle(TDocStd_Application) anApp = new TDocStd_Application();
anApp->DefineFormat ("OCAF-myApplication",                    // 格式名
                     "MyApplication Document Version 1.0",
                     "sta",                                    // 扩展名
                     new BinDrivers_DocumentRetrievalDriver(), // 读取驱动
                     new BinDrivers_DocumentStorageDriver());  // 存储驱动

Handle(TDocStd_Document) aDoc;
anApp->NewDocument ("OCAF-myApplication", aDoc);

文档的本质是一棵标签树 ,每个标签(TDF_Label)可以挂任意多个属性(靠 GUID 区分):

cpp 复制代码
TDF_Label aRoot = aDoc->Main();                     // 根标签 0
TDataStd_Name::Set    (aRoot, "Assembly");          // 挂名字

TDF_Label aBoxLabel = aRoot.FindChild (1);          // 子标签 0:1
TDataStd_Name::Set    (aBoxLabel, "Box");
TDataStd_Integer::Set (aBoxLabel, 1);               // 挂整数
TDataStd_Comment::Set (aBoxLabel, "示例零件");

TDF_Label aDimLabel = aBoxLabel.FindChild (1);      // 孙标签 0:1:1
TDataStd_Real::Set (aDimLabel, 50.0);               // 挂实数(尺寸参数)

TDF_Label aRefLabel = aRoot.FindChild (3);
TDF_Reference::Set (aRefLabel, aBoxLabel);          // 跨分支引用(持久化)

记住这个心智模型:OCAF ≈ CAD 版的数据库------Label 是表,Attribute 是字段,TDF_Delta 是事务日志,StorageDriver 是存储引擎。

四、第 3 课:自定义属性(官方文档有 3 个 bug)

想要自己的业务数据(比如"零件的位姿变换"),继承 TDF_Attribute。核心四件套:

  1. GetID() ------ 属性唯一 GUID
  2. Set() ------ Find-or-Create 惯用法
  3. 修改前 Backup() ------ 撤销的根基
  4. Restore/NewEmpty/Paste 三个虚方法 ------ 撤销/重做/复制粘贴所需
cpp 复制代码
class MyPackage_Transformation : public TDF_Attribute
{
public:
  static const Standard_GUID& GetID();
  static Handle(MyPackage_Transformation) Set (const TDF_Label& theLabel);

  gp_Trsf Get() const;                       // 按记录的类型组装 gp_Trsf
  void SetRotation (const gp_Ax1&, Standard_Real angle);
  void SetTranslation (const gp_Vec&);
  // ... 镜像/缩放/复合

  const Standard_GUID& ID() const override;
  void Restore (const Handle(TDF_Attribute)&) override;
  Handle(TDF_Attribute) NewEmpty() const override;
  void Paste (const Handle(TDF_Attribute)&,
              const Handle(TDF_RelocationTable)&) const override;  // ← 注意 const
  Standard_OStream& Dump (Standard_OStream&) const override;
private:
  gp_TrsfForm myType;
  gp_Ax1 myAx1; gp_Ax2 myAx2; gp_Ax3 myFirstAx3, mySecondAx3;
  Standard_Real myAngle, myScale;
  gp_Pnt myFirstPoint, mySecondPoint;
};

官方文档(ocaf.md)的代码在 7.9 上有 3 处编译错误,踩坑实录:

文档 bug 修正
Dump() 实现里用了未声明的 anOS(参数是 theOS) 改用 theOS
Dump 声明非 const、实现 const,签名打架 统一 const(基类虚函数就是 const)
Paste 实现缺 const(7.9 基类签名是 const = 0) 补 const
Set 声明按值传 TDF_Label、实现按引用 统一 const TDF_Label&

撤销/重做有三个前置条件,缺一个都不生效(我第一次跑 Undo 纹丝不动,查了半天):

cpp 复制代码
aDoc->SetUndoLimit (10);          // ① 默认撤销是【禁用】的,必须设上限

aDoc->NewCommand();               // ② 每组修改必须包在事务里
aTrsf->SetRotation (gp_Ax1 (gp_Pnt(0,0,0), gp_Dir(0,0,1)), M_PI / 4.0);
aDoc->CommitCommand();            // ③ Commit 时才生成 TDF_Delta

aDoc->Undo();                     // 回滚到上一个 delta
aDoc->Redo();

五、第 4-5 课:TNaming 与持久化

把形状挂到标签上,附带"演化历史"(PRIMITIVE=新图元/MODIFY=修改/...):

cpp 复制代码
TDF_Label aBoxLabel = aRoot.FindChild (1);
BRepPrimAPI_MakeBox aMakeBox (50, 50, 50);
TNaming_Builder aBuilder (aBoxLabel);
aBuilder.Generated (aMakeBox.Shape());    // PRIMITIVE 演化

保存与打开:

cpp 复制代码
anApp->SaveAs (aDoc, "ocaf_tutorial.sta");
anApp->Close (aDoc);
anApp->Open ("ocaf_tutorial.sta", aDoc2);   // 标准属性、NamedShape 全部还原

六、解剖 .sta 文件:里面到底存了什么?

二进制文件直接 strings + xxd 就能看出结构(源码在 BinLDrivers_DocumentStorageDriver.cxx):

go 复制代码
"BINFILE"                          ← 魔数 7 字节
01 02 03 04                        ← 字节序标记(小端)
FSD_FileHeader(12×int32)          ← 各 section 偏移表(未用=-1)
── ASCII 信息区(明文!)──
"10/22/2026"
"FILE_FORMAT: OCAF-myApplication"
"REFERENCE_COUNTER: 0"
"MODIFICATION_COUNTER: 1"
"START_TYPES"                      ← 类型注册表(类型名↔整数ID)
  TDataStd_Name / TDataStd_Integer / TDataStd_Comment
  / TDataStd_RealArray / TNaming_NamedShape / ...
"END_TYPES"
── 二进制体 ──
递归标签树: [int32 tag] + 属性流[类型ID, 对象ID, 载荷...]
形状数据: TNaming 的 BRep 二进制(v12+ 内联在属性流)

心智模型:OCAF ≈ CAD 版数据库 ------Label 是表,Attribute 是字段,.sta 是存储引擎的落盘格式。

七、第 6 课:TPrsStd 三步法,数据接上 3D 视图

ocaf_tutorial 是纯控制台的(数据层本来就没 GUI)。要让数据"被看见",官方 TPrsStd_Sample 给了三步法:

cpp 复制代码
// ① 把 "AIS 视图器" 作为【属性】挂到根标签 ------ 数据框架从此知道自己连着哪个视图
TPrsStd_AISViewer::New (aDoc->Main(), myV3dViewer);

// ② 从数据框架取回该属性
Handle(TPrsStd_AISViewer) anAisViewer;
TPrsStd_AISViewer::Find (aDoc->Main(), anAisViewer);

// ③ 交互上下文不自己 new, 而是从数据框架里拿!
myContext = anAisViewer->GetInteractiveContext();

// 把形状标签变成可显示对象 (关联它的 TNaming_NamedShape)
Handle(TPrsStd_AISPresentation) aPrs =
    TPrsStd_AISPresentation::Set (shapeLabel, TNaming_NamedShape::GetID());
aPrs->SetColor (Quantity_NOC_GREEN);
aPrs->Display (true);

第 ③ 步是 OCAF 的灵魂:AIS_InteractiveContext 不在 main 里创建,而是作为属性存在文档里------"哪些对象显示、什么颜色"成了文档数据的一部分,换台电脑打开,显示状态原样还原。

我把 glfw 窗口与这套数据接起来,3D 窗口里出现了绿色线框立方体 ------它就是从 .sta 里读出来的那个方块。

绿色方块 = 文档 0:1 标签上的 TNaming 形状;金色圆锥 = 0:2 标签(顺带也放进文档了,但还没给它接显示)

八、数据驱动显示:同一形状,不同姿态

真正有意思的来了。给每个零件标签挂一个独立的变换属性:

css 复制代码
0 "Assembly"
├─ 0:1 "Box"   NamedShape(方块@原点) + Transformation(绕Z旋转45°)
├─ 0:2 "Cone"  NamedShape(圆锥@(25,125,0)) + Transformation(平移100,0,0)
└─ 0:3 Reference → 0:1

viewer 端遍历零件标签,把变换属性读出来作用到显示对象:

cpp 复制代码
for (TDF_ChildIterator it (myDoc->Main()); it.More(); it.Next())
{
  TDF_Label aPart = it.Value();
  Handle(TNaming_NamedShape) aNS;
  if (!aPart.FindAttribute (TNaming_NamedShape::GetID(), aNS)) continue;

  Handle(TPrsStd_AISPresentation) aPrs =
      TPrsStd_AISPresentation::Set (aPart, TNaming_NamedShape::GetID());

  // ★ 数据驱动显示: 读变换属性 → 作用到 AIS 对象
  Handle(MyPackage_Transformation) aTrsf;
  if (aPart.FindAttribute (MyPackage_Transformation::GetID(), aTrsf))
  {
    aPrs->GetAIS()->SetLocalTransformation (aTrsf->Get());
  }
  aPrs->Display (true);
}

结果:形状几何完全相同,变换属性不同 → 显示姿态完全不同。我给 Box 设的是绕 (10,0,1) 斜轴旋转 45°,Cone 平移 (100,0,0) ------这就是参数化 CAD 的底层逻辑------改数据,不改几何。

两个零件各自的 Transformation 属性被 viewer 读取并作用到显示对象上:绿色方块绕斜轴倾斜 45°,金色圆锥平移到了远处(对比第 6 课配图中二者的建模位置)

一个调试经验:验证变换是否生效,选让姿态变化最明显的参数------绕 Z 轴转 45° 的方块在透视相机下几乎看不出区别,我一开始还以为代码没生效,后来把轴改成斜轴才确认。

九、翻车实录:改了参数,viewer 纹丝不动

得意没多久就翻车了:我改了 tutorial 里 SetTranslation/SetRotation 的参数,重新生成 .sta,viewer 显示毫无变化。

排查后发现是两个原因叠加:

原因 1:撤销/重做演示的事务覆盖。 我的演示是"事务1 平移 → 事务2 旋转 → Undo → Redo",最终状态=事务2 的旋转。改事务1 的平移参数,永远不会体现在最终结果(纯演示用)。

原因 2(致命):自定义属性根本不持久化。 回顾第六节:.sta 的类型表里没有 MyPackage_Transformation------自定义属性没写持久化驱动,保存时被直接跳过 。于是 viewer 打开文件发现"没有变换属性"→ 触发我写的"旧数据重建"逻辑 → 用它自己硬编码的默认参数(45°/100)新建了文档。改什么参数都白改,viewer 永远显示默认值。

修复 :用 OCAF 的正统方案------镜像到标准属性 (ocaf.md 开篇原话:"能用标准属性就用标准属性")。给属性类加两个静态方法,把 3×4 变换矩阵写进 TDataStd_RealArray(1,12)(标准属性自动存盘):

cpp 复制代码
void MyPackage_Transformation::StoreMatrix (const TDF_Label& theLabel, const gp_Trsf& theTrsf)
{
  Handle(TDataStd_RealArray) anArr = TDataStd_RealArray::Set (theLabel, 1, 12);
  int k = 1;
  for (int i = 1; i <= 3; ++i)
    for (int j = 1; j <= 4; ++j, ++k)
      anArr->SetValue (k, theTrsf.Value (i, j));
}

Standard_Boolean MyPackage_Transformation::ReadMatrix (const TDF_Label& theLabel, gp_Trsf& theTrsf)
{
  Handle(TDataStd_RealArray) anArr;
  if (!theLabel.FindAttribute (TDataStd_RealArray::GetID(), anArr) || anArr->Length() < 12)
    return false;
  theTrsf.SetValues (anArr->Value(1), anArr->Value(2), anArr->Value(3), anArr->Value(4),
                     anArr->Value(5), anArr->Value(6), anArr->Value(7), anArr->Value(8),
                     anArr->Value(9), anArr->Value(10), anArr->Value(11), anArr->Value(12));
  return true;
}

数据流变成:

makefile 复制代码
tutorial: SetRotation/SetTranslation → StoreMatrix(RealArray) → .sta 含 RealArray ✓
viewer:   打开 .sta → ReadMatrix → gp_Trsf → SetLocalTransformation → 显示 ✓

第5课读回验证:变换矩阵(RealArray 镜像)读回: 成功。再次修改参数 → viewer 姿态跟着变 ✓。

教训:自定义属性是"内存里的富 API",持久化要么写 BinM 驱动(进阶),要么镜像到标准属性(实用)。

验证对比(下图):修复后重跑 tutorial + viewer,方块的斜轴旋转终于生效了(对比第 8 节配图前的状态------修改参数前 viewer 永远显示默认姿态)。

十、总结

收获 说明
OCAF 分层观 几何内核管形状,OCAF 管模型的一切"其他数据"
标签树 + 属性 万物皆 Label + GUID 属性,Find-or-Create 惯用法
撤销三件套 SetUndoLimit + NewCommand/CommitCommand + 修改前 Backup(),缺一不可
持久化真相 标准属性自动存盘;自定义属性不存(需 BinM 驱动或标准属性镜像)
TPrsStd 三步法 AISViewer 挂根标签 → Find → GetInteractiveContext(),显示状态即数据
7.9 vs 老文档 DefineFormat 替代资源文件;TKTDF 不存在;TNaming 在 TKCAF

完整代码在 GitHub 仓库 learning/ocaf/ 目录:ocaf_tutorial.cpp(5 课数据层)、ocaf_viewer.cpp(第 6 课显示)、MyPackage_Transformation.hxx/.cpp(自定义属性)。

发掘金时:文中配图在 blog/images/ 目录,上传到掘金图床后替换相对路径即可。

下一步计划:OCAF Function Mechanism(ocaf_func.md)------参数驱动重算,改一个圆锥参数自动重建整根"钉子",彻底实现参数化建模。


若有帮助,点个赞再走;有错误欢迎评论区指正。

相关推荐
SmalBox3 小时前
05-07-源码-引用计数与资源生命周期
unity3d·游戏开发
雪庭4 小时前
用 OCCT 造一只啤酒瓶:完整实录
unity3d
SmalBox1 天前
05-06-源码-异步操作体系(OperationSystem)
unity3d·游戏开发
侑虎科技1 天前
深度分析屏幕空间反射算法以及在URP中的实现(一)
unity3d
SmalBox2 天前
05-05-源码-资源加载与Provider体系
unity3d·游戏开发
SmalBox3 天前
05-04-源码-资源定位系统
unity3d·游戏开发
SmalBox4 天前
05-03-源码-运行模式体系(PlayMode)
unity3d·游戏开发
SmalBox5 天前
05-02-源码-全局入口与包管理(YooAssets & ResourcePackage)
unity3d·游戏开发
SmalBox6 天前
05-01-源码-运行时总览
unity3d·游戏开发