简介
三角网格是多边形网格的一种,多边形网格又被称为“Mesh”,是计算机图形学中用于为各种不规则物体建立模型的一种数据结构。现实世界中的物体表面直观上看都是由曲面构成的;而在计算机世界中,由于只能用离散的结构去模拟现实中连续的事物。所以现实世界中的曲面实际上在计算机里是由无数个小的多边形面片去组成的。比如下图的这些模型,在计算机渲染后由肉眼看是十分平滑的曲面,而实际上,计算机内部使用了大量的小三角形片去组成了这样的形状。这样的小面片的集合就被称作Mesh。Mesh既可以由三角形组成,也可以由其他平面形状如四边形,五边形等组成;由于平面多边形实际上也能再细分成三角形。所以,使用全由三角形组成的三角网格(Triangle Mesh)来表示物体表面也是具有一般性的。
下面的图片显示三角网格用于为物体建模的多个例子。
实际上,使用无数三角形面片来组成物体,现实世界存在的实物都能以这样的方式建模。在计算机内存中一旦有了一个物体的Mesh,即意味着计算机有能力渲染显示这样的物体。具体的渲染算法,以及渲染着色方式等问题,可以参考相关的资料(OPENGL的知识),内部都有详细的叙述。
Mesh数据结构
为了表示Mesh,首先需要定义点和三角形的数据结构,而Mesh则应该表示为这些结构的集合。本文采用C++语言来定义相应的类型。首先是点Point3d结构,表示三维空间的一个点,具有float类型的XYZ坐标:
struct Point3d { public: float X; float Y; float Z; Point3d(float x, float y, float z) { this->X=x; this->Y=y; this->Z=z; } };
三角形结构按常理理解应该是包含了组成其顶点的3个Point3d的信息,不过这里有两种方式来定义这个Triangle结构。一种是采用这三个点的地址(指针):
struct Triangle { public : Point3d* P0; Point3d* P1; Point3d* P2; Triangle(Point3d* p0, Point3d* p1, Point3d* p2) { this->P0=p0; this->P1=p1; this->P2=p2;
} };
第二种是使用索引:
struct Triangle { public : int P0Index; int P1Index; int P2Index; Triangle(int p0index, int p1index, int p2index) { this->P0Index=p0index; this->P1Index=p1index; this->P2Index=p2index; } };
注意,这个索引定义方式只有针对Mesh中的三角片才有意义,因为这个索引表示这个三角形的顶点在Mesh的点集数组中的位置。笔者比较推荐这种方式,之后文章的Mesh的三角形都是这样定义,原因下文有提到。
综上Mesh类可以有类似下面两种定义,一种使用指针,一种使用索引:
class Mesh { public: std::vector<Point3d*> Vertices; std::vector<Triangle*> Faces; Mesh(); ~Mesh(); void AddVertex(Point3d* toAdd); void AddFace(Triangle* tri); };
class Mesh { public: std::vector<Point3d> Vertices; std::vector<Triangle> Faces; Mesh(); ~Mesh(); void AddVertex(Point3d& toAdd); void AddFace(Triangle& tri); };
这样最为简单的Mesh类型就定义完成。
不难发现采用点地址实际上比采用索引要占更多的空间。因为在对于一个有效的Point3d地址来说,它必然位于堆上,而不能使用栈上的地址或者vector中的地址(使用vector中的地址必须保证vector不因为添加新点而自动执行内部扩容操作,实际应用中这点一般不能保证),因而基于点地址的Triangle结构实际上多存了指针的空间。而若采用索引,则占用的最小空间等于点集的空间加索引的空间,所以一般推荐采用索引表示的三角形来构成Mesh中的三角形集合。
Mesh除了点集和三角形集合外,根据具体的应用还可以有很多附加的信息。比如三角形结构可以增加成员变量存储法向量。点结构也可以增加成员存放点法向量,当然这些结构也可以不存在相应的三角形和点中,可以放在Mesh中以数组形式来存,位置和三角形集合和点集合分别一一对应。
网格文件格式
一个Mesh网格就是一个物体的模型,上文介绍了Mesh在内存中的形式。实际应用中,当一个物体的Mesh被计算出来后,往往需要把这个Mesh写成文件永久保存,这就涉及到了三维模型文件格式的问题。在业界,三维模型文件的格式有多种多样,同时也有不少的开源3d显示软件支持打开和保存这些文件。比较著名的一个三维模型编辑开源软件是VCG实验室的MeshLab。下图是这个软件的主界面的样子。
MeshLab支持import和export 各种网格文件,可以从他支持的文件列表中看出三维网格文件的各种格式,比较常见的有stl、obj、ply、3ds、off等。每一种格式都有自己的特点。这里简单的讲讲本文这样定义的Mesh使用ply格式如何读写。无论什么文件格式,无非是使用一个表达方式把建立一个Mesh所必须的信息记录下来。PLY格式文件除了记录Mesh的点和三角形之外,还能支持很多其他方式表示的Mesh,比如使用点集和边集表示的Mesh。不过这些复杂深入的知识不在本文介绍,详细的PLY格式能以怎样的方式记录怎样的信息可以在维基百科上获得。
下图是本文定义的Mesh写成ASCII文本形式的PLY文件的大概形式:
将本文索引方式定义的Mesh,使用C++语言读写.ply文件的函数如下:
void ReadFile(Mesh& mesh,const char* fileName) { int vcount=0; int fcount=0; FILE * nfile = fopen(fileName,"r"); fscanf(nfile,"ply\nformat ascii 1.0\ncomment VCGLIB generated\nelement vertex %d\n",&vcount); fscanf(nfile,"property float x\nproperty float y\nproperty float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\nelement face %d\n",&fcount); fscanf(nfile,"property list int int vertex_indices\nend_header\n"); float v1=0,v2=0,v3=0; int r=0,g=0,b=0; int i1=0,i2=0,i3=0; for(int i=0;i<vcount;i++) { fscanf(nfile,"%f %f %f %d %d %d\n",&v1,&v2,&v3,&r,&g,&b); Point3d p3d(v1,v2,v3); mesh.AddVertex(p3d); } for(int j=0;j<fcount;j++) { fscanf(nfile,"3 %d %d %d\n",&i1,&i2,&i3); Triangle t(i1,i2,i3); mesh.AddFace(t); } fclose(nfile); }
void Output(Mesh& mesh,const char* filename) { FILE * nfile = fopen(filename,"wb"); fprintf(nfile,"ply\n"); fprintf(nfile,"format ascii 1.0\n"); fprintf(nfile,"comment VCGLIB generated\n"); fprintf(nfile,"element vertex %d\n",mesh.Vertices.size()); fprintf(nfile,"property float x\n"); fprintf(nfile,"property float y\n"); fprintf(nfile,"property float z\n"); fprintf(nfile,"property uchar red\n"); fprintf(nfile,"property uchar green\n"); fprintf(nfile,"property uchar blue\n"); fprintf(nfile,"element face %d\n",mesh.Faces.size()); fprintf(nfile,"property list int int vertex_indices\n"); fprintf(nfile,"end_header\n"); for(size_t i=0;i<mesh.Vertices.size();i++) { fprintf(nfile,"%.2f %.2f %.2f %d %d %d\n",mesh.Vertices[i].Data[0],mesh.Vertices[i].Data[1],mesh.Vertices[i].Data[2],255,255,255); } for(size_t i=0;i<mesh.Faces.size();i++) { fprintf(nfile,"%d %d %d %d\n",3,mesh.Faces[i].P0Index,mesh.Faces[i].P1Index,mesh.Faces[i].P2Index); } fclose(nfile); }
续篇:”三角网格数据结构-2“
PS:以上IO代码不是读写ply文件的通用方法,因为这个格式的通用性较强,其实读写它不可能像上面那么简单,这里提供一个比较好的读写ply文件的代码。
爬网的太疯狂了,转载本文要注明出处啊:http://www.cnblogs.com/chnhideyoshi/