ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于OCCT与OSG的轻量化多格式CAD/CAE可视化平台架构实践

基于OCCT与OSG的轻量化多格式CAD/CAE可视化平台架构实践 前阵子接了个内部需求把客户积攒了十几年的各种CAD图纸和模型统一在一个离线环境里“看得起来”。格式嘛STEP、IGES、BREP是常态偶尔冒出来几个STL、OBJ、FBXCAE那边还要挂网格和结果文件。我一开始天真地以为找个现成查看器就行调研一圈发现能看的不能算能算的不能看能拖进来的又跑不动。最后被逼着走了一条OpenCASCADE加OSG的老路。这篇文章不是课程算是我把这套组合从选型到落地的复盘写给正在纠结“轻量化多格式CAD/CAE核心平台”怎么搞的人。1. 为什么是OCCTOSG而不是看起来更省事的现成方案1.1 先绕开一个陷阱FreeCAD内核和Paraview都不能直接当底座很多团队一上来会想既然要开源、要支持多格式是不是直接基于FreeCAD改或者干脆用ParaView做可视化这两个我都认真试过结论是它们适合当“成品工具”使用不适合当“平台内核”去二次生长。FreeCAD的内核本身就是OpenCASCADE但它上面挂了大量的GUI逻辑、工作台机制、参数化约束系统。你把它拉下来当底座相当于买了一套带精装修的房子然后要把墙敲掉重新走水电改造成本远高于毛坯房直接装修。ParaView同理它的可视化管线确实强大但那是面向科学计算结果设计的处理CAD的B-rep边界表示、拓扑命名、精确几何操作完全不是它的主场。这个阶段我的判断标准很简单渲染只是平台的一部分平台的灵魂在于“几何数据能不能被精确解析、轻量化重构、并建立可追溯的映射关系”。这条路只能自己搭。1.2 OpenCASCADE解决的是“几何”问题不是“显示”问题OpenCASCADE简称OCCT在圈内一般是当“几何引擎”用它跟显示没有一毛钱关系。它提供的是B-rep边界表示的数据结构也就是用面、边、顶点来精确描述一个实体模型STEP、IGES、BREP等主流CAD交换格式的成熟读写器布尔运算、倒角、放样、扫掠等造型算法一个叫OCAF的文档框架可以在模型上挂属性比如颜色、图层、名称、PMI标注网格化工具BRepMesh把精确几何离散成三角网格。注意最后一项这是打通CAD和实时渲染的关键桥梁。CAD的精确模型在屏幕上是没法直接绘制的显卡只认识三角形。OCCT负责把精确几何“翻译”成三角形OSG负责把这些三角形高效地画出来。两者分工非常清晰。1.3 OSG解决的是“渲染与交互”问题而且足够轻OSGOpenSceneGraph可能不如Three.js在Web圈那么热闹但在桌面级工业软件里算是老牌开源场景图了。选它有几个非常现实的原因场景图结构天然适合组织CAD装配体根节点下面挂零件节点零件节点下面挂几何节点再用MatrixTransform表达装配位姿NodeVisitor访问器模式可以非常方便地遍历整棵树做统计、修改、拾取插件机制灵活osgDB可以加载很多三方格式甚至可以自己写插件渲染状态管理严谨状态树和渲染树分开大批量几何提交时效率有保障体积和依赖控制得好不像某些引擎动辄带一整套编辑器生态。更重要的是OSG的抽象层级恰到好处。它不会强迫你用某种固定的数据组织方式你可以把它当成一个“高级画布”自己决定怎么往上摆东西。这对做CAD/CAE这种垂直场景来说是极大的自由度。1.4 两套库的数据契约拓扑形状到场景节点的映射OCCT和OSG之间的数据契约是平台设计里最需要想清楚的一件事。我的做法是建立两层映射第一层是拓扑层级映射。OCCT里一个装配体是TopoDS_Compound里面装各种TopoDS_ShapeOSG这边对应建一个osg::Group每个Shape对应一个osg::Geode。映射关系用名字和ID一起维护名字给人看ID给程序追溯。第二层是网格数据映射。OCCT网格化后生成Poly_Triangulation我们把节点坐标、三角形索引、法线取出来填进osg::Geometry的Vec3Array和DrawElementsUInt。这一步看似搬运工但很多平台做不好问题往往出在索引类型、内存拷贝、坐标变换等细节上。提示设计数据契约时能用ID就不要用名字做关联。CAD装配里重名零件太常见了只靠名字迟早出幺蛾子。2. 平台总体架构从文件字节到屏幕像素整条链路怎么搭2.1 分层设计IO层、数据层、服务层、渲染层平台搭成了四层结构每一层职责单一不越界架构图用文字描述大概是这样的IO层负责打开文件、探测格式、解析数据。不关心几何长什么样只负责把文件变成内存里的原始数据对象数据层是OCCT为主场的领域层包含B-rep形状、OCAF文档属性、网格缓冲、CAE结果数据。这一层是整个平台的核心资产服务层把数据层的能力封装成服务比如格式转换、轻量化、装配树提取、包围盒计算、网格修复、结果云图映射渲染层基于OSG构建场景图提供显示、交互、拾取、截面、爆炸图等操作。IO层只跟文件打交道渲染层只跟OSG节点打交道服务层夹在中间做翻译和加工。这样做的直接好处是任何一个格式的解析器出问题不会拖垮渲染渲染要做特效也不要去碰OCCT的数据结构。2.2 多格式接入策略原生的走OCAF网格的走assimp点云走自定义多格式支持的实现策略是分三路并行CAD原生格式STEP、IGES、BREP直接用OCCT的Reader解析数据进OCAF文档保留几何和属性通用网格格式STL、OBJ、FBX、glTF、DAE等用assimpOpen Asset Import Library解析转换成OSG能直接用的网格节点。这一步规避了OCCT对网格格式支持孱弱的问题点云格式LAS、LAZ、XYZ等LAS文件本质是二进制点记录没有拓扑概念。我们用自定义解析器读取点坐标和属性直接生成osg::Geometry以GL_POINTS方式绘制配合八叉树做LOD。网上很多人问osg能不能加载las答案是可以的但不要指望现成插件自己写个解析器反而更可控。三路数据最终汇聚到同一个渲染场景里用不同的节点类型区分比如用NodeVisitor识别节点Name前缀或者维护一个std::maposg::Node*, DataSourceType。2.3 OCAF文档与OSG场景树的桥接模块OCAF文档在OCCT里是一个树形结构跟OSG的场景树很像但完全是两套体系。桥接模块的主要职责是遍历OCAF的TDataStd_Name、TDataStd_Color等属性提取装配结构将装配结构映射为OSG的Group/MatrixTransform层级维护拓扑Shape与OSG节点的双向映射方便后续拾取、高亮、隐藏、属性编辑在OSG节点上挂用户数据osg::Callback或osg::Referenced派生类保存对应的OCCT Shape的TShape哈希值。这个桥接模块写得好不好直接决定平台“能不能改模型”。如果只做查看器单向映射就够了但要做标注、测量、剖切之后还能回到OCCT做布尔运算就必须有完整的双向映射。2.4 插件化模块的注册与加载机制整个平台的格式支持不能写死在核心代码里否则每加一种格式都要重新编译主程序。我们设计了插件注册表机制每个格式解析器实现一个统一接口bool canParse(const std::string ext)和ParseResult parse(const std::string path, ParseOptions options)插件在启动时通过配置文件注册或者放指定目录自动扫描渲染层每种数据源CAD、网格、点云各自维护一个加载器列表按格式扩展名路由。这样做还有一个额外的好处客户现场只要了STEP和STL我们就可以只发布两个插件连OCCT都不装体积和分发成本大幅下降。3. 多格式加载链路STEP/IGES/BREP怎么一步步变成能转的网格3.1 CAD格式的解析入口与参数陷阱OCCT读取STEP的代码非常简单核心就几步#include STEPControl_Reader.hxx #include IGESControl_Reader.hxx STEPControl_Reader reader; IFSelect_ReturnStatus status reader.ReadFile(model.step); if (status ! IFSelect_RetDone) { // 处理读取失败 } reader.TransferRoots(); TopoDS_Shape shape reader.OneShape();看起来简单坑在细节。STEP文件里的单位可能是英寸OCCT默认按毫米处理坐标缩放错了后面全乱套。IGES文件更麻烦很多老图纸是从各种上古CAD系统导出的非流形几何、缝隙、重复面比比皆是。读取之后必须先做一轮健壮性检查不能直接进网格化。实际项目中建议在IO层显式读文件头识别单位必要时做整体缩放对读取结果做BRepCheck_Analyzer检查统计无效面、无效边对检查不过的模型做ShapeFix_Shell、ShapeFix_Solid修复再进下一步全程记录转换日志方便排查“模型显示不对”到底是解析问题还是渲染问题。倒角、圆角这些特征在STEP文件里是精确曲面表示OCCT能完整保留但三角化之后如果参数不对显示出来就是一圈一圈的棱边这是后面网格化要解决的事。3.2 B-rep到三角网格BRepMesh_IncrementalMesh的参数选择OCCT做网格化最常用的类是BRepMesh_IncrementalMesh#include BRepMesh_IncrementalMesh.hxx double linearDeflection 0.1; // 线性偏差单位毫米 double angularDeflection 0.5; // 角度偏差单位度 BRepMesh_IncrementalMesh mesh(shape, linearDeflection, angularDeflection); shape.Nullify(); // 释放原始形状引用减少内存占用这个线性偏差是最关键参数。0.1意味着网格化后的三角形相对于原始曲面最大偏移不超过0.1毫米对大多数机械零件足够。但整车或大型建筑级别的模型0.1会导致三角形数量爆炸轻量化就无从谈起。建议的做法是按模型包围盒尺寸动态计算偏差Bnd_Box bb; BRepBndLib::Add(shape, bb); double xmin, ymin, zmin, xmax, ymax, zmax; bb.Get(xmin, ymin, zmin, xmax, ymax, zmax); double diagonal sqrt(pow(xmax - xmin, 2) pow(ymax - ymin, 2) pow(zmax - zmin, 2)); double deflection diagonal / 5000.0;这样大模型自动降低精度小模型保证细节整体三角形数量可控。网格化完成后遍历面取出三角形#include TopExp_Explorer.hxx #include Poly_Triangulation.hxx TopExp_Explorer exp(shape, TopAbs_FACE); while (exp.More()) { TopoDS_Face face TopoDS::Face(exp.Current()); TopLoc_Location loc; const Handle(Poly_Triangulation) tri BRep_Tool::Triangulation(face, loc); if (!tri.IsNull()) { // 取 tri-Nodes() 和 tri-Triangles() // 注意坐标要乘 loc.Transformation() } exp.Next(); }取出节点和三角形之后还有一步容易被忽略去重合并顶点。OCCT每个面各自生成三角形相邻面的共享边会产生重复顶点。直接把所有三角形丢给OSG顶点数会多一到两倍。我们用std::unordered_map以坐标哈希为键合并顶点再生成索引缓冲模型内存和渲染性能都能提升不少。3.3 assimp在网格格式链路中的角色与边界网上经常有人问assimp怎么转成osg问题本身反映出对assimp定位的误解。assimp是一个通用的“网格导入库”它擅长的是把OBJ、FBX、glTF这类已经离散化好的网格格式解析成统一的aiScene数据结构但它不擅长几何内核级别的数据。我们层的处理逻辑是这样的先用assimp解析出aiScene拿节点树、网格、材质、纹理把aiMesh的顶点、法线、纹理坐标、骨骼权重按需填入OSG的osg::Geometry把aiNode层级映射成osg::Group/osg::Transform层级材质纹理用OSG的osg::Texture2D和osg::StateSet还原有骨骼动画的就走osgAnimation但CAD/CAE场景基本用不上我们直接忽略。assimp转OSG的适配代码量不小但逻辑是线性的主要工作量在字段映射而非算法。需要留意的坑有三个一是FBX的坐标系和单位容易变需要统一到毫米和右手系二是glTF的PBR材质参数跟OSG传统材质模型不完全对应需要做换算三是assimp对超大网格几百万面的内存占用比较高解析完成后及时释放aiScene。3.4 扇区与LOD大装配体显示不卡的真正解法加载链路走通之后下一个问题是一个整车或整机装配可能包含几千上万个子零件直接全部丢进OSG必然卡成幻灯片。我们的轻量化策略在加载阶段就引入扇区化解析装配树时自动以包围盒为中心把模型切成若干扇区Sector每个扇区生成一个osg::PagedLOD节点数据不在视野内时完全不加载配合OSG的osgDB::DatabasePager实现按需调度每个零件生成两层LOD近处用完整网格远处用低精度简化网格。这套组合下来一个几千零件的装配体实际同时渲染的面数基本能控制在几百万以内主流工作站的帧率可以稳定在30帧以上。注意osg::PagedLOD的文件调度默认从磁盘加载做内存内调度需要自定义osgDB::ReadFileCallback。这个坑我踩了很久默认配置下内存里已经加载好的节点不会自动参与卸载。4. 轻量化策略模型能不能“跑得动”差别全在这里4.1 数据轻量化三角化参数、顶点压缩与属性剔除最基础的轻量化是数据层面的瘦身。三角化参数前面说过按包围盒尺寸动态计算。顶点压缩上OCCT输出的坐标是双精度浮点但对于显示来说单精度足够。OSG的osg::Vec3Array默认也是单精度不做额外处理反而自然。属性剔除容易被忽略。OCAF文档里每个面可能挂一堆属性有些是给CAM用的加工信息显示场景完全用不上。在桥接模块里我们只保留颜色、图层、名称、透明度这几类渲染相关属性其余全部丢弃避免内存浪费。去掉不可见内部面也有奇效。机械零件内部空腔里的面从外观上看不见但网格化会原样生成。做法是按零件包围盒做一次粗略的遮挡分析判定为内部的面直接不生成网格。这一步可以再砍掉20%~30%的三角形。4.2 场景轻量化实例化、LOD、视锥剔除场景层级的轻量化核心是实例化。紧固件、标准件这类零件往往重复出现几十上百次它们的几何完全相同只是位姿不同。如果每个实例都生成一份完整网格内存浪费相当可观。用OSG的osg::MatrixTransform组合osg::Instance可以做到一份几何数据被多个Transform引用渲染时提交一次顶点缓冲每次只更新矩阵。实测一套包含2000多个螺栓的装配体只此一项内存就降了一半以上。LOD前面提到过再补充一点LOD切换的阈值要跟视口大小关联而不是简单的距离。同一距离下小屏幕和大屏幕上模型占的像素数完全不同。用osg::LOD::RangeMode设置为PIXEL_SIZE_ON_SCREEN按屏幕像素占比决定切换效果比距离范围好得多。视锥剔除OSG默认开启但前提是节点都设置了合理的包围盒。OCCT的Shape计算包围盒很简单Bnd_Box bb; BRepBndLib::Add(shape, bb);但注意装配根节点需要重新计算组合包围盒很多性能问题源于根节点的包围盒是NULL导致所有子节点无法被正确剔除。4.3 加载轻量化异步IO、后台线程网格化、流式解析加载是一个容易让用户崩溃的环节。一个200MB的STEP文件读取加网格化可能要几十秒如果UI线程卡死在这个过程体验是很糟糕的。我们的方案是三层流水线IO线程只做文件字节读取快速返回后台线程池做解析和网格化完成后把OSG节点推送到渲染队列渲染线程在帧循环里查询完成队列拿到节点再挂到场景树上。这里有个OSG的线程注意点OSG的渲染遍历和更新遍历是分离的修改场景树要在更新遍历的回调里做不能直接在后台线程往场景树里addChild否则运行时会随机崩溃。用osg::GraphicsContext::makeCurrent加注册回调的方式或者干脆在UpdateCallback里消费队列。4.4 文件轻量化自定义二进制序列化而不是直接啃原始CAD文件平台上线一段时间后发现用户经常重复加载同一批模型。每次重新解析STEP文件都是几十秒。我们引入了一级缓存首次加载时把OCCT造型和OSG网格数据序列化成一个自定义二进制格式扩展名用.ocbOCCT Binary后续再加载就直接读这个文件。序列化格式的设计要点文件头写版本号、模型包围盒、单位、坐标系几何区域按顺序写Shape的拓扑信息和网格缓冲属性区域写OCAF颜色、图层、名称尾部写索引表支持直接定位到某个零件实现局部加载。实测从STEP首次加载约30秒二次用.ocb缓存只需不到2秒。对于大装配体平台强烈建议做这个环节投入产出比极高。5. 交互与显示层的工程问题拾取、变换、坐标系5.1 鼠标拾取从射线求交到高亮反馈OSG提供了osgUtil::IntersectionVisitor用鼠标射线跟场景做求交。CAD场景里拾取有特殊要求用户期望拾取到“面”而不是“线框”而且高亮显示要清晰。我们的拾取实现分三步转换屏幕坐标到世界射线用osgUtil::IntersectionVisitor求交得到最前面的交点通过交点所在的几何节点反查节点上挂的OCCT拓扑ID得到当前拾取的是哪个面或哪个零件。高亮不能简单地换颜色因为模型本身的颜色信息是有意义的。我们用了一个技巧给高亮几何生成一个放大1.002倍的线框壳用半透明的亮色渲染叠加在原始网格外层。这样既不遮挡原模型属性信息又能让用户一眼看清选中的范围。5.2 视图操作从包围盒适配到轨迹球控制OSG自带的osgGA::TrackballManipulator可以直接用但CAD场景需要额外的约束模型的坐标轴通常要和屏幕轴对齐不能让用户把模型旋转到“天山倒转”的状态缩放不能无限缩进穿透模型。我们在Trackball基础上包了一层限制俯仰角范围在-89°到89°缩放范围限制在包围盒对角线的千分之一到十倍之间。适配视图时用模型的包围盒中心作为场景中心把摄像机距离设置为包围盒对角线长度一个home键就能快速找回视野。5.3 多坐标系对齐CAD模型和CAE网格/结果怎么叠在一起这是平台比较特殊的一点不仅要看CAD模型还要把CAE网格和计算结果叠加显示。CAD模型用的是建模坐标系CAE网格通常是分析坐标系两者之间往往只有部分特征对齐。我们的做法是在导入CAE结果时提供一个“坐标对齐向导”用户在CAD模型上选三个不共线的点在CAE网格上选对应三个点平台根据两组点计算刚体变换矩阵把CAE结果变换到CAD坐标系下。计算用经典的Kabsch算法代码不多但是非常实用。对齐之后用户就可以在CAD模型上看应力云图叠加这对工程沟通价值极大。5.4 点云、粒子特效等其他数据类型的接入热搜词里有人问osg粒子效果顺带说一下。OSG的粒子系统可用但对粒子数量有上限瓶颈几百万粒子时性能下降明显。实际项目中我们用自定义的osg::DrawArrays(GL_POINTS)替代粒子节点配合GLSL着色器做大小衰减和颜色渐变性能反而好很多。LAS点云接入后有一个数据显示层面的注意点LAS文件本身就带分类地面、植被、建筑等可以根据分类赋予不同颜色。直接在OSG里给每个点设置颜色属性用GL_POINTS绘制配合八叉树LOD几百GB的倾斜摄影点云也能流畅浏览。6. 踩坑实录从“能显示”到“能用”的关键问题6.1 大型模型帧率骤降从顶点数组到索引缓冲再到实例化的优化全过程第一次把一辆整车的STEP导入平台转完网格挂到OSG场景树之后旋转视角的帧率只有个位数。用osg::Stats一看每帧渲染的顶点数高达上亿。排查过程分了三轮优化第一轮把OCCT导出的所有三角形直接塞进osg::Geometry法线也不合并顶点重复严重。合并顶点后顶点缓冲从1.2GB降到400MB帧率到了15帧左右。第二轮发现每个零件都各自绑定一套Shader状态状态切换非常频繁。把材质一致的零件合并成同一个osg::Geometry用osg::DrawElementsUInt一次性绘制帧率到了25帧。第三轮发现大量重复的标准件还在逐个提交。改用osg::Instance实例化后帧率稳定在了60帧。这个优化过程也算是一个经典路线先解决顶点冗余再解决状态切换最后解决重复提交。6.2 科学计数法显示问题CAD尺寸标注里“2.1616e”是怎么来的热搜词里有“cad画直线显示2.1616e什么原因”这其实是CAD显示精度设置或数据精度问题。我们在平台里也遇到过类似现象OCCT导出STEP时某些坐标值因为单位换算或浮点运算变成了科学计数法的极小数比如2.1616e-14看起来像有几何错误实际只是浮点噪声。处理办法有几个在IO层统一坐标容差坐标绝对值小于1e-9的按零处理显示尺寸时按有效数字格式化例如保留3~6位小数不用科学计数法在网格化之前用BRepBuilderAPI_Transform对形状做一次“圆整”把坐标吸附到微米级别。CAD里很多莫名奇妙的显示问题根子都是浮点噪声。做平台的兄弟建议在一开始就建立统一的数值容差体系不要事后再给各个接口零散打补丁。6.3 CAD标注、图层、颜色信息丢失的根源与还原CAD文件里最常丢的东西标注尺寸、图层名、颜色、装配约束。OCCT读取STEP时实体几何没问题但这些非几何属性要看STEP文件里有没有写。很多CAD系统导出STEP时默认不写颜色和图层那就真没办法只能在导出端设置。对于写了属性的STEPOCCT的OCAF机制可以提取。我们写了个属性提取器// 使用XCAFDoc_ShapeTool遍历装配 Handle(XCAFDoc_ShapeTool) shapeTool XCAFDoc_DocumentTool::ShapeTool(doc-Main()); TDF_LabelSequence labels; shapeTool-GetFreeShapes(labels); // 遍历每个label读取TDataStd_Name和XCAFDoc_Color实测下来NX和Catia导出的STEP颜色信息保留得比较完整SolidWorks看版本和设置。如果客户对颜色有强需求建议在平台里做一个“颜色映射表”按图层名或零件名批量设置颜色比依赖原始文件属性可靠。6.4 多线程加载与渲染线程冲突崩溃排查链路的完整记录平台上线后的第一个崩溃现场加载大模型时后台线程还在往OSG场景树里挂节点用户快速旋转视图程序直接Segmentation fault。崩溃堆栈指向osg::Scene::update和std::vector的内存操作。排查过程复现用200MB的STEP反复加载高概率崩溃二分定位注释掉后台线程挂节点的逻辑崩溃消失恢复后崩溃重现确认是线程同步问题查OSG文档OSG的节点没有线程安全保证跨线程修改场景树是未定义行为重新设计后台线程只负责生成osg::ref_ptrosg::Node挂载动作通过osg::NodeCallback在更新遍历阶段消费队列执行加锁保护队列本身用std::mutex保护简单可靠验证连续加载20轮不再崩溃。这个问题的教训是不要把OSG当线程安全框架用它的线程模型是渲染线程、更新线程、剔除线程的分工不是通用多线程容器。6.5 平台跨平台构建与依赖管理的建议OCCT和OSG都是跨平台库但编译配置各有脾气。我们最终的构建方案Windows上用vcpkg管理依赖指定OCCT和OSG版本Linux上直接用发行版包管理安装基础依赖业务模块单独编译CMake统一配置两套库的find_package配置写好发布时注意OCCT的TK*系列库要打包全缺失某个库是客户现场最常遇到的问题。另外要提醒的是OCCT版本升级要慎重。它的API在不同小版本之间都有变动比如Poly_Triangulation从Triangle(n)改到Triangles()升级一次要改的地方不少。平台定版之后没有重大功能需求不建议频繁升级。7. 这套组合的边界与补充思考结语把OCCTOSG这套组合跑了一段时间后我最大的感受是它不是一个开箱即用的组合而是一组需要深度理解和耐心调教的工具。OCCT负责把CAD模型的“灵魂”留下来OSG负责把几何的“躯体”画出来两者之间那层胶水才是平台真正的技术含量所在。如果你也打算做类似的轻量化多格式CAD/CAE平台有几个建议供参考先想清楚平台到底解决谁的什么问题是给设计看图纸还是给工艺查模型还是给仿真叠结果不同定位决定架构重心数据格式支持不要一开始做大而全把STEP、IGES、STL三条线打通覆盖80%的场景其余按客户需求迭代网格化和轻量化的参数要独立成配置层不要写死在代码里现场调优会是你最常用的操作OCCT和OSG都有学习曲线建议先做一个小Demo跑通整条链路再加业务功能别一上来就铺大摊子。最后分享一个小技巧在OSG里给每个CAD零件设置osg::Node::setDescription把OCCT的TShape ID和版本号写进去。后续做模型比对、变更检测、版本追溯你会发现当初这行代码有多值钱。这套方案不完美也不算新潮但对工业级离线场景来说它是目前我试过的最稳妥、最可控的一条路。
返回列表