相关概念
网格切割是指两个输入网格相互切割,然后根据过滤器输出对应Patch。这里的网格通常指流形网格(manifold),流形网格是指网格中所有顶点都是流行顶点,所有边都是流行边。
流行顶点:对于网格的任意顶点 v,可用 N(v)表示网格中共享顶点 v 的所有三角形(多边形)形成的邻域。如果 N(v)中所有三角形(多边形)按序排列且使得序列中所有 2 个相邻三角形(多边形)都共享一条以顶点 v 为端点的,则将顶点 v 叫做流形顶点。=>顶点的邻域三角形(多边形)只能是“一个”扇形且相邻三角形(多边形)之间无空隙。
流行边:如果网格中的一条边 e 恰好被 2 个三角形(多边形,以反向的方式)共享或者只属于某个单个三角形(多边形),则将边 e 叫做流形边。如果边 e 只属于一个三角形,将边 e 叫做边界边。注意,两个三角形(多边形)以同向共享一条边的情况也是非流形边,如:两个相邻三角形(多边形)法向相反。
可以根据非流行点非流行边将非流行网格拆分为多个流形块(Patch) 或多个流行网格。
同样可以拆分非流行点或反转三角形将非流行网格转为流形网格。
Patch:网格切割后根据非流行边拆分出来的无流行边及流行点的网格流行块。
注意:流形网格可能包含多个流行块。可以存在重复点,允许自相交。
自相交:网格内部三角形(多边形)相互穿插或覆盖(overlay)。
闭合网格:水密(watertight),闭合(closed)网格,指无边界边、无自相交且无退化三角形(多边形)的流形网格,最完美的网格。
原始网格:输入的第一个网格,Source
切割网格:输入的第一个网格,Cut
过滤器:Patch与原始输入网格的关系。有以下几种:
FromSource:来自源网格
InsideSource:在源网格内部,源网格需闭合,通常这部分Patch来自切割网格
OutSideSource: 在源网格外部,源网格需闭合,通常这部分Patch来自切割网格
FromCut:来自切割网格
InsideCut:在切割网格内部,切割网格需闭合,通常这部分Patch来自源网格
OutSideCut: 在切割网格外部,切割网格需闭合,通常这部分Patch来自源网格
Above_Cut:在切割网格上,切割网格流向上方(非闭合且相交的情况)
Below_Cut: 在切割网格下,切割网格流向上方(非闭合且相交的情况)
Above_Source: 在源网格上
Below_ Source:在源网格下
From_Source_And_Cut:即来自源网格也来自切割网格,针对相互覆盖的情况
Undefine:未定义,对于不完全切割或不相交且不闭合时网格无法判断其位置关系
All:所有Patch.
流程步骤
假设输入的都是流形网格,自相交理论不会影响正确性,但会影响切割性能。
输入:源网格 A 、切割网格 B 及过滤器。
输出:得到满足过滤器的最终Patch。
步骤 1. 合并 A 和 B
步骤 2. 检测并计算相交对象及记录所在三角形(可并行)
步骤 3. 根据步骤2的结果重新进行局部remesh(可并行)
步骤 4. 将remesh后的所有网格按照非流行点及非流行边拆分成多个patch;
步骤 5. 判断每个patch与输入网格的关系(可并行);
步骤 6. 根据输入的过滤器输出patch
步骤 1 Merge
合并源网格A 和切割网格B,这一步仅是单纯的合并网格,无其他复杂操作,需要记录源网格A的三角形个数。
步骤 2 Resolve Self-Intersections
这一步其实分为两步
1、检测并记录相交对象
可用CGAL::box_intersection_d及CGAL::box_self_intersection_d检测。这里需要注意一点,如果输入网格存在自相交需要检测所有的三角形,即A中三角形不仅需要与B中所有三角形检测,还需与A中除自身外的其余三角形检测,非常耗时。如果存在自相交但不这样检测可能会影响切割结果。如果不存在自相交则只需要检测网格A的三角形与网格B的三角形。
2、计算并记录相交对象及其三角形。
三角形相交也可用CGAL::intersection来计算,后续文章会介绍CGAL三角形相交的算法。
三空间角形相交的可能:
不在同一平面:点、线段
在同一平面:点、、线段、三角形、四边形、五边形、六边形,以下未展示相交未为点及线段的情况。
需要注意共平面三角形,所有共平面的三角形在后续局部约束剖分时需要一起剖分。
步骤 3 Remesh Intersections
根据步骤2的结果重新进行局部remesh。将三角形边界与相交对象作为约束剖分的约束做三角剖分=>局部remesh,得到重新三角化的局部三角网。可用CGAL 约束剖分处理。相关示例:
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/Constrained_Delaunay_triangulation_2.h>
#include <CGAL/Constrained_triangulation_plus_2.h>
typedef CGAL::Exact_predicates_exact_constructions_kernel K;
typedef CGAL::Constrained_Delaunay_triangulation_2<K> CDT;
typedef CGAL::Constrained_triangulation_face_base_2<K> Face;
typedef CGAL::Triangulation_vertex_base_2<K> Vertex;
typedef CGAL::Triangulation_data_structure_2<Vertex, Face> TDS;
typedef CGAL::Constrained_triangulation_plus_2<CDT, TDS> CTP;
int main() {
CTP ctp;
// 添加约束边
ctp.insert_constraint(K::Point_2(0,0), K::Point_2(1,1));
ctp.insert_constraint(K::Point_2(1,1), K::Point_2(2,0));
ctp.insert_constraint(K::Point_2(2,0), K::Point_2(0,0));
// 插入点
ctp.insert(K::Point_2(1,0.5));
// 输出所有三角形
for (CTP::Finite_faces_iterator fit = ctp.finite_faces_begin(); fit != ctp.finite_faces_end(); ++fit) {
std::cout << "Triangle: " << ctp.triangle(fit) << std::endl;
}
return 0;
}
后续文章会介绍约束剖分算法。
步骤 4 Extract manifold patch
将步骤3中得到的局部三角网替换对应原三角形,按照非流行边拆分为多个Patch。提取流行块时需要记录Patch的临边情况以及非流形边周围的三角形情况,此外哪些流行块包含哪些三角形也是需要记录的。
步骤 5 Classify Patch
判断每个Patch与输入网格的关系。这一步是所有布尔运算的难点。我们采用相交三角形法向来判断Patch与原始网格的关系(可能会有一些特殊情况需要处理),即Patch的产生是因为三角形相交或非流形边导致的。下图两个三角形相交产生的公共边即为非流形边,这条非流形边将Tri2分为上下两部分,Tri1完全切割Tri2,但Tri1不能被Tri2完全分割,所以会有Undefine这个过滤器。判断关系时我们不需要对每个Patch判断,只需要判断非流形边邻接,当然会有一些Patch不是由非流形边提取的,比如输入存在多个连通区域,其中一些连通区域的所有三角形均不与其他区域三角形相交,像这种情况,如果另一个输入网格是闭合的则可以判断当前Patch与其关系,如果不是闭合的则归为Undefine.
步骤 6 Output
根据输入的过滤器输出patch。将步骤5的分类结果按输入的过滤器输出即可,外部按照需求合并成网格即可。针对闭合网格的交并差可由以下组合实现:也可根据过滤器同时实现A-B B-A,A XOR B(异或)等各种复杂的结果
交:InsideSource| InsideCut | From_Source_And_Cut
并:OutSideSource | OutSideCut | From_Source_And_Cut
差:OutSideCut | OutSideSource 其中OutSideSource需要翻转。
关键技术点
1、 浮点运算精度问题。
浮点运算精度问题在计算几何中是个非常令人头痛的问题!但布尔运算实际上用到的运算基本只会涉及到线段相交、线段与三角形求交,简单的加减乘除即可,输入通常为3个double,采用CGAL的EPECK运算核基本能解决精度问题。注:CGAL5.4以下版本不支持多线程
2、空间三角形相交
3、任意平面内约束剖分
4、点与平面的关系判断
5、点是否在多面体内判断
相关算法已集成到SharpCAD1.2版本以及布尔运算工具中: