OpenCascade源码分析之StlAPI(网格离散化后导出stl文件)
奈何调库满足不了项目需求,并且网上没有详细资料啊 大家都偷懒的话只能自己来分析了,那就分析一下源码 看它对曲面的离散的步骤是怎么样的吧
一、StlAPI.hxx|StlAPI.cxx
我们在调用将离散网格数据写入stl的时候,一般会用到如下方法:
//需要离散的拓扑Shape
TopoDS_Shape cur;
//保存的文件名
const char* name = "a.stl";
StlAPI::Write(cur,name);
那么对于Write方法是如何运作的呢,接下来来到源码部分
//=============================================================================
//function : Write
//purpose :
//=============================================================================
Standard_Boolean StlAPI::Write (const TopoDS_Shape& theShape,
const Standard_CString theFile,
const Standard_Boolean theAsciiMode)
{
StlAPI_Writer aWriter;
aWriter.ASCIIMode() = theAsciiMode;
return aWriter.Write (theShape, theFile);
}
代码中调用了 StlAPI_Writer.hxx 同时设置了写文件是以文本格式还是二进制模式输出
接下来来到StlAPI_Writer部分查看实现
二、StlAPI_Writer.hxx | StlAPI_Writer.cxx
首先补充几个步骤 方便之后的代码阅读
关于Handle(Poly_Triangulation) aTriangulation = BRep_Tool::Triangulation (TopoDS::Face (aFace), aLoc);
Handle(Poly_Triangulation) aTriangulation = BRep_Tool::Triangulation (TopoDS::Face (aFace), aLoc);
该句代码主要调用了
const Poly_ListOfTriangulation& BRep_Tool::Triangulations (const TopoDS_Face& theFace,
TopLoc_Location& theLocation)
{
theLocation = theFace.Location();
const BRep_TFace* aTFace = static_cast<const BRep_TFace*>(theFace.TShape().get());
return aTFace->Triangulations();
}
//而aTFace调用了下面的方法
//=======================================================================
//function : Triangulation
//purpose :
//=======================================================================
const Handle(Poly_Triangulation)& BRep_TFace::Triangulation (const Poly_MeshPurpose thePurpose) const
{
//这边thePurpose默认为NONE
if (thePurpose == Poly_MeshPurpose_NONE)
{
return ActiveTriangulation();
}
xxxxx //之后的代码 没用到 就不解读了先删除
}
//ActiveTrigulation()返回了成员Handle(Poly_Triangulation) myActiveTriangulation;
const Handle(Poly_Triangulation)& ActiveTriangulation() const { return myActiveTriangulation; }
此文件中只实现了一个方法,就是Write方法
引用的头文件为:
#include <StlAPI_Writer.hxx>
#include <Bnd_Box.hxx>
#include <BRepBndLib.hxx>
#include <Message.hxx>
#include <Message_Messenger.hxx>
#include <OSD_Path.hxx>
#include <OSD_OpenFile.hxx>
#include <RWStl.hxx>
#include <BRep_Tool.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Face.hxx>
#include <TopExp_Explorer.hxx>
#include <Poly_Triangulation.hxx>
实现部分:(解读部分写成注释了)
Standard_Boolean StlAPI_Writer::Write (const TopoDS_Shape& theShape,
const Standard_CString theFileName,
const Message_ProgressRange& theProgress)
{
Standard_Integer aNbNodes = 0;
Standard_Integer aNbTriangles = 0;
// 计算离散之后点的总数目和面的总数目
//例如:一个圆柱体由三个TopoDS_Face组成,顶面、底面、侧面,对每个面进行离散化后
// 每个面上都会产生三角面和顶点,计算总数即可。
for (TopExp_Explorer anExpSF (theShape, TopAbs_FACE); anExpSF.More(); anExpSF.Next())
{
TopLoc_Location aLoc;
Handle(Poly_Triangulation) aTriangulation = BRep_Tool::Triangulation (TopoDS::Face (anExpSF.Current()), aLoc);
if (! aTriangulation.IsNull())
{
aNbNodes += aTriangulation->NbNodes ();
aNbTriangles += aTriangulation->NbTriangles ();
}
}
//这里如果没有执行过BRepMesh_IncrementalMesh的离散的话
//TopoDS_Face是没有三角信息的
if (aNbTriangles == 0)
{
// No triangulation on the shape
// 这个Shape上没有面
return Standard_False;
}
// 建立临时的Poly_Triangulation
//在OpenCascade中曲面的三角剖分的网格数据都保存在类Poly_Triangulation中
//TopoDS_Shape是不存储实际数据的
//实际上是初始化了如下成员变量
//Poly_ArrayOfNodes myNodes; //存放点
//Poly_Array1OfTriangle myTriangles; //存放面
Handle(Poly_Triangulation) aMesh = new Poly_Triangulation (aNbNodes, aNbTriangles, Standard_False);
// 记录没有进行离散的TopoDS_Face数量
Standard_Integer aNbFacesNoTri = 0;
// 为填充数据做准备
Standard_Integer aNodeOffset = 0;
Standard_Integer aTriangleOffet = 0;
//遍历Shape上所有的面
for (TopExp_Explorer anExpSF (theShape, TopAbs_FACE); anExpSF.More(); anExpSF.Next())
{
const TopoDS_Shape& aFace = anExpSF.Current();
TopLoc_Location aLoc;
//获取了一个用于实际存储数据的句柄进行操作
Handle(Poly_Triangulation) aTriangulation = BRep_Tool::Triangulation (TopoDS::Face (aFace), aLoc);
//如果句柄为NULL说明该面没有被离散
if (aTriangulation.IsNull())
{
//记录没有离散面的数量
++aNbFacesNoTri;
continue;
}
//如果面被离散过了 并且有了三角形数据则进行下面的操作
// 复制顶点
//gp_Trsf是用于将2D点到3D的转换
gp_Trsf aTrsf = aLoc.Transformation();
//遍历所有的点,下标从1开始
for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter)
{
//gp_Pnt用于存储点的实体数据
gp_Pnt aPnt = aTriangulation->Node (aNodeIter);
//坐标转换
aPnt.Transform (aTrsf);
//SetNode(theIndex,thePnt)调用了myNodes.SetValue (theIndex - 1, thePnt);
//将aMesh中存放点的数组增加点进去
//aNodeOffset是防止点覆盖到相同的下标上去 因为每个面的aNodeIter都是从1开始计算的
aMesh->SetNode (aNodeIter + aNodeOffset, aPnt);
}
// 存储三角网格的数据 和上面差不多
// 获得三角形的朝向,对这个不理解的可以去阅读一下TopoDS的文档,这是拓扑定义
const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation();
for (Standard_Integer aTriIter = 1; aTriIter <= aTriangulation->NbTriangles(); ++aTriIter)
{
Poly_Triangle aTri = aTriangulation->Triangle (aTriIter);
//获得顶点的下标 在离散化后会自动生成index->pnt 一个下标对应一个点
//Standard_Integer就是int类型 只不过被typedef了 写成int也可以
Standard_Integer anId[3];
aTri.Get (anId[0], anId[1], anId[2]);
//如果面翻转了,则翻回来
if (anOrientation == TopAbs_REVERSED)
{
// Swap 1, 2.
Standard_Integer aTmpIdx = anId[1];
anId[1] = anId[2];
anId[2] = aTmpIdx;
}
// 防止遍历不同面的时候产生相同的id,加上偏移量重新生成三角形
anId[0] += aNodeOffset;
anId[1] += aNodeOffset;
anId[2] += aNodeOffset;
// 重新设置面上点的id值
aTri.Set (anId[0], anId[1], anId[2]);
aMesh->SetTriangle (aTriIter + aTriangleOffet, aTri);
}
//偏移量增加
aNodeOffset += aTriangulation->NbNodes();
aTriangleOffet += aTriangulation->NbTriangles();
}
OSD_Path aPath (theFileName);
Standard_Boolean isDone = (myASCIIMode
? RWStl::WriteAscii(aMesh, aPath, theProgress)
: RWStl::WriteBinary(aMesh, aPath, theProgress));
return isDone;
}
OK 现在我们已经理解了如何设置Poly_Triangulation来保存一个被离散过的TopoDS_Shape的实体数据
接下来就是RWStl::WriteAscii(aMesh, aPath, theProgress); 让我们来看看它是如何通过Poly_Triangulation实现写入操作
三、写入操作 RWStl.hxx | RWStl.cxx
话不多说,直接上源码
stl文件的写入还是简单的
//=============================================================================
//function : writeASCII
//purpose :
//=============================================================================
Standard_Boolean RWStl::writeASCII (const Handle(Poly_Triangulation)& theMesh,
FILE* theFile,
const Message_ProgressRange& theProgress)
{
// note that space after 'solid' is necessary for many systems
// 写入文件中
if (fwrite ("solid \n", 1, 7, theFile) != 7)
{
return Standard_False;
}
//这边都是基础知识 应该很好理解,也可以改写为iostream的方式读写
char aBuffer[512];
memset (aBuffer, 0, sizeof(aBuffer));
//获取三角面的个数
const Standard_Integer NBTriangles = theMesh->NbTriangles();
//这玩意是个进度条,没什么用 不用管,除非有进度可视化需求
Message_ProgressScope aPS (theProgress, "Triangles", NBTriangles);
//创建了一个保存顶点id的数组
Standard_Integer anElem[3] = {0, 0, 0};
for (Standard_Integer aTriIter = 1; aTriIter <= NBTriangles; ++aTriIter)
{
const Poly_Triangle aTriangle = theMesh->Triangle (aTriIter);
//获取面上的顶点id
aTriangle.Get (anElem[0], anElem[1], anElem[2]);
const gp_Pnt aP1 = theMesh->Node (anElem[0]);
const gp_Pnt aP2 = theMesh->Node (anElem[1]);
const gp_Pnt aP3 = theMesh->Node (anElem[2]);
//两条边用来计算法向量
const gp_Vec aVec1 (aP1, aP2);
const gp_Vec aVec2 (aP1, aP3);
//得到法向量
gp_Vec aVNorm = aVec1.Crossed (aVec2);
if (aVNorm.SquareMagnitude() > gp::Resolution())
{
aVNorm.Normalize();
}
else
{
aVNorm.SetCoord (0.0, 0.0, 0.0);
}
//写入面的数据,stl的格式可以自行搜索一下 stl的格式非常的简单
Sprintf (aBuffer,
" facet normal % 12e % 12e % 12e\n"
" outer loop\n"
" vertex % 12e % 12e % 12e\n"
" vertex % 12e % 12e % 12e\n"
" vertex % 12e % 12e % 12e\n"
" endloop\n"
" endfacet\n",
aVNorm.X(), aVNorm.Y(), aVNorm.Z(),
aP1.X(), aP1.Y(), aP1.Z(),
aP2.X(), aP2.Y(), aP2.Z(),
aP3.X(), aP3.Y(), aP3.Z());
if (fprintf (theFile, "%s", aBuffer) < 0)
{
return Standard_False;
}
//每当处理了1000个三角面的时候 更新一次进度条
if ((aTriIter % IND_THRESHOLD) == 0)
{
if (!aPS.More())
return Standard_False;
aPS.Next(IND_THRESHOLD);
}
}
//stl文件的结束符
if (fwrite ("endsolid\n", 1, 9, theFile) != 9)
{
return Standard_False;
}
return Standard_True;
}
好啦,至此整体的源码就已经读完了,如果还有不懂的可以评论区评论,或者自己读读源码咯。