第2章 几何基础:点、线、面、曲线环与物理组
2.1 学习目标
- 理解 Gmsh 几何实体的层次结构:Point(点) → Line(线) → CurveLoop(曲线环) → PlaneSurface(平面)
- 掌握
lc(目标网格尺寸)的作用及其对局部网格密度的控制 - 理解 Gmsh 的 tag(标签)机制:每个几何维度的实体标签独立
- 学会使用物理组(Physical Group)对几何实体进行分组,用于边界条件指定和材料属性分配
- 掌握
synchronize()的作用——将 CAD 数据同步到 Gmsh 内部模型
2.2 核心概念说明
2.2.1 几何实体的层次结构
Gmsh 的几何构造遵循严格的层次关系,如下图所示:
Point (0D)
└── Line / Spline / Circle (1D) —— 以两个 Point 为端点
└── CurveLoop —— 有序、有向的曲线闭合列表
└── PlaneSurface (2D) —— 由一个或多个 CurveLoop 围成
└── Volume (3D) —— 由一组 Surface 围成
本章聚焦于从 Point 到 PlaneSurface 的完整链路。这套层级关系在 Gmsh 中是强制性的:你不能跳过 CurveLoop 直接由 Line 定义 Surface,也不能跳过 Line 直接由 Point 定义 Surface。
2.2.2 为什么需要 CurveLoop?
CurveLoop(曲线环)是 Gmsh 中连接"边"与"面"的桥梁。它不仅仅是一个曲线集合,更是一个有序、有向的序列:
- 有序:曲线的顺序决定了它们首尾相连的拓扑关系。
- 有向:每条曲线前的符号(正号或负号)表示该曲线的走向。正号表示使用曲线的自然方向(从起点到终点),负号表示反向。
例如,假设一个矩形由 4 条线组成,其中曲线 2 的自然方向与环的遍历方向相反,那么 CurveLoop 中就会写成 -2。
2.2.3 Tag 机制
在 Gmsh 中,每个几何实体都由一个正整数标签(tag)唯一标识。关键规则是:
- 同一维度内的标签必须唯一。例如,不能有两个 Point 都使用标签
1。 - 不同维度之间的标签可以重复。Point 1 和 Line 1 可以同时存在,互不冲突。
- 标签可以手动指定,也可以由 Gmsh 自动生成。手动指定时,标签作为函数的最后一个可选参数传入;省略时,Gmsh 自动分配并返回新标签。
2.2.4 物理组的作用
物理组(Physical Group)将多个几何实体分组,为后续的有限元分析提供语义信息:
- 边界条件指定:例如将矩形底部的一系列曲线归入物理组
"bottom",在求解器中为该组施加固定约束。 - 材料属性分配:例如将不同区域的表面归入不同的物理组(
"steel","aluminum")。 - 选择性导出:如果定义了物理组,Gmsh 默认只导出属于至少一个物理组的网格单元。未分组的几何实体不会被导出到网格文件。
物理组也按维度区分:0D 物理组(物理点)、1D 物理组(物理曲线)、2D 物理组(物理曲面)、3D 物理组(物理体积)。
2.3 C++ 代码逐段讲解
本节以一个完整的矩形面(10 cm × 30 cm)为例,逐步讲解每个 API 的用法。
2.3.1 初始化与模型创建
#include <gmsh.h>
int main(int argc, char **argv)
{
gmsh::initialize(); // 初始化 Gmsh
gmsh::model::add("t1"); // 创建名为 "t1" 的新模型
gmsh::initialize()必须在调用任何 Gmsh API 之前执行,它会初始化内部数据结构、注册 CAD 内核和 GUI 模块。gmsh::model::add("t1")创建一个名为"t1"的模型。如果不调用该函数,Gmsh 会在需要时自动创建一个匿名模型,但显式命名有助于调试和多模型管理。
2.3.2 定义目标网格尺寸
double lc = 1e-2; // 目标网格尺寸 = 0.01 m = 1 cm
lc(characteristic length,特征长度)是 Gmsh 中控制网格密度的核心参数。它的含义是:在几何实体附近,期望的网格单元边长约为 lc。
lc越小,网格越密,计算精度越高但计算量越大。lc定义在每个 Point 上。Gmsh 会在几何体内通过插值的方式确定任意位置的网格尺寸。- 如果不指定
lc,Gmsh 会基于模型整体尺寸使用一个默认的均匀粗糙网格。
更高级的网格尺寸控制方法(如尺寸场、背景网格)将在第5章、第7章和第8章中介绍。
2.3.3 创建点(Point)
gmsh::model::geo::addPoint(0, 0, 0, lc, 1); // 点1: (0, 0, 0)
gmsh::model::geo::addPoint(0.1, 0, 0, lc, 2); // 点2: (0.1, 0, 0)
gmsh::model::geo::addPoint(0.1, 0.3, 0, lc, 3); // 点3: (0.1, 0.3, 0)
int p4 = gmsh::model::geo::addPoint(0, 0.3, 0, lc); // 点4: (0, 0.3, 0),自动分配标签
addPoint 的函数签名(位于 gmsh::model::geo 命名空间下):
addPoint(x, y, z, meshSize, tag)
| 参数 | 含义 |
|---|---|
x, y, z |
点的三维坐标(double 类型) |
meshSize |
该点附近的目标网格尺寸(可选,默认使用全局粗网格) |
tag |
点的标签(可选,正整数;省略则自动分配) |
返回值:点的标签(int)。
关键观察:前三个点手动指定了标签(1, 2, 3),而第四个点让 Gmsh 自动分配标签(返回值 p4)。两种方式都可以使用,但在构建复杂几何时,手动指定标签能使代码更清晰、更易于后续引用。
2.3.4 创建线段(Line)
gmsh::model::geo::addLine(1, 2, 1); // 线1: 点1 → 点2(底边)
gmsh::model::geo::addLine(3, 2, 2); // 线2: 点3 → 点2(右边,从上到下)
gmsh::model::geo::addLine(3, p4, 3); // 线3: 点3 → 点4(顶边,从右到左)
gmsh::model::geo::addLine(4, 1, p4); // 线4: 点4 → 点1(左边,从上到下)
addLine 的函数签名:
addLine(startPointTag, endPointTag, tag)
| 参数 | 含义 |
|---|---|
startPointTag |
线段起点的标签 |
endPointTag |
线段终点的标签 |
tag |
线段的标签(可选,省略则自动分配) |
关键观察:线的标签(1, 2, 3, p4)和点的标签(1, 2, 3, p4)存在重复。线 1 的标签是 1,点 1 的标签也是 1——这完全合法,因为点和线属于不同的几何维度。
矩形四条边的定义方向如下图所示(点坐标单位为米):
(0, 0.3) p4 ●──────────────● (0.1, 0.3) 点3
↖ Line 3 (3→4) ↗
│ │
Line 4 │ │ Line 2
(4→1) │ │ (3→2)
│ │
↓ │
(0, 0) 点1 ●──────────────→● (0.1, 0) 点2
Line 1 (1→2)
2.3.5 创建曲线环(CurveLoop)
gmsh::model::geo::addCurveLoop({4, 1, -2, 3}, 1);
addCurveLoop 的函数签名:
addCurveLoop({curveTags with signs}, tag)
第一个参数是一个整数向量(使用 C++ 初始化列表 {...}),每条曲线的标签可带符号:
- 正号(如
4):按曲线的自然方向(从起点到终点)使用。 - 负号(如
-2):按曲线的反方向使用。
让我们分析这个 CurveLoop 的构成:
| 顺序 | 曲线标签 | 方向 | 含义 |
|---|---|---|---|
| 1 | 4 (Line 4) |
正向:点4→点1 | 左边,从上到下 |
| 2 | 1 (Line 1) |
正向:点1→点2 | 底边,从左到右 |
| 3 | -2 (Line 2) |
反向:点2→点3 | 右边,从下到上 |
| 4 | 3 (Line 3) |
正向:点3→点4 | 顶边,从右到左 |
为什么要将 Line 2 取反?因为 Line 2 的自然方向是点 3 → 点 2(从上到下),而环的遍历方向要求从点 2 到点 3(从下到上)。取反后,-2 等价于从点 2 走到点 3,与环的方向一致。
最终形成一个逆时针的闭合环路:点4 → 点1 → 点2 → 点3 → 点4。
2.3.6 创建平面(PlaneSurface)
gmsh::model::geo::addPlaneSurface({1}, 1);
addPlaneSurface 的函数签名:
addPlaneSurface({curveLoopTags}, tag)
第一个参数是一个整数向量,包含围成该平面的 CurveLoop 标签(不带符号):
- 第一个 CurveLoop 定义外边界(外部轮廓)。
- 后续的 CurveLoop 定义内边界(孔洞)。本章示例只有一个外边界,多孔示例见第4章。
2.3.7 同步 CAD 数据
gmsh::model::geo::synchronize();
synchronize() 将内置 CAD 内核中的几何数据同步到 Gmsh 的内部模型数据结构中。这是一个必须调用的函数——在同步之前,由 gmsh::model::geo::* 创建的实体不能被网格生成器或其他非 CAD 模块使用。
关于调用时机:
- 可以在每个 CAD 命令后立即同步,但不推荐——
synchronize()涉及不可忽略的计算量。 - 最佳实践:在完成一批几何创建操作后,集中调用一次
synchronize()。 - 如果在同步后又创建了新几何实体,需要再次调用
synchronize()。
2.3.8 定义物理组
gmsh::model::addPhysicalGroup(1, {1, 2, 4}, 5);
gmsh::model::addPhysicalGroup(2, {1}, -1, "My surface");
addPhysicalGroup 的函数签名:
addPhysicalGroup(dim, {entityTags}, tag, name)
| 参数 | 含义 |
|---|---|
dim |
几何维度(0=点, 1=线, 2=面, 3=体) |
entityTags |
要分组的几何实体标签列表 |
tag |
物理组标签(可选,-1 表示自动分配) |
name |
物理组名称(可选,如 "bottom", "steel") |
第一条语句创建了一个 1D 物理组(标签 5),将曲线 1(底边)、2(右边)、4(左边)归为一组。这个组可以用于施加边界条件(如固定位移、强制对流等)。注意曲线 3(顶边)没有被包含在内,因此它不会被导出(在导出网格时,顶边的网格单元不会出现)。
第二条语句创建了一个 2D 物理组(标签由 Gmsh 自动分配,名称为 "My surface"),包含表面 1。这在有限元分析中可用于指定该区域的材料属性。
重要:如果不定义任何物理组,Gmsh 会导出所有网格元素。一旦定义了至少一个物理组,默认行为变为只导出属于物理组的元素。可以通过
gmsh::option::setNumber("Mesh.SaveAll", 1)强制导出所有元素。
2.3.9 生成网格
gmsh::model::mesh::generate(2); // 生成2D网格
mesh::generate(dim) 的参数为网格维度:
1:仅生成 1D 网格(线上的线段单元)。2:生成 2D 网格(面上的三角形/四边形单元),同时会自动生成所需的 1D 网格。3:生成 3D 网格(体上的四面体/六面体单元),同时会自动生成所需的 1D 和 2D 网格。
本例生成 2D 网格,Gmsh 会先在四条曲线上生成 1D 线段单元,再在矩形平面上生成 2D 三角形单元。
2.3.10 保存网格文件
gmsh::write("t1.msh");
write(filename) 将网格保存到文件。通过文件扩展名可以指定格式:
| 扩展名 | 格式 |
|---|---|
.msh |
Gmsh 原生网格格式(最新版) |
.msh2 / .msh4 |
指定 Gmsh 格式版本 |
.unv |
I-Deas Universal 格式 |
.stl |
STL 三角面片格式 |
.vtk |
VTK 格式 |
也可以使用 gmsh::option::setNumber("Mesh.MshFileVersion", x) 显式指定 MSH 格式版本。
2.3.11 GUI 可视化(可选)
std::set<std::string> args(argv, argv + argc);
if(!args.count("-nopopup")) gmsh::fltk::run(); // GUI 显示
gmsh::finalize(); // 清理 Gmsh 资源
return 0;
}
gmsh::fltk::run() 启动内置的 FLTK 图形界面,用于交互式查看几何和网格。参数 -nopopup 用于批处理模式(例如在 CI/CD 流水线或脚本中)跳过 GUI。
gmsh::finalize() 应在程序结束时调用,释放 Gmsh 占用的所有资源。
2.4 完整可运行代码
// ch02_rectangle.cpp —— 矩形面几何定义与网格生成
#include <set>
#include <gmsh.h>
int main(int argc, char **argv)
{
// === 1. 初始化 ===
gmsh::initialize();
gmsh::model::add("ch02");
// === 2. 目标网格尺寸 ===
double lc = 1e-2; // 1 cm
// === 3. 创建四个角点 ===
gmsh::model::geo::addPoint(0, 0, 0, lc, 1);
gmsh::model::geo::addPoint(0.1, 0, 0, lc, 2);
gmsh::model::geo::addPoint(0.1, 0.3, 0, lc, 3);
int p4 = gmsh::model::geo::addPoint(0, 0.3, 0, lc); // 自动标签
// === 4. 创建四条边 ===
gmsh::model::geo::addLine(1, 2, 1); // 底边: 点1→点2
gmsh::model::geo::addLine(3, 2, 2); // 右边: 点3→点2
gmsh::model::geo::addLine(3, p4, 3); // 顶边: 点3→点4
gmsh::model::geo::addLine(4, 1, p4); // 左边: 点4→点1
// === 5. 曲线环(逆时针) ===
// 曲线2自然方向是3→2,但环需要2→3,因此取反
gmsh::model::geo::addCurveLoop({4, 1, -2, 3}, 1);
// === 6. 平面 ===
gmsh::model::geo::addPlaneSurface({1}, 1);
// === 7. 同步到 Gmsh 模型 ===
gmsh::model::geo::synchronize();
// === 8. 物理组 ===
gmsh::model::addPhysicalGroup(1, {1, 2, 4}, 5); // 底+右+左
gmsh::model::addPhysicalGroup(2, {1}, -1, "My surface");
// === 9. 生成2D网格 ===
gmsh::model::mesh::generate(2);
// === 10. 保存 ===
gmsh::write("ch02_rectangle.msh");
// === 11. GUI(可选) ===
std::set<std::string> args(argv, argv + argc);
if(!args.count("-nopopup")) gmsh::fltk::run();
gmsh::finalize();
return 0;
}
编译与运行(假设 Gmsh SDK 已配置,参见第1章):
g++ -o ch02_rectangle ch02_rectangle.cpp -lgmsh
./ch02_rectangle # 启动 GUI
./ch02_rectangle -nopopup # 批处理模式,仅生成 ch02_rectangle.msh
2.5 关键 API 速查表
| API 函数 | 所属命名空间 | 参数说明 | 返回值 |
|---|---|---|---|
initialize() |
gmsh |
无参数 | void |
model::add(name) |
gmsh |
name: 模型名称(字符串) |
void |
geo::addPoint(x, y, z, meshSize, tag) |
gmsh::model |
x,y,z: 坐标; meshSize: 局部网格尺寸(可选); tag: 点标签(可选) |
int (标签) |
geo::addLine(p1, p2, tag) |
gmsh::model |
p1,p2: 起止点标签; tag: 线标签(可选) |
int (标签) |
geo::addCurveLoop(tags, tag) |
gmsh::model |
tags: 带符号的曲线标签向量; tag: 环标签(可选) |
int (标签) |
geo::addPlaneSurface(tags, tag) |
gmsh::model |
tags: 曲线环标签向量; tag: 面标签(可选) |
int (标签) |
geo::synchronize() |
gmsh::model |
无参数 | void |
addPhysicalGroup(dim, tags, tag, name) |
gmsh::model |
dim: 维度; tags: 实体标签向量; tag: 物理组标签(可选); name: 名称(可选) |
int (标签) |
mesh::generate(dim) |
gmsh::model |
dim: 网格维度(1/2/3) |
void |
write(filename) |
gmsh |
filename: 输出文件路径 |
void |
fltk::run() |
gmsh |
无参数 | void |
finalize() |
gmsh |
无参数 | void |
option::setNumber(name, value) |
gmsh |
name: 选项名; value: 数值 |
void |
2.6 常见问题与要点回顾
-
忘记调用
synchronize():这是最常见的错误。如果在同步前尝试生成网格,网格生成器将看不到 CAD 实体。 -
CurveLoop 中的符号错误:如果曲线方向与环的遍历方向不匹配,CurveLoop 将无法闭合,导致
addPlaneSurface失败。画出草图并在代码中标注每条线的方向是一种良好的习惯。 -
标签冲突:同一维度内标签必须唯一。当手动指定标签与自动分配标签混用时,需确保手动标签不会与自动标签冲突。建议要么全手动指定,要么全自动分配。
-
物理组导出行为:物理组一旦定义,只有属于物理组的元素会被导出。如果发现网格文件缺少某些边界,检查是否忘记将这些边界加入物理组,或设置
Mesh.SaveAll = 1。 -
lc 的选取:
lc应根据分析精度需求和计算资源综合选取。作为经验法则,对于线弹性分析,在特征尺寸方向上有 5-10 个单元通常能获得合理的精度。例如,若矩形宽度为 0.1 m,lc = 0.01意味着宽度方向上约有 10 个单元。