本文根据 CAD2D 当前实现及偏心环形区域混合网格的实际开发过程整理,供后续新增二维网格程序时参考。这里记录的是当前代码的真实行为,其中部分接口依赖函数指针和调用顺序,使用前应特别注意相应约束。
CAD2D 将网格生成分为三层:
- 用参数化 edge 函数描述几何边和网格拓扑。
- 用
RectRegion生成一个结构化四边形区域,或用MeshRegion导入/构造一个普通区域。 - 用
MeshRegions合并多个区域、定义物理边界,并输出 Nektar++ XML 或 Gmsh GEO。
常用对象之间的关系如下:
edge function / LineEdge / CompositEdge
|
v
RectRegion ----+
|
Gmsh MSH ----> MeshRegion -----+----> MeshRegions
| |
| +--> outXml + outCOMPO
+----------> outOuterRegion/outInnerRegion
对于“物面结构化边界层 + 中间非结构网格”问题,推荐采用两阶段流程:
生成边界层
-> 从边界层的实际外边界节点输出 core.geo
-> Gmsh 生成 MSH2 核心网格并尽量重组为四边形
-> loadFromMsh 导入核心网格
-> 检查接口节点完全一致
-> AddRegion 合并
-> 定义物理边界并输出 XML
MeshRegion 是纯网格拓扑容器,主要成员为:
std::vector<std::vector<double>> m_pts; // 点坐标
std::vector<std::vector<int>> m_edges; // 每条边的两个点编号
std::vector<std::vector<int>> m_cells; // 每个单元包含的边编号
std::set<int> m_bndPts; // 当前认为是边界的点最容易误解的是:m_cells 保存的是边编号,不是点编号。三角形包含 3 条边,四边形包含 4 条边。
辅助索引和边界数据包括:
m_edgesIndex:无方向点对std::set<int>到边编号的映射。m_unSharedPts:extractBoundaryPoints()提取出的多条不相连边界环。m_unSharedPtsSet:上述边界环对应的点集合。m_tolerance:点合并、边界匹配使用的几何容差。
改变点、边或单元后,通常需要:
region.ResetBndPts();
region.rebuildEdgesIndex();ResetBndPts() 会根据“仅被一个单元使用的边”重新提取真实边界;rebuildEdgesIndex() 会重建无方向边索引。
MeshRegions 继承自 MeshRegion,增加:
- 多区域合并
AddRegion(); - 物理边界与曲边定义;
- Nektar++ XML 输出;
- Gmsh GEO 边界输出。
它既是最终组合网格,也可以临时保存一组边界层区域。
CAD2D 中连续边函数的约定是:
std::vector<double> edge(double s); // s in [-1, 1]返回值至少包含 x、y 两个分量。参数端点为:
s = -1:边起点;s = 1:边终点。
当前接口通过 void * 保存函数地址,再强制转换为普通函数指针。因此应使用全局函数、命名空间函数或 static 函数。不要直接传入捕获 lambda 或普通非静态成员函数。
典型直线/圆弧函数为:
std::vector<double> wallEdge(double s) {
const double theta = 0.5 * (1.0 - s) * theta0
+ 0.5 * (1.0 + s) * theta1;
return {cx + radius * std::cos(theta),
cy + radius * std::sin(theta)};
}LineEdge::Evaluate(s) 在两个端点之间插值,并通过不同 refine type 改变参数分布。常用构造方式为:
LineEdge edge(p0, p1, N, refineType, h0, h1);其中 p0、p1 是 double *,其生命周期必须长于 LineEdge。建议像现有代码一样使用全局数组或其他稳定存储,不能传入很快失效的局部临时数组。
主要分布类型如下:
| 类型 | 作用 |
|---|---|
UNIFORM |
均匀分布 |
EXPREFINE0/1 |
在参数起点/终点指数加密 |
SINREFINE0/1/2 |
单端或双端正弦加密 |
QUDREFINE0/1 |
单端二次加密 |
BOUNDARYLAYER0/1/2/4 |
显式指定端部边界层层数、首层和增长率 |
SMALLSTRETCH0/1 |
根据两端尺寸和增长率自动计算分段数 |
注意:这里的 BOUNDARYLAYER* 是 LineEdge 的一维点分布类型,不是 RectRegion::MeshGen() 使用的 eBoundaryLayer* 二维网格生成方法,两者不要混淆。
带显式边界层参数的构造函数为:
LineEdge(p0, p1, N, refineType,
h0, q0, NBlayers0,
h1, q1, NBlayers1);当一条物面由多段函数组成,但希望用一个 RectRegion 连续生成时,可以使用 CompositEdge:
LineEdge partCount0(a0, a1, n0, UNIFORM, 0.0, 0.0);
LineEdge partCount1(a1, a2, n1, UNIFORM, 0.0, 0.0);
CompositEdge wall;
void initialiseWall() {
static bool initialised = false;
if (initialised) return;
wall.addEdge(partCount0, reinterpret_cast<void *>(wallPart0));
wall.addEdge(partCount1, reinterpret_cast<void *>(wallPart1));
initialised = true;
}
std::vector<double> completeWall(double s) {
return wall.Evaluate(s);
}CompositEdge 中 LineEdge 的主要作用是提供每一段的单元数 m_N;实际几何坐标由传入的 edge 函数计算。各段连接点必须在容差内重合。
总切向单元数为:
wall.m_N不要重复调用 addEdge(),否则段和单元数会重复累加。使用一次性初始化函数是最稳妥的方式。
普通四边区域可以用四个连续 edge 函数构造:
std::vector<void *> edges = {
reinterpret_cast<void *>(edge0),
reinterpret_cast<void *>(edge1),
reinterpret_cast<void *>(edge2),
reinterpret_cast<void *>(edge3)
};
RectRegion block(edges, "block", true, tolerance);
block.MeshGen(M, N, eIsoparametric);也可以传入四组离散边界点。M 是 edge0/edge2 方向的单元数,N 是 edge1/edge3 方向的单元数。
网格方法包括:
| 方法 | 行为 |
|---|---|
eIsoparametric |
四边 Coons 型等参插值 |
eStraightLine |
对向边节点连成直线并求交点 |
eBoundaryLayer0 |
从 edge0 生长,使用相邻离散点估计内部法向 |
eBoundaryLayer1 |
从 edge0 生长,使用连续 edge 的数值导数计算法向 |
若自动连通性检查判断了错误的边方向,可以用:
std::vector<int> directions = {1, -1, 1, -1};
block.SetEdgesDirec(directions);生成物面边界层时,真正需要定义的只有:
edge0:物面切向曲线;edge1:法向累计生长距离。
edge2、edge3 在边界层算法中不会被使用,可以置空。此时必须将 connectivityCheck 设置为 false:
std::vector<void *> edges = {
reinterpret_cast<void *>(wallEdge),
reinterpret_cast<void *>(normalDistribution),
nullptr,
nullptr
};
RectRegion layer(edges, "WallBL", false, tolerance);
layer.MeshGen(nWallCells, nNormalCells, eBoundaryLayer1);法向分布函数只使用返回值的第一个分量:
std::vector<double> normalDistribution(double s) {
int layer = std::lround(0.5 * (s + 1.0) * nNormalCells);
return {cumulativeDistance(layer), 0.0};
}这里必须返回从物面开始的累计距离:第 0 个节点为 0,最后一个节点为总边界层厚度。
边界层法向由 edge0 的切向导数 (dx, dy) 计算:
normal = (-dy, dx) / |d(edge0)/ds|
因此网格总是向 edge0 参数正方向的左侧生长。改变物面函数的参数方向即可改变生长方向。
对于环形流体域:
- 内圆物面应顺时针参数化,使左法向指向圆外、进入流体;
- 外圆物面应逆时针参数化,使左法向指向圆内、进入流体。
如果方向错误,通常会出现边界层长进固体、区域重叠或负 Jacobian。
库中已有两种做法:
MeshTool::setRadiusMesh()、setRadiusLayers()和radiusEdge();- 自己实现累计距离 edge。
第一种的典型流程为:
setRadiusMesh(firstHeight, growth, maxHeight);
int n = findNlayers(firstHeight, growth, totalThickness, maxHeight);
setRadiusLayers(n);
// edge1 使用 radiusEdge
region.MeshGen(nTangential, n, eBoundaryLayer1);需要注意:findNlayers() 返回累计厚度首次达到目标值时的层数,因此最后得到的总厚度可能略大于指定值。radiusEdge() 还依赖一个全局状态,不适合并发使用;每次改变层数前都要调用 setRadiusLayers()。
如果总厚度必须精确,推荐像当前环形网格一样:
- 根据首层、目标增长率、最大层高和总厚度计算层数;
- 调整有效增长率或归一化累计距离;
- 强制最后一个累计距离等于指定总厚度。
使用 MeshRegions::AddRegion() 合并区域:
MeshRegions combined("Combined", tolerance);
combined.AddRegion(region0);
combined.AddRegion(region1);
combined.ResetBndPts();
combined.rebuildEdgesIndex();实现上的重要约束:
- 只有源区域中标记为边界点的点,才有机会与目标区域已有边界点合并;
- 点匹配使用
0.5 * (|dx| + |dy|) < tolerance; - 匹配后的公共边会根据无方向端点集合合并;
- 核心区和边界层接口必须拥有完全相同的节点坐标和分段拓扑。
因此不要分别对同一接口做两套近似采样。推荐用已经生成的边界层接口节点直接输出核心区几何。
在完成一组区域合并后,再添加另一个大区域之前,先执行:
combined.ResetBndPts();
combined.rebuildEdgesIndex();这样 m_bndPts 才代表当前组合网格的真实外露边界。
MeshRegions 的底层 outGeo() 是私有函数,通常通过以下公开函数间接调用:
outOuterRegion(filename, outerLoop, selectorCenter, selectorRadius, exclude);
outInnerRegion(filename, breakPoints, selectorCenter, selectorRadius);outOuterRegion() 中的 selectorRadius 实际使用的是下面的 L1 距离判据,而不是严格的欧氏半径:
|x - center.x| + |y - center.y| < selectorRadius
使用它选择复杂边界时,应检查最终 .geo 文件中的边界环是否正确。对于需要完全可控的多环几何,也可直接使用 util::OutGeo(),或在上层显式整理边界点。
偏心环形区域的推荐做法是:
- 从外壁边界层中找出其核心接口节点,作为 GEO 的外环;
- 从内壁边界层中排除物面边界,保留核心接口作为孔洞;
- 调用
outOuterRegion()写出平面区域; - 追加 Gmsh 网格尺寸和重组参数。
关键点是使用边界层对象中的实际节点,而不是重新调用解析 edge 函数采样。
GEO 输出会把接口相邻节点逐一连接成独立的 Line。为了防止 Gmsh 在每条接口线上插入新节点,可追加:
Transfinite Line {firstLine:lastLine} = 2;这里的 2 表示每条线只有两个端点。若 Gmsh 改变接口离散,后续 AddRegion() 无法形成共形接口。
典型核心区设置为:
Mesh.CharacteristicLengthMin = coreMeshSize;
Mesh.CharacteristicLengthMax = coreMeshSize;
Mesh.Algorithm = 5;
Mesh.RecombinationAlgorithm = 1;
Mesh.MshFileVersion = 2.2;outGeo() 已经写入 Recombine Surface。Blossom 重组允许保留少量未合并的三角形,通常比强制得到全四边形更稳健。
当前 loadFromMsh() 是面向传统 ASCII MSH 的简单解析器。建议明确使用 MSH2:
gmsh core.geo -2 -algo del2d -format msh2 -o core.msh解析器只导入:
- Gmsh 类型 2:三角形;
- Gmsh 类型 3:四边形。
线单元及其他类型会被忽略。不要依赖二进制 MSH、MSH4 实体块或高阶单元格式。
const double maxAngle = 120.0 / 180.0 * std::acos(-1.0);
MeshRegions core("Core", tolerance);
int status = core.loadFromMsh("core.msh", maxAngle);该参数使用弧度。对于四边形,如果最大内角大于阈值,导入时会沿对角线拆成两个三角形。当前实现只有在阈值大于 90° 时才启用此拆分逻辑。
该参数只检查并拆分导入的四边形,不会检查已有三角形的最大内角,也不是通用网格质量优化器。
导入核心网格后,建议至少检查:
core.m_bndPts.size() == expectedInterfacePointCount并对每一个核心边界点调用边界层组合网格的:
boundaryLayers.pointIsExist(core.m_pts[id], matchedId)全部匹配后再执行 AddRegion(core)。这样可以在输出非共形网格前尽早失败。
已知边界函数和边数时,使用:
combined.defineBoundary(
reinterpret_cast<void *>(wallEdge),
nEdges,
boundaryId,
nCurvedPoints,
direction);它会按相同参数位置计算每条边的两个端点,并在当前边界中查找对应 edge。
nEdges必须与真实边界分段数一致;nCurvedPoints > 2时,会为每条边输出PolyEvenlySpaced曲边点;direction < 0可反转解析 edge 的参数方向;- 定义边界前要保证
m_bndPts和m_edgesIndex已更新。
对于由 CompositEdge 表示的边界,既可以用完整 composite edge 和 composite.m_N 定义,也可以逐段调用并使用同一个边界 ID。
也可以先按转角拆分外露边界,再用条件函数选择:
std::map<int, void *> conditions;
conditions[0] = reinterpret_cast<void *>(isWall);
conditions[1] = reinterpret_cast<void *>(isFarField);
combined.defineBoundary(conditions, splitAngle);条件函数签名为:
bool condition(std::vector<double> &point);一个条件命中某一边界段上的任意一点后,整段会被分配给该边界 ID。条件应能唯一识别目标边界段。
完整输出必须按下面的顺序执行:
combined.CheckMesh();
combined.outXml("mesh.xml");
combined.outCOMPO("mesh.xml", {0});outXml() 写入:
VERTEX;EDGE;- 三角形/四边形
ELEMENT; CURVED。
但它不会关闭整个 XML 文档。outCOMPO() 以追加方式写入:
- 边界 composite;
- 单元 composite;
DOMAIN;- 可选的
EXPANSIONS; - XML 结束标签。
因此不能只调用 outXml()。编译时定义 OUTPUTEXP 才会输出默认 expansion 配置。
outCOMPO() 会先将四边形单元排在三角形单元之前,以便生成连续的 Q[...]、T[...] composite 范围。
传给 outCOMPO() 的整数列表用于进一步切分单元 composite;简单单区域网格通常传 {0}。
combined.CheckMesh();输出包括:
- isolated points;
- isolated edges;
- negative Jacobi elements。
其中仅被一个单元使用的边就是外露边界,所以 isolated edges 的数量通常等于物理边界边数,不要求为零。真正需要重点关注的是:
- isolated points 应为 0;
- negative Jacobi elements 应为 0;
- isolated edges 应与预期的所有边界分段数一致。
loadFromMsh() 和 loadFromXml() 内部会调用 FixMesh() 修正负方向单元,但最终合并后仍应再次检查。
xmllint --noout mesh.xml
rg -c '<Q ' mesh.xml
rg -c '<T ' mesh.xml还应检查 Gmsh 日志中的:
- 重组后的四边形和三角形数量;
- invalid quads;
- 最小质量;
- 是否出现接口线重新剖分相关错误。
对于具体算例,推荐保持下面的职责划分:
params.h
几何尺寸、独立边界分段数、首层高度、增长率、总厚度、core mesh size
edgefunctions.h
几何 edge、LineEdge、CompositEdge、法向累计距离、派生层数
mesh.cpp
参数检查、区域生成、GEO 输出、MSH 导入、接口检查、区域合并、最终输出
参数检查至少应包括:
- 几何半径和位置合法;
- 边界层不重叠并留有核心区域;
- 首层、增长率、总厚度和核心尺寸为正;
- 分段数与
CompositEdge的分段方式兼容; - Gmsh 接口总边数适合预期的重组策略;
- 曲边点数不少于 3。
#include "CAD2D/MeshRegions.h"
#include "CAD2D/RectRegion.h"
std::vector<double> wall(double s) {
// 顺时针内圆:左法向指向圆外
double theta = -std::acos(-1.0) * (s + 1.0);
return {cx + r * std::cos(theta), cy + r * std::sin(theta)};
}
std::vector<double> growth(double s) {
int j = std::lround(0.5 * (s + 1.0) * nLayers);
return {cumulativeDistance(j), 0.0};
}
int main() {
std::vector<void *> edges = {
reinterpret_cast<void *>(wall),
reinterpret_cast<void *>(growth),
nullptr,
nullptr
};
RectRegion bl(edges, "BL", false, 1.0e-10);
bl.MeshGen(nWallCells, nLayers, eBoundaryLayer1);
MeshRegions mesh("Mesh", 1.0e-10);
mesh.AddRegion(bl);
mesh.ResetBndPts();
mesh.rebuildEdgesIndex();
mesh.CheckMesh();
}SplineEdge:从文件读取二维点,构造 cardinal cubic B-spline,并建立近似弧长表;Evaluate(s)按归一化弧长参数[-1,1]取点。Cylinder:按起止角度生成圆弧点,但不包含圆心平移。BLMeshModule:边界层几何模块的抽象基类,使用字符串到数值的参数表。BLEllipse:椭圆/圆柱边界层示例。BLFlatPlate、BLRectangle:多段物面的边界层拼接示例。BLAirfoil、airfoil.*:NACA 或楔形翼型及其边界层模块。MeshTool:全局法向层分布、局部变厚边界层及按边界嵌套层级输出 GEO 的辅助函数。util:距离、包围盒、拓扑树、GEO 输出和 XML 数字解析等工具。
检查 edge0 的参数方向。CAD2D 使用左法向,不根据“内壁/外壁”自动判断流体侧。
普通四边区域检查四条边端点;必要时用 SetEdgesDirec()。只有 edge0 + edge1 的边界层区域必须关闭检查,另外两条边使用 nullptr。
通常是节点坐标、节点数或容差不一致。确认 Gmsh 没有在线上插入节点,并使用边界层实际节点输出 GEO。
确认:
nEdges与网格真实分段数一致;- edge 函数端点与网格节点在
m_tolerance内一致; - 已调用
ResetBndPts()和rebuildEdgesIndex(); - composite edge 已完成一次性初始化。
必须先调用 outXml(),再对同一文件调用 outCOMPO()。
允许保留少量三角形,并在 loadFromMsh() 中设置合理的最大四边形内角,例如 120°。不要为了全四边形破坏接口或产生高畸变单元。
任何会改变点、边、单元或区域连接关系的操作后,重新执行:
ResetBndPts();
rebuildEdgesIndex();只使用核心结构化生成、合并和 XML 输出时,通常至少需要编译:
CAD2D/CompositEdge.cpp
CAD2D/LineEdge.cpp
CAD2D/MeshRegion.cpp
CAD2D/MeshRegions.cpp
CAD2D/RectRegion.cpp
CAD2D/tinyxml2.cpp
CAD2D/util.cpp
使用 radiusEdge()、findNlayers() 辅助流程时还需加入 CAD2D/MeshTool.cpp。使用样条、翼型或预制边界层模块时,再加入对应源文件。