14.1 网格处理管道
网格处理管道建立在 Surface_mesh 数据结构之上,与 各向同性重网格化 密切相关。
0. 动机:为什么需要网格处理管道?
现实问题引入
想象你是一位3D打印工程师,收到了客户发来的模型文件:
- 模型有50万个面,打印需要100小时
- 表面有许多小孔洞,无法打印
- 三角形质量差,有狭长的面片
- 需要分成多个部分打印
或者你是一位游戏美术师:
- 高模雕刻细节丰富,但无法在实时引擎中运行
- 需要生成LOD(多细节层次)模型
- 需要修复拓扑问题以便动画
传统方法的局限
手动处理:
- 耗时耗力,容易出错
- 难以保持一致性
- 无法处理大规模数据
单一工具:
- 只能解决特定问题
- 缺乏流程整合
- 难以自动化
管道化的优势
系统化:从输入到输出的完整流程 自动化:减少人工干预 可重复:相同输入得到相同输出 模块化:每个阶段可独立优化
如果不使用管道
- 质量不稳定:每次处理结果不同
- 效率低下:重复劳动,浪费时间
- 错误累积:问题在下游放大
- 难以维护:流程混乱,无法追踪
1. 直观理解:工厂流水线
1.1 “网格处理就像汽车装配”
想象一个汽车装配流水线:
阶段1:原材料检验
- 检查零件是否合格
- 剔除损坏的部件
- 分类整理
阶段2:粗加工
- 切割大体形状
- 去除多余材料
- 初步成型
阶段3:精加工
- 打磨表面
- 添加细节
- 质量检查
阶段4:装配
- 组装部件
- 涂装
- 最终检验
网格处理管道的对应:
网格处理管道:
输入网格 简化/清理 修复/优化 细分/增强 输出网格
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│原始 │ → │粗模 │ → │净模 │ → │精模 │ → │最终 │
│数据 │ │简化 │ │修复 │ │增强 │ │产品 │
└─────┘ └─────┘ └─────┘ └─────┘ └─────┘
│ │ │ │ │
└─ 检测问题 └─ 减少复杂度 └─ 保证质量 └─ 提升细节 └─ 交付
标记缺陷 保留特征 修复孔洞 添加细节 质量检验
1.2 “管道就像医院的体检流程”
挂号登记(输入):
- 记录基本信息
- 分配体检单
基础检查(清理):
- 身高体重
- 血压心率
- 剔除明显异常
详细检查(修复):
- 血液化验
- 影像检查
- 发现问题并治疗
康复增强(优化):
- 营养补充
- 运动指导
- 定期复查
健康档案(输出):
- 完整报告
- 后续建议
1.3 “数据在管道中的旅程”
数据流可视化:
输入: 原始扫描网格
│
│ ┌─────────────────────────────────────┐
│ │ 阶段1: 简化与清理 │
│ │ • 移除重复顶点 │
│ │ • 简化密集区域 │
│ │ • 删除退化三角形 │
│ └─────────────────────────────────────┘
▼
顶点: 100,000 → 25,000
面片: 200,000 → 50,000
│
│ ┌─────────────────────────────────────┐
│ │ 阶段2: 修复与优化 │
│ │ • 填充孔洞 │
│ │ • 修复非流形边 │
│ │ • 平滑噪声 │
│ └─────────────────────────────────────┘
▼
孔洞: 15 → 0
非流形边: 23 → 0
质量最差角度: 1.2° → 15°
│
│ ┌─────────────────────────────────────┐
│ │ 阶段3: 细分与增强 │
│ │ • 自适应细分 │
│ │ • 特征保持 │
│ │ • 纹理坐标生成 │
│ └─────────────────────────────────────┘
▼
面片: 50,000 → 120,000
纹理坐标: 已生成
UV展开: 完成
│
│ ┌─────────────────────────────────────┐
│ │ 阶段4: 质量检验与输出 │
│ │ • 几何检验 │
│ │ • 拓扑检验 │
│ │ • 格式转换 │
│ └─────────────────────────────────────┘
▼
输出: 可打印/可用的网格
2. 网格处理管道架构
2.1 标准四阶段管道
┌─────────────────────────────────────────────────────────────────┐
│ 网格处理管道 │
├─────────────┬─────────────┬─────────────┬───────────────────────┤
│ 阶段1 │ 阶段2 │ 阶段3 │ 阶段4 │
│ 简化与清理 │ 修复与优化 │ 细分与增强 │ 检验与输出 │
├─────────────┼─────────────┼─────────────┼───────────────────────┤
│ • 去重 │ • 孔洞填充 │ • 自适应细分│ • 几何检验 │
│ • 简化 │ • 边缝合 │ • 特征增强 │ • 拓扑检验 │
│ • 降噪 │ • 平滑 │ • 纹理生成 │ • 格式导出 │
│ • 裁剪 │ • 重网格化 │ • LOD生成 │ • 元数据记录 │
└─────────────┴─────────────┴─────────────┴───────────────────────┘
2.2 各阶段详细说明
阶段1:简化与清理(Simplification & Cleaning)
目标:减少数据量,移除明显错误
主要操作:
- 顶点去重:合并距离小于阈值的顶点
- 面片简化:使用QEM算法减少面片数
- 孤立组件移除:删除小连通分量
- 退化面片删除:移除面积接近零的三角形
输入输出示例:
输入: 原始扫描数据
顶点: 500,000
面片: 1,000,000
问题: 大量重复点, 孤立噪声
处理:
1. 去重: 0.01mm阈值
2. 简化: 保留50%面片
3. 清理: 移除面积<1e-6的面片
输出: 清理后数据
顶点: 120,000
面片: 240,000
改善: 无重复, 无孤立点
阶段2:修复与优化(Repair & Optimization)
目标:修复几何和拓扑问题,提升质量
主要操作:
- 孔洞填充:检测并填充边界环
- 非流形修复:拆分非流形边和顶点
- 平滑去噪:保持特征的同时平滑噪声
- 重网格化:改善三角形质量
输入输出示例:
输入: 清理后数据
孔洞: 12个
非流形边: 5条
非流形顶点: 2个
最小角: 0.5°
处理:
1. 填充孔洞: 使用相邻曲率
2. 修复非流形: 拆分顶点
3. 平滑: 10次迭代, 保持特征
4. 重网格化: 目标边长2.0mm
输出: 修复后数据
孔洞: 0个
非流形: 0
最小角: 18°
阶段3:细分与增强(Refinement & Enhancement)
目标:增加细节,准备最终输出
主要操作:
- 自适应细分:在曲率高处增加面片
- 特征增强:锐化边缘和角点
- 纹理坐标生成:UV展开和打包
- LOD生成:创建多细节层次
输入输出示例:
输入: 修复后数据
面片: 240,000
曲率变化: 平缓
无纹理坐标
处理:
1. 自适应细分: 曲率阈值0.1
2. 特征增强: 锐化角度>60°的边
3. UV展开: 最小化扭曲
4. LOD生成: 高(100%), 中(50%), 低(25%)
输出: 增强后数据
面片: 450,000
纹理坐标: 已生成
LOD: 3个级别
阶段4:检验与输出(Validation & Export)
目标:确保质量,转换为所需格式
主要操作:
- 几何检验:检查自相交、法向一致性
- 拓扑检验:验证流形性、连通性
- 格式导出:STL、OBJ、PLY等
- 元数据记录:记录处理参数和统计
3. CGAL 实现详解
3.1 核心组件
CGAL 提供了管道各阶段所需的算法:
CGAL 网格处理管道组件:
简化与清理
├── Polygon_mesh_processing::remove_duplicate_points
├── Polygon_mesh_processing::remove_isolated_vertices
├── Surface_mesh_simplification::Edge_collapse_visitor
└── Polygon_mesh_processing::repair_polygon_soup
修复与优化
├── Polygon_mesh_processing::hole_filling
├── Polygon_mesh_processing::smooth_mesh
├── Polygon_mesh_processing::remesh
└── Polygon_mesh_processing::experimental::remove_self_intersections
细分与增强
├── Subdivision_method_3::CatmullClark_subdivision
├── Subdivision_method_3::Loop_subdivision
├── Polygon_mesh_processing::refine
└── Surface_mesh_parameterization 包
检验与输出
├── Polygon_mesh_processing::does_self_intersect
├── Polygon_mesh_processing::is_outward_oriented
└── CGAL::IO::write_OBJ/STL/OFF
3.2 完整管道代码示例
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Polygon_mesh_processing.h>
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
#include <CGAL/IO/OBJ.h>
#include <CGAL/IO/STL.h>
#include <iostream>
#include <vector>
#include <string>
namespace PMP = CGAL::Polygon_mesh_processing;
namespace SMS = CGAL::Surface_mesh_simplification;
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef CGAL::Surface_mesh<Point> Mesh;
// 网格统计信息
struct MeshStats {
size_t vertices;
size_t faces;
size_t edges;
size_t boundaries;
double min_angle;
double max_angle;
double avg_quality;
bool is_manifold;
bool is_closed;
bool has_self_intersections;
};
MeshStats analyze_mesh(const Mesh& mesh) {
MeshStats stats;
stats.vertices = mesh.number_of_vertices();
stats.faces = mesh.number_of_faces();
stats.edges = mesh.number_of_edges();
// 计算边界数
stats.boundaries = 0;
for (auto e : mesh.edges()) {
if (mesh.is_border(e)) stats.boundaries++;
}
// 计算角度统计
stats.min_angle = 180.0;
stats.max_angle = 0.0;
double total_quality = 0.0;
for (auto f : mesh.faces()) {
auto he = mesh.halfedge(f);
auto p0 = mesh.point(mesh.source(he));
auto p1 = mesh.point(mesh.target(he));
auto p2 = mesh.point(mesh.target(mesh.next(he)));
// 计算三个角
for (int i = 0; i < 3; ++i) {
auto v1 = p1 - p0;
auto v2 = p2 - p0;
double angle = std::acos(v1 * v2 / (std::sqrt(v1.squared_length()) *
std::sqrt(v2.squared_length()))) * 180.0 / M_PI;
stats.min_angle = std::min(stats.min_angle, angle);
stats.max_angle = std::max(stats.max_angle, angle);
// 质量度量:等边三角形为1,退化时为0
double quality = std::min({angle, 180.0 - angle}) / 60.0;
total_quality += quality;
// 轮换顶点
std::swap(p0, p1);
std::swap(p1, p2);
}
}
stats.avg_quality = total_quality / (stats.faces * 3);
stats.is_manifold = PMP::is_manifold(mesh);
stats.is_closed = PMP::is_closed(mesh);
stats.has_self_intersections = PMP::does_self_intersect(mesh);
return stats;
}
void print_stats(const std::string& stage, const MeshStats& stats) {
std::cout << "\n=== " << stage << " ===" << std::endl;
std::cout << "Vertices: " << stats.vertices << std::endl;
std::cout << "Faces: " << stats.faces << std::endl;
std::cout << "Edges: " << stats.edges << std::endl;
std::cout << "Boundary edges: " << stats.boundaries << std::endl;
std::cout << "Min angle: " << stats.min_angle << "°" << std::endl;
std::cout << "Max angle: " << stats.max_angle << "°" << std::endl;
std::cout << "Avg quality: " << stats.avg_quality << std::endl;
std::cout << "Manifold: " << (stats.is_manifold ? "yes" : "no") << std::endl;
std::cout << "Closed: " << (stats.is_closed ? "yes" : "no") << std::endl;
std::cout << "Self-intersections: " << (stats.has_self_intersections ? "yes" : "no") << std::endl;
}
// 阶段1: 简化与清理
void stage1_simplify_and_clean(Mesh& mesh, double target_edge_length) {
std::cout << "\n[Stage 1] Simplification and Cleaning..." << std::endl;
// 1.1 移除孤立顶点
size_t removed = PMP::remove_isolated_vertices(mesh);
std::cout << " Removed " << removed << " isolated vertices" << std::endl;
// 1.2 边折叠简化
SMS::Count_stop_predicate<Mesh> stop(mesh.number_of_edges() / 2);
SMS::edge_collapse(mesh, stop,
CGAL::parameters::get_cost(SMS::Edge_length_cost<Mesh>())
.get_placement(SMS::Midpoint_placement<Mesh>())
);
std::cout << " Simplified to " << mesh.number_of_edges() << " edges" << std::endl;
// 1.3 清理退化面片
std::vector<Mesh::Face_index> degenerate;
for (auto f : mesh.faces()) {
auto he = mesh.halfedge(f);
auto p0 = mesh.point(mesh.source(he));
auto p1 = mesh.point(mesh.target(he));
auto p2 = mesh.point(mesh.target(mesh.next(he)));
if (CGAL::collinear(p0, p1, p2)) {
degenerate.push_back(f);
}
}
for (auto f : degenerate) {
mesh.remove_face(f);
}
mesh.collect_garbage();
std::cout << " Removed " << degenerate.size() << " degenerate faces" << std::endl;
}
// 阶段2: 修复与优化
void stage2_repair_and_optimize(Mesh& mesh, int smoothing_iterations) {
std::cout << "\n[Stage 2] Repair and Optimization..." << std::endl;
// 2.1 填充孔洞
std::vector<Mesh::Halfedge_index> border_cycles;
PMP::extract_boundary_cycles(mesh, std::back_inserter(border_cycles));
std::cout << " Found " << border_cycles.size() << " boundary cycles" << std::endl;
for (auto h : border_cycles) {
std::vector<Mesh::Face_index> patch;
PMP::triangulate_hole(mesh, h, std::back_inserter(patch));
}
std::cout << " Filled holes" << std::endl;
// 2.2 平滑(保持特征)
PMP::smooth_mesh(mesh, smoothing_iterations,
CGAL::parameters::number_of_iterations(smoothing_iterations)
.use_area_smoothing(true)
.use_angle_smoothing(true)
.use_Delaunay_flips(true)
);
std::cout << " Smoothed mesh (" << smoothing_iterations << " iterations)" << std::endl;
// 2.3 重网格化
PMP::isotropic_remeshing(
mesh.faces(),
2.0, // 目标边长
mesh,
CGAL::parameters::number_of_iterations(3)
.protect_constraints(true)
);
std::cout << " Remeshed" << std::endl;
}
// 阶段3: 细分与增强
void stage3_refine_and_enhance(Mesh& mesh, int subdivision_steps) {
std::cout << "\n[Stage 3] Refinement and Enhancement..." << std::endl;
// 3.1 自适应细分(基于曲率)
// 这里使用简单的Loop细分作为示例
// 实际应用中可以根据曲率自适应细分
if (subdivision_steps > 0) {
CGAL::Subdivision_method_3::Loop_subdivision(mesh, subdivision_steps);
std::cout << " Subdivided (" << subdivision_steps << " steps)" << std::endl;
}
// 3.2 计算法向
mesh.add_property_map<Mesh::Vertex_index, Kernel::Vector_3>("v:normal");
PMP::compute_vertex_normals(mesh,
CGAL::parameters::vertex_normal_map(mesh.property_map<Mesh::Vertex_index, Kernel::Vector_3>("v:normal").first)
);
std::cout << " Computed vertex normals" << std::endl;
}
// 阶段4: 检验与输出
bool stage4_validate_and_export(const Mesh& mesh, const std::string& output_prefix) {
std::cout << "\n[Stage 4] Validation and Export..." << std::endl;
bool success = true;
// 4.1 几何检验
if (PMP::does_self_intersect(mesh)) {
std::cerr << " WARNING: Mesh has self-intersections\!" << std::endl;
success = false;
} else {
std::cout << " ✓ No self-intersections" << std::endl;
}
// 4.2 拓扑检验
if (\!PMP::is_manifold(mesh)) {
std::cerr << " WARNING: Mesh is not manifold\!" << std::endl;
success = false;
} else {
std::cout << " ✓ Mesh is manifold" << std::endl;
}
// 4.3 法向一致性
if (\!PMP::is_outward_oriented(mesh)) {
std::cout << " Reorienting faces..." << std::endl;
// 注意:这里需要非const引用,实际实现中可能需要拷贝
} else {
std::cout << " ✓ Faces outward oriented" << std::endl;
}
// 4.4 导出多种格式
std::string obj_file = output_prefix + ".obj";
std::string stl_file = output_prefix + ".stl";
std::string off_file = output_prefix + ".off";
CGAL::IO::write_OBJ(obj_file, mesh);
std::cout << " Exported: " << obj_file << std::endl;
CGAL::IO::write_STL(stl_file, mesh);
std::cout << " Exported: " << stl_file << std::endl;
CGAL::IO::write_OFF(off_file, mesh);
std::cout << " Exported: " << off_file << std::endl;
return success;
}
// 主处理流程
int main(int argc, char* argv[])
{
// 参数解析
std::string input_file = (argc > 1) ? argv[1] : "input.obj";
std::string output_prefix = (argc > 2) ? argv[2] : "output";
// 配置参数
double target_edge_length = (argc > 3) ? std::stod(argv[3]) : 2.0;
int smoothing_iterations = (argc > 4) ? std::stoi(argv[4]) : 5;
int subdivision_steps = (argc > 5) ? std::stoi(argv[5]) : 1;
std::cout << "========================================" << std::endl;
std::cout << "CGAL Mesh Processing Pipeline" << std::endl;
std::cout << "========================================" << std::endl;
std::cout << "Input: " << input_file << std::endl;
std::cout << "Output prefix: " << output_prefix << std::endl;
std::cout << "Target edge length: " << target_edge_length << std::endl;
std::cout << "Smoothing iterations: " << smoothing_iterations << std::endl;
std::cout << "Subdivision steps: " << subdivision_steps << std::endl;
// 读取输入
Mesh mesh;
if (\!CGAL::IO::read_OBJ(input_file, mesh)) {
std::cerr << "Failed to read input file: " << input_file << std::endl;
return 1;
}
std::cout << "\nInput mesh loaded successfully" << std::endl;
// 初始分析
MeshStats stats_initial = analyze_mesh(mesh);
print_stats("Initial Mesh", stats_initial);
// 阶段1: 简化与清理
stage1_simplify_and_clean(mesh, target_edge_length);
MeshStats stats_stage1 = analyze_mesh(mesh);
print_stats("After Stage 1", stats_stage1);
// 阶段2: 修复与优化
stage2_repair_and_optimize(mesh, smoothing_iterations);
MeshStats stats_stage2 = analyze_mesh(mesh);
print_stats("After Stage 2", stats_stage2);
// 阶段3: 细分与增强
stage3_refine_and_enhance(mesh, subdivision_steps);
MeshStats stats_stage3 = analyze_mesh(mesh);
print_stats("After Stage 3", stats_stage3);
// 阶段4: 检验与输出
bool success = stage4_validate_and_export(mesh, output_prefix);
MeshStats stats_final = analyze_mesh(mesh);
print_stats("Final Mesh", stats_final);
// 总结
std::cout << "\n========================================" << std::endl;
std::cout << "Processing Summary" << std::endl;
std::cout << "========================================" << std::endl;
std::cout << "Vertices: " << stats_initial.vertices << " → " << stats_final.vertices << std::endl;
std::cout << "Faces: " << stats_initial.faces << " → " << stats_final.faces << std::endl;
std::cout << "Min angle: " << stats_initial.min_angle << "° → " << stats_final.min_angle << "°" << std::endl;
std::cout << "Avg quality: " << stats_initial.avg_quality << " → " << stats_final.avg_quality << std::endl;
std::cout << "Status: " << (success ? "SUCCESS" : "WARNING") << std::endl;
return success ? 0 : 1;
}4. 实际应用案例
4.1 案例一:3D打印准备
场景:将扫描得到的文物模型准备用于3D打印。
需求:
- 封闭无孔洞
- 无自相交
- 合理的面片数(<100万)
- 良好的三角形质量
管道配置:
// 3D打印优化管道
void prepare_for_3d_printing(const std::string& input, const std::string& output) {
Mesh mesh;
CGAL::IO::read_OBJ(input, mesh);
// 阶段1: 简化到50万面以下
if (mesh.number_of_faces() > 500000) {
SMS::Count_stop_predicate<Mesh> stop(500000);
SMS::edge_collapse(mesh, stop);
}
// 阶段2: 必须封闭
if (\!PMP::is_closed(mesh)) {
PMP::triangulate_hole(mesh, ...); // 填充所有孔洞
}
// 修复自相交
if (PMP::does_self_intersect(mesh)) {
PMP::experimental::remove_self_intersections(mesh);
}
// 阶段3: 轻微细分保证平滑
CGAL::Subdivision_method_3::Loop_subdivision(mesh, 1);
// 阶段4: 导出STL
CGAL::IO::write_STL(output, mesh);
}4.2 案例二:游戏LOD生成
场景:为游戏模型生成多细节层次。
管道配置:
// LOD生成管道
void generate_lods(const std::string& input, const std::string& output_prefix) {
Mesh mesh;
CGAL::IO::read_OBJ(input, mesh);
// LOD 0: 高细节 (100%)
CGAL::IO::write_OBJ(output_prefix + "_lod0.obj", mesh);
// LOD 1: 中细节 (50%)
Mesh lod1 = mesh;
SMS::Count_stop_predicate<Mesh> stop1(mesh.number_of_edges() * 0.5);
SMS::edge_collapse(lod1, stop1);
CGAL::IO::write_OBJ(output_prefix + "_lod1.obj", lod1);
// LOD 2: 低细节 (25%)
Mesh lod2 = mesh;
SMS::Count_stop_predicate<Mesh> stop2(mesh.number_of_edges() * 0.25);
SMS::edge_collapse(lod2, stop2);
CGAL::IO::write_OBJ(output_prefix + "_lod2.obj", lod2);
// LOD 3: 极低细节 (10%)
Mesh lod3 = mesh;
SMS::Count_stop_predicate<Mesh> stop3(mesh.number_of_edges() * 0.10);
SMS::edge_collapse(lod3, stop3);
CGAL::IO::write_OBJ(output_prefix + "_lod3.obj", lod3);
}4.3 案例三:医学图像处理
场景:从CT扫描重建骨骼模型并优化。
管道配置:
// 医学模型处理管道
void process_medical_model(const std::string& input, const std::string& output) {
Mesh mesh;
CGAL::IO::read_OFF(input, mesh); // 从MC算法读取
// 阶段1: 去除MC算法产生的噪声
PMP::smooth_mesh(mesh, 10); // 多次平滑
// 阶段2: 修复拓扑(骨骼应该封闭)
while (\!PMP::is_closed(mesh)) {
PMP::triangulate_hole(mesh, ...);
}
// 阶段3: 保持特征的细化
PMP::isotropic_remeshing(mesh.faces(), 0.5, mesh,
CGAL::parameters::protect_constraints(true) // 保护解剖特征
);
// 阶段4: 导出为医学格式
CGAL::IO::write_STL(output, mesh);
}5. 参数调优指南
5.1 根据应用场景选择参数
| 应用场景 | 阶段1 | 阶段2 | 阶段3 | 阶段4 |
|---|---|---|---|---|
| 3D打印 | 简化到<100万面 | 必须封闭 | 轻微细分 | STL格式 |
| 游戏LOD | 激进简化 | 保持特征 | 不细分 | 多级别输出 |
| 可视化 | 适度简化 | 平滑噪声 | 纹理坐标 | OBJ格式 |
| 仿真分析 | 保持细节 | 高质量网格 | 各向同性 | 导出CFD格式 |
| 存档 | 不简化 | 修复为主 | 不细分 | 多格式备份 |
5.2 质量检查清单
处理前检查:
□ 文件格式是否正确
□ 坐标系统是否已知
□ 是否有纹理/颜色信息需要保留
□ 目标应用场景
处理中检查:
□ 每阶段后验证拓扑
□ 监控面片数量变化
□ 检查特征保留情况
□ 内存使用情况
处理后检查:
□ 无自相交
□ 法向一致
□ 无退化面片
□ 目标格式验证
6. 故障排除
6.1 常见问题与解决
问题1:简化后特征丢失
- 原因:简化算法未保护特征边
- 解决:使用
protect_constraints(true)选项
问题2:孔洞填充失败
- 原因:孔洞太大或形状复杂
- 解决:分多次填充,或手动干预
问题3:平滑后变形
- 原因:平滑迭代次数过多
- 解决:减少迭代次数,或使用特征保持平滑
问题4:导出后文件损坏
- 原因:非流形几何
- 解决:确保所有阶段后都检查流形性
参考文献
-
Botsch, M., Kobbelt, L., Pauly, M., Alliez, P., & Lévy, B. (2010). Polygon Mesh Processing. CRC Press.
-
CGAL Documentation: Polygon Mesh Processing. https://doc.cgal.org/latest/Polygon_mesh_processing/
-
CGAL Documentation: Surface Mesh Simplification. https://doc.cgal.org/latest/Surface_mesh_simplification/
-
Garland, M., & Heckbert, P. S. (1997). Surface simplification using quadric error metrics. In Proceedings of SIGGRAPH (pp. 209-216).