19
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

glTFを完全に理解したので、PythonでglTFを作成してみる

Posted at

ごめんなさい調子に乗りました。ただチュートリアルを眺めて要点をまとめてみただけです。

glTFとは

  • 3Dスキャンのデータは一般にOBJ・PLY・STL・FBXなどで保存される
    • が、これらにはシーンの構造・レンダリングの方法などは含まれない
    • 点群データはいわゆる「表形式」であるため、点ごとの属性を持てるが、面に属性を持たせることを考慮している形式はほとんどない
  • 3DモデリングツールのMayaは.ma、3ds Maxは.max、Blenderは.blendなどにシーンの構造・ライトのセットアップ・カメラ・アニメーション・3Dオブジェクト本体を保存できる
    • が、これらは専用のインポーター・ローダー・コンバーターを作成する必要がある
  • glTFはこれら含めた3Dコンテンツ表現のための標準仕様を定義すること

特徴

  • JSONで記述され、コンパクトかつ簡単に解析が可能
  • 3Dオブジェクト自体は一般的なツールで読み取れる形式で保存

基本的な構造

  • 3Dシーンの内容全体がJSONで格納されている
  • ※*シーン構造として記述※*され、シーングラフを定義するノードの階層がある
  • シーンに表示される3Dオブジェクトは、*ノードにアタッチされたメッシュ*が使用される
  • マテリアルやアニメーション・スケルトンやカメラも定義される

各種要素の概要

  • sceneは、glTFに格納されているシーンの記述のエントリーポイントである。シーングラフを定義するノードを指す
  • nodeはシーングラフ階層の1つのノードである。それは、変換(例えば、回転や平行移動)を含むことができ、さらに(子)ノードを参照することができる。さらに、ノードに「アタッチ」されているメッシュやカメラのインスタンス、またはメッシュの変形を記述するスキンを参照することもできます
  • cameraは、シーンをレンダリングするためのビュー構成を定義します
  • meshは、シーンに表示されるジオメトリ オブジェクトを記述します。実際のジオメトリ データへのアクセスに使用されるアクセッサ オブジェクトと、レンダリング時のオブジェクトの外観を定義するマテリアルを参照します
  • skinには、頂点スキニングに必要なパラメータが定義されており、バーチャル キャラクタのポーズに基づいてメッシュを変形させることができます。これらのパラメータの値は、アクセサから取得します
  • animationは、特定のノードの変形(回転や平行移動など)が時間とともにどのように変化するかを記述します
  • accessorは、任意のデータの抽象的なソースとして使用されます。メッシュ、スキン、アニメーションで使用され、ジオメトリデータ、スキニングパラメータ、時間依存のアニメーション値を提供します。これは、実際の生のバイナリデータを含むバッファの一部であるbufferViewを参照します
  • materialは、オブジェクトの外観を定義するパラメータを含んでいます。通常は、レンダリングされたジオメトリに適用されるテクスチャオブジェクトを参照します
  • textureはサンプラーとイメージによって定義されます。サンプラーは、テクスチャイメージをオブジェクト上にどのように配置するかを定義します
  • bufferおよびimageはバイナリ化し、外部(.bin・.jpg/.png)に定義される
  • 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とnode
    • scene: 0はscenes配列の0番目のインデックスを一番最初に読むことを示している
    • scenesにはnodeオブジェクトのインデックスを含む配列があり、さらにnodesの0番目のインデックスを取得することを示している
  "scene": 0,
  "scenes" : [
    {
      "nodes" : [ 0 ]
    }
  ],
  
  "nodes" : [
    {
      "mesh" : 0
    }
  ],
  • mesh
    • 3Dの幾何学的オブジェクトのことを示している
    • メッシュは肥大化しがちなので、mesh.primitiveオブジェクトの配列のみを示すことが多いが、サンプルではmeshesの中に直接mesh.primitivesを記述している
    • mesh.primitiveはattributesとindicesを持ち、attributesには頂点を示すPOSITION属性を持っている
    • indicesは頂点を示すインデックスを記述する
 "meshes" : [
    {
      "primitives" : [ {
        "attributes" : {
          "POSITION" : 1
        },
        "indices" : 0
      } ]
    }
  ],
  • buffer
    • bufferは生の構造化されていないデータブロックを示す
    • uri属性は外部ファイルを示すか、JSONファイルのバイナリデータを示す
      • つまり、uri属性に頂点と三角形の情報が入っている?
    • 44バイトを含む1つのバッファがあることを示している
  "buffers" : [
    {
      "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
      "byteLength" : 44
    }
  ],
  • bufferViews
    • bufferViewsはbufferへのビューを記述する
    • どちらもbuffer: 0なので同じbufferを指定しており、指定しているバイトが異なる
  "bufferViews" : [
    {
      "buffer" : 0,
      "byteOffset" : 0,
      "byteLength" : 6,
      "target" : 34963
    },
    {
      "buffer" : 0,
      "byteOffset" : 8,
      "byteLength" : 36,
      "target" : 34962
    }
  ],
  • accessor
    • データ型とレイアウトを指定し、bufferViewをどのように解釈するかを定義する
  "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 ]
    }
  ],
  • さらに、primitivesはaccessorのインデックスを使って参照することができる
  "meshes" : [
    {
      "primitives" : [ {
        "attributes" : {
          "POSITION" : 1
        },
        "indices" : 0
      } ]
    }
  ],
  • 実際はもっと複雑に定義されるが、とりあえずここまで

結論

結果的に、独自にglTFを作成したいなら…
- sceneからnodeが参照され
- nodeがmeshを参照し
- mesh.primitivesがbufferを参照して
- bufferに3次元オブジェクトをバイナリ化したものが定義して
- bufferViewに読み込む範囲などが定義され
- accessorに読み方が記載されていれば
- ファイルとして成り立ちます

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

3Dでよく利用されるverticestrianglesのnp.ndarrayから作る場合は以下のようになります
(※型も大事っぽいので合わせる必要がある)

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を参照してください。

拡張機能(extensionsとextras)

"nodes" : [
    {
        "extensions" : {
            "KHR_lights_punctual" : {
                "light" : 0
            }
        }
    }            
]
  • これとは別にextrasと呼ばれる自由記述の拡張機能があり、meshやbuffer・nodeなどどのプロパティにも追加することができる
  "nodes": [
    {
      "extras": {
        "id": "0a4a5478b66849b3890a3d5b1de98e18",
        "name": "\u307f\u3069\u308a\u306e\u7a93\u53e3"
      },
      "mesh": 0
    }
  ],
  • blenderではnodeのextrasに追加してあげることで、参照可能となります。

おわりに

冒頭で述べた通り、ほとんどの3Dデータ形式は属性のようなものを持てず、面を保持するだけの機能に留まりますので、3DのGISデータとして取り扱うことはできません。

ただ、glTFであれば拡張機能を利用して自由に属性を保持することが出来るので3次元のGIS解析なども行えるようになるかも知れせん。

これからはglTFの時代や!

19
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?