1 引言
表面网格简化是指在保持整体形状、体积和边界尽可能不变的情况下,减少表面网格中面片数量的过程。它与细分相反。
本文介绍的算法可以简化任何有向的二维流形表面,无论其连接组件的数量、是否有边界(边界或孔)以及把手(任意种类)。该方法称为边折叠。大致来说,该方法包括迭代地用一个单一顶点替换一条边,每次折叠移除2个三角形。
边缘根据用户提供的代价函数按优先级折叠,替换顶点的坐标由另一个用户提供的放置函数确定。当满足用户提供的停止条件时(例如达到所需的边数),算法终止。
这里实现的算法是通用的,因为它不要求表面网格是特定类型,但需要符合MutableFaceGraph和HalfedgeListGraph概念的模型。我们提供了Surface_mesh、Polyhedron_3和OpenMesh的示例。
该设计是policy-based,这意味着你可以通过传递一组策略对象来自定义过程的某些方面。每个策略对象指定算法的特定方面,例如如何选择边以及替换顶点的位置。所有策略都有一个合理的默认值。此外,API使用所谓的命名参数技术,这允许你以任意顺序仅传递相关参数,省略那些默认值适用的参数。
2 简化过程概述
实现简化算法的自由函数不仅接收表面网格和期望的停止条件,还接收许多额外参数以控制和监控简化过程。本节简要描述了该过程,为讨论算法的参数设置背景。
有两种稍微不同的“边”折叠操作。一种称为边折叠(edge-collapse),另一种称为半边折叠(halfedge-collapse)。给定连接顶点w和v的边e,边折叠操作用新顶点r替换e、w和v,而半边折叠操作将v拉入w,消除e并保留w。在这两种情况下,该操作都移除了边e及其相邻的两个三角形。
该包使用的是半边折叠操作,通过额外移除一个顶点(v)和两个边(每个相邻三角形一个)来实现。它还可以选择将剩余顶点(w)移动到一个新位置,称为放置(placement),这种情况下其净效果与边折叠操作相同。
自然地,边折叠后得到的表面网格会在一定程度上偏离初始表面网格。由于简化的目标是在尽可能保留表面网格整体外观的同时减少三角形的数量,因此有必要测量这种偏离程度。一些方法试图测量从初始表面网格到完全简化后的表面网格的总偏离量,例如,通过跟踪累计误差并保留简化更改的历史记录。其他方法,例如本包实现的方法,只尝试测量每次单个边折叠的成本(单次简化步骤引入的局部偏离),并将整个过程规划为成本逐渐增加的步骤序列。
全局误差跟踪方法会产生高度准确的简化,但需要大量额外空间。而基于成本的方法,如本包中的方法,会产生稍微不那么准确的简化,但占用的额外空间要少得多,甚至在某些情况下不需要额外空间。
该包提供了两种基于成本的方法。第一种方法即Lindstrom&Turk方法,主要基于文献[4]和[5],并结合了[3]、[2]和[1]的贡献。第二种方法即Garland&Heckbert方法,主要基于文献[2],并结合了Trettner和Kobbelt[7]的增强。
算法分两个阶段进行。在第一阶段,称为收集阶段,为表面网格中的每条边分配初始折叠成本。然后在第二阶段,称为折叠阶段,按照成本递增的顺序处理边。一些处理过的边会被折叠,而一些则会被丢弃。折叠的边被顶点替换,替换顶点的所有边的折叠成本重新计算,影响剩余未处理边的顺序。
并非所有选中处理的边都会被折叠。如果不满足某些拓扑和几何条件,处理过的边可以直接丢弃,而不进行折叠。
文献[2]提出的算法通过将某些顶点对视为形成伪边,从而不仅折叠边还折叠任意顶点对,并以[4]和[5]相同的方式折叠边和伪边。然而,折叠任意顶点对可能导致非流形表面网格,但目前该包只能处理流形表面网格,因此只能折叠边。也就是说,该包不能用作顶点折叠的框架。因此,我们对文献[2]的实现仅折叠边。
3 成本策略
计算折叠成本和顶点放置的具体方式称为成本策略。用户可以选择不同的策略,这些策略以策略和相关参数的形式传递给算法。
当前版本的包提供了一组策略,实现了三种策略:默认的Lindstrom-Turk策略、Garland-Heckbert家族的策略,以及由边长成本和可选中点放置组成的策略(速度更快但准确性较低)。
3.1 Lindstrom-Turk成本和放置策略
文献[4]和[5]提出的策略的主要特点是,简化后的表面网格在每一步都不与原始表面网格(或前一步的表面网格)比较,因此不需要保留额外信息,如原始表面网格或局部更改的历史记录,因此称为无记忆简化。
在每一步中,所有剩余边都是潜在的折叠候选,选择成本最低的一条边进行折叠。边折叠的成本由替换它的顶点位置决定。
替换顶点的位置通过求解一个3个线性独立线性等式约束系统来计算。每个约束是通过对一个二次目标函数进行约束最小化得到的。有几种可能的候选约束,并按重要性顺序考虑每个约束。候选约束可能与先前接受的约束不兼容,这种情况下它会被拒绝,考虑下一个约束。一旦接受了3个约束,系统会求解顶点位置。首先考虑的约束保留了表面网格边界的形状(如果边的轮廓有边界边)。接下来的约束保留了表面网格的总体积。如果需要,接下来的约束优化局部体积和边界形状的变化。最后,如果仍需要约束(因为之前计算的约束不兼容),会添加第三个(也是最后一个)约束以偏向等边三角形而不是细长三角形。
成本是形状、体积和边界优化项的加权和,用户可以为每个项指定单位加权因子。
局部变化是独立于每条边计算的,仅使用当前相邻的三角形,当边即将折叠时(即在所有先前折叠之后)计算。因此,最小局部变化的传递路径最终会产生一个接近绝对最小值的全局变化。
3.2 Garland-Heckbert成本和放置策略
与Lindstrom-Turk策略一样,文献[2]提出的Garland-Heckbert策略不比较结果网格与原始网格,也不依赖于局部变化的历史。相反,它通过将四面体矩阵分配给每个顶点来编码与原始网格的近似距离。
在其经典版本中,一个四面体矩阵Q分配给每个顶点v,并编码了任何点p到v的邻近面的总平方距离,这由矩阵乘积p′Qp给出。在每一步中,选择最小化折叠成本的边进行折叠操作。折叠边的成本通过最小化误差函数p′Qp计算,其中Q是边端点的四面体矩阵的组合,p是最小化成本的点(即决策变量)。最小化目标函数的点p被选为新的放置点。由于误差函数是二次的,找到其最小值只需计算梯度并将其设为零。如果由于奇点而失败,则在边上找到最佳点和成本。放置新顶点后,通过简单地将折叠边两端的四面体矩阵相加为它分配新的四面体矩阵。为了尽可能保留网格的锐利边界,边界顶点的四面体矩阵中加入了与邻近面垂直的附加伪面。
Trettner和Kobbelt[7]提出的Garland-Heckbert扩展引入了概率四面体的概念。在这种新方法中,能量最小化不再使用到输入平面或多边形的距离,而是通过在输入(顶点位置和面法线)中引入高斯噪声来使输入几何变得不确定。这种方差自然会削弱结果的紧密性,但另一方面,它能创建更均匀的三角化,并且该方法对噪声更具容忍性,同时仍保持特征敏感性。
图 71.1 萨福的头部模型(最左边,34882 个顶点)。从左到右分别为简化后的输出(1745 个顶点)和四种 Garland-Heckbert 变体的对称 Hausdorff 距离:平面(0.217912)、概率平面(0.256801)、三角形(0.268872)和概率三角形(0.490846)。
3.3 成本策略策略
算法使用的成本策略通过三个策略选择:GetPlacement、GetCost 和 Filter。
GetPlacement 策略用于计算半边折叠后剩余顶点的新位置。它返回一个可选值,如果边不应该折叠,这个值可以为空。
GetCost 策略用于计算折叠边的成本。该策略使用放置来计算成本(即误差度量)并确定边的排序。
算法维护一个内部数据结构(一个可变的优先队列),允许按成本递增顺序处理每条边。这样的数据结构需要一些每边额外的信息,例如边的成本。如果每边额外信息记录占用 N 字节的存储空间,那么简化一个有 100 万条边(一个正常大小)的表面网格需要 100 万倍 N 字节的额外存储空间。因此,为了最小化简化表面网格所需的额外内存,只附加每条边的成本信息,其他信息一律不附加。
但这是一个权衡:折叠成本是放置(为剩余顶点选择的新位置)的函数,因此在为每条边调用 GetCost 之前,还必须调用 GetPlacement 以获得成本函数的放置参数。但这个放置是一个 3D 点,没有附加到每条边上,因为这样会轻易地使额外存储需求增加三倍。一方面,这大大节省了内存,但另一方面,这在处理上是浪费的,因为当一条边实际上被折叠时,必须再次调用 GetPlacement 来确定剩余顶点的位置。
早期原型表明,将放置附加到边上,从而避免边折叠后对放置函数的冗余调用,对总运行时间影响不大。这是因为每条边的成本不只是计算一次,而是在过程中多次变化,因此放置函数也必须多次调用。缓存放置只能避免最后一次调用(边折叠时),但无法避免之前的所有调用,因为放置(和成本)在变化。
最后,我们解释 PlacementFilter 策略。虽然成本是优先队列中使用的标量,但可能有额外的标准来决定是否执行边折叠。虽然这种标准可以很容易地集成到成本函数中,即将成本设置为无限大以不被认为是折叠候选,但我们仅在下一条边即将折叠时测试标准。如果标准的计算非常耗时,例如检查简化后的网格是否在输入网格的公差包络内,这使得网格简化更快。
4 API
4.1 API概述
由于算法没有健壮性问题,因此不需要精确的谓词和构造,Simple_cartesian 可以安全使用。
简化算法实现为自由模板函数 Surface_mesh_simplification::edge_collapse()。该函数有两个必需参数和几个可选参数。
4.2 必需参数
算法的两个主要参数是要简化的表面网格(就地)和停止谓词。
要简化的表面网格必须是 MutableFaceGraph 和 HalfedgeListGraph 概念的模型。
在选择每条边进行处理后、将其分类为可折叠或不可折叠之前(因此在其折叠之前),调用停止谓词。如果停止谓词返回 true,算法终止。
4.3 可选命名参数
命名参数的概念也在 BGL 中引入。你可以在[6]或以下网站了解更多信息:https://www.boost.org/libs/graph/doc/bgl_named_params.html。命名参数允许用户只指定真正需要的参数,通过名称,使参数顺序不重要。
假设有一个函数 f() 接受三个参数:name、age 和 gender,你有变量 n、a 和 g 作为该函数的参数。没有命名参数时,你会这样调用它:f(n, a, g),但有了命名参数,你可以这样调用它:f(name(n).age(a).gender(g))。
即,通过将每个参数包装到一个与参数同名的函数中,给每个参数命名。整个命名参数列表实际上是由点(.)分隔的函数调用的组合。因此,如果函数接受混合的必需和命名参数,你使用逗号将最后一个非命名参数与第一个命名参数分隔开,如下所示:
f(non_named_par0, non_named_pa1, name(n).age(a).gender(g))
使用命名参数时,顺序无关紧要,因此:f(name(n).age(a).gender(g)) 等同于 f(age(a).gender(g).name(n)),你可以省略任何有默认值的命名参数。
4.4 示例调用
/*
surface_mesh : 要简化的表面网格
stop_predicate : 指示何时完成简化的策略
vertex_index_map(vimap) : 为每个顶点分配唯一整数索引的属性映射
edge_index_map(eimap) : 为每条边分配唯一整数索引的属性映射
edge_is_constrained_map(ebmap): 指定边是否为受约束边的属性映射
get_cost(cf) : 计算折叠成本的函数对象
get_placement(pf) : 计算剩余顶点放置的函数对象
filter(filter) : 拒绝下一个边折叠候选的函数对象
visitor(vis) : 跟踪简化过程的函数对象
*/
int r = edge_collapse(surface_mesh, stop_predicate,
CGAL::parameters::vertex_index_map(vimap)
.edge_index_map(eimap)
.edge_is_border_map(ebmap)
.get_cost(cf)
.get_placement(pf)
.filter(filter)
.visitor(vis));
… TODO