A-3 2023/11/16
Open-cascade/qt/C++
Opencascsde的相关算法,它是如何实现的,从底层算法说起。
说明
C++是一个面向对象编程的语言,而OCC是一个C++的几何函数库,因此OCC本质上是一个靠类堆叠起来的庞大建筑。先得思考明白一个问题:在人类眼中具象的几何图形,比如一个点,一条线,一个曲面,一个三角形等等,在计算机里是什么?
答案是不言而喻的,这些几何图形在计算机中都是数字。三维的点是三个数字,直线就是一个点加一个方向;而方向又是三个数字,那么直线就是六个数字,一个面是一个点加一个法向,也是六个数字和直线一样。
画瓶子
- 画瓶子的代码在上述网页的最后有,当然OCCT文件下也有相关代码,打开在下面路径下的MakeBottle.cxx文件:
OpenCASCADE-7.7.0-vc14-64\opencascade-7.7.0\samples\qt\tutorial\src
- 说了这么多,那么如何对几何体进行建模和表示呢?或者是,如何用open cascade对一个长方体进行建模呢?OCC提供了两个概念来表示一个几何体:几何与拓扑。简单的说就是,一个长方体有两个属性,分别是几何属性和拓扑属性。
几何类
- 此几何非彼几何。这里的几何表示长方体的几何属性。几何属性主要用于描述模型的几何性质,即关于点、线、曲线、曲面等的具体几何特征。几何类提供了创建和处理几何元素的方法,如计算曲线的参数位置、计算曲面上的点等。这些类的实例主要存储了几何元素的几何属性,如点的坐标、曲线的方程等。
类别 | 类名 | 构造函数 | 说明 |
---|---|---|---|
点类 | gp_Pnt | gp_Pnt(const Standard_Real theXp, const Standard_Real theYp, const Standard_Real theZp) | 给三个数字就能创建一个点 |
线段类 | GC_MakeSegment | GC_MakeSegment(const gp_Pnt &P1, const gp_Pnt &P2) | 给两个点类就能生成它们之间的线段 |
曲线类 | GC_MakeArcOfCircle | GC_MakeArcOfCircle(const gp_Pnt &P1, const gp_Pnt &P2, const gp_Pnt &P3) | 给三个点,画一段经过这三个点的圆弧 |
- 以上给出了OCC里面的三个几何类,但是实际上occ里面还有很多几何类,比如vector、direction、Circle等等。
- 说起一个点,在人类的脑海里,浮现的就是白纸上的一个小黑点。而在计算机眼中,一个点是三个数字。gp_Pnt这个类就表示点,我们根据其源码,可以发现,它的私有成员变量里面就有三个浮点型,表示的是点的坐标,也代表了这个点。
- 于是乎,一个点就从脑海中,现实中具象的样子,转变成了拥有严格数学定义的点了,并且在计算机中实现了点类。同理,一个向量也可以定义为三个数字,一条直线,就是一个点加一个方向,而一个圆就是一个点加上一个数字,数字表示半径。如下图就是OCC中圆几何类的定义,gp_Ax2表示圆心,这个类里包含了一个坐标系以及坐标轴中的一个点。
拓扑类
- 既然有了几何属性,那么为何还要有拓扑属性呢?拓扑属性主要用于描述模型的拓扑结构,即关于模型中各个几何元素之间的关系和连接。拓扑类提供了一种层次结构来组织几何元素,并定义了它们之间的拓扑关系。例如,一个面可以包含多个边,一个边可以连接两个顶点等。这些类的实例主要存储了拓扑元素之间的连接关系,使得可以在模型中导航和查询拓扑关系。
- OCC中有以下拓扑类:
Shape | Open CASCADE Technology Class | Description |
---|---|---|
Vertex | TopoDS_Vertex | Zero dimensional shape corresponding to a point in geometry. |
Edge | TopoDS_Edge | One-dimensional shape corresponding to a curve and bounded by a vertex at each extremity. |
Wire | TopoDS_Wire | Sequence of edges connected by vertices. 由点连接的线的集合 |
Face | TopoDS_Face | Part of a surface bounded by a closed wire(s). |
Shell | TopoDS_Shell | Set of faces connected by edges. 由线连接的面的集合 |
Solid | TopoDS_Solid | Part of 3D space bounded by Shells. |
CompSolid | TopoDS_CompSolid | Set of solids connected by their faces. |
Compound | TopoDS_Compound | Set of any other shapes described above. |
当然OCC中拓扑类也不只这几种。拓扑属性里包括了点的连接情况,面的朝向等等情况。
比如现在平面上有四个点,要是没有拓扑属性,那么你就不知道这四个点构成了两个三角形还是长方形。当然拓扑还包含了其他属性,这里就不一一列举。
画瓶子的部分代码如下所示:
// Profile : Define Support Points
gp_Pnt aPnt1(-myWidth / 2., 0, 0);
gp_Pnt aPnt2(-myWidth / 2., -myThickness / 4., 0);
gp_Pnt aPnt3(0, -myThickness / 2., 0);
gp_Pnt aPnt4(myWidth / 2., -myThickness / 4., 0);
gp_Pnt aPnt5(myWidth / 2., 0, 0);
// Profile : Define the Geometry
Handle(Geom_TrimmedCurve) anArcOfCircle = GC_MakeArcOfCircle(aPnt2, aPnt3, aPnt4);
Handle(Geom_TrimmedCurve) aSegment1 = GC_MakeSegment(aPnt1, aPnt2);
Handle(Geom_TrimmedCurve) aSegment2 = GC_MakeSegment(aPnt4, aPnt5);
// Profile : Define the Topology
TopoDS_Edge anEdge1 = BRepBuilderAPI_MakeEdge(aSegment1);
TopoDS_Edge anEdge2 = BRepBuilderAPI_MakeEdge(anArcOfCircle);
TopoDS_Edge anEdge3 = BRepBuilderAPI_MakeEdge(aSegment2);
TopoDS_Wire aWire = BRepBuilderAPI_MakeWire(anEdge1, anEdge2, anEdge3);
首先第一步,定义了一些几何点;第二步,创建几何圆弧和两个几何线段;第三步,根据几何圆弧和几何线段创建拓扑边;第四步,将三个拓扑边都并入到拓扑曲线中.也就是说最后的aWire包含了上述的所有信息。
利用可视化库将这段曲线显示出来就是这样的:
上述就是几何和拓扑两大属性去描述分割一个几何体的大致过程与思想。也是画瓶子的第一步,然后用OCC的 数学工具,慢慢地一步步的画出瓶子。就按照上述过程,定义几何属性,变成拓扑属性,然后不断的往一个大拓扑里面加入这些小拓扑。
最后附上画瓶子的代码
TopoDS_Shape
MakeBottle(const Standard_Real myWidth, const Standard_Real myHeight,
const Standard_Real myThickness)
{
// Profile : Define Support Points
gp_Pnt aPnt1(-myWidth / 2., 0, 0);
gp_Pnt aPnt2(-myWidth / 2., -myThickness / 4., 0);
gp_Pnt aPnt3(0, -myThickness / 2., 0);
gp_Pnt aPnt4(myWidth / 2., -myThickness / 4., 0);
gp_Pnt aPnt5(myWidth / 2., 0, 0);
// Profile : Define the Geometry
Handle(Geom_TrimmedCurve) anArcOfCircle = GC_MakeArcOfCircle(aPnt2,aPnt3,aPnt4);
Handle(Geom_TrimmedCurve) aSegment1 = GC_MakeSegment(aPnt1, aPnt2);
Handle(Geom_TrimmedCurve) aSegment2 = GC_MakeSegment(aPnt4, aPnt5);
// Profile : Define the Topology
TopoDS_Edge anEdge1 = BRepBuilderAPI_MakeEdge(aSegment1);
TopoDS_Edge anEdge2 = BRepBuilderAPI_MakeEdge(anArcOfCircle);
TopoDS_Edge anEdge3 = BRepBuilderAPI_MakeEdge(aSegment2);
TopoDS_Wire aWire = BRepBuilderAPI_MakeWire(anEdge1, anEdge2, anEdge3);
// Complete Profile
gp_Ax1 xAxis = gp::OX();
gp_Trsf aTrsf;
aTrsf.SetMirror(xAxis);
BRepBuilderAPI_Transform aBRepTrsf(aWire, aTrsf);
TopoDS_Shape aMirroredShape = aBRepTrsf.Shape();
TopoDS_Wire aMirroredWire = TopoDS::Wire(aMirroredShape);
BRepBuilderAPI_MakeWire mkWire;
mkWire.Add(aWire);
mkWire.Add(aMirroredWire);
TopoDS_Wire myWireProfile = mkWire.Wire();
// Body : Prism the Profile
TopoDS_Face myFaceProfile = BRepBuilderAPI_MakeFace(myWireProfile);
gp_Vec aPrismVec(0, 0, myHeight);
TopoDS_Shape myBody = BRepPrimAPI_MakePrism(myFaceProfile, aPrismVec);
// Body : Apply Fillets
BRepFilletAPI_MakeFillet mkFillet(myBody);
TopExp_Explorer anEdgeExplorer(myBody, TopAbs_EDGE);
while(anEdgeExplorer.More()){
TopoDS_Edge anEdge = TopoDS::Edge(anEdgeExplorer.Current());
//Add edge to fillet algorithm
mkFillet.Add(myThickness / 12., anEdge);
anEdgeExplorer.Next();
}
myBody = mkFillet.Shape();
// Body : Add the Neck
gp_Pnt neckLocation(0, 0, myHeight);
gp_Dir neckAxis = gp::DZ();
gp_Ax2 neckAx2(neckLocation, neckAxis);
Standard_Real myNeckRadius = myThickness / 4.;
Standard_Real myNeckHeight = myHeight / 10.;
BRepPrimAPI_MakeCylinder MKCylinder(neckAx2, myNeckRadius, myNeckHeight);
TopoDS_Shape myNeck = MKCylinder.Shape();
myBody = BRepAlgoAPI_Fuse(myBody, myNeck);
// Body : Create a Hollowed Solid
TopoDS_Face faceToRemove;
Standard_Real zMax = -1;
for(TopExp_Explorer aFaceExplorer(myBody, TopAbs_FACE); aFaceExplorer.More(); aFaceExplorer.Next()){
TopoDS_Face aFace = TopoDS::Face(aFaceExplorer.Current());
// Check if <aFace> is the top face of the bottle抯 neck
Handle(Geom_Surface) aSurface = BRep_Tool::Surface(aFace);
if(aSurface->DynamicType() == STANDARD_TYPE(Geom_Plane)){
Handle(Geom_Plane) aPlane = Handle(Geom_Plane)::DownCast(aSurface);
gp_Pnt aPnt = aPlane->Location();
Standard_Real aZ = aPnt.Z();
if(aZ > zMax){
zMax = aZ;
faceToRemove = aFace;
}
}
}
TopTools_ListOfShape facesToRemove;
facesToRemove.Append(faceToRemove);
BRepOffsetAPI_MakeThickSolid aSolidMaker;
aSolidMaker.MakeThickSolidByJoin(myBody, facesToRemove, -myThickness / 50, 1.e-3);
myBody = aSolidMaker.Shape();
// Threading : Create Surfaces
Handle(Geom_CylindricalSurface) aCyl1 = new Geom_CylindricalSurface(neckAx2, myNeckRadius * 0.99);
Handle(Geom_CylindricalSurface) aCyl2 = new Geom_CylindricalSurface(neckAx2, myNeckRadius * 1.05);
// Threading : Define 2D Curves
gp_Pnt2d aPnt(2. * M_PI, myNeckHeight / 2.);
gp_Dir2d aDir(2. * M_PI, myNeckHeight / 4.);
gp_Ax2d anAx2d(aPnt, aDir);
Standard_Real aMajor = 2. * M_PI;
Standard_Real aMinor = myNeckHeight / 10;
Handle(Geom2d_Ellipse) anEllipse1 = new Geom2d_Ellipse(anAx2d, aMajor, aMinor);
Handle(Geom2d_Ellipse) anEllipse2 = new Geom2d_Ellipse(anAx2d, aMajor, aMinor / 4);
Handle(Geom2d_TrimmedCurve) anArc1 = new Geom2d_TrimmedCurve(anEllipse1, 0, M_PI);
Handle(Geom2d_TrimmedCurve) anArc2 = new Geom2d_TrimmedCurve(anEllipse2, 0, M_PI);
gp_Pnt2d anEllipsePnt1 = anEllipse1->Value(0);
gp_Pnt2d anEllipsePnt2 = anEllipse1->Value(M_PI);
Handle(Geom2d_TrimmedCurve) aSegment = GCE2d_MakeSegment(anEllipsePnt1, anEllipsePnt2);
// Threading : Build Edges and Wires
TopoDS_Edge anEdge1OnSurf1 = BRepBuilderAPI_MakeEdge(anArc1, aCyl1);
TopoDS_Edge anEdge2OnSurf1 = BRepBuilderAPI_MakeEdge(aSegment, aCyl1);
TopoDS_Edge anEdge1OnSurf2 = BRepBuilderAPI_MakeEdge(anArc2, aCyl2);
TopoDS_Edge anEdge2OnSurf2 = BRepBuilderAPI_MakeEdge(aSegment, aCyl2);
TopoDS_Wire threadingWire1 = BRepBuilderAPI_MakeWire(anEdge1OnSurf1, anEdge2OnSurf1);
TopoDS_Wire threadingWire2 = BRepBuilderAPI_MakeWire(anEdge1OnSurf2, anEdge2OnSurf2);
BRepLib::BuildCurves3d(threadingWire1);
BRepLib::BuildCurves3d(threadingWire2);
// Create Threading
BRepOffsetAPI_ThruSections aTool(Standard_True);
aTool.AddWire(threadingWire1);
aTool.AddWire(threadingWire2);
aTool.CheckCompatibility(Standard_False);
TopoDS_Shape myThreading = aTool.Shape();
// Building the Resulting Compound
TopoDS_Compound aRes;
BRep_Builder aBuilder;
aBuilder.MakeCompound (aRes);
aBuilder.Add (aRes, myBody);
aBuilder.Add (aRes, myThreading);
return aRes;
}
半边结构
在计算机图形学上,表达表面网格的数据结构有三种,分别是面列表( List of faces)、邻接矩阵(Adjacency matrix)、半边结构(Half-edge)。
以下是这几种结构的图示,对于给定的一张网格的三种表达方式。
面列表,邻接矩阵的两种存储方式都很好理解。就不细说了。
半边:把一条边分成两条有向边,记作e1,e2,方向相反。这两条有向边称之为半边。
在一个半边结构类中需要存储6个东西,半边、起始点、对应边的另一条半边、半边所在的面、
下一条半边和前一条半边。因为有方向的缘故,所以有下一条和前一条的概念。
半边结构的具体代码定义如下:
#include <iostream>
#include <vector>
struct Vertex {
float x, y, z; //点就是三个数字,代表坐标
HalfEdge* halfEdge; // 从这个顶点出发的半边
};
struct Face {
HalfEdge* halfEdge; // 任意一条从这个面出发的半边
};
struct HalfEdge {
Vertex* vertex; // 半边的起始顶点
Face* face; // 半边所在的面
HalfEdge* next; // 下一条半边(同一面上)
HalfEdge* pre; // 上一条半边
HalfEdge* twin; // 对向半边
HalfEdge* edge; // 这条半边
};
class Mesh { //定义网格
public:
std::vector<Vertex> vertices;
std::vector<Face> faces;
std::vector<HalfEdge> halfEdges;
void AddVertex(float x, float y, float z) {//
Vertex newVertex = {x, y, z, nullptr};
vertices.push_back(newVertex);
}
void AddFace(Vertex* v1, Vertex* v2, Vertex* v3) {
Face newFace = {nullptr};
faces.push_back(newFace);
HalfEdge* he1 = new HalfEdge();
HalfEdge* he2 = new HalfEdge();
HalfEdge* he3 = new HalfEdge();
he1->vertex = v1;
he2->vertex = v2;
he3->vertex = v3;
he1->face = &faces.back();
he2->face = &faces.back();
he3->face = &faces.back();
he1->next = he2;
he2->next = he3;
he3->next = he1;
v1->halfEdge = he1;
v2->halfEdge = he2;
v3->halfEdge = he3;
halfEdges.push_back(*he1);
halfEdges.push_back(*he2);
halfEdges.push_back(*he3);
}
};
int main() {
Mesh mesh;
mesh.AddVertex(0.0f, 0.0f, 0.0f);
mesh.AddVertex(1.0f, 0.0f, 0.0f);
mesh.AddVertex(0.0f, 1.0f, 0.0f);
mesh.AddFace(&mesh.vertices[0], &mesh.vertices[1], &mesh.vertices[2]);
return 0;
}