模型简化是三维资产轻量化、实时渲染和网络传输中的关键步骤。简化后的网格如果出现轮廓塌陷、棱边变钝、局部凹凸消失,通常不是误差度量本身失效,而是特征边在折叠序列中被过早消除。要解决变形问题,必须让简化算法在计算折叠代价时区分普通边和重要特征边,对后者施加更强的保留约束。本文将围绕这一思路展开,介绍特征边的判定、加权策略与算法选择,并给出可参考的实现代码。

一、模型简化变形的根源:特征边被误折叠
主流的模型简化算法以边折叠为核心操作,每次选择代价最小的边收缩为一个顶点,并在局部更新网格拓扑。二次误差度量(QEM)用顶点到相关平面距离的平方和来评估折叠误差,优点是计算量小、整体形状保持较好,但它对局部尖锐特征并不敏感。当一条处于两个夹角较大平面之间的边被折叠时,新顶点往往落在平均位置附近,原本尖锐的折痕会迅速变圆。边界边、材质接缝、UV岛边缘等还可能因为缺少相邻面约束而被优先删除,导致轮廓收缩或贴图拉伸。因此,简化后的变形主要来自特征边的误折叠,而不是全局误差的简单累积。
特征边可以从几何和属性两个维度定义。几何特征边通常指相邻面二面角超过阈值(如30度到60度)的边;属性特征边包括边界边、UV接缝、法线不连续边、颜色或材质分界边。对CAD类机械模型,棱边通常非常锐利,二面角接近90度,必须严格保留;对扫描或雕刻类模型,特征边阈值可以适当放宽,以避免过度保护影响减面率。理解模型的来源和用途,是后续选择算法和参数的前提。
最直接的改进思路是在边折叠代价函数中引入特征惩罚项,使特征边被折叠的代价显著增大。但这会带来两个问题:惩罚系数如何确定,以及特征边折叠后若确实需要折叠,应该如何生成新顶点以避免突兀过渡。下文会围绕这两个问题展开。
二、保持特征边的简化算法与权重策略
基于QEM的特征加权方法是最常用的基线方案。标准QEM为每条边计算二次误差矩阵Q,折叠代价为v转置乘Q乘v。改进时,可以先检测特征边,若某条边属于特征边,则在折叠代价上乘以一个大于1的系数,例如5到50。更稳定的做法不是简单乘法,而是在特征边两端顶点的二次误差矩阵中额外加入约束平面:对于锐利边,沿边方向的平面约束可以限制新顶点偏离折痕线。这样即使特征边最终被折叠,新顶点也会尽量落在原始折痕附近,变形更小。
半边折叠与顶点对收缩的选择同样影响特征保持效果。若要求保持拓扑边界一致,半边折叠把一条边的起点合并到终点,能减少顶点漂移;但特征边上选择哪个顶点保留也值得注意。常见策略是保留与更多特征边相连的顶点,避免把一个关键拐点合并到普通平面区域。对于特征密集区域,还可以启用禁止折叠标记,在简化初期完全不折叠特征边,等普通边减少到一定程度后再逐步放开。
| 算法或策略 | 特征保持方式 | 优点 | 缺点 |
|---|---|---|---|
| 标准QEM | 不区分特征边 | 实现简单,减面率高 | 锐利边容易变钝 |
| 特征加权QEM | 特征边代价乘以惩罚系数 | 改动小,能保留大部分特征 | 权重过大可能影响减面率 |
| 带约束QEM | 在特征边附近加入平面约束 | 新顶点贴近原始折痕 | 计算更复杂,需要额外矩阵 |
| 半边折叠加特征锁定 | 对特征边禁止折叠或延后折叠 | 轮廓和边界保持好 | 参数敏感,容易过度保护 |
实践中常用特征加权QEM作为基线,因为它改动小、计算开销低,适合快速验证效果。若模型需要高精度保留棱边,则引入特征平面约束;若追求极限减面率,则使用分级阈值动态调整权重,让远距离LOD层级允许更多特征边折叠。
三、特征加权边折叠算法实现
下面用一个Python示例展示特征边检测和折叠代价计算的主要逻辑。示例基于顶点列表和面列表,使用numpy计算面法线,构建边与相邻面的映射。代码包括一个简化器类,初始化时检测特征边,计算每条边的折叠代价,其中特征边代价乘以特征权重。
import numpy as np
class EdgeCollapseSimplifier:
def __init__(self, vertices, faces, feature_angle_deg=30.0, feature_weight=10.0):
self.vertices = vertices
self.faces = faces
self.feature_angle_deg = feature_angle_deg
self.feature_weight = feature_weight
self.edge_cost = {}
self.edge_feature = {}
self.compute_feature_edges()
self.compute_initial_costs()
def build_edge_face_map(self):
# 实际项目中根据顶点索引构建边到相邻面列表的映射
return {}
def face_normal(self, face):
# 实际项目中根据顶点坐标叉积计算面法线
return np.array([0.0, 0.0, 1.0])
def qem_cost(self, edge):
# 简化示意:返回基础二次误差,实际需要累加Q矩阵
return 0.0
def compute_feature_edges(self):
edge_face_map = self.build_edge_face_map()
for edge, adj_faces in edge_face_map.items():
if len(adj_faces) != 2:
self.edge_feature[edge] = True
continue
f1, f2 = adj_faces
n1 = self.face_normal(f1)
n2 = self.face_normal(f2)
angle = np.degrees(np.arccos(np.clip(np.dot(n1, n2), -1.0, 1.0)))
self.edge_feature[edge] = angle > self.feature_angle_deg
def compute_initial_costs(self):
for edge in self.edge_feature:
self.edge_cost[edge] = self.compute_collapse_cost(edge)
def compute_collapse_cost(self, edge):
cost = self.qem_cost(edge)
if self.edge_feature.get(edge, False):
cost *= self.feature_weight
return cost
上面的代码中,compute_feature_edges通过二面角与阈值比较来标记特征边,边界边因只有一个相邻面也直接标记。compute_collapse_cost先调用qem_cost得到基础二次误差,再根据标记乘以惩罚系数。实际工程中,qem_cost需要累加边两个端点关联平面的二次误差矩阵,并在折叠后更新相关边代价,这个更新通常放入优先队列中增量维护。
参数调优方面,feature_angle_deg常用30到45度,机械零件可以降到20度,生物模型可提高到60度。feature_weight常用10到100,过大虽然能减少棱角丢失,但会让平坦区域先被简化殆尽,导致减面率不达标。建议先固定角度阈值,再从小到大调整权重,通过可视化检查特征边的保留比例。还可以加入最大法线变化限制:当折叠前后相邻面法线变化超过90度时直接拒绝该次折叠,这是防止局部翻面的有效手段。
代码中未列出的折叠操作会更新网格拓扑,需要同步维护边面映射和Q矩阵,但核心代价逻辑保持不变。只要在折叠后重新计算受影响边的代价,并将新代价压入优先队列,就能保证每次折叠选择的是当前代价最小的边。
四、算法选择与常见误区
选择算法时,先判断模型类型。CAD或硬表面模型应优先考虑特征加权QEM并配合边界锁定,因为这类模型的棱边本身就是设计意图的一部分,丢失后视觉损失非常明显。扫描或雕塑模型可以使用标准QEM加轻度特征加权,因为表面本身没有大量尖锐折痕,过度保护反而会降低简化效率。游戏LOD则需要分级简化,在远距离LOD中允许特征边折叠,在近处LOD中严格保留,这样可以在不同距离下平衡性能和外观。
常见误区之一是把特征边等同于高曲率边。实际上很多高曲率区域是平滑过渡的曲面,不一定需要作为特征边保留;而一些低曲率但属于材质分界的边,在视觉上却非常重要。另一个误区是过度保护所有特征边,导致平坦区域被过度简化,整体三角形分布不均匀。忽略UV和材质边界同样会导致纹理拉伸或接缝偏移。建议将简化结果与原始模型做Hausdorff距离和法线偏差双重评估,特别关注轮廓边缘和尖锐棱线处的最大偏差,而不是只看平均误差。
没有哪一种算法能绝对避免变形,关键是让简化目标和特征约束匹配。对大多数应用,特征加权QEM配合分级阈值和边界锁定,是成本与效果平衡较好的选择。通过合理检测特征边、设置惩罚权重并在折叠后更新局部误差,可以在保持重要外形特征的同时获得更高的减面率,减少后续人工修复的工作量。