前言
虽然https://blog.csdn.net/baidu_34931359/article/details/129962825?spm=1001.2014.3001.5501
已经实现了平面点云的三角化,但是其在效率上还存在较大的优化空间,在非法边的检索过程中会变已生成三角形进行遍历,若可提前直到非法边所对应的三角形,可大幅提高检索效率,将该过程的时间复杂度由n降至常数。
在邓老师的计算几何中提及到双向链接边表(Doubly-Connected Edge List,DCEL),它是一种用来表示平面图拓扑信息的数据结构,DCEL 结构(或者半边结构)可以高效的进行点、边和面等各种相互关系的查询。
DCEL介绍:
在DCEL中,将每条边的两端分别作为一条半边。半边有方向性,由起点出发,指向另一个端点。一条边的两条半边互为孪生半边。
半边数据结构中包含了三种对象:顶点、半边和面片。每个对象均为固定长度,分别存有下列几何的及拓扑的信息:
顶点:在对应于顶点vetrx 的顶点记录中,除了存储点的坐标信息,还有一个指向半边的指针,指向以V为起点的某一条半边。
半边:在对应于半边edge 的半边记录中,存储了半边的起点,一个半边的指针指向其孪生半边twins。一个面指针指向位于半边左侧的面face。此外还有2个半边指针,分别指向沿着face(e)边界方向的前一条半边和后一条半边。
面片:在对应于面face的记录中,只存储了一个指向半边的指针,指向该面边界上的某一条半边。
DCEL优势:
半边(half-edge)数据结构是一种略微复杂的边表示方法,优点是可以方便、快速地获得以下信息:
- 哪些面使用了这个顶点
- 哪些边使用了这个顶点
- 哪些面使用了这条边
- 哪些边构成了这个面
- 哪些面和这个面相邻
如果用最简单的顶点+索引的方式存储网格,问题1,2,3,5都是很难解决的,需要遍历所有顶点。而半边结构可以在常数时间内完成上述操作。
缺点是网格连接关系变动后,需要维护的信息也比较多。另外,半边结构表达的网格需要是流形结构,半边结构的构造也需要一定的时间开销。
DCEL数据结构实现:
下面给出了关键的数据结构Point、Vertex、HalfEdge与Triangle的具体实现。
Point:
class Point
{
public:
long long id;
double x;
double y;
bool used = 0; //used or not
Face* bucket = NULL; //the bucket of the point
Point(long long id_, double x_, double y_)
{
this->id = id_;
this->x = x_;
this->y = y_;
}
~Point(){}
};
Vertex:
class Vertex
{
public:
Point* p = NULL; //the coordinate of the vertex
HalfEdge* half_edge = NULL; //reference to the first outgoing incident half-edge
Vertex(Point* p_)
{
this->p = p_;
this->half_edge = NULL;
}
~Vertex(){}
};
HalfEdge
class HalfEdge
{
public:
bool valid = 1; //whether the edge is valid
HalfEdge* twin = NULL; //reference to the twin half edge
Vertex* source = NULL; //reference to the source vertex
Vertex* target = NULL;
Face* face = NULL; //reference to the left incident face
HalfEdge* next = NULL; //reference to CCW next half-edge
HalfEdge()
{
this->valid = 1;
}
~HalfEdge(){}
};
Face
class Face
{
public:
HalfEdge* half_edge = NULL; //the first half edge incident to the face from left
vector<Point*> bucket_points; //the unassigned points of the bucket
Face()
{
this->bucket_points.clear();
}
~Face()
{
this->bucket_points.clear();
}
};
逐点将点P插入三角形中是该算法的关键,在构件新的三角形的同时,需要更新DCEL数据结构,并删除旧的三角形。
void DelaunayTriangulation::ConnectTriangle(Vertex* p, Face* original_face)
{
Face* abc = original_face;
HalfEdge* ab = abc->halfEdge;
HalfEdge* bc = ab->next;
HalfEdge* ca = bc->next;
Vertex* a = ab->source;
Vertex* b = bc->source;
Vertex* c = ca->source;
HalfEdge* pa = new HalfEdge();
HalfEdge* pb = new HalfEdge();
HalfEdge* pc = new HalfEdge();
HalfEdge* ap = new HalfEdge();
HalfEdge* bp = new HalfEdge();
HalfEdge* cp = new HalfEdge();
Face* pab = new Face();
Face* pbc = new Face();
Face* pca = new Face();
p->halfEdge = pa;
pa->source = p;
pa->target = a;
pb->source = p;
pb->target = b;
pc->source = p;
pc->target = c;
ap->source = a;
ap->target = p;
bp->source = b;
bp->target = p;
cp->source = c;
cp->target = p;
pa->face = pab;
pb->face = pbc;
pc->face = pca;
ap->face = pca;
bp->face = pab;
cp->face = pbc;
ab->face = pab;
bc->face = pbc;
ca->face = pca;
pa->twin = ap;
ap->twin = pa;
pb->twin = bp;
bp->twin = pb;
pc->twin = cp;
cp->twin = pc;
pa->next = ab;
ab->next = bp;
bp->next = pa;
pb->next = bc;
bc->next = cp;
cp->next = pb;
pc->next = ca;
ca->next = ap;
ap->next = pc;
pab->halfEdge = pa;
pbc->halfEdge = pb;
pca->halfEdge = pc;
this->all_half_edges.push_back(pa);
this->all_half_edges.push_back(pb);
this->all_half_edges.push_back(pc);
this->all_half_edges.push_back(ap);
this->all_half_edges.push_back(bp);
this->all_half_edges.push_back(cp);
//update bucket points
for (int i = 0; i < abc->bucketPoints.size(); i++)
{
Point* the_point = abc->bucketPoints[i];
if (the_point->used == 1)
{
continue;
}
if (InTriangle(pab, the_point))
{
pab->bucketPoints.push_back(the_point);
the_point->bucket = pab;
}
else if (InTriangle(pbc, the_point))
{
pbc->bucketPoints.push_back(the_point);
the_point->bucket = pbc;
}
else
{
pca->bucketPoints.push_back(the_point);
the_point->bucket = pca;
}
}
abc->bucketPoints.clear();
delete(abc);
}
Reference:
http://www.holmes3d.net/graphics/dcel/
https://www.flipcode.com/archives/The_Half-Edge_Data_Structure.shtml