既然你已经理解了 glTF、glb,试着用 Python 来创建它

什么是glTF?

  • 3D扫描数据通常存储在OBJ、PLY、STL、FBX等格式中。
  • 但这些不包括场景的结构和渲染方法
  • 由于点云数据采用所谓的“表格格式”,它可以为每个点提供属性,但很少有格式考虑为表面设置属性
  • 像Maya这样的3D建模工具可以在.max.blend中存储场景结构、光照设置、摄像机、动画和3D对象,如.马、3ds Max和Blend中的.blend。
  • 然而,这些需要专门的导入器加载器转换器
  • glTF定义了3D内容表示的标准规范,包括以下内容

特色

  • 用 JSON 编写,简洁且易于分析
  • 3D对象本身以普通工具可读取的格式保存

基本结构

  • 3D场景的全部内容都存储在JSON
  • 它被描述为场景结构,并具有定义场景图的节点层级结构。
  • 场景中显示的3D对象使用连接在节点上的网格
  • 材质、动画、骨骼和摄像机也被定义。

各元素概述

  • ​scene​​是描述存储在glTF中场景的入门点。 指向定义场景图的节点
  • ​node​​是场景图层级中的一个节点。 它可以包含变换(例如旋转或平移),还可以指代(子)节点。 此外,你还可以提及“附加”在节点上的网格或摄像实例,或者描述网格变体的皮肤
  • ​camera​​定义渲染场景的视图配置
  • ​mesh​​描述场景中出现的几何对象。 指用于访问实际几何数据的访问对象,以及定义渲染时物体外观的材质
  • ​skin​​定义顶点蒙皮所需的参数,允许你根据虚拟角色的姿态来变换网格。 这些参数的值由访问器获得
  • ​animation​​描述特定节点的变形(如旋转或平移)随时间的变化
  • ​accessor​​作为任何数据的抽象源。它被用于网格、皮肤和动画中,提供几何数据、蒙皮参数和时间相关的动画值。这里指的是bufferView,它是包含实际原始二进制数据的缓冲区的一部分
  • ​material​​包含定义物体外观的参数。通常,它指的是应用于渲染几何体的纹理对象
  • ​texture​​由采样器和图像定义。采样器定义了纹理图像如何放置在物体上
  • ​buffer​​和 是二元化并外部定义的(.bin・.jpg/.png)​​image​
  • ​buffer​​表示几何形状
  • ​image​​表示纹理

最小的glTF资产

基于上述,以下是最小的glTF:
它有层级结构,从场景中延伸而来,所以即使是最基本的结构也有些复杂。

{
  "scene": 0,
  "scenes" : [
    {
      "nodes" : [ 0 ]
    }
  ],
  
  "nodes" : [
    {
      "mesh" : 0
    }
  ],
  
  "meshes" : [
    {
      "primitives" : [ {
        "attributes" : {
          "POSITION" : 1
        },
        "indices" : 0
      } ]
    }
  ],

  "buffers" : [
    {
      "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
      "byteLength" : 44
    }
  ],
  "bufferViews" : [
    {
      "buffer" : 0,
      "byteOffset" : 0,
      "byteLength" : 6,
      "target" : 34963
    },
    {
      "buffer" : 0,
      "byteOffset" : 8,
      "byteLength" : 36,
      "target" : 34962
    }
  ],
  "accessors" : [
    {
      "bufferView" : 0,
      "byteOffset" : 0,
      "componentType" : 5123,
      "count" : 3,
      "type" : "SCALAR",
      "max" : [ 2 ],
      "min" : [ 0 ]
    },
    {
      "bufferView" : 1,
      "byteOffset" : 0,
      "componentType" : 5126,
      "count" : 3,
      "type" : "VEC3",
      "max" : [ 1.0, 1.0, 0.0 ],
      "min" : [ 0.0, 0.0, 0.0 ]
    }
  ],
  
  "asset" : {
    "version" : "2.0"
  }
}

各关的说明

  • 场景与节点
  • scene: 0表示它首先读取场景数组的第0个索引
  • scenes 有一个包含节点对象索引的数组,并进一步表明它获得了第 0 个节点索引
"scene": 0,
  "scenes" : [
    {
      "nodes" : [ 0 ]
    }
  ],
  
  "nodes" : [
    {
      "mesh" : 0
    }
  ],
  • 网格
  • 展示三维几何对象
  • 网格通常会显得臃肿,所以它们通常只显示 mesh.primitive 对象数组,但在示例中,我们直接在网格内写入 mesh.primitives
  • mesh.primitive 有属性和索引,属性有一个 POSITION 属性表示顶点
  • 索引描述一个指向顶点的索引
"meshes" : [
    {
      "primitives" : [ {
        "attributes" : {
          "POSITION" : 1
        },
        "indices" : 0
      } ]
    }
  ],
  • 缓冲区
  • 缓冲区指的是原始的、非结构化的数据块。
  • URI 属性表示外部文件或 JSON 文件中的二进制数据
  • 换句话说,URI 属性是否包含顶点和三角形的信息?
  • 表示有一个缓冲区包含44字节
"buffers" : [
    {
      "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
      "byteLength" : 44
    }
  ],
  • 缓冲视图
  • bufferViews 描述缓冲区的视图
  • 由于两者的缓冲区都是0,它们指定相同的缓冲区,但指定的字节不同。
"bufferViews" : [
    {
      "buffer" : 0,
      "byteOffset" : 0,
      "byteLength" : 6,
      "target" : 34963
    },
    {
      "buffer" : 0,
      "byteOffset" : 8,
      "byteLength" : 36,
      "target" : 34962
    }
  ],
  • 访问器
  • 指定数据类型和布局,并定义缓冲视图应如何解释。
"accessors" : [
    {
      "bufferView" : 0,
      "byteOffset" : 0,
      "componentType" : 5123,
      "count" : 3,
      "type" : "SCALAR",
      "max" : [ 2 ],
      "min" : [ 0 ]
    },
    {
      "bufferView" : 1,
      "byteOffset" : 0,
      "componentType" : 5126,
      "count" : 3,
      "type" : "VEC3",
      "max" : [ 1.0, 1.0, 0.0 ],
      "min" : [ 0.0, 0.0, 0.0 ]
    }
  ],
  • 此外,原语还可以通过访问器的索引来引用
"meshes" : [
    {
      "primitives" : [ {
        "attributes" : {
          "POSITION" : 1
        },
        "indices" : 0
      } ]
    }
  ],
  • 事实上,它的定义更为复杂,但目前,这就是

结论

因此,如果你想创建自己的 glTF......
- 节点从 scene
引用 - node 指网格
- mesh.primitives 指缓冲区
- 缓冲区定义为三维对象
的二进制 - bufferView 定义了要读取
的范围——如果访问器告诉你如何读取
,它将是一个文件

试试用Python来做

  • 如果你用 Python 的 pygltflib 做几乎一模一样的东西,它看起来是这样。
from pygltflib import *

gltf = GLTF2()
scene = Scene()
mesh = Mesh()
primitive = Primitive()
node = Node()
buffer = Buffer()
bufferView1 = BufferView()
bufferView2 = BufferView()
accessor1 = Accessor()
accessor2 = Accessor()

buffer.uri = "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA="
buffer.byteLength = 44

bufferView1.buffer = 0
bufferView1.byteOffset = 0
bufferView1.byteLength = 6
bufferView1.target = ELEMENT_ARRAY_BUFFER

bufferView2.buffer = 0
bufferView2.byteOffset = 8
bufferView2.byteLength = 36
bufferView2.target = ARRAY_BUFFER

accessor1.bufferView = 0
accessor1.byteOffset = 0
accessor1.componentType = UNSIGNED_SHORT
accessor1.count = 3
accessor1.type = SCALAR
accessor1.max = [2]
accessor1.min = [0]

accessor2.bufferView = 1
accessor2.byteOffset = 0
accessor2.componentType = FLOAT
accessor2.count = 3
accessor2.type = VEC3
accessor2.max = [1.0, 1.0, 0.0]
accessor2.min = [0.0, 0.0, 0.0]

primitive.attributes.POSITION = 1
node.mesh = 0
scene.nodes = [0]

gltf.scenes.append(scene)
gltf.meshes.append(mesh)
gltf.meshes[0].primitives.append(primitive)
gltf.nodes.append(node)
gltf.buffers.append(buffer)
gltf.bufferViews.append(bufferView1)
gltf.bufferViews.append(bufferView2)
gltf.accessors.append(accessor1)
gltf.accessors.append(accessor2)

gltf.save("triangle.gltf")

从 np.ndarray(3D 领域常用)创建时,画面是这样的
(※ 由于类型也很重要,你需要相应调整)​​vertices​​​​triangles​

import numpy as np
import pygltflib

points = np.array(
    [
        [-0.5, -0.5, 0.5],
        [0.5, -0.5, 0.5],
        [-0.5, 0.5, 0.5],
        [0.5, 0.5, 0.5],
        [0.5, -0.5, -0.5],
        [-0.5, -0.5, -0.5],
        [0.5, 0.5, -0.5],
        [-0.5, 0.5, -0.5],
    ],
    dtype="float32",
)
triangles = np.array(
    [
        [0, 1, 2],
        [3, 2, 1],
        [1, 0, 4],
        [5, 4, 0],
        [3, 1, 6],
        [4, 6, 1],
        [2, 3, 7],
        [6, 7, 3],
        [0, 2, 5],
        [7, 5, 2],
        [5, 7, 4],
        [6, 4, 7],
    ],
    dtype="uint8",
)

triangles_binary_blob = triangles.flatten().tobytes()
points_binary_blob = points.tobytes()
gltf = pygltflib.GLTF2(
    scene=0,
    scenes=[pygltflib.Scene(nodes=[0])],
    nodes=[pygltflib.Node(mesh=0)],
    meshes=[
        pygltflib.Mesh(
            primitives=[
                pygltflib.Primitive(
                    attributes=pygltflib.Attributes(POSITION=1), indices=0
                )
            ]
        )
    ],
    accessors=[
        pygltflib.Accessor(
            bufferView=0,
            componentType=pygltflib.UNSIGNED_BYTE,
            count=triangles.size,
            type=pygltflib.SCALAR,
            max=[int(triangles.max())],
            min=[int(triangles.min())],
        ),
        pygltflib.Accessor(
            bufferView=1,
            componentType=pygltflib.FLOAT,
            count=len(points),
            type=pygltflib.VEC3,
            max=points.max(axis=0).tolist(),
            min=points.min(axis=0).tolist(),
        ),
    ],
    bufferViews=[
        pygltflib.BufferView(
            buffer=0,
            byteLength=len(triangles_binary_blob),
            target=pygltflib.ELEMENT_ARRAY_BUFFER,
        ),
        pygltflib.BufferView(
            buffer=0,
            byteOffset=len(triangles_binary_blob),
            byteLength=len(points_binary_blob),
            target=pygltflib.ARRAY_BUFFER,
        ),
    ],
    buffers=[
        pygltflib.Buffer(
            byteLength=len(triangles_binary_blob) + len(points_binary_blob)
        )
    ],
)
gltf.set_binary_blob(triangles_binary_blob + points_binary_blob)

现在你应该能输出你想要的glTF。
如需更详细的说明,请参阅官方README。

 

扩展(扩展和额外内容)

  • 注释
  • https://github.com/KhronosGroup/glTF/blob/main/extensions/README.md
  • 一些扩展保留为 的前缀中的规范​​KHR_​
  • 示例:向节点添加灯光
"nodes" : [
    {
        "extensions" : {
            "KHR_lights_punctual" : {
                "light" : 0
            }
        }
    }            
]
  • 还有一种自由形式的扩展称为额外扩展,可以添加到任何属性上,如网格、缓冲区、节点等。
"nodes": [
    {
      "extras": {
        "id": "0a4a5478b66849b3890a3d5b1de98e18",
        "name": "\u307f\u3069\u308a\u306e\u7a93\u53e3"
      },
      "mesh": 0
    }
  ],
  • 在Blender里,你可以通过把它加到节点的额外部分来引用它。

结论

如开头所述,大多数3D数据格式不能被视为3D GIS数据,因为它们的功能与表面不同,仅保留表面。​​属性​

不过,glTF允许你通过扩展自由保留属性,所以你可能能做3D GIS分析。

从现在开始,这就是glTF的时代!

posted @ 2026-05-19 09:12  吃饭不忘  阅读(22)  评论(0)    收藏  举报