先来看看 VCGlib 能做什么 :
VCGlib 的文档很简陋, 在线文档 并不是很全,可以自己用 Doxygen从下载的源代码生成 html API 文档 ,为此只需要(Windows 用户):
VCGlib 是纯头文件库,要 安装 只需将下载 VCGlib库目录添加到程序的头文件包含路径(有些IO函数如读写PLY需要包含相应.cpp文件)。
后面按照如下步骤讲解:
定义 Mesh 类型的典型代码如下(API 文档主页 Basic Concepts,在线版 ):
#include "vcg/complex/complex.h" // 类型声明 class MyVertex;class MyEdge; class
抛开 MyUseTypes 不看,上面代码定义的网格类型为:
VCGlib 使用 Reference 数据结构,对每个边、面用指针记录其顶点、邻接面等信息,其他网格数据结构见wikipedia Polygon Mesh 条目。
为了做到足够通用, VCGlib 使用了C++ templatemetaprogramming(模板元编程)方法 。上面代码中的MyVertex、MyEdge、MyFace、GLMesh等类型包含哪些属性(模板参数)、属性的顺序(模板参数顺序)都是可以根据需要随意指定的(当然,必须包含足够的属性以执行相应网格算法),一般来说,最好使顶点、边、面包含标志位属性(BitFlags),BitFlags指示该顶点、边、面是否可写、可读、已删除(为了效率,例如,删除顶点操作可能并不立即删除顶点数据,而仅仅打个标志位,待所有操作完成再更新顶点数据)等。不去深入VCGlib元编程机理(说实话我还没弄清楚),可选个数模板参数是通过默认模板参数实现的,vcg::Vertex/Edge/Face<>将继承其模板参数。
下面列举所有可选的模板参数:
访问 Mesh 数据示例代码如下:
// load mesh ... int i=0, j=0; // 见 vcg::tri::TriMesh<> ------------------------------------------------------------- mesh.VN(); mesh.EN(); mesh.FN(); // 顶点、边、面个数,可能小于 vs/es/fs.size() // 因为有些元素被删除时仅仅打了标志位而并未删除存储数据 std::vector& vs = mesh.vert; // 顶点数组 std::vector& es = mesh.edge; // 边数组 std::vector& fs = mesh.face; // 面数组 // 见 vcg::Vertex<> 及其 模板参数 ------------------------------------------------------- GLMesh::VertexType& v = mesh.vert[i]; // 第 i 个顶点,假设 v.isD()==false,即未标志为已删除 v.P().Z(); v.P().V(j); // 顶点坐标,其xyz分量 v.N().X(); // 顶点法向,其x分量 // 见 vcg::Edge<> 及其 模板参数 --------------------------------------------------------- GLMesh::EdgeType& e = mesh.edge[i]; // 第 i 个边,假设 e.isD()==false GLMesh::VertexType* pve = e.V(j); // j=0,1,边的两个端点顶点的指针 GLMesh::FaceType* pfa = e.EFp(); // 边-面邻接信息,该边连接的第一个面 // 见 vcg::Face<> 及其 模板参数 --------------------------------------------------------- GLMesh::FaceType& f = mesh.face[i]; // 第 i 个面(三角形),假设 f.isD()==false GLMesh::VertexType* pvf = f.V(j); // j=0,1,2,三角形面的三个顶点的指针 f.N(); // 面的法向量 GLMesh::FaceType* pfb = f.FFp(j); // 面-面邻接信息,j=0,1,2,面 f 通过其第j个边连接的第一个面 // 可以通过返回的引用(左值)修改数据,但不要随便修改,见下文 ------------------------------------ v.P().Y() += 3.2f; e.V(j) = &v; f.V(j) = &v; // 遍历所有顶点、边、面需要跳过标记为已删除的元素 --------------------------------------------- for(size_t i=0; ii){ if(vs[i].IsD()) continue; // do some thing for each vertex vs[i] ... } // 除非已经删除了所有标记为已删除元素的存储数据,比如: vcg::tri::Allocator::CompactVertexVector(mesh); vcg::tri::Allocator::CompactEdgeVector(mesh); vcg::tri::Allocator::CompactFaceVector(mesh); for(size_t i=0; ii){ // do some thing for each face fs[i] ... }
填充(Fill)Mesh 数据的示例代码如下 (API 文档主页 Creating and destroyingelements, 在线版 ,代码摘自那里):
// VCGlib Reference 数据结构,依赖于指针,直接操作顶点、边、面数组 mesh.vert/edge/face 可能
// 产生 std::vector<> 存储重新分配,此时,相关指针将失效,vcg::tri::Allocator<> 处理这些问题
GLMesh m;
GLMesh::VertexIterator vi = vcg::tri::Allocator<GLMesh>::AddVertices(m, 3);
GLMesh::FaceIterator fi = vcg::tri::Allocator<GLMesh>::AddFaces(m, 1);
GLMesh::VertexPointer ivp[4];
ivp[0]=&*vi; vi->P()=GLMesh::CoordType(0.0f,0.0f,0.0f); ++vi;
ivp[1]=&*vi; vi->P()=GLMesh::CoordType(1.0f,0.0f,0.0f); ++vi;
ivp[2]=&*vi; vi->P()=GLMesh::CoordType(0.0f,1.0f,0.0f); ++vi;
fi->V(0)=ivp[0]; fi->V(1)=ivp[1]; fi->V(2)=ivp[2];
// Alternative, more compact, method for adding a single vertex
ivp[3]= &*vcg::tri::Allocator<GLMesh>::AddVertex(m,GLMesh::CoordType(1.0f,1.0f,0.0f));
// Alternative, method for adding a single face (once you have the vertex pointers)
vcg::tri::Allocator<GLMesh>::AddFace(m, ivp[1],ivp[0],ivp[3]);
// 同理,如果自己保存了顶点等数据指针,需要在修改顶点、边、面数组后更新该指针 --------------------
// a potentially dangerous pointer to a mesh element
GLMesh::FacePointer fp = &m.face[0];
vcg::tri::Allocator<GLMesh>::PointerUpdater<GLMesh::FacePointer> pu;
// now the fp pointer could be no more valid due to eventual re-allocation of the m.face
vcg::tri::Allocator<GLMesh>::AddVertices(m,3);
vcg::tri::Allocator<GLMesh>::AddFaces(m,1,pu);
// check if an update of the pointer is needed and do it.
if(pu.NeedUpdate()) pu.Update(fp);
// 删除元素的代码如下 --------------------------------------------------------------------
vcg::tri::Allocator<GLMesh>::DeleteFace(m,m.face[0]);
// 拷贝网格的代码如下,GLMesh 没有拷贝构造函数,也没有 operator= -----------------------------
GLMesh m2;
vcg::tri::Append<GLMesh,GLMesh>::MeshCopy(m2,m);
IO,读写网格文件示例代码如下(API 文档主页 Loading and savingmeshes, 在线版 ):
// Mesh 文件一般至少包含顶点数组信息,还可以包含连接信息(三角形)、顶点法向量、顶点颜色、面颜色、
// 面法向量、纹理坐标等等属性,用 mask 的二进制位来标记或控制读取或写入了 Mesh 文件的哪些属性
// 见 vcg::tri::io::Mask,读取 PLY 需要包含文件 "vcglib/wrap/ply/plylib.cpp"(见这里)
// 头文件包含:#include "wrap/io_trimesh/import.h" #include "wrap/io_trimesh/export.h"
GLMesh m; int mask;
// 读取 PLY 文件,并检查返回值,参数 mask 为可选,mask 是返回参数:读入了哪些属性
if( vcg::tri::io::ImporterPLY::Open(m, "file_to_open.ply", mask)
!= vcg::ply::E_NOERROR ) {
std::cout << "Load PLY file ERROR\n";
}
// some modification to m and mask ...
// 保存 PLY 文件,mask 是输入参数,控制 m 的哪些属性被写入到文件
vcg::tri::io::ExporterPLY<GLMesh>::Save(m, "file_to_save.ply", mask);
// 读取或写入 OBJ 文件的代码,mask 作用同上
if( vcg::tri::io::ImporterOBJ<GLMesh>::Open(m, "file_to_open.obj", mask)
!= vcg::tri::io::ImporterOBJ<GLMesh>::E_NOERROR ) {
std::cout << "Load OBJ file ERROR\n";
}
// some modification to m and mask ...
vcg::tri::io::ExporterOBJ<GLMesh>::Save(m, "file_to_save.obj", mask);
// 读取、写入网格文件,将根据文件扩展名自动匹配文件格式 ---------------------------------------
int oerr = vcg::tri::io::Importer<GLMesh>::Open(m, "file_to_open.off", mask);
if( oerr != 0 ){
std::cout << "Load mesh file ERROR: "
<< vcg::tri::io::Importer<GLMesh>::ErrorMsg(oerr) << '\n';
}
// some modification to m and mask ...
int serr = vcg::tri::io::Exporter<GLMesh>::Save(m, "file_to_save.3ds", mask);
if( serr != 0 ){
std::cout << "Save mesh file ERROR: "
<< vcg::tri::io::Exporter<GLMesh>::ErrorMsg(oerr) << '\n';
}
构造网格拓扑信息示例代码如下 (API 文档主页 Adjacency andTopology, 在线版 ):
// load mesh ...
vcg::tri::UpdateNormal<GLMesh>::PerFaceNormalized(mesh); // 计算顶点法向量,并单位化
vcg::tri::UpdateNormal<GLMesh>::PerVertexNormalized(mesh); // 计算面法向量,并单位化
vcg::tri::UpdateTopology<GLMesh>::FaceFace(mesh); // 计算面-面邻接信息
vcg::tri::UpdateTopology<GLMesh>::AllocateEdge(mesh); // 计算边-面邻接信息,需要面-面信息
vcg::Matrix44f mat(&glm::translate(glm::vec3(1,2,3))[0][0]);
vcg::tri::UpdatePosition<GLMesh>::Matrix(mesh, mat, true); // 更新顶点位置,并更新法向量
// 在调用 UpdateTopology<>::FaceFace() 和 UpdateTopology<>::AllocateEdge() 后就构造了边到面
// 的信息,对于 manifold 网格,每个边必连接两个三角形面,下面代码对边 i 查找其连接的面 fa 和 fb
int i=0; GLMesh::EdgeType& e = mesh.edge[i];
GLMesh::FaceType* fa = e.EFp();
GLMesh::FaceType* fb = fa->FFp(e.EFi());
在准备这篇博客之初,研究 VCGlib 时,发现了 VCGlib 的一个 BUG,已经报告给开发者并得到确认( 见这里 ,看看时间,发现这篇博客因为一些原因拖了20多天...)。
网格处理示例代码如下:
vcg::tri::Clean<GLMesh>::RemoveDuplicateVertex(mesh); // 去除重合的顶点
vcg::tri::Smooth<GLMesh>::VertexNormalLaplacian(mesh, 5); // 平滑顶点法向量
float maxSizeHole = 2.0f; // fill 所有直径小于 maxSizeHole 的洞
vcg::tri::Hole<GLMesh>::EarCuttingIntersectionFill
<vcg::tri::SelfIntersectionEar>>(mesh, maxSizeHole, false);
进一步学习的资源: