第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 − A。Subtract 永远是”从第一个减去其余”,顺序不能颠倒。
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),按这个清单排查:
- 交集不重叠:两个形体根本没有公共空间 → 检查位置。
Intersection只传了 1 个操作数 → 见 6.5,会返回空。- 差集减没了:被减实体完全包含在减数里 → 检查尺寸。
- 形体本身就是空的:如零半径
Cylinder/Sphere、零尺寸Cube(第 5 章)。
6.7.5 复用不可变实体
再次强调第 4 章的不可变性:所有布尔与变换都返回新实体,绝不修改输入。所以你能放心地把同一个”孔模板”“配重球”复用到多个位置,而无需担心污染。
6.8 本章小结
- 三种布尔运算各有实例(
a.Union/Subtract/Intersect)和静态(Solids.Union/Difference/Intersection)两套等价形式,可自由混用。 - 并集焊接形体、对不相交情形有快速路径、内置原点居中大坐标精度修复;差集用于挖洞切削且不满足交换律;交集只留公共部分、不重叠则为空。
- 静态
Intersection传 0 或 1 个操作数会返回空实体,是最隐蔽的陷阱。 - 链式组合能表达任意复杂形状(如三向通孔的 CSG 图标)。
- 实战守则:让挖孔实体略微超出以避免共面、注意布尔顺序对性能的影响、用低分辨率迭代高分辨率导出、系统化排查空结果、放心复用不可变实体。
下一章讲几何变换——平移、旋转、缩放、镜像,把形体摆放到正确的位置和姿态,这是配合布尔运算完成复杂建模不可或缺的另一半。