网格简化算法 --- Edge Collapse(边塌缩)
一、算法概述
1.1 算法名称与定位
算法名称:基于二次误差度量(Quadric Error Metric, QEM)的边塌缩(Edge Collapse)网格简化算法
算法类型:迭代式边塌缩网格简化(Iterative Edge Collapse Mesh Simplification)
1.2 算法解决的问题
三维实景建模中,通过摄影测量或三维扫描获取的原始网格模型通常包含数百万甚至上千万个三角面片。高密度网格存在以下问题:
- 实时渲染性能:三维引擎的渲染性能与面数直接相关,面数过多导致帧率下降
- 存储与传输:高密度网格占用大量磁盘空间和网络带宽
- 后续编辑效率:网格编辑操作(补洞、切割、纹理映射等)在面数过多时效率极低
- LOD(Level of Detail)生成:多细节层次显示需要不同精度的模型
1.3 算法核心思想
边塌缩法 的核心思想是:在保持模型几何外观的前提下,通过反复移除一条边(将两个顶点合并为一个顶点),来减少网格的面数和顶点数。
每次塌缩选择"代价最小"的边来执行。代价由**二次误差度量(Quadric Error Metric, QEM)**来衡量------即塌缩后新顶点到原始网格对应区域所有支撑平面的距离平方和。
一句话概括:每次选择一个错误度量最小的边,将其塌缩为一个顶点,重复此过程直到达到目标面数。
二、算法原理与数学公式
2.1 网格拓扑数据结构的建立
算法的第一步是将输入的三角网格(顶点数组 + 面索引数组)转换为包含邻接关系的全拓扑结构。
顶点
每个顶点除了存储坐标 (x, y, z) 外,还存储:
- 关联三角形集合:所有以该顶点为端点的三角形
- 属性列表 :纹理坐标
(u, v)等附加属性 - 受保护标记:边界顶点自动受保护,防止塌缩
边
每条边存储:
- 两个端点指针(排序保证唯一性,避免 (A,B) 和 (B,A) 被视为两条不同的边)
- 共享三角形集合 :所有包含这条边的三角形。集合大小为 1 的边是边界边,受保护
- 误差度量值:塌缩代价,决定了边在优先队列中的位置
- 最优塌缩位置:缓存塌缩后新顶点的最佳坐标
- 最大法线偏差:塌缩后三角面法线的最大偏移量
三角形
每个三角形存储:
- 三个顶点指针(排序保证唯一性,避免同一三角形被重复存储)
- 三条边指针
- 平面方程系数 :
ax + by + cz + d = 0
有序队列
所有边的指针按误差度量值升序存储在有序集合中。使用有序集合代替堆的原因是:边塌缩后,受影响边的误差度量需要动态更新(删除旧值再插入新值),有序集合支持高效的此类操作。
2.2 误差度量数学模型
点到平面的距离
给定一个平面方程 ax+by+cz+d=0ax + by + cz + d = 0ax+by+cz+d=0(系数满足 a2+b2+c2=1a^2 + b^2 + c^2 = 1a2+b2+c2=1),点 v=(x,y,z)T\mathbf{v} = (x, y, z)^Tv=(x,y,z)T 到该平面的有符号距离为:
d(v)=ax+by+cz+d=nTv+dd(\mathbf{v}) = ax + by + cz + d = \mathbf{n}^T\mathbf{v} + dd(v)=ax+by+cz+d=nTv+d
其中 n=(a,b,c)T\mathbf{n} = (a, b, c)^Tn=(a,b,c)T 是单位法向量。
二次误差度量(QEM)
对于顶点 v\mathbf{v}v,它关联了一组三角形 T\mathcal{T}T。该顶点到这些三角形平面的距离平方和为:
Q(v)=∑T∈TdT(v)2=∑T∈T(nTTv+dT)2Q(\mathbf{v}) = \sum_{T \in \mathcal{T}} d_T(\mathbf{v})^2 = \sum_{T \in \mathcal{T}} (\mathbf{n}_T^T\mathbf{v} + d_T)^2Q(v)=T∈T∑dT(v)2=T∈T∑(nTTv+dT)2
展开成二次型:
Q(v)=vTAv+2bTv+cQ(\mathbf{v}) = \mathbf{v}^T A\mathbf{v} + 2\mathbf{b}^T\mathbf{v} + cQ(v)=vTAv+2bTv+c
其中:
A=∑T∈TnTnTTA = \sum_{T \in \mathcal{T}} \mathbf{n}_T\mathbf{n}_T^TA=T∈T∑nTnTT
b=∑T∈TdTnT\mathbf{b} = \sum_{T \in \mathcal{T}} d_T\mathbf{n}_Tb=T∈T∑dTnT
c=∑T∈TdT2c = \sum_{T \in \mathcal{T}} d_T^2c=T∈T∑dT2
边塌缩代价
对于边 e=(v1,v2)e = (v_1, v_2)e=(v1,v2),塌缩后的新顶点为 vnew\mathbf{v}_{new}vnew,其代价定义为新顶点到两个端点所有关联三角形平面的平均距离:
Cost(e)=1∣Tadj∣∑T∈Tadj∣nTTvnew+dT∣Cost(e) = \frac{1}{|\mathcal{T}{adj}|} \sum{T \in \mathcal{T}_{adj}} |\mathbf{n}T^T\mathbf{v}{new} + d_T|Cost(e)=∣Tadj∣1T∈Tadj∑∣nTTvnew+dT∣
其中 Tadj\mathcal{T}_{adj}Tadj 是 v1v_1v1 和 v2v_2v2 关联的三角形的并集。
法线偏差约束
为防止塌缩造成表面严重变形,引入法线偏差约束。对于三角面 TTT,塌缩后新法线 nTnew\mathbf{n}_T^{new}nTnew 与原始法线 nT\mathbf{n}_TnT 的偏差为:
Ndev(T)=1−nT⋅nTnewN_{dev}(T) = 1 - \mathbf{n}_T \cdot \mathbf{n}_T^{new}Ndev(T)=1−nT⋅nTnew
边的最大法线偏差为所有受影响三角形中偏差的最大值:
Ndev(e)=maxT∈TadjNdev(T)N_{dev}(e) = \max_{T \in \mathcal{T}{adj}} N{dev}(T)Ndev(e)=T∈TadjmaxNdev(T)
约束规则:
- 若 Ndev(e)≤1.0N_{dev}(e) \leq 1.0Ndev(e)≤1.0 且边不是边界邻接边:使用正常 QEM 代价
- 否则:代价设为无穷大,永不塌缩
2.3 最优塌缩点位置
有两种策略确定塌缩后新顶点的位置:
策略一:中点插值(默认)
vnew=v1+v22\mathbf{v}_{new} = \frac{\mathbf{v}_1 + \mathbf{v}_2}{2}vnew=2v1+v2
纹理坐标同样线性插值:
(unew,vnew)=(u1+u22,v1+v22)(u_{new}, v_{new}) = \left(\frac{u_1 + u_2}{2}, \frac{v_1 + v_2}{2}\right)(unew,vnew)=(2u1+u2,2v1+v2)
策略二:基于边长的简化模式
误差度量直接使用边的长度:
Cost(e)=∥v1−v2∥2Cost(e) = \|\mathbf{v}_1 - \mathbf{v}_2\|_2Cost(e)=∥v1−v2∥2
此模式下简化速度更快,但质量略低。
三、完整算法流程
3.1 总体流程
输入: 三角网格 (顶点V[], 面F[], 纹理坐标T[])
参数: 简化比例 ratio (保留面数的比例)
Step 1: 数据初始化
├── 创建顶点对象列表(复制坐标和纹理坐标)
└── 为每个顶点分配索引
Step 2: 拓扑构建
├── 遍历所有面 F[i]
├── 为每个三角形创建 Triangle 对象
│ ├── 建立三角形 → 三个顶点的关联
│ └── 建立三角形 → 三条边的关联
├── 为每条边创建 Edge 对象(自动去重)
├── 建立顶点 → 关联三角形的映射
└── 计算每个三角形的平面方程系数
Step 3: 初始误差计算
├── 遍历所有边
├── 计算最优塌缩位置
├── 计算法线偏差
└── 计算误差度量并加入优先队列
Step 4: 迭代塌缩主循环
while (当前三角形数 > 初始面数 × ratio 且 优先队列非空) {
├── 从优先队列中取出代价最小的边
├── if (代价为无穷大) 跳出循环
├── 执行边塌缩:
│ ├── 保存塌缩边两个端点的关联三角形
│ ├── 移除所有受影响的三角形
│ ├── 将两个端点替换为新顶点,重新创建三角形
│ └── 更新受影响边的误差度量(重新入队)
└── 继续循环
}
Step 5: 输出结果
├── 遍历所有剩余顶点,写入顶点数组
├── 遍历所有剩余三角形,写入面索引数组
└── 更新纹理坐标数组
3.2 单次边塌缩的详细步骤
塌缩前: 塌缩后:
v3 v3
\ / \
\ T2 / \
e2 / v2 \ e3 / \
/ \ / \
/ \ / \
T1 T3 / 新顶点 \
/ \ / pNew \
/ \ / \
v1 / e4 / v3 / e5 / v4 v1 / e4 / v3 / e5 / v4
步骤:
1) 识别受影响的三角形:
- 关联到 v1 但不共享被塌缩边的三角形 → group_A
- 关联到 v2 但不共享被塌缩边的三角形 → group_B
- 共享被塌缩边的两个三角形 → group_C
2) 移除所有受影响三角形及其邻接关系:
- 从三角形集合移除 group_A, group_B, group_C
- 将这些三角形从关联的点和边的引用中移除
3) 创建新三角形:
- 对 group_A 中的每个三角形:
用 pNew 替换 v1,创建新三角形
- 对 group_B 中的每个三角形:
用 pNew 替换 v2,创建新三角形
- 新三角形自动创建新边,或找到已存在的边复用
4) 更新误差度量:
- 收集所有以 pNew 为端点的边及其邻域边
- 重新计算这些边的代价
- 更新它们在优先队列中的位置
3.3 边的唯一性保证
通过对两个端点指针排序来保证同一条边只有一种表示:
创建边(A,B)时:
if (指针(A) < 指针(B)) p1=A, p2=B
else p1=B, p2=A
在优先队列中查找(p1,p2)是否已存在
若存在 → 复用已有边对象
若不存在 → 插入新边
3.4 三角形的唯一性保证
对三角形的三个顶点指针排序,保证同一三角形只有一种表示:
创建三角形(v0,v1,v2)时:
找出三个顶点中"最小"的指针作为 p1
顺时针顺序确定 p2 和 p3
使用有序集合存储,自动去重
四、纹理坐标处理
4.1 作为顶点属性保存
纹理坐标 (u, v) 作为顶点的属性值存储。对每个顶点,在属性数组中依次压入 u 和 v。
4.2 塌缩时线性插值
塌缩时,新顶点的纹理坐标通过对两个原顶点的纹理坐标线性插值得到:
unew=(1−r)⋅u1+r⋅u2u_{new} = (1-r) \cdot u_1 + r \cdot u_2unew=(1−r)⋅u1+r⋅u2
vnew=(1−r)⋅v1+r⋅v2v_{new} = (1-r) \cdot v_1 + r \cdot v_2vnew=(1−r)⋅v1+r⋅v2
其中默认 r=0.5r = 0.5r=0.5(取中点)。
4.3 结果输出
简化完成后,从各个顶点的属性数组中取出插值后的 (u, v) 值,写回三角网的纹理坐标数组。
五、边界保护机制
5.1 边界识别
- 边界边:只被一个三角形共享的边
- 边界点:只要关联的三角形中有边界边,即为边界点(或显式标记受保护的顶点)
5.2 保护策略
- 对每条边计算法线偏差约束
- 如果边的最大法线偏差超过阈值 或边邻接边界,则将其代价设为无穷大
- 代价为无穷大的边在优先队列中处于末尾,永远不会被最早选择塌缩
- 只有当所有非边界边都被塌缩后,边界边才有可能被处理
六、并行加速
对于带有多个子三角网的复合网格,每个子网的简化是相互独立的,可以采用多线程并行:
对每个需要简化的子三角网:
创建独立的 EdgeCollapse 实例
独立执行从初始化到塌缩再到输出的全过程
[多线程无竞争,无需加锁]
并行粒度:每个子三角网为一个任务。对于包含大量子网的复合网格(如每个瓦块有多个纹理三角网),可充分利用多核 CPU。
七、应用场景
7.1 LOD 多细节层次生成
| LOD级别 | 保留面数比例 | 应用场景 |
|---|---|---|
| LOD0(原始) | 100% | 近景交互编辑 |
| LOD1(轻度简化) | 50% | 中近距离浏览 |
| LOD2(中度简化) | 25% | 远距离概览 |
| LOD3(重度简化) | 10% | 全局鸟瞰 |
7.2 模型编辑前预处理
在执行补洞、切割、纹理映射等操作前,先简化高密度网格,可显著提升编辑效率。
7.3 移动端/Web端适配
将桌面端的高精度模型简化后发布到移动端或Web端。
7.4 实时渲染优化
根据物体在屏幕上的投影大小,动态选择不同LOD级别的模型进行渲染。
7.5 存储和传输优化
将模型从数百MB简化至数MB,大幅节省存储空间。
九、算法优缺点
优点
- 高保真度:QEM 误差度量保证了几何精度
- 纹理保持:支持纹理坐标插值
- 边界保护:自动保护网格边界
- 法线约束:防止表面严重变形
- 精确控制:通过 ratio 参数精确控制简化面数
缺点
- 计算量大:每次塌缩需更新邻域边的代价
- 不保证拓扑保持:极端简化可能导致自相交
- 单串行依赖:单个三角网的塌缩无法并行化
- 对噪声敏感:原始网格噪声大时结果可能不理想
十一、数学公式汇总
-
点到平面距离:dT(v)=nTTv+dTd_T(\mathbf{v}) = \mathbf{n}_T^T\mathbf{v} + d_TdT(v)=nTTv+dT
-
二次误差:Q(v)=∑TdT(v)2Q(\mathbf{v}) = \sum_T d_T(\mathbf{v})^2Q(v)=∑TdT(v)2
-
边塌缩代价:Cost(e)=1∣T∣∑T∣dT(vnew)∣Cost(e) = \frac{1}{|\mathcal{T}|}\sum_T |d_T(\mathbf{v}_{new})|Cost(e)=∣T∣1∑T∣dT(vnew)∣
-
法线偏差:Ndev(T)=1−nT⋅nTnewN_{dev}(T) = 1 - \mathbf{n}_T \cdot \mathbf{n}_T^{new}Ndev(T)=1−nT⋅nTnew
-
最大法线偏差:Ndev(e)=maxTNdev(T)N_{dev}(e) = \max_T N_{dev}(T)Ndev(e)=maxTNdev(T)
-
纹理插值:(unew,vnew)=(u1+u22,v1+v22)(u_{new}, v_{new}) = \left(\frac{u_1+u_2}{2}, \frac{v_1+v_2}{2}\right)(unew,vnew)=(2u1+u2,2v1+v2)
-
目标面数:Ftarget=Foriginal×ratioF_{target} = F_{original} \times ratioFtarget=Foriginal×ratio