DengQN·一个普通程序员;
STL文件解析及转换到GLTF
2024-06-07 01:36 447
#STL#GLTF#3d

实现

本文已经实现并开源到【stl2gltf】

STL

数据规范

stl有二进制和ascii两种格式,这里只考虑二进制的情况

起始有80个字节文件头用于存储文件名,紧接4个字节表示三角形数量,而每个三角面片占用固定的50个字节,3个4字节浮点数(法线矢量),3个4字节浮点数(第一个顶点坐标),3个4字节浮点数(第二个顶点坐标),3个4字节浮点数(第三个顶点坐标),接着2个字节描述三角形基本属性,那么一个完整的二进制STL文件的字节大小就是三角形面数乘50再加上84字节

复制代码
UINT8[80]Header             // 文件头
UINT32Number of triangles   // 三角形数量

foreach triangle
REAL32[3]Normal vector      // 法线矢量
REAL32[3]Vertex 1           // 第一个顶点坐标
REAL32[3]Vertex 2           // 第二个顶点坐标
REAL32[3]Vertex 3           // 第三个顶点坐标
UINT16Attribute byte count  // 文件属性
end

解析读取

知道格式,读取就方便了。这里我用golang读取到一个STL struct里。其实文件头没什么用,关注面数和每个面的数据就好了。

go 复制代码
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
}

读取的时候顺便把各个顶点的最大最小值、法向量给读出来。

GLTF

接下来需要把这些stl数据转换到gltf格式了。经查,gltf也有两种格式,一种是json格式,顶点数据以外部文件引用的方式或base64嵌入的方式加载。另一种是glb格式,直接把json和二进制数据拼接成一个二进制文件。这里选择生成glb文件。主要是不想多挂接一个bin文件或者base64数据大小膨胀过于厉害。

glb文件数据规范

image.png
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依靠json来描述如何读取附带的二进制数据。不论gltf还是glb中的json都是同一套格式,glb情况下,buffer字段内不需要提供uri字段。

一个最简单的gltf json

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"
  }
}