znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第06章:布尔运算——并集、差集、交集

布尔运算是 CSG 的灵魂。有了第 5 章的基础形体,本章教你用并集(Union)差集(Subtract/Difference)交集(Intersect/Intersection) 把它们组合成任意复杂的实体。我们会讲清两套调用形式(实例方法 vs 静态方法)、多操作数的语义、运算顺序的影响、背后的原点居中修复,以及一整套实战陷阱与规避技巧。

6.1 三种运算与两套调用形式

回顾第 3 章:三种布尔运算对应点集的并、差、交。OpenCSG.NET 提供了两套等价的调用形式——实例方法静态方法,返回结果完全一致,选哪套纯看代码可读性。

运算 实例方法(在 Solid 上) 静态方法(在 Solids 上) 集合语义
并集 a.Union(b, c, ...) Solids.Union(a, b, c, ...) A ∪ B ∪ C
差集 a.Subtract(b, c, ...) Solids.Difference(a, b, c, ...) A − B − C
交集 a.Intersect(b, c, ...) Solids.Intersection(a, b, c, ...) A ∩ B ∩ C

因为通常都会 using static Csg.Solids;,静态方法可以直接写成 Union(...)Difference(...)Intersection(...)。官方示例里两种风格都在用:

// 静态风格:读起来像数学表达式,适合"平铺"的组合
var u = Union(cube, sphere);
var d = Difference(cube, sphere);
var i = Intersection(cube, sphere);

// 实例/链式风格:适合"以某个主体为中心"的连续加工
var part = cube.Intersect(sphere).Subtract(cylinder);

官方 Runner.Examples 特别注释道:”你也可以通过成员函数调用这些操作,返回的对象没有任何区别。”两套形式底层调用同一份代码。

6.2 并集 Union:把形体焊接在一起

Union 把多个实体合并成一个。即使它们没有重叠,也会被组合进同一个 Solid(只是彼此分离)。

var a = Cube(size: 1, center: true);
var b = Sphere(r: 0.6, center: true);

var merged = Union(a, b);               // 或 a.Union(b)

Union 支持任意多个操作数:

var barbell = Union(
    bar,
    weight.Translate(y: -0.9),
    weight.Translate(y: +0.9)
);

6.2.1 不相交时的快速路径

内部有一个重要优化:Union 会先用包围盒判断两个实体是否可能重叠MayOverlap)。如果明显不相交,它直接把两组多边形拼接起来,跳过昂贵的 BSP 裁剪——这是把”焊接分离形体”变得极快的关键。所以,用 Union 把许多互不接触的零件收集到一个实体里(例如官方示例把所有演示形体排成一排)几乎没有性能负担。

6.2.2 原点居中:大坐标精度修复

Union 还内置了第 3 章讲过的原点居中修复:它先算出所有操作数的合并包围盒中心,把几何整体平移到原点附近做布尔,最后再平移回去。这解决了在大坐标(如 (49256, 12000, 5))附近做并集时的浮点精度丢失问题,由测试 LargeCoordinateUnionTest 专门守护。

你要做的:一般无需干预,享受这个修复即可。但仍建议让模型工作在原点附近的合理数量级,把”搬到最终位置”留到最后。

6.3 差集 Subtract:挖洞与切削

Subtract(静态名 Difference)从第一个实体上挖掉后续所有实体占据的部分——这是打孔、开槽、倒角、切削的核心工具。

// 一根管子:外圆柱减内圆柱
var outer = Cylinder(r: 1,    h: 4, center: true);
var inner = Cylinder(r: 0.95, h: 4, center: true);
var pipe  = Difference(outer, inner);    // 或 outer.Subtract(inner)

多操作数时,等价于从主体依次减去每一个:

// 从盘体上依次挖掉中心孔和四个螺栓孔
var flange = plate.Subtract(centerBore, bolt1, bolt2, bolt3, bolt4);

6.3.1 差集不满足交换律

牢记:A − B ≠ B − ASubtract 永远是”从第一个减去其余”,顺序不能颠倒。

var x = cube.Subtract(sphere);   // 方块被球挖掉一块
var y = sphere.Subtract(cube);   // 球被方块挖掉一块——完全不同的结果!

而并集和交集满足交换律,操作数顺序不影响结果(但可能影响中间计算的数值细节)。

6.4 交集 Intersect:只保留公共部分

Intersect(静态名 Intersection)只保留所有实体共同占据的空间。它常用于”用一个形体去限制另一个的范围”,比如给方块削出球面圆角:

// 方块 ∩ 球 = 六面被球面削圆的"方块"
var rounded = Intersection(
    Cube(size: 1.4, center: true),
    Sphere(r: 1, center: true)
);

如果两个实体完全不重叠,交集就是空实体(没有任何公共空间):

var empty = Intersection(
    Cube(size: 1, center: true),
    Cube(size: 1, center: true).Translate(x: 100)  // 相距很远
);
// empty.Polygons.Count == 0

6.5 静态方法的操作数个数语义(重要陷阱)

静态 Solids.Union/Difference/Intersection不同操作数个数有专门处理,其中交集有一个反直觉的行为,务必注意:

操作数个数 Union Difference Intersection
0 个 空实体 空实体 空实体
1 个 返回该实体本身 返回该实体本身 空实体(!)
2 个及以上 正常并集 正常差集 正常交集

也就是说,Intersection 传入 0 个或 1 个实体,都会返回空实体——因为”一个东西和自己的交集”在这个实现里被定义为需要至少两个操作数。如果你写了一个循环动态收集要相交的实体,最后只收集到 1 个,结果会意外变空。规避方法:相交前先判断个数,或对单元素情形直接返回该元素。

实例方法 a.Intersect()(不传参数)则不同——它的循环体不执行,直接返回 a 本身。两种形式在”退化情形”下的行为不一致,是容易被忽略的细节。

6.6 链式组合:像搭积木一样建模

实例方法天然支持链式调用,把复杂建模写成一条流畅的”加工流水线”。复刻官方示例里 CSG 维基百科经典图标:

var rod = Cylinder(r: 0.5, h: 2, center: true);   // 一根沿 Y 轴的圆柱

var logo = Difference(
    // 先求"圆角方块":立方体 ∩ 球
    Intersection(
        Cube(size: 1.4, center: true),
        Sphere(r: 1, center: true)
    ),
    // 再挖掉三个正交的圆柱(形成三向通孔)
    Union(
        rod,             // 沿 Y
        rod.RotateX(90), // 转成沿 Z
        rod.RotateZ(90)  // 转成沿 X
    )
);

这段代码把三种运算全用上了:交集做圆角方块、并集把三根正交圆柱合成”十字钻头”、差集把钻头从方块里挖掉。这正是 CSG 最经典的演示,也展示了”组合基础形体 + 三种布尔”的巨大表达力。

6.7 实战陷阱与规避技巧

布尔运算在数学上很干净,但在浮点世界里有一些反复出现的坑。掌握这些技巧能让你的模型稳定、干净、可制造。

6.7.1 避免精确共面:让挖孔实体略微超出

这是最重要的一条。当你用差集挖穿一个实体时,如果挖孔实体的端面与被挖实体的表面恰好共面,BSP 分割会进入”共面”分支,容易残留薄面、破面或歧义结果。解决办法是让挖孔实体在穿透方向上略微超出

double plateH = 6;
var plate = Cylinder(r: 30, h: plateH, center: true).RotateX(90);

// 孔比盘体高 2(每侧各多出 1),保证干净穿透,避免共面
var hole  = Cylinder(r: 10, h: plateH + 2, center: true).RotateX(90);

var result = plate.Subtract(hole);

同理,做”齐平”的并集/交集时,也应让接触面略微交叠而非精确对齐。

6.7.2 布尔顺序影响性能

虽然 Union(a, b, c, ...) 结果与顺序无关,但中间结果的复杂度会受顺序影响。一个经验法则:先合并邻近、相交的形体,把远离的、独立的形体放到最后,可以让每一步 BSP 处理的面数更少。对大量形体,考虑”分组并集再合并”而非”一个接一个累加”。

6.7.3 分辨率与布尔代价

第 5 章说过,曲面形体分辨率越高,面数越多。布尔运算的代价大致随面数增长,两个高分辨率球做差集会明显变慢。策略:建模、调试阶段用低分辨率(12)快速验证逻辑,确认无误后再统一提高分辨率导出。

6.7.4 结果为空时如何排查

如果布尔结果出乎意料地为空(Polygons.Count == 0),按这个清单排查:

  1. 交集不重叠:两个形体根本没有公共空间 → 检查位置。
  2. Intersection 只传了 1 个操作数 → 见 6.5,会返回空。
  3. 差集减没了:被减实体完全包含在减数里 → 检查尺寸。
  4. 形体本身就是空的:如零半径 Cylinder/Sphere、零尺寸 Cube(第 5 章)。

6.7.5 复用不可变实体

再次强调第 4 章的不可变性:所有布尔与变换都返回新实体,绝不修改输入。所以你能放心地把同一个”孔模板”“配重球”复用到多个位置,而无需担心污染。

6.8 本章小结

  • 三种布尔运算各有实例a.Union/Subtract/Intersect)和静态Solids.Union/Difference/Intersection)两套等价形式,可自由混用。
  • 并集焊接形体、对不相交情形有快速路径、内置原点居中大坐标精度修复;差集用于挖洞切削且不满足交换律交集只留公共部分、不重叠则为空。
  • 静态 Intersection 传 0 或 1 个操作数会返回空实体,是最隐蔽的陷阱。
  • 链式组合能表达任意复杂形状(如三向通孔的 CSG 图标)。
  • 实战守则:让挖孔实体略微超出以避免共面、注意布尔顺序对性能的影响、用低分辨率迭代高分辨率导出、系统化排查空结果、放心复用不可变实体。

下一章讲几何变换——平移、旋转、缩放、镜像,把形体摆放到正确的位置和姿态,这是配合布尔运算完成复杂建模不可或缺的另一半。


← 上一章 目录 下一章 →