本文已经实现并开源到【stl2gltf】
stl有二进制和ascii两种格式,这里只考虑二进制的情况
起始有80个字节文件头用于存储文件名,紧接4个字节表示三角形数量,而每个三角面片占用固定的50个字节,3个4字节浮点数(法线矢量),3个4字节浮点数(第一个顶点坐标),3个4字节浮点数(第二个顶点坐标),3个4字节浮点数(第三个顶点坐标),接着2个字节描述三角形基本属性,那么一个完整的二进制STL文件的字节大小就是三角形面数乘50再加上84字节
UINT8[80] – Header // 文件头
UINT32 – Number of triangles // 三角形数量
foreach triangle
REAL32[3] – Normal vector // 法线矢量
REAL32[3] – Vertex 1 // 第一个顶点坐标
REAL32[3] – Vertex 2 // 第二个顶点坐标
REAL32[3] – Vertex 3 // 第三个顶点坐标
UINT16 – Attribute byte count // 文件属性
end
知道格式,读取就方便了。这里我用golang读取到一个STL struct里。其实文件头没什么用,关注面数和每个面的数据就好了。
func Load(filePath string) (stl STL) {
file, err := os.OpenFile(filePath, os.O_RDONLY, os.ModePerm)
if err != nil {
log.Panic("open file " + filePath + " error: " + err.Error())
}
// load 80 header
headerBuf := make([]byte, 80)
file.Read(headerBuf)
// int32
triangleNumBuf := make([]byte, 4)
file.Read(triangleNumBuf)
stl.Header = string(headerBuf)
stl.TriangleNum = int(binary.LittleEndian.Uint32(triangleNumBuf))
stl.Triangles = make([]Triangle, 0)
max := []float32{0, 0, 0}
min := []float32{0, 0, 0}
for range stl.TriangleNum {
normalBuf := make([]byte, 4*3)
file.Read(normalBuf)
positionBuf := make([]byte, 4*3*3)
file.Read(positionBuf)
// find max/min xyz
for r := range 3 {
x := math.Float32frombits(binary.LittleEndian.Uint32(positionBuf[:4+(r*4)]))
y := math.Float32frombits(binary.LittleEndian.Uint32(positionBuf[(r * 4) : (r*4)+4]))
z := math.Float32frombits(binary.LittleEndian.Uint32(positionBuf[(r*4)+4 : (r*4)+8]))
if max[0] < x {
max[0] = x
}
if max[1] < y {
max[1] = y
}
if max[2] < z {
max[2] = z
}
if min[0] > x {
min[0] = x
}
if min[1] > y {
min[1] = y
}
if min[2] > z {
min[2] = z
}
}
attributeBuf := make([]byte, 2)
file.Read(attributeBuf)
stl.Triangles = append(stl.Triangles, Triangle{
Normal: normalBuf,
Position: positionBuf,
Attribute: attributeBuf,
})
}
stl.Max = max
stl.Min = min
return stl
}
读取的时候顺便把各个顶点的最大最小值、法向量给读出来。
接下来需要把这些stl数据转换到gltf格式了。经查,gltf也有两种格式,一种是json格式,顶点数据以外部文件引用的方式或base64嵌入的方式加载。另一种是glb格式,直接把json和二进制数据拼接成一个二进制文件。这里选择生成glb文件。主要是不想多挂接一个bin文件或者base64数据大小膨胀过于厉害。
1、glb分为header和chunk
2、header: macgic值固定为0x46546C67 4B + 版本号 uint32(2) 4B + 文件长度uint32 4B;共 4 + 4 + 4 = 12B
3、chunk 有两种, json chunk 和 bin chunk
4、json chunk: chunk长度(json字节数)4B + chunk类型(0x4E4F534A)4B + json byte[]
5、bin chunk: chunk长度(json字节数)4B + chunk类型(0x004E4942)4B + json byte[]
6、把 header、jsonchunk、bin chunk 拼接在一起,写入到文件内即可。
gltf依靠json来描述如何读取附带的二进制数据。不论gltf还是glb中的json都是同一套格式,glb情况下,buffer字段内不需要提供uri字段。
一个最简单的gltf json
{
// scene表示默认显示scenes中第0个序列的场景
"scene": 0,
// 解析从scenes开始,可以包含多个场景,每个scene由nodes组成
"scenes" : [
{
// 这个场景只包含一个nodes,对应nodes第0个序列
"nodes" : [ 0 ]
}
],
"nodes" : [
{
// 这个nodes只包含一个mesh网格,对应meshes第0个序列,mesh表示一个真正的几何物体
"mesh" : 0
}
],
"meshes" : [
{
"primitives" : [ {
"attributes" : {
// 1表示对应bufferViews[1]的数据,描述的是顶点坐标数据
"POSITION" : 1
},
// 0表示对应bufferViews[0]的数据,描述的是顶点索引数据
"indices" : 0
} ]
}
],
// buffers是存储3D物体真实数据的地方,也可以引用外部文件的方式指定
"buffers" : [
{
// 这里直接将顶点索引和顶点坐标数据通过base64编码存放到uri中,具体十六进制表示见后面的图片展示
"uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
// 表示数据共占用44字节
"byteLength" : 44
}
],
// bufferViews表示如何分割buffers中的数据
"bufferViews" : [
{
// 对应buffers第0个序列
"buffer" : 0,
// 字节偏移为0
"byteOffset" : 0,
// 字节长度为6字节
"byteLength" : 6,
// 34963对应WebGL中的缓存数据类型ELEMENT_ARRAY_BUFFER
"target" : 34963
},
{
// 对应buffers第0个序列
"buffer" : 0,
// 字节偏移为8
"byteOffset" : 8,
// 字节长度为36字节
"byteLength" : 36,
// 34962对应WebGL中的缓存数据类型ARRAY_BUFFER
"target" : 34962
}
],
// accessors表示如何解析bufferViews中的数据
"accessors" : [
{
// 对应bufferViewss第0个序列
"bufferView" : 0,
// 字节偏移量
"byteOffset" : 0,
// 5123表示UNSIGNED_SHORT,占用两个字节
"componentType" : 5123,
// 表示这个bufferView表示的数据共包含3组数据
"count" : 3,
// 每个数据类型为标量
"type" : "SCALAR",
// 最大值为2
"max" : [ 2 ],
// 最小值为0
"min" : [ 0 ]
},
{
// 对应bufferViewss第1个序列
"bufferView" : 1,
// 字节偏移量
"byteOffset" : 0,
// 5126表示float,占用四个字节
"componentType" : 5126,
// 表示这个bufferView表示的数据共包含3组数据
"count" : 3,
// 每个数据类型为VEC3,三维向量
"type" : "VEC3",
// 最大值和最小值,表达的是几何体的包围盒空间
"max" : [ 1.0, 1.0, 0.0 ],
"min" : [ 0.0, 0.0, 0.0 ]
}
],
"asset" : {
// gltf版本号
"version" : "2.0"
}
}