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

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

- 参考
	- https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_001_Introduction.md

## 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になります。
シーンから降っていく階層構造になっているので、最小限の構造でも多少複雑ですね。

```json
{
  "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番目のインデックスを取得することを示している 
```json
  "scene": 0,
  "scenes" : [
    {
      "nodes" : [ 0 ]
    }
  ],
  
  "nodes" : [
    {
      "mesh" : 0
    }
  ],
```
- **mesh**
	- 3Dの幾何学的オブジェクトのことを示している
	- メッシュは肥大化しがちなので、mesh.primitiveオブジェクトの配列のみを示すことが多いが、サンプルではmeshesの中に直接mesh.primitivesを記述している
	- mesh.primitiveはattributesとindicesを持ち、attributesには頂点を示すPOSITION属性を持っている
	- indicesは頂点を示すインデックスを記述する
```json
 "meshes" : [
    {
      "primitives" : [ {
        "attributes" : {
          "POSITION" : 1
        },
        "indices" : 0
      } ]
    }
  ],
```
- **buffer**
	- bufferは生の構造化されていないデータブロックを示す
	- uri属性は外部ファイルを示すか、JSONファイルのバイナリデータを示す
		- つまり、uri属性に頂点と三角形の情報が入っている？
	- 44バイトを含む1つのバッファがあることを示している
```json
  "buffers" : [
    {
      "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
      "byteLength" : 44
    }
  ],
```
- **bufferViews**
	- bufferViewsはbufferへのビューを記述する
	- どちらもbuffer: 0なので同じbufferを指定しており、指定しているバイトが異なる
```json
  "bufferViews" : [
    {
      "buffer" : 0,
      "byteOffset" : 0,
      "byteLength" : 6,
      "target" : 34963
    },
    {
      "buffer" : 0,
      "byteOffset" : 8,
      "byteLength" : 36,
      "target" : 34962
    }
  ],
```
- **accessor**
	- データ型とレイアウトを指定し、bufferViewをどのように解釈するかを定義する
```json
  "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のインデックスを使って参照することができる
```json
  "meshes" : [
    {
      "primitives" : [ {
        "attributes" : {
          "POSITION" : 1
        },
        "indices" : 0
      } ]
    }
  ],
```
- 実際はもっと複雑に定義されるが、とりあえずここまで

### 結論

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

## pythonでやってみる

- これとほぼ同じものをPythonの[pygltflib](https://pypi.org/project/pygltflib/)で作成しようとするとこんな感じらしい

```Python
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でよく利用される`vertices`と`triangles`のnp.ndarrayから作る場合は以下のようになります
(※型も大事っぽいので合わせる必要がある)

```python
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://pypi.org/project/pygltflib/

## 拡張機能（extensionsとextras）

- 参考
	- https://github.com/KhronosGroup/glTF/blob/main/extensions/README.md
- いくつかのextensionsが`KHR_`のprefixで仕様として予約されている
- 例: ノードにライトを追加する
```json
"nodes" : [
    {
        "extensions" : {
            "KHR_lights_punctual" : {
                "light" : 0
            }
        }
    }            
]
```
- これとは別に**extras**と呼ばれる自由記述の拡張機能があり、meshやbuffer・nodeなどどのプロパティにも追加することができる
```json
  "nodes": [
    {
      "extras": {
        "id": "0a4a5478b66849b3890a3d5b1de98e18",
        "name": "\u307f\u3069\u308a\u306e\u7a93\u53e3"
      },
      "mesh": 0
    }
  ],
```
- blenderではnodeのextrasに追加してあげることで、参照可能となります。

## おわりに

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

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

これからはglTFの時代や！