最近在学习Cesium这个平台,了解到Cesium只支持glTF格式三维模型的可视化,因此手撕了一下STL文件向glTF文件转换的代码,整个过程在Qt中用C++实现。
将STL文件转换为glTF文件,首先要分别了解二者的文件格式。
STL文件如下图示:
它以三角面为单元进行描述,每个三角面以facet normal开始,以endfacet结束,其中facet normal描述当前面的法向量,vertex描述三角面的三个顶点坐标,可以看出STL文件格式还是非常简单的。
而glTF文件就相对复杂很多,可能是因为它要成为3D模型界的JPG格式,所以其中内容必然要包罗万象。对于glTF格式,我也是初学,我给大家推荐一下我所学的博客https://zhuanlan.zhihu.com/p/65265611
然后我再说一下我自己学习中的心得:glTF文件中可以包含的东西很多,比如:camera,skin,animation,position,normal,material等等,但我们只需要找出其中必须的参数,不需要的参数一概不要,以此为目标来构建一个最简单的glTF文件。正如这里所讲:https://zhuanlan.zhihu.com/p/65267849
一个简单的三角单元的glTF文件中只描述了顶点编号和顶点位置信息,那我们可类比,在STL文件向glTF文件转换中,我们只保留STL文件中的顶点信息,至于每个三角面的法向量信息,咱们就不要了,所以对于STL文件咱们只需要读取有"vertex"的那一行。
下面我们只需要看看glTF文件是怎么存储顶点编号和顶点位置信息的。我们依然是从这个网址中学习:https://zhuanlan.zhihu.com/p/65267849
那么这段内容包含了一个scene对象,这一scene对象引用了一个索引为0的node对象,这个node对象又引用了索引为0的mesh对象,mesh对象里又说明数据只包含"indices"索引和“POSITION”位置两种信息。并没有找到如何存储数据,所以咱们继续往下看。
从博主的介绍中可以看出,buffer对象存储了数据。它可以通过"uri":"000.bin"这种形式存储,也可以通过
"uri":"data:application/octet-stream;base64,AABBCCDDEEFFGG(base64编码)";
这种形式直接将数据存储于uri的后面,只不过数据并不是直接以二进制块的形式写,而是进行了base64编码,因为人家规定了数据必须进行base64编码之后才能放在里面,那么我们后面就以这种方法生成glTF文件。但值得注意的是,这里说的byteLength指的是二进制数据块(base64编码)之前的长度。
后面的bufferviews,顾名思义:“观察buffer的一个view”。这里面描述了buffer中的数据是如何排列的,这里的bufferview里面有两块内容,第一块内容说我这里的数据的存储位置(byteoffset)从0开始,长度总共是6,数据的使用方式是ELEMENT_ARRAY_BUFFER(34963),第二块内容说我这里的数据的存储位置从8开始,长度是36,数据的使用方式是ARRAY_BUFFER(34962)。可以看到这块内容说明了存储位置和存储长度,但感觉描述的还不够,每块数据有多少个元素?每个元素多少个字节长都没说,所以咱们继续往后看。
那么它在accessor中说了,他说到:我的bufferview0(即bufferview中的第一个大括号)里面说到的数据,都是unsigned short(5123)类型,有3个, 类型都是标量(SCALAR),其中的最大值是2,最小值是0;我的bufferview1(即bufferview中的第二个大括号)里面说到的数据,都是float(5126)类型,数量也是3,类型是3D向量(VEC3),最大最小值也说了。
至此,我们对一个最简单的glTF格式一清二楚,将STL文件转换为glTF文件的思路也水到渠成:首先读取STL文件中所有顶点的坐标和顶点的个数,其中顶点坐标为float形式,顶点索引为unsigned short形式,然后将顶点坐标和索引生成二进制数据块,并进行base64编码,再以文本文件读写的方法写入scene、node、mesh、buffer、bufferview、accessor和asset等信息即可。 按照这个思路,代码如下文示:
void MainWindow::on_actSTLToGLTF_triggered()
{
//获取STL文件的路径
QString fileName = QFileDialog::getOpenFileName(this,
QString("Open STL File"), ".",
QString("STL File(*.stl)"));
int count=0;
if (!fileName.isEmpty())
{
QFile aFile(fileName);
QFile aFile1(fileName);
if(!aFile.exists())
return ;
if(!aFile.open(QIODevice::ReadOnly|QIODevice::Text))
return ;
if(!aFile1.exists())
return ;
if(!aFile1.open(QIODevice::ReadOnly|QIODevice::Text))
return ;
QTextStream aStream(&aFile);
aStream.setAutoDetectUnicode(true);
QTextStream aStream2(&aFile1);
aStream2.setAutoDetectUnicode(true);
QFileInfo fileinfo=QFileInfo(fileName);
QString sname = fileinfo.fileName();
sname = sname.section(".",0,0); //sname为STL的文件名
float max_x=0,max_y=0,max_z=0;
float min_x=10000,min_y=10000,min_z=10000;
//前文是一些读取的准备工作,可以从这开始看
//开始读取STL文件
while(!aStream.atEnd())
{
QString str=aStream.readLine(); //按行读取
string info=str.toStdString();
if(str.contains("endfacet")) count++; //count记录了其中三角面的数量
//读取包含顶点vertex的行,并记录其中的最大x,y,z 最小x,y,z. 用于生成glTF文件
if(str.contains("vertex"))
{
int pos = info.find_first_of(" ");
string temp = info.substr(pos+1, info.size()-1-pos);
QString tt = QString::fromStdString(temp);
float x = (tt.section(" ",0,0)).toFloat();
float y = (tt.section(" ",1,1)).toFloat();
float z = (tt.section(" ",2,2)).toFloat();
if(x>max_x) max_x=x;
if(xmax_y) max_y=y;
if(ymax_z) max_z=z;
if(z((const unsigned char*)databuf.c_str());
string encoded;
encoded = Encode(data,databuf.length()); //将databuf中的二进制数据块进行base64编码,编码后的数据放到encoded字符串里
//写剩余的bufferview,mesh和node中的内容
QString str5 = QString::number(databuf.length())+",\"uri\":\"data:application/octet-stream;base64,"+QString::fromStdString(encoded);
QString str6 = "\"}],\"bufferViews\":[{\"name\":\"bufferView_0\",\"buffer\":0,\"byteLength\":"+QString::number(pan_len)+",\"byteOffset\":0,\"target\":34962},";
QString str7 = "{\"name\":\"bufferView_1\",\"buffer\":0,\"byteLength\":"+QString::number(datalen-pan_len)+",\"byteOffset\":"+QString::number(pan_len)+",\"target\":34963}],";
QString str8 = "\"materials\":[{\"name\":\"default\",\"pbrMetallicRoughness\":{\"baseColorFactor\":[0.5,0.0,0.0,1],\"metallicFactor\":0,\"roughnessFactor\":1},\"emissiveFactor\":[0,0,0],\"alphaMode\":\"OPAQUE\",\"doubleSided\":false}],";
QString str9 = "\"meshes\":[{\"name\":\"0_1\",\"primitives\":[{\"attributes\":{\"POSITION\":0},\"indices\":1,\"material\":0,\"mode\":4}]}],";
QString str10 = "\"nodes\":[{\"name\":\"0\",\"mesh\":0}],\"scene\":0,\"scenes\":[{\"nodes\":[0]}]}";
aFile2.write((str0+str1+str2+str3+str4+str5+str6+str7+str8+str9+str10).toUtf8());
aFile2.close();
}
}
//BASE64编码代码
string MainWindow::Encode(const unsigned char * str,int bytes)
{
std::string _encode_result;
const unsigned char * current;
current = str;
while(bytes > 2) {
_encode_result += _base64_table[current[0] >> 2];
_encode_result += _base64_table[((current[0] & 0x03) << 4) + (current[1] >> 4)];
_encode_result += _base64_table[((current[1] & 0x0f) << 2) + (current[2] >> 6)];
_encode_result += _base64_table[current[2] & 0x3f];
current += 3;
bytes -= 3;
}
if(bytes > 0)
{
_encode_result += _base64_table[current[0] >> 2];
if(bytes%3 == 1) {
_encode_result += _base64_table[(current[0] & 0x03) << 4];
_encode_result += "==";
} else if(bytes%3 == 2) {
_encode_result += _base64_table[((current[0] & 0x03) << 4) + (current[1] >> 4)];
_encode_result += _base64_table[(current[1] & 0x0f) << 2];
_encode_result += "=";
}
}
return _encode_result;
}
下面有一个我将白色金具的STL文件转换为红色金具的glTF文件的效果图。
因为我是在QT里面做的,所以里面代码都充满了浓厚的QT气息。我觉得用到的主要是三个知识:1、QT中读写文件;2、BASE64编码;3、int、float等类型数据分解成字节的形式存储到字符串中。大家可以看看,如果有没说清楚的地方,大家可以留言,我看到会更新。
总的来说,主要是搞懂gltf文件格式就很容易做了,我觉得难度还挺大,花了我一个星期的时间,所以大家做时莫急。
QQ:1826380364,欢迎大家前来讨论。