初识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,可以在上面函数的下划线_后面加上ccwcw来得到,二者的区别是前者是逆时针的,后者是顺时针的。注意这种顺序式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类:

  1. 在三角形网格和多边形网格之间选择一个。
  2. 确定mesh kernel
  3. 使用Trait类来参数化网格。
  4. 通过自定义属性动态地将数据绑定到网格或网格的组成成分(顶点、(半)边、面)

选择三角形网格还是多边形网格?

你应该尽量优先选择三角形网格。渲染三角形要比渲染任意多边形快得多。而且某些算法只针对三角形网格实现。

参见:

OpenMesh::PolyMeshT

OpenMesh::TriMeshT

选择合适的Kernel

网格kernel指定了网格实体(顶点、(半)边、面)的内部存储方式。事实上,这些实体被保存在所谓的属性(properties)中。一个属性本身提供了一个类似数组的接口。kernel定义了相应的处理类型,也就是项目之间相互引用的方式。因为属性有一个类似数组的接口,所以句柄在内部被表示为索引。

默认的kernel是ArrayKernelT。这对大多数情况来说是合适的。但根据应用的不同,有些其他kernel会更好。

参见:

Mesh Kernels

Mesh Traits

前两个小节描述的是现成mesh和kernel,而本节描述的是用户自定义的东西。

最后的 MyMesh 数据结构将会提供以下类型:

  • 点和标量类型: MyMesh::Point and MyMesh::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管理着属性的生命周期,并提供对其值的读/写访问。

你可以使用VPropHPropEPropFPropMProp这些typedef来创建一个分别附加在顶点、半边、边、面和网格上的PropertyManager。每一个都把附加在每个元素上的属性值的类型作为模板参数(例如intdouble,等等)。

有两种自定义属性,命名的和临时的。

  • 要创建临时属性,只需要向构造函数提供该属性要附着的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

上面提到的VPropHPropEPropFPropMProp都属于较为高层的接口,使用方便。这些接口的底层是另一套API,必须手动管理属性。这个底层接口可以通过一个网格的add_property(), get_property(), remove_property(), 和 property()函数,以及一些属性处理类(OpenMesh::VPropHandleT, OpenMesh::HPropHandleT, OpenMesh::EPropHandleT, OpenMesh::FPropHandleT, OpenMesh::MPropHandleT)来访问。

posted @ 2023-03-19 17:34  Eslzzyl  阅读(1492)  评论(0编辑  收藏  举报