十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

OCC+Gmsh+OSG集成:从网格建模到三维可视化的工程实践

OCC+Gmsh+OSG集成:从网格建模到三维可视化的工程实践 简介面向 CAD 领域开发者这份集成 OCC、Gmsh 与 OSG 的测试程序把几何建模、网格划分与三维可视化串联为完整工作流适合需要快速搭建 CAD 原型或研究三者在 CAE 前处理与后处理中协同工作的工程师。压缩包共 40 个文件包括 cpp 源码、sln 工程、msh 网格数据、exe 可执行程序以及 lib/dll 依赖库等包体约 100.7MB目录结构清晰便于按模块查阅。已有 2470 人学习下载。程序演示了读取 Gmsh 网格数据后通过 OpenCASCADE 完成几何处理再由 OpenSceneGraph 构建可交互的三维场景支持旋转、缩放、平移等操作。同时提供 recombine 系列多个阶段的网格文件涵盖原始、平滑、拓扑处理、质量评估等关键环节便于对照分析不同处理流程对网格的影响。整个项目可作为 CAD/CAE 工具链二次开发的基础模板降低多库集成时的配置与接口对接成本对想深入理解 OCC、Gmsh、OSG 协同机制的开发者尤其有参考价值。1. 为什么把 OCC、Gmsh、OSG 拧在一起做测试程序先给结论OCC 管几何建模Gmsh 管网格剖分OSG 管实时渲染这三者单独拿出来都有大量成熟案例但真正把它们串成一条完整数据链的工程示例非常少。很多团队做 CAD 相关的预研时经常卡在OCC 画完模型导不出 Gmsh 能用的格式Gmsh 剖完网格 OSG 又读不进去这样的接口泥潭里。TestProject 这个压缩包的价值不在于它实现了多复杂的算法而在于它把三条开源技术栈的调用顺序、数据格式转换、渲染节点组织方式固化成了一个可运行的骨架你拿到手能直接跑跑通了再往里面填自己的业务逻辑。这个项目适合三类人一是刚接触 OCC 与可视化集成的开发者想用最短路径看到模型从建模到渲染的完整链路二是做有限元前处理或后处理工具的人需要把 Gmsh 的 msh 网格数据变成可交互的 3D 场景三是想评估OCC OSG这套组合能否替代商业 CAD 内核的架构决策者。下文所有分析都以 TestProject 中的文件结构为准重点拆解 recombine 系列 msh 文件在程序里扮演的角色、OCC 与 Gmsh 数据交换的格式约定以及 OSG 加载网格时的性能陷阱。2. Gmsh 网格数据解析recombine 系列文件的真实含义2.1 recombine_0before.msh 到 recombine_5final.msh 是什么TestProject 压缩包里最容易被忽略的是那一串 recombine_**.msh 文件。这不是随机生成的中间产物而是 Gmsh 执行recombine算法时按顺序落盘的快照。理解这六个文件就理解了 Gmsh 从原始网格到高质量四边形网格的完整演变过程。文件名阶段含义网格特征recombine_0before.msh初始三角剖分结果全部为三角形单元节点稀疏recombine_1raw.msh第一次 recombination 原始输出三角形部分合并为四边形边界未处理recombine_2smoothed.msh光滑化后的网格节点位置调整单元质量初步改善recombine_3topo.msh拓扑修复后消除非流形边修复悬挂节点recombine_4quality.msh质量优化后四边形单元的最小角最大化recombine_5final.msh最终网格满足目标尺寸与质量阈值在 TestProject.cpp 中程序读取的应该是recombine_5final.msh这个最终文件。其他五个文件存在的意义是调试——当最终网格出现畸形单元时你可以逐文件回溯看问题出在拓扑修复阶段还是质量优化阶段。2.2 msh 文件格式解析与代码实现Gmsh 的 msh 格式有 2.2 和 4.1 两个常用版本TestProject 里的文件是 2.2 版旧版兼容性好OCC 读取也方便。解析逻辑并不复杂核心是按关键字分段// Gmsh msh 2.2 格式解析核心代码节选 #include fstream #include sstream #include map #include vector struct MshNode { int id; double x, y, z; }; struct MshElement { int type; // 1三角形, 3四边形, 4四面体, 5六面体 std::vectorint nodeIds; }; bool parseMshFile(const std::string path, std::vectorMshNode nodes, std::vectorMshElement elements) { std::ifstream in(path); if (!in.is_open()) return false; std::string line; bool inNodes false, inElements false; while (std::getline(in, line)) { if (line $Nodes) { inNodes true; continue; } if (line $EndNodes) { inNodes false; continue; } if (line $Elements) { inElements true; continue; } if (line $EndElements) { inElements false; continue; } if (inNodes) { std::istringstream iss(line); MshNode n; iss n.id n.x n.y n.z; nodes.push_back(n); } else if (inElements) { std::istringstream iss(line); int id, type, numTags; iss id type numTags; // 跳过 numTags 个标签 int tag; for (int i 0; i numTags; i) iss tag; MshElement e; e.type type; int nodeId; while (iss nodeId) e.nodeIds.push_back(nodeId); elements.push_back(e); } } return true; }注意这个解析器只处理了节点和单元两个段而实际 msh 文件里还有$PhysicalNames和$ElementData比如质量标量场。TestProject 之所以能精简掉这些段是因为它的目标是可视化展示不需要物理属性。如果你要拿这个程序去做有限元后处理必须补上$ElementData段的解析并且要把质量值映射到 OSG 的颜色数组上。2.3 单元类型映射关系Gmsh 的单元类型编号与可视化库的图元类型不是一一对应的需要做映射。下面是 TestProject 需要的映射表Gmsh 类型编号Gmsh 含义OSG 对应图元顶点数1三角形GL_TRIANGLES33四边形GL_QUADS或三角化44四面体GL_TRIANGLES拆 4 个面125六面体GL_QUADS拆 6 个面24对于可视化建议把体单元拆成表面三角面片而不是直接用 GL_TETRAHEDRA——因为 OSG 的 primitive set 不支持体渲染拆面后还能用背面剔除来提升帧率。常见的做法是在解析阶段就完成拆面把节点坐标复制到对应的顶点数组中虽然内存占用多了三倍但省去了每帧做拓扑解码的 CPU 开销。3. OCC 几何处理从网格数据到实体模型的桥接3.1 为什么要用 OCC 处理 Gmsh 网格Gmsh 生成的是离散网格而 OCC 的专长是连续几何NURBS 曲面、布尔运算、放样。如果你只是把 Gmsh 网格丢给 OSG 渲染那完全不需要 OCC。但 TestProject 的集成方案里加入 OCC是为了解决一个真实工程痛点网格模型需要和 CAD 模型做对比验证或者需要修改局部几何后再重新剖分。常见做法是建立一条网格 → OCC 拓扑 → 重新剖分的闭环把 Gmsh 的三角形面片用 OCC 的BRepBuilderAPI_MakeFace构造成曲面然后利用 OCC 的 heal 工具修复缝隙和重叠再导出为 STEP 或 IGES 供其他 CAD 软件使用。这也是 TestProject 里 OCC 部分的核心价值——它不是用来画图的而是用来做几何修复和格式转换的。3.2 用 OCC 构建 BRep 面并修复// 将 Gmsh 三角形网格转换为 OCC 的 TopoDS_Face #include BRepBuilderAPI_MakeFace.hxx #include BRepBuilderAPI_MakeWire.hxx #include BRepBuilderAPI_MakeEdge.hxx #include TopoDS_Shape.hxx #include gp_Pnt.hxx #include vector // 输入节点坐标和三角形索引 TopoDS_Shape buildShapeFromMesh( const std::vectorgp_Pnt points, const std::vectorstd::vectorint triangles) { // 用第一个三角形构造初始面 BRepBuilderAPI_MakeEdge edge1(points[triangles[0][0]], points[triangles[0][1]]); BRepBuilderAPI_MakeEdge edge2(points[triangles[0][1]], points[triangles[0][2]]); BRepBuilderAPI_MakeEdge edge3(points[triangles[0][2]], points[triangles[0][0]]); BRepBuilderAPI_MakeWire wire; wire.Add(edge1.Edge()); wire.Add(edge2.Edge()); wire.Add(edge3.Edge()); BRepBuilderAPI_MakeFace face(wire.Wire()); // 这里仅演示单个三角形的转换 // 实际项目需要先合并共享节点再逐面构建最后用 BRepAlgoAPI_Fuse 合成 return face.Shape(); }这个示例只处理了一个三角形真实场景下几百上千个三角形逐一构造面效率极低而且会产生海量拓扑边。我在实际项目中一般不这样逐个构面而是先把 Gmsh 网格写入 STL 文件再用 OCC 的StlAPI_Reader直接导入#include StlAPI_Reader.hxx TopoDS_Shape readStlAsShape(const std::string stlPath) { StlAPI_Reader reader; TopoDS_Shape shape; reader.Read(shape, stlPath.c_str()); return shape; }StlAPI_Reader会把 STL 中的三角面片合并成 OCC 的 Compound 形状速度比逐面构建快一个量级而且自动处理共享顶点。这里有个参数细节STL 是离散三角形描述没有拓扑信息导入后 OCC 生成的是一条条独立的边做布尔运算前必须用ShapeFix_Shape进行修复。3.3 交换格式msh 与 STEP 之间的取舍TestProject 里 OCC 和 Gmsh 之间没有直接的数据通道它们是围绕 msh 文件解耦的。但如果你需要把 OCC 的实体模型交给 Gmsh 重新剖分就必须经过中间格式。常见的交换路径是路径中间格式优点缺点OCC → GmshSTEP (.step)保留 NURBS 精度Gmsh 对 STEP 曲面支持有限OCC → GmshBREP完整拓扑Gmsh 原生需用 OpenCASCADE 内核编译OCC → GmshSTL简单通用丢失曲面曲率网格精度受限于三角形密度我一般建议使用 STEP因为 Gmsh 从 4.0 开始内置了 OpenCASCADE 内核可以直接导入 STEP 并识别其中的面自动生成符合曲率分布的网格。TestProject 没有演示这一步因为它的重点是网格 → 显示而不是实体 → 网格你如果要做逆向流程只需在 Gmsh 里Merge model.step即可。4. OSG 可视化把网格数据变成可交互场景4.1 OSG 节点树与几何体构建OSG 的核心思想是场景图Scene Graph所有可绘制对象都挂在一棵节点树下。TestProject 的渲染模块需要做三件事把网格数据转换成osg::Geometry、把多个 Geometry 组织成osg::Geode、再把 Geode 挂到osg::Group下交给 Viewer。// Gmsh 网格转 OSG Geometry #include osg/Geometry #include osg/Geode #include osg/Group #include osgViewer/Viewer #include vector osg::ref_ptrosg::Geometry buildGeometryFromMesh( const std::vectorfloat vertices, // 已拆分的三角面顶点每 9 个 float 一个三角形 const std::vectorfloat normals) { // 法线数组与顶点一一对应 osg::ref_ptrosg::Geometry geom new osg::Geometry(); // 顶点数组 osg::ref_ptrosg::Vec3Array vArray new osg::Vec3Array(); for (size_t i 0; i vertices.size(); i 3) { vArray-push_back(osg::Vec3(vertices[i], vertices[i1], vertices[i2])); } geom-setVertexArray(vArray.get()); // 法线数组用于光照 osg::ref_ptrosg::Vec3Array nArray new osg::Vec3Array(); for (size_t i 0; i normals.size(); i 3) { nArray-push_back(osg::Vec3(normals[i], normals[i1], normals[i2])); } geom-setNormalArray(nArray.get(), osg::Array::BIND_PER_VERTEX); // 图元类型三角形列表 geom-addPrimitiveSet(new osg::DrawArrays(GL_TRIANGLES, 0, vArray-size())); // 为每个顶点设置颜色这里用单元质量伪彩示例给灰色 osg::ref_ptrosg::Vec4Array color new osg::Vec4Array(); for (size_t i 0; i vArray-size(); i) { color-push_back(osg::Vec4(0.7f, 0.7f, 0.7f, 1.0f)); } geom-setColorArray(color.get(), osg::Array::BIND_PER_VERTEX); return geom.get(); }这里的参数绑定方式值得说明BIND_PER_VERTEX意味着每个顶点都绑定一个法线和颜色内存开销大但渲染最准确如果模型只有平面面片可以用BIND_PER_PRIMITIVE_SET为每个图元绑定一个法线能省不少内存。TestProject 里如果网格很大超过 100 万三角形建议改用共享顶点的 DrawElements 方式把顶点和索引分开存储否则节点缓存会膨胀。4.2 交互操作旋转、缩放、平移的实现OSG 默认的osgGA::TrackballManipulator已经提供了鼠标拖拽旋转、滚轮缩放、中键平移的全部功能但 TestProject 要作为 CAD 测试程序往往还需要自定义键盘快捷键来切换视图模式。#include osgGA/StateSetManipulator #include osgViewer/ViewerEventHandlers osg::ref_ptrosgViewer::Viewer createViewer(osg::ref_ptrosg::Group root) { osg::ref_ptrosgViewer::Viewer viewer new osgViewer::Viewer(); viewer-setSceneData(root.get()); // 轨迹球操作器支持旋转/缩放/平移 viewer-setCameraManipulator(new osgGA::TrackballManipulator()); // 键盘事件按 w 切换线框模式按 f 切换全屏 viewer-addEventHandler(new osgGA::StateSetManipulator( viewer-getCamera()-getOrCreateStateSet())); // 统计信息按 s 显示帧率和绘制细节 viewer-addEventHandler(new osgViewer::StatsHandler()); return viewer; }TrackballManipulator是默认操作器CAD 场景里更多人会选osgGA::OrbitManipulator区别在于轨道模式始终围绕目标点旋转适合观察模型局部轨迹球模式旋转中心会随着拖拽漂移适合漫游。TestProject 没有强制指定你可以在 viewer 的 setup 阶段用viewer-setCameraManipulator切换参数上注意设置setHomePosition把相机初始位置放在网格包围球半径的 2 到 3 倍处避免模型刚加载时被裁剪。4.3 网格质量可视化把 recombine_4quality.msh 的标量数据呈现出来这是 TestProject 最有价值的一个扩展点。Gmsh 在质量优化阶段会为每个单元计算质量值0 到 11 表示完美单元这些值记录在 msh 文件的$ElementData段中。OSG 可以用它做伪彩渲染让畸形单元一眼可见。// 将单元质量映射为颜色 void applyQualityColors(osg::Geometry* geom, const std::vectorfloat quality) { osg::ref_ptrosg::Vec4Array colors new osg::Vec4Array(); for (float q : quality) { // 质量低显示红色中等绿色优秀蓝色 if (q 0.5f) { colors-push_back(osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f)); } else if (q 0.8f) { colors-push_back(osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f)); } else { colors-push_back(osg::Vec4(0.0f, 0.0f, 1.0f, 1.0f)); } } geom-setColorArray(colors.get(), osg::Array::BIND_PER_PRIMITIVE_SET); }这个函数里我直接用了BIND_PER_PRIMITIVE_SET因为质量值是单元级的每个单元只有一个标量不需要插值。如果你的网格质量参数是节点级的Gmsh 也能输出节点质量那就要改成BIND_PER_VERTEX并且在解析时把每个节点对应的质量值填进去。伪彩映射的阈值不是固定的应该根据你的网格规模动态用分位数切分否则绝大部分单元都挤在一个颜色段里看不出问题。5. 常用功能原理与实现5.1 法线计算从网格几何到光照效果OSG 渲染时如果没有法线模型就是一片黑的因为默认光源的漫反射无法计算。TestProject 如果遇到网格没有法线的情况需要在代码中显式计算。常见做法是按面计算法线再对顶点平均// 计算顶点法线支持共享顶点 std::vectorfloat computeVertexNormals( const std::vectorfloat vertices, const std::vectorunsigned int indices) { size_t numVertices vertices.size() / 3; std::vectorfloat normals(numVertices * 3, 0.0f); // 遍历每个三角形累加面法线到三个顶点 for (size_t i 0; i indices.size(); i 3) { unsigned int i0 indices[i]; unsigned int i1 indices[i1]; unsigned int i2 indices[i2]; float ax vertices[i1*3] - vertices[i0*3]; float ay vertices[i1*31] - vertices[i0*31]; float az vertices[i1*32] - vertices[i0*32]; float bx vertices[i2*3] - vertices[i0*3]; float by vertices[i2*31] - vertices[i0*31]; float bz vertices[i2*32] - vertices[i0*32]; // 叉积得到面法线 float nx ay * bz - az * by; float ny az * bx - ax * bz; float nz ax * by - ay * bx; normals[i0*3] nx; normals[i0*31] ny; normals[i0*32] nz; normals[i1*3] nx; normals[i1*31] ny; normals[i1*32] nz; normals[i2*3] nx; normals[i2*31] ny; normals[i2*32] nz; } // 归一化 for (size_t i 0; i numVertices; i) { float len sqrt(normals[i*3] * normals[i*3] normals[i*31] * normals[i*31] normals[i*32] * normals[i*32]); if (len 1e-8) { normals[i*3] / len; normals[i*31] / len; normals[i*32] / len; } } return normals; }注意这段代码有个工程细节如果索引数组里顶点是重复存储的即非索引网格那么累加法线会失败。所以在调用前要判断网格是否带索引。TestProject 解析 msh 后建议统一转成索引网格这样不仅法线计算正确还能用osg::DrawElementsUInt减少显存占用。5.2 网格域的可视化和相交检测在某些场景下需要点击模型上的某一点然后判断这个点位于哪个网格域Physical Group里。Gmsh 的 msh 文件中每个单元可以带一个 physical tag表示它的域编号。OSG 的相交检测接口是osgUtil::Intersector可以返回命中点的坐标和 primitive 索引再把这个索引映射回单元编号和域号。常见做法是给每个域建一个独立的osg::Geode这样相交检测的结果可以直接通过getNodePath()获取节点指针然后从节点名中读出域号。TestProject 虽然没有实现拾取但这个套路在网格质量检测工具里非常通用代码结构不过是给每个 Geometry 加个setName(domain_3)然后重写osgGA::GUIEventHandler的handle方法调用computeIntersections。5.3 多文件加载与场景组织一个 CAD 项目通常需要同时加载多个 msh 文件比如一个零件包含流体域和固体域。TestProject 里的TestProject.cpp没有做多文件管理但推荐的做法是维护一个std::mapstd::string, osg::ref_ptrosg::Node来管理场景节点文件名作为 key。加载时用osgDB::readNodeFile读取已有的 osg 格式文件而不是重新走一遍 msh 解析——这也是为什么我建议做一次性的 msh → osg 转换工具把解析结果序列化之后每次启动程序加载速度能快一个量级。5.4 批量渲染和半透明如果网格包含体单元四面体、六面体单一实体模式无法看到内部结构。常见做法是先渲染外层表面再开启混合模式渲染内部单元// 设置半透明渲染状态 osg::ref_ptrosg::StateSet stateset geom-getOrCreateStateSet(); stateset-setMode(GL_BLEND, osg::StateAttribute::ON); stateset-setRenderingHint(osg::StateSet::TRANSPARENT_BIN); stateset-setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));注意半透明渲染要先把外表面绘制完再绘制内部体否则深度测试会出错。OSG 的TRANSPARENT_BIN会自动把透明物体排在最后绘制但如果内外物体都在同一个 bin 里且交叉建议手动设置setRenderBinDetails(10, DepthSortedBin)让 OSG 按深度排序。这在 TestProject 中并不是默认启用需要你在场景根节点做一次遍历设置。6. 进阶技巧把 Gmsh 的 recombine 流程接到 OCC 场景树中如果你要把 TestProject 从显示网格升级为交互式网格优化工具一个很实用的技巧是把 Gmsh 的 recombine 流程嵌入到 OCC 的场景树中让用户在 OSG 视口中框选一片区域程序自动对该区域的网格做重新剖分并实时刷新。实现上分三步第一步用 Gmsh 的 C API 或命令行工具对局部区域重新剖分。Gmsh 支持在现有网格上定义新的尺寸函数和重组规则核心命令是gmsh model.msh -recombine -order 2 -save -o model_optimized.msh第二步把新生成的 msh 解析为 OSG 节点用osg::Switch节点同时挂载旧网格和新网格通过开关控制显示。这样切换时不会出现场景空白osg::ref_ptrosg::Switch meshSwitch new osg::Switch(); meshSwitch-addChild(oldGeom.get(), true); meshSwitch-addChild(newGeom.get(), false); // 切换显示 meshSwitch-setValue(0, false); meshSwitch-setValue(1, true);第三步也是最关键的一步把质量指标写回 OCC 拓扑。Gmsh 计算出的单元质量可以通过gmsh::model::getQuality获取但 OCC 本身不关心网格质量它只关心几何形状。所以这里的正确做法是用质量分布数据生成一份密度场反馈给 OCC 的网格自适应模块让 OCC 在下一次构建 BRep 时增加曲率大的区域的采样密度。常见做法是遍历质量低于 0.3 的单元取它们的质心作为控制点在 Gmsh 中设置局部尺寸函数// 使用 Gmsh C API 设置局部尺寸函数 gmsh::model::mesh::setSize(gmsh::model::getEntities(0), size); for (const auto badCellCenter : badCellCenters) { gmsh::model::mesh::setSize(std::vectorstd::pairint,int{{0, 0}}, badCellCenter, 0.05); // 该点附近尺寸设为 0.05 } gmsh::model::mesh::generate(2);这里setSize的第二个参数传入坐标点Gmsh 会把该点作为尺寸函数的中心形成向周围扩散的尺寸场。尺寸值越小网格越密。这个参数需要根据模型尺度调整TestProject 里的默认网格如果是 10x10x10 的立方体0.05 的局部尺寸会产生非常密的网格实际使用时建议从原始平均尺寸的 1/4 开始试。最后说说验证方法。当你把 recombine 流程串进 OCC 场景后应该同时显示单元 Jacobian 分布图和 Gmsh 自带的gmsh::model::mesh::getQuality返回的数值。如果优化后最小 Jacobian 不升反降优先检查是否把物理域标识搞丢了——recombine 后的 msh 文件如果没有$PhysicalNames段OCC 的 ShapeFix 会认为所有面都是独立的导致后续布尔运算产生非流形边。此时回退到recombine_3topo.msh那个阶段重新处理不要直接改质量优化参数。掌握了这个环节TestProject 就从一个静态查看器变成了能驱动 Gmsh 做网格自适应的测试平台。本文还有配套的精品资源点击获取
返回列表