STL文件是一种用许多空间小三角形面片逼近三维实体表面的3D模型。STL模型给出了组成三角形法向量的3个分量(用于确定三角面片的正反方向)及三角形的3个顶点坐标。一个完整的STL文件记录了组成实体模型的所有三角形面片的法向量数据和顶点坐标数据信息。STL文件格式包括二进制文件(BINARY)和文本文件(ASCII)两种。
二进制STL文件用固定的字节数给出三角面片的几何信息。文件起始的80个字节是文件头,用于存储文件名;紧接着用4个字节的整数来描述模型的三角面片个数,后面逐个给出每个三角面片的几何信息。每个三角面片占用固定的50个字节,依次是3个4字节浮点数(面片的法向量),3个4字节浮点数(第1个顶点的坐标),3个4字节浮点数(第2个顶点的坐标),3个4字节浮点数(第3个顶点的坐标),最后2个字节用来描述三角面片的属性信息。一个完整二进制STL文件的大小为三角形面片数乘以50再加上84个字节,总共134个字节。
ASCII码格式的STL文件逐行给出三角面片的几何信息,每一行以1个或2个关键字开头。在STL文件中的三角面片的信息单元facet 是一个带矢量方向的三角面片,STL三维模型就是由一系列这样的三角面片构成。整个STL文件的首行给出了文件路径或文件名。在一个STL文件中,每一个 facet 由7行数据组成,facet normal 是三角面片指向实体外部的法矢量坐标,outer loop说明随后的3行数据分别是三角面片的3个顶点坐标,3顶点沿指向实体外部的法矢量方向逆时针排列。
ASCII格式的STL文件结构如下:
通过对STL两种文件格式的分析可知,二进制格式文件较小(通常是ASCII码格式的1/5),节省文件存储空间,而ASCII码格式的文件可读性更强,更容易进行进一步的数据处理。
读取和保存需要的数据结构定义如下,代码文件Mesh.h
:
#pragma once
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// 数据结构----顶点、法向、三角形
typedef cv::Point3f Vertex;
typedef cv::Point3f Normal;
struct Tri
{
int v1;
int v2;
int v3;
};
struct VtxIdxSortItem {
int i;
Vertex value;
};
class Mesh
{
public:
std::vector<Vertex> vtx; /// 顶点
std::vector<Tri> tris; /// 三角面片
std::vector<Normal> vtxNrm; /// 顶点法向
std::vector<Normal> faceNrm; /// 面法向
cv::Point3f center; /// 模型中心
float radius; /// 模型半径
std::vector<Vertex> mkh_joints;
std::vector<Vertex> scan_joints;
std::string modelname;
std::string scan_joint_path;
public:
Mesh();
~Mesh();
// 解析STL模型
bool readSTL_ASCII(std::string cfilename);
bool saveSTL_ASCII(const char *filename, std::vector<Vertex>&vtx, std::vector<Tri> &tris, std::vector<Normal> &nor);
void UnifyDuplicatedVertices(vector<Vertex> &vtx, vector<Tri> &tris);
bool readSTL_Binary(std::string cfilename);
bool Mesh::saveSTL_Binary(const char *filename, std::vector<Vertex>&vtx, std::vector<Tri> &tris, std::vector<Normal> &nor);
};
下面代码是读取和保存Binary
格式的代码,代码文件Mesh.cpp
:
bool Mesh::readSTL_Binary(std::string fileName)
{
ifstream ifs(fileName.c_str(), ios::binary);
if (!ifs)
{
ifs.close();
cout << "read stl error" << endl;
return false;
}
vtx.clear();
tris.clear();
int intSize = sizeof(int);
int floatSize = sizeof(float);
ifs.ignore(80);
// 面的个数
int num_tris;
ifs.read((char*)(&num_tris), intSize);
cout << "面片数量:" << num_tris << endl;
float tn0, tn1, tn2;
float v0, v1, v2;
float cx = 0.0, cy = 0.0, cz = 0.0;
for (int i = 0; i < num_tris; i++)
{
ifs.read((char*)(&tn0), floatSize);
ifs.read((char*)(&tn1), floatSize);
ifs.read((char*)(&tn2), floatSize);
//如果模型进行坐标变换,需要重新计算法向量
// faceNrm.push_back(Normal(tn0, tn1, -tn2));
// 01-STL model
ifs.read((char*)(&v0), floatSize);
ifs.read((char*)(&v1), floatSize);
ifs.read((char*)(&v2), floatSize);
vtx.push_back(Vertex(v0, v1, v2));
cx += v0; cy += v1; cz += v2;
ifs.read((char*)(&v0), floatSize);
ifs.read((char*)(&v1), floatSize);
ifs.read((char*)(&v2), floatSize);
vtx.push_back(Vertex(v0, v1, v2));
cx += v0; cy += v1; cz += v2;
ifs.read((char*)(&v0), floatSize);
ifs.read((char*)(&v1), floatSize);
ifs.read((char*)(&v2), floatSize);
vtx.push_back(Vertex(v0, v1, v2));
cx += v0; cy += v1; cz += v2;
// 建立面片索引,确定顶点顺序
Tri tri;
tri.v1 = i * 3 + 0;
tri.v2 = i * 3 + 1;
tri.v3 = i * 3 + 2;
tris.push_back(tri);
ifs.ignore(2);
}
ifs.close();
// 重新计算面片法向量
if (0 == vtxNrm.size())
{
for (int i = 0; i < tris.size(); i++)
{
Vertex v12, v23;
v12 = vtx[tris[i].v2] - vtx[tris[i].v1];
v23 = vtx[tris[i].v3] - vtx[tris[i].v2];
Normal faceN;
faceN = v12.cross(v23);
faceN /= sqrt(faceN.dot(faceN));
faceNrm.push_back(faceN);
}
}
else
{
for (int i = 0; i < tris.size(); i++)
{
Normal faceN;
faceN = vtxNrm[tris[i].v1] + vtxNrm[tris[i].v2] + vtxNrm[tris[i].v3];
faceN /= sqrt(faceN.dot(faceN));
faceNrm.push_back(faceN);
}
}
// 计算中心位置
center.x = cx / (num_tris * 3);
center.y = cy / (num_tris * 3);
center.z = cz / (num_tris * 3);
//计算半径
radius = 0;
for (int i = 0; i < vtx.size(); i++)
{
vtx[i] = vtx[i] - center;
float lens;
lens = sqrt((vtx[i]).dot(vtx[i]));
if (lens > radius)
{
radius = lens;
}
}
center.x = 0;
center.y = 0;
center.z = 0;
return true;
}
bool Mesh::saveSTL_Binary(const char *filename, std::vector<Vertex>&vtx, std::vector<Tri> &tris, std::vector<Normal> &nor)
{
ofstream fs(filename, ios::binary);
if (!fs) { fs.close(); return false; }
int intSize = sizeof(int);
int floatSize = sizeof(float);
// 文件头
char* fileHead = " ";
fs.write(fileHead, sizeof(char) * 3);
// 附加信息
char fileInfo[77];
for (int i = 0; i<77; i++)
fileInfo[i] = ' ';
fs.write(fileInfo, sizeof(char) * 77);
// 面的个数
int num_tris = int(tris.size());
fs.write((char*)(&num_tris), intSize);
// 点列表,面列表
char a[2];
streamsize a_size = sizeof(char) * 2;
Normal tn;
for (int i = 0; i<num_tris; i++)
{
int PIndex0 = tris[i].v1;
int PIndex1 = tris[i].v2;
int PIndex2 = tris[i].v3;
Vertex P0 = vtx[PIndex0];
Vertex P1 = vtx[PIndex1];
Vertex P2 = vtx[PIndex2];
Normal N0 = nor[i];
//Normal N1 = faceNrm[i].y;
//Normal N2 = faceNrm[i].z;
//tn = N0 + N1 + N2;
fs.write((char*)(&(N0.x)), floatSize);
fs.write((char*)(&(N0.y)), floatSize);
fs.write((char*)(&(N0.z)), floatSize);
// 保存顶点
fs.write((char*)(&(P0.x)), floatSize);
fs.write((char*)(&(P0.y)), floatSize);
fs.write((char*)(&(P0.z)), floatSize);
fs.write((char*)(&(P1.x)), floatSize);
fs.write((char*)(&(P1.y)), floatSize);
fs.write((char*)(&(P1.z)), floatSize);
fs.write((char*)(&(P2.x)), floatSize);
fs.write((char*)(&(P2.y)), floatSize);
fs.write((char*)(&(P2.z)), floatSize);
fs.write(a, a_size);
}
fs.close();
return true;
}
下面代码是读取和保存ASCII
格式的代码,代码文件Mesh.cpp
:
bool Mesh::readSTL_ASCII(std::string fileName)
{
ifstream ifs(fileName.c_str(), ios::binary);
if (!ifs)
{
ifs.close();
cout << "read stl error" << endl;
return false;
}
vtx.clear();
tris.clear();
int intSize = sizeof(int);
int floatSize = sizeof(float);
float tn0, tn1, tn2;
float v0, v1, v2;
float cx = 0.0, cy = 0.0, cz = 0.0;
string name1, name2;
ifs >> name1 >> name2;
//cout << "name: " << name1 << " " << name2 << endl;
int j = 3, t = 0;
while (!ifs.eof()) // ifs.good()
{
string temp2;
ifs >> temp2;
//cout << "temp2: " << temp2 << endl;
if (temp2 == "facet")
{
string temp_normal;
ifs >> temp_normal;
ifs >> tn0 >> tn1 >> tn2;
//cout << "normal: " << tn0 << " " << tn1 << " " << tn2 << endl;
Normal faceN;
faceN.x = tn0;
faceN.y = tn1;
faceN.z = tn2;
faceNrm.push_back(faceN);
ifs.ignore(11);
string temp;
ifs >> temp;
//cout << "SSS:" << temp << endl;
while (temp == "vertex")
{
//cout << "=========================" << endl;
ifs >> v0 >> v1 >> v2;
//cout << "vertex: " << v0 << " " << v1 << " " << v2 << endl;
vtx.push_back(Vertex(v0, v1, v2));
cx += v0;
cy += v1;
cz += v2;
ifs >> temp;
//cout << "temp: " << temp << endl;
}
//cout << "end1: " << temp << endl;
ifs >> temp;
//cout << "end2: " << temp << endl;
{
Tri tri;
tri.v1 = t * 3 + 0;
tri.v2 = t * 3 + 1;
tri.v3 = t * 3 + 2;
tris.push_back(tri);
t = t + 1;
}
}
cout << "end string " << temp2 << endl;
}
ifs.close();
// 计算模型的中心位置
center.x = cx / (tris.size() * 3);
center.y = cy / (tris.size() * 3);
center.z = cz / (tris.size() * 3);
//计算模型的半径
radius = 0;
for (int i = 0; i < vtx.size(); i++)
{
vtx[i] = vtx[i] - center;
float lens;
lens = sqrt((vtx[i]).dot(vtx[i]));
if (lens > radius)
{
radius = lens;
}
}
center.x = 0;
center.y = 0;
center.z = 0;
return true;
}
bool Mesh::saveSTL_ASCII(const char *filename, std::vector<Vertex>&vtx, std::vector<Tri> &tris, std::vector<Normal> &nor)
{
// vtx:模型顶点
// tris:三角面片
// nor:法线
ofstream fs(filename);
if (!fs) { fs.close(); return false; }
int i=0;
int intSize = sizeof(int);
int floatSize = sizeof(float);
int num_tris = tris.size();
cout << nor.size() << endl;
cout << num_tris << endl;
fs << "solid WRAP" << endl;
for (i = 0; i<num_tris; i++)
{
fs << "facet normal " << nor[i].x << " " << nor[i].y << " " << nor[i].z << endl;
fs << "outer loop" << endl;
fs << "vertex " << vtx[tris[i].v1].x << " " << vtx[tris[i].v1].y << " " << vtx[tris[i].v1].z << endl;
fs << "vertex " << vtx[tris[i].v2].x << " " << vtx[tris[i].v2].y << " " << vtx[tris[i].v2].z << endl;
fs << "vertex " << vtx[tris[i].v3].x << " " << vtx[tris[i].v3].y << " " << vtx[tris[i].v3].z << endl;
fs << "end loop" << endl;
fs << "end facet" << endl;
}
fs << "endsolid WRAP";
fs.close();
return true;
}