本章详细介绍 OpenMesh 的基本使用方法,涵盖定义网格类型、添加顶点和面、读写网格文件以及执行基本的网格操作。这些内容是 OpenMesh 二次开发的基础,帮助开发者快速掌握网格处理的核心技能。OpenMesh 是一个功能强大的 C++ 库,基于半边数据结构,广泛应用于计算机图形学和几何处理。本章通过详细的代码示例和说明,确保开发者能够轻松上手并实现常见网格处理任务。
3.1 定义网格类型
OpenMesh 提供了几种预定义的网格类型,适用于不同的应用场景。最常用的两种类型是:
OpenMesh::PolyMesh_ArrayKernelT<>
:用于表示任意多边形网格,面可以有任意数量的边,适合需要灵活拓扑的场景。OpenMesh::TriMesh_ArrayKernelT<>
:用于表示三角形网格,所有面均为三角形,适合需要高效算法的场景(如网格平滑或细分)。
对于大多数基本应用,预定义的网格类型已经足够。以下是如何定义一个多边形网格的示例:
#include <OpenMesh/Core/Mesh/PolyMesh_ArrayKernelT.hh>
typedef OpenMesh::PolyMesh_ArrayKernelT<> MyMesh;
此代码定义了一个名为 MyMesh
的多边形网格类型,使用数组内核(ArrayKernel),在性能和内存使用之间取得了平衡。如果需要自定义网格类型(例如添加特定属性),可以使用 traits 机制,这将在后续高级主题中讨论。
3.2 添加顶点和面
构建网格需要先添加顶点,然后通过顶点句柄定义面。这是 OpenMesh 中创建几何体的基本步骤。
3.2.1 添加顶点
使用 add_vertex
方法添加顶点,该方法接受一个 MyMesh::Point
对象,表示顶点的 3D 坐标。例如,创建一个立方体需要添加 8 个顶点:
MyMesh mesh;
// 添加顶点
MyMesh::VertexHandle vhandle[8];
vhandle[0] = mesh.add_vertex(MyMesh::Point(-1, -1, 1));
vhandle[1] = mesh.add_vertex(MyMesh::Point( 1, -1, 1));
vhandle[2] = mesh.add_vertex(MyMesh::Point( 1, 1, 1));
vhandle[3] = mesh.add_vertex(MyMesh::Point(-1, 1, 1));
vhandle[4] = mesh.add_vertex(MyMesh::Point(-1, -1, -1));
vhandle[5] = mesh.add_vertex(MyMesh::Point( 1, -1, -1));
vhandle[6] = mesh.add_vertex(MyMesh::Point( 1, 1, -1));
vhandle[7] = mesh.add_vertex(MyMesh::Point(-1, 1, -1));
每个顶点通过 add_vertex
返回一个 VertexHandle
,用于后续定义面。
3.2.2 添加面
面通过一组顶点句柄定义,使用 add_face
方法。顶点句柄需要按逆时针顺序排列,以确保正确的面法线方向。例如,立方体有 6 个四边形面:
// 添加面
std::vector<MyMesh::VertexHandle> face_vhandles;
// 前
face_vhandles.clear();
face_vhandles.push_back(vhandle[0]);
face_vhandles.push_back(vhandle[1]);
face_vhandles.push_back(vhandle[2]);
face_vhandles.push_back(vhandle[3]);
mesh.add_face(face_vhandles);
// 后
face_vhandles.clear();
face_vhandles.push_back(vhandle[4]);
face_vhandles.push_back(vhandle[7]);
face_vhandles.push_back(vhandle[6]);
face_vhandles.push_back(vhandle[5]);
mesh.add_face(face_vhandles);
// 左
face_vhandles.clear();
face_vhandles.push_back(vhandle[0]);
face_vhandles.push_back(vhandle[3]);
face_vhandles.push_back(vhandle[7]);
face_vhandles.push_back(vhandle[4]);
mesh.add_face(face_vhandles);
// 右
face_vhandles.clear();
face_vhandles.push_back(vhandle[1]);
face_vhandles.push_back(vhandle[5]);
face_vhandles.push_back(vhandle[6]);
face_vhandles.push_back(vhandle[2]);
mesh.add_face(face_vhandles);
// 上
face_vhandles.clear();
face_vhandles.push_back(vhandle[3]);
face_vhandles.push_back(vhandle[2]);
face_vhandles.push_back(vhandle[6]);
face_vhandles.push_back(vhandle[7]);
mesh.add_face(face_vhandles);
// 下
face_vhandles.clear();
face_vhandles.push_back(vhandle[0]);
face_vhandles.push_back(vhandle[4]);
face_vhandles.push_back(vhandle[5]);
face_vhandles.push_back(vhandle[1]);
mesh.add_face(face_vhandles);
此代码创建了一个由 8 个顶点和 6 个四边形面组成的立方体网格。
3.3 读写网格文件
OpenMesh 的 IO 模块支持多种网格文件格式(如 OBJ、OFF、PLY、STL 等)的读写操作。通过 OpenMesh::IO::read_mesh
和 OpenMesh::IO::write_mesh
函数,开发者可以轻松导入和导出网格数据。
3.3.1 读取网格文件
使用 read_mesh
函数从文件中加载网格:
#include <OpenMesh/Core/IO/MeshIO.hh>
MyMesh mesh;
if (!OpenMesh::IO::read_mesh(mesh, "input.obj")) {
std::cerr << "错误:无法从文件读取网格。" << std::endl;
return 1;
}
3.3.2 写入网格文件
使用 write_mesh
函数将网格保存到文件:
if (!OpenMesh::IO::write_mesh(mesh, "output.off")) {
std::cerr << "错误:无法将网格写入文件。" << std::endl;
return 1;
}
3.3.3 使用选项控制读写
通过 OpenMesh::IO::Options
可以控制读写行为,例如指定是否以二进制格式读写,或是否包含特定属性(如顶点法线、颜色等)。以下是一个读取包含顶点法线的网格的示例:
OpenMesh::IO::Options ropt;
ropt += OpenMesh::IO::Options::VertexNormal;
if (!OpenMesh::IO::read_mesh(mesh, "input.obj", ropt)) {
std::cerr << "错误:无法读取包含顶点法线的网格。" << std::endl;
return 1;
}
支持的格式和选项如下表所示:
格式/选项 | ASCII | 二进制 | MSB | LSB | Swap | 顶点法线 | 顶点颜色 | 顶点纹理坐标 | 边颜色 | 面法线 | 面颜色 | 面纹理坐标 | 颜色Alpha | 颜色浮点 | 自定义 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
OBJ | x | x | x* | x | x | x | x | ||||||||
OFF | x | x | x | x | x | x | x | x | x | ||||||
PLY | x | x | x | x | x | x | x | x | x | x | x** | ||||
OM | x | x | x | x | x | x | x | x | x | x | |||||
STL | x | x | x | x | |||||||||||
VTK | x |
注释:
- OBJ 支持非标准顶点颜色(仅浮点数)。
- PLY 支持顶点和面属性的基本类型,无需预请求自定义属性。
3.4 基本网格操作
OpenMesh 提供多种方法来操作网格,包括遍历网格元素、访问邻居顶点以及修改网格拓扑。
3.4.1 遍历网格元素
OpenMesh 使用迭代器遍历网格中的顶点、边和面。例如,遍历所有顶点并打印其坐标:
for (MyMesh::VertexIter v_it = mesh.vertices_begin(); v_it != mesh.vertices_end(); ++v_it) {
MyMesh::Point p = mesh.point(*v_it);
std::cout << "顶点 " << v_it->idx() << ": " << p << std::endl;
}
遍历所有面:
for (MyMesh::FaceIter f_it = mesh.faces_begin(); f_it != mesh.faces_end(); ++f_it) {
std::cout << "面 " << f_it->idx() << std::endl;
}
3.4.2 访问邻居顶点
使用环形迭代器(circulators)可以访问一个顶点的一环邻域(直接相邻的顶点)。例如,计算一个顶点的一环邻域的重心:
MyMesh::VertexHandle vhandle = mesh.vertex_handle(0); // 示例顶点
MyMesh::Point cog(0, 0, 0);
int count = 0;
for (MyMesh::VertexVertexIter vv_it = mesh.vv_iter(vhandle); vv_it.is_valid(); ++vv_it) {
cog += mesh.point(*vv_it);
++count;
}
if (count > 0) {
cog /= count;
}
std::cout << "顶点 0 的重心: " << cog << std::endl;
3.4.3 修改网格拓扑
OpenMesh 支持删除顶点、边和面,但需要先请求状态属性以标记删除:
mesh.request_face_status();
mesh.request_vertex_status();
mesh.request_edge_status();
MyMesh::FaceHandle fhandle = mesh.face_handle(0); // 示例面
mesh.delete_face(fhandle, false);
mesh.garbage_collection();
类似地,删除顶点:
MyMesh::VertexHandle vhandle = mesh.vertex_handle(0); // 示例顶点
mesh.delete_vertex(vhandle, false);
mesh.garbage_collection();
调用 garbage_collection()
确保已标记为删除的元素从内存中移除。
3.5 代码示例
以下是一个完整的示例,展示如何读取一个三角形网格,执行拉普拉斯平滑(将每个顶点移动到其一环邻域的重心),然后将结果保存到文件。
3.5.1 示例代码
#include <OpenMesh/Core/IO/MeshIO.hh>
#include <OpenMesh/Core/Mesh/TriMesh_ArrayKernelT.hh>
#include <iostream>
#include <vector>
typedef OpenMesh::TriMesh_ArrayKernelT<> MyMesh;
void smooth(MyMesh& mesh, int iterations) {
for (int i = 0; i < iterations; ++i) {
// 计算每个顶点的一环邻域重心
std::vector<MyMesh::Point> cogs(mesh.n_vertices(), MyMesh::Point(0, 0, 0));
for (MyMesh::VertexIter v_it = mesh.vertices_begin(); v_it != mesh.vertices_end(); ++v_it) {
MyMesh::Point cog(0, 0, 0);
int count = 0;
for (MyMesh::VertexVertexIter vv_it = mesh.vv_iter(*v_it); vv_it.is_valid(); ++vv_it) {
cog += mesh.point(*vv_it);
++count;
}
if (count > 0) {
cog /= count;
}
cogs[v_it->idx()] = cog;
}
// 更新顶点位置
for (MyMesh::VertexIter v_it = mesh.vertices_begin(); v_it != mesh.vertices_end(); ++v_it) {
mesh.set_point(*v_it, cogs[v_it->idx()]);
}
}
}
int main(int argc, char** argv) {
if (argc != 4) {
std::cerr << "用法: " << argv[0] << " <迭代次数> <输入文件> <输出文件>" << std::endl;
return 1;
}
int iterations = std::stoi(argv[1]);
MyMesh mesh;
if (!OpenMesh::IO::read_mesh(mesh, argv[2])) {
std::cerr << "错误: 无法从 " << argv[2] << " 读取网格" << std::endl;
return 1;
}
smooth(mesh, iterations);
if (!OpenMesh::IO::write_mesh(mesh, argv[3])) {
std::cerr << "错误: 无法将网格写入 " << argv[3] << std::endl;
return 1;
}
return 0;
}
3.5.2 示例说明
- 输入:程序接受三个命令行参数:平滑迭代次数、输入文件名和输出文件名。
- 操作:读取输入文件中的三角形网格,执行指定次数的拉普拉斯平滑(将每个顶点移动到其一环邻域的重心)。
- 输出:将平滑后的网格写入输出文件。
- 用途:此示例展示了如何结合网格读写、遍历和修改操作,适用于网格优化等场景。
3.6 总结
本章介绍了 OpenMesh 的基本使用方法,包括定义网格类型、添加顶点和面、读写网格文件以及执行基本的网格操作。通过详细的代码示例,开发者可以快速掌握这些核心功能,为更复杂的几何处理任务奠定基础。后续章节将探讨高级主题,如自定义属性和网格扩展。