初识OpenMesh
本文是笔者修读《计算机图形学》时,课程作业的副产物。
本文的大部分内容是官方文档的“Using and understanding OpenMesh”一章的翻译。
简介
OpenMesh是一个通用且高效的库,它提供了用于表示和操作多边形网格的数据结构。它允许用户根据应用程序的具体需要创建自定义的网格类型。用户既可以提供自己的数据结构来表示顶点、边和面,也可以方便地使用OpenMesh的预定义结构。此外,OpenMesh还提供了动态属性(properties),允许用户在运行期间将数据附加到网格中。
OpenMesh的官方文档:此处
OpenMesh的特点:
- 不仅限于三角形网格,还可以处理各种多边形网格。
- 可以方便地抽象顶点、半边、边、面等。
- 可以高效地访问顶点的1-邻接顶点。
- 可以处理非流形网格。
OpenMesh的C++实现尤其注重灵活性、高效性和类型安全。
“半边”数据结构
多边形网格的表示有许多方式。OpenMesh使用了一种“半边”数据结构。顾名思义,半边就是边的一半。OpenMesh使用如下的规则记录顶点、边和面:
- 每个顶点记录所有从自己出发的半边。(1)
- 每个面记录一条自己周围的半边。多个面就可记录多条半边。(2)
- 每条半边记录:
- 自己指向的顶点 (3)
- 自己归属的面 (4)
- 按照逆时针方向排列的自己归属的面上的下一条半边 (5)
- 和自己贴着的那条半边(这两条半边共同组成了一条边) (6)
- (可选)自己归属的面上相对于自己的上一条半边 (7)
这样,通过任意的半边、顶点和面都可以循环地遍历到整个网格。这种循环工具被封装在所谓的Circulator中。
相比其他的网格描述方式,半边数据结构需要消耗更多内存,但获得了如下的优势:
- 可以方便地在同一个网格中混合使用包含不同定点数的面。
- 能够方便地表示顶点、边和面。
- 围绕一个顶点循环以获得其1-邻接顶点是多边形网格上许多种算法的一个重要操作。对于基于面的结构,这会导致许多分支结构,基于半边结构的遍历可以在恒定的时间内完成且没有条件分支。
网格的Iterator和Circulator
迭代器
OpenMesh提供了针对顶点、半边、面提供了线性迭代器,可用于方便地遍历这些对象。
所有的迭代器都定义在OpenMesh::Iterators
命名空间中。这些迭代器都是模板类,期望一个自定义的网格类型作为模板参数被完全指定。因此你应该使用网格本身提供的迭代器类型,即MyMesh::VertexIter
而不是OpenMesh::Iterators::VertexIterT<MyMesh>
。
使用例子:
MyMesh mesh;
// 遍历所有顶点
for (MyMesh::VertexIter v_it=mesh.vertices_begin(); v_it!=mesh.vertices_end(); ++v_it)
...; // do something with *v_it, v_it->, or *v_it
// 遍历所有半边
for (MyMesh::HalfedgeIter h_it=mesh.halfedges_begin(); h_it!=mesh.halfedges_end(); ++h_it)
...; // do something with *h_it, h_it->, or *h_it
// 遍历所有边
for (MyMesh::EdgeIter e_it=mesh.edges_begin(); e_it!=mesh.edges_end(); ++e_it)
...; // do something with *e_it, e_it->, or *e_it
// 遍历所有面
for (MyMesh::FaceIter f_it=mesh.faces_begin(); f_it!=mesh.faces_end(); ++f_it)
...; // do something with *f_it, f_it->, or *f_it
这些迭代器对应的const
版本分别是:
ConstVertexIter
,ConstHalfedgeIter
,ConstEdgeIter
,ConstFaceIter
.
使用迭代器时,处于性能方面的考虑,建议使用前置自增写法即++it
而不是it++
。
为了获取迭代器指向的对象,应该简单地对迭代器解引用。早期版本使用的是迭代器的handle()
成员,但这种用法已经废弃了。
跳跃迭代器
OpenMesh使用的是惰性删除,如果你在删除一个对象之后,还没有执行garbage_collection()
,那么这个对象实际上还是存在着的,此时迭代器的行为会有不同。如果进行过垃圾回收,元素将被重排,迭代器就会给出连续的结果了。
-
标准的迭代器仍然会列举出已被删除但尚未被垃圾回收的对象。
-
跳跃迭代器(Skipping Iterators)会跳过这些项。所有的迭代器都有对应的跳跃迭代器,它们可以通过调用这些函数来获得:
vertices_sbegin()
,edges_sbegin()
,halfedges_sbegin()
,faces_sbegin()
这些跳跃迭代器的尾后迭代器(
end
)仍然是标准形式。
Circulators
OpenMesh还提供了所谓的Circulators,可用于列举与同一类型或另一类型的项目相邻的项目的方法。例如,VertexVertexIter
允许列举紧邻一个顶点的所有顶点。类似地,FaceHalfedgeIter
枚举了属于一个面的所有半边。
一般来说,CenterItem_AuxiliaryInformation_TargetItem_Iter
指定了一个Circulator,它枚举了一个给定中心项周围的所有目标项。
关于顶点的Circulators
VertexVertexIter
: 遍历所有邻接顶点VertexIHalfedgeIter
遍历所有指向自己的半边VertexOHalfedgeIter
:遍历所有从自身出发的半边VertexEdgeIter
:遍历所有邻接的边VertexFaceIter
:遍历所有邻接的面
关于面的Circulators
FaceVertexIter
:遍历这个面上所有顶点FaceHalfedgeIter
:遍历属于这个面的所有半边FaceEdgeIter
:遍历这个面的所有边FaceFaceIter
:遍历所有和自身相邻的面。
其他
HalfedgeLoopIter
:遍历一个半边序列(如围绕一个面的所有半边)
OpenMesh提供如下的一系列函数来获得针对不同对象的Circulators:
/**************************************************
* 顶点的 circulators
**************************************************/
// 获得顶点 _vh 的所有1-邻接顶点
VertexVertexIter OpenMesh::PolyConnectivity::vv_iter (VertexHandle _vh);
// 获得所有指向顶点 _vh 的半边
VertexIHalfedgeIter OpenMesh::PolyConnectivity::vih_iter (VertexHandle _vh);
// 获得所有从顶点 _vh 出发的半边
VertexOHalfedgeIter OpenMesh::PolyConnectivity::voh_iter (VertexHandle _vh);
// 获得关于顶点 _vh 的所有 Vertex-Edge(边?)
VertexEdgeIter OpenMesh::PolyConnectivity::ve_iter (VertexHandle _vh);
// 获得关于顶点 _vh 的所有 Vertex-Face(面?)
VertexFaceIter OpenMesh::PolyConnectivity::vf_iter (VertexHandle _vh);
/**************************************************
* 面的 circulators
**************************************************/
// 获得和面 _fh 相邻的所有顶点
FaceVertexIter OpenMesh::PolyConnectivity::fv_iter (FaceHandle _fh);
// 获得和面 _fh 相邻的所有半边
FaceHalfedgeIter OpenMesh::PolyConnectivity::fh_iter (FaceHandle _fh);
// 获得和面 _fh 相邻的所有边
FaceEdgeIter OpenMesh::PolyConnectivity::fe_iter (FaceHandle _fh);
// 获得和面 _fh 相邻的所有面
FaceFaceIter OpenMesh::PolyConnectivity::ff_iter (FaceHandle _fh);
可以看到这些函数都定义在OpenMesh::PolyConnectivity
中。
标准的Circulator可能是乱序的,如果你希望获得严格按照某个方向遍历的Circulator,可以在上面函数的下划线_
后面加上ccw
或cw
来得到,二者的区别是前者是逆时针的,后者是顺时针的。注意这种顺序式Circulator可能会稍慢。
VertexVertexIter vvit = mesh.vv_iter(some_vertex_handle); // 最快(乱序)
VertexVertexCWIter vvcwit = mesh.vv_cwiter(some_vertex_handle); // 顺时针方向
VertexVertexCCWIter vvccwit = mesh.vv_ccwiter(some_vertex_handle); // 逆时针方向
这两种Circulator还可以通过构造函数互相转换。转换后的Circulator仍然指向同一个对象,但推进的顺序颠倒过来了。
所有Circulator同样存在const
版本。它们的类型名对应地加上Const
前缀,获取的函数对应地加上c
前缀:
ConstVertexVertexIter cvvit = mesh.cvv_iter(some_vertex_handle);
构建自己的Mesh类
通过以下四个步骤来构建自己的Mesh类:
- 在三角形网格和多边形网格之间选择一个。
- 确定mesh kernel
- 使用
Trait
类来参数化网格。 - 通过自定义属性动态地将数据绑定到网格或网格的组成成分(顶点、(半)边、面)
选择三角形网格还是多边形网格?
你应该尽量优先选择三角形网格。渲染三角形要比渲染任意多边形快得多。而且某些算法只针对三角形网格实现。
参见:
选择合适的Kernel
网格kernel指定了网格实体(顶点、(半)边、面)的内部存储方式。事实上,这些实体被保存在所谓的属性(properties)中。一个属性本身提供了一个类似数组的接口。kernel定义了相应的处理类型,也就是项目之间相互引用的方式。因为属性有一个类似数组的接口,所以句柄在内部被表示为索引。
默认的kernel是ArrayKernelT
。这对大多数情况来说是合适的。但根据应用的不同,有些其他kernel会更好。
参见:
Mesh Traits
前两个小节描述的是现成mesh和kernel,而本节描述的是用户自定义的东西。
最后的 MyMesh
数据结构将会提供以下类型:
- 点和标量类型:
MyMesh::Point
andMyMesh::Scalar
. - 网格实体:
MyMesh::Vertex
,MyMesh::Halfedge
,MyMesh::Edge
,MyMesh::Face
. - 句柄类型:
MyMesh::VertexHandle
,MyMesh::HalfedgeHandle
,MyMesh::EdgeHandle
,MyMesh::FaceHandle
.
其中,只有句柄类型是固定的,其他类型都可以定制。每个网格类型(见预定义的网格类型)都可以使用所谓的trait
类来参数化。通过使用这个机制,用户可以
- 修改坐标类型
MyMesh::Point
和它对应的标量类型MyMesh::Scalar == MyMesh::Point::value_type
; - 修改normal类型
MyMesh::Normal
; - 修改颜色类型
MyMesh::Color
; - use predefined attributes like normal vector, color, texture coordinates, ... for the mesh items.
- add arbitrary classes to the mesh items.
所有这些修改都封装在MyTraits
类中,该类将被用于特化网格类的模板:
struct MyTraits {
// 你的修改
};
typedef PolyMesh_ArrayKernelT<MyTraits> MyMesh;
本节的剩余内容讲述了如何编写MyTraits
类,此处略去。
自定义属性
本节介绍:
- 如何添加和移除自定义属性。
- 如何设置或获得一个自定义属性的值。
自定义属性可以通过创建 OpenMesh::PropertyManager
类型的对象来方便地创建和附加到网格上。PropertyManager
管理着属性的生命周期,并提供对其值的读/写访问。
你可以使用VProp
、HProp
、EProp
、FProp
和MProp
这些typedef
来创建一个分别附加在顶点、半边、边、面和网格上的PropertyManager
。每一个都把附加在每个元素上的属性值的类型作为模板参数(例如int
,double
,等等)。
有两种自定义属性,命名的和临时的。
- 要创建临时属性,只需要向构造函数提供该属性要附着的mesh。一旦
PropertyManager
超出它的作用域,这些属性就会被删除。 - 如果除了提供mesh之外,还提供了一个属性名称,就会创建命名属性,这类属性即使在
PropertyManager
超出作用域之后也会保持有效。如果对PropertyManager
指定一个已经存在的属性的名字,那么它将提供对该属性的读写访问。
可以使用hasProperty()来检查一个网格是否包含了指定的属性:
if (OpenMesh::hasProperty<OpenMesh::FaceHandle, double>(mesh, "face_area")) {
// 属性存在
auto valley = OpenMesh::FProp<bool>(mesh, "face_area");
} else {
// 属性不存在
}
例:
// 添加一个针对每个顶点存储一个double值的临时属性
auto temperature = OpenMesh::VProp<double>(mesh);
OpenMesh::VertexHandle vh = ...;
temperature[vh] = 1.0;
// 当handle到达作用域结束处时,这个温度属性将被删除
// 获得一个针对每个半边存储一个2D向量的已经存在的属性,若该属性不存在,将创建该属性。
// 然后用Vector(1,1)来初始化它
auto uv = OpenMesh::HProp<OpenMesh::Vec2d>(mesh, "uv", OpenMesh::Vec2d(1,1));
OpenMesh::VertexHandle heh = ...;
std::cout << temperature[heh][0] << " " << temperature[heh][1] << std::endl;
// Obtain an existing mesh property (or create that property if it does not exist already)
// containing a description string
auto desc = OpenMesh::MProp<std::string>(mesh, "desc");
*desc = "This is a very nice mesh.";
一个更完整的例子:
#include <OpenMesh/Core/IO/MeshIO.hh>
#include <OpenMesh/Core/Mesh/DefaultTriMesh.hh>
#include <OpenMesh/Core/Utils/PropertyManager.hh>
#include <iostream>
#include <vector>
using MyMesh = OpenMesh::TriMesh;
int main(int argc, char** argv)
{
// 读取命令行参数
MyMesh mesh;
if (argc != 4) {
std::cerr << "Usage: " << argv[0] << " #iterations infile outfile" << std::endl;
return 1;
}
const int iterations = argv[1];
const std::string infile = argv[2];
const std::string outfile = argv[3];
// 读取网格文件
if (!OpenMesh::IO::read_mesh(mesh, infile)) {
std::cerr << "Error: Cannot read mesh from " << infile << std::endl;
return 1;
}
{
// 添加一个存储重心的顶点的属性
auto cog = OpenMesh::VProp<MyMesh::Point>(mesh);
// 将mesh平滑若干次
for (int i = 0; i < iterations; ++i) {
// 迭代所有顶点来计算重心
for (const auto& vh : mesh.vertices()) {
cog[vh] = {0,0,0};
int valence = 0;
// 遍历 vh 的所有1-邻接顶点
for (const auto& vvh : mesh.vv_range(vh)) {
cog[vh] += mesh.point(vvh);
++valence;
}
cog[vh] /= valence;
}
// 将所有顶点移动到之前计算出的位置
for (const auto& vh : mesh.vertices()) {
mesh.point(vh) = cog[vh];
}
}
} // cog 属性到达作用域末尾,被从mesh中移除
// 写网格文件
if (!OpenMesh::IO::read_mesh(mesh, outfile)) {
std::cerr << "Error: Cannot write mesh to " << outfile << std::endl;
return 1;
}
}
底层属性API
上面提到的VProp
、HProp
、EProp
、FProp
和MProp
都属于较为高层的接口,使用方便。这些接口的底层是另一套API,必须手动管理属性。这个底层接口可以通过一个网格的add_property()
, get_property()
, remove_property()
, 和 property()
函数,以及一些属性处理类(OpenMesh::VPropHandleT, OpenMesh::HPropHandleT, OpenMesh::EPropHandleT, OpenMesh::FPropHandleT, OpenMesh::MPropHandleT)来访问。