C++实现STL文件向glTF文件的转换

最近在学习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(x<min_x) min_x=x;
                if(y>max_y) max_y=y;
                if(y<min_y) min_y=y;
                if(z>max_z) max_z=z;
                if(z<min_z) min_z=z;
            }
        }

        //开始生成glTF文件,默认取名为1.gltf,str0和str1存储了accessor中的顶点信息,str2存储了每个顶点的索引信息。
        QFile aFile2("1.gltf");
        if(!(aFile2.open(QIODevice::WriteOnly)))
            return ;
        QString str0 = "{\"accessors\":[{\"name\":\"0_1_0_positions\",\"componentType\":5126,\"count\":";
        QString str1 = QString::number(count*3)+",\"min\":["+QString::number(min_x)+","+QString::number(min_y)+","+QString::number(min_z)+"],\"max\":["+QString::number(max_x)+","+QString::number(max_y)+","+QString::number(max_z)+"],\"type\":\"VEC3\",\"bufferView\":0,\"byteOffset\":0},";
        QString str2 = "{\"name\":\"0_1_0_indices\",\"componentType\":5123,\"count\":"+QString::number(count*3)+",\"min\":[0],\"max\":["+QString::number(count*3-1)+"],\"type\":\"SCALAR\",\"bufferView\":1,\"byteOffset\":0}],";
        QString str3 = "\"asset\":{\"generator\":\"obj2gltf\",\"version\":\"2.0\"},";
        QString str4 = "\"buffers\":[{\"name\":\""+sname+"\",\"byteLength\":";

        //开始生成buffer中的数据,先生成二进制的形式,生成之后存储在databuf这个字符串中
        string databuf;
        while(!aStream2.atEnd())
        {
            QString str=aStream2.readLine();
            string info=str.toStdString();
            //再次读取STL文件,将其中的顶点坐标全部以float形式存储到databuf中
            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();
                databuf.push_back(((BYTE*)&x)[0]);
                databuf.push_back(((BYTE*)&x)[1]);
                databuf.push_back(((BYTE*)&x)[2]);
                databuf.push_back(((BYTE*)&x)[3]);
                databuf.push_back(((BYTE*)&y)[0]);
                databuf.push_back(((BYTE*)&y)[1]);
                databuf.push_back(((BYTE*)&y)[2]);
                databuf.push_back(((BYTE*)&y)[3]);
                databuf.push_back(((BYTE*)&z)[0]);
                databuf.push_back(((BYTE*)&z)[1]);
                databuf.push_back(((BYTE*)&z)[2]);
                databuf.push_back(((BYTE*)&z)[3]);
            }
        }

        int pan_len=databuf.size();  //pan_len代表了顶点坐标即POSITION数据块的长度

        //接着这里将每个顶点的索引数据存储到databuf中
        for(unsigned short i=0; i<count*3;i++)
        {
            BYTE C1,C2;
            C1 = ((BYTE*)&i)[0];
            C2 = ((BYTE*)&i)[1];
            databuf.push_back(C1);
            databuf.push_back(C2);
        }

        //因为要求四字节对齐,所以写了这部分代码
        int datalen = databuf.size();
        BYTE CC = 0X00;
        if(datalen%4==1) {databuf.push_back(CC);databuf.push_back(CC);databuf.push_back(CC);}
        if(datalen%4==2) {databuf.push_back(CC);databuf.push_back(CC);}
        if(datalen%4==3) databuf.push_back(CC);
        datalen = databuf.size();


        const unsigned char* data = const_cast<unsigned char *>((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,欢迎大家前来讨论。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值