Blenderのデータ構造を辿り、fbxから頂点座標を抽出する

　Blenderを使って、FBXデータから頂点座標のみを抜き出し配列化するスクリプトを作成したので、解説がてら紹介します。

# Blenderのデータ構造

　まずは見慣れたこの図（初期状態）について。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/b89b4ebf-6e80-a635-b88d-af8e8a93c23d.png)

　さて、Blenderの3Dビューは以下のような形で管理されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/1e92b571-61bb-8f86-0856-af1aab37d996.png)

　Pythonスクリプトで言うと

```Python
bpy.context.scene.collection
```

　にあたるところです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/ef7ea6ba-9d13-c93b-b457-8d2d2b49bcdf.png)

　GUIでいうとここですね。

　大元にシーン　→　コレクションがあり、コレクションの中にオブジェクトがあります。シーンとコレクションの違いはよくわかりません。ただ、オブジェクト（いわゆるキューブやカメラ等）はデータとして存在するものを、リンクという紐づけによってコレクションに呼んでいます。つまり、ビューに現れるのはリンクされたオブジェクトだけで、**見えなくともデータとして存在するオブジェクトもありえる**、ということです。こうすることで同じデータを使いまわせたりするようです。操作する方としてはややこしいですけど。

　そしてオブジェクトと同様に、それに付随するメッシュやマテリアルにもデータリストがあって、それをリンクすることによって必要な分だけ使う、ということになります。

　例えばここで、

```Python
bpy.data.objects.new("hoge", bpy.data.meshes.new("fuga"))
```

　という命令をすると、見かけ上のシーンには何も変化が起きませんが、

```Python
for i in bpy.data.objects:
    print(i)
```
　で、

```Python:Output
<bpy_struct, Object("Camera")>
<bpy_struct, Object("Cube")>
<bpy_struct, Object("hoge")>
<bpy_struct, Object("Light")>
```

　が返ってくるので、`hoge`という謎のオブジェクトが追加されていることがわかります。また、

```Python
bpy.data.objects['hoge'].to_mesh()
```

　で、

```Python:Output
bpy.data.meshes['fuga']
```

　と返ってくるので、このオブジェクトは`fuga`というメッシュにリンクされていることがわかります。もちろんこれはただ名前をつけただけの空データですので、今ここで`hoge`をコレクションにリンクしても、何も頂点を持たない名前だけの無が召喚されます。

　メッシュについて詳しく見ています。メッシュの頂点情報は以下のような形で管理されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/2fd8d393-d1d4-c8c1-534b-90eabed82cf8.png)

　オブジェクトがメッシュをリンクしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/9306bba7-b4c0-af01-d73b-1e0b54ca7e24.png)

　GUIでいうとここですね。

　メッシュデータが更に頂点データを格納しているので、頂点の情報が欲しい場合はそこにアクセスすることになります。今度はちゃんと形のあるキューブを対象にやっていきましょう。

```Python
bpy.data.objects['Cube'].to_mesh().vertices
```

　これは上の絵をほぼそのままなぞったものです。

```Python:Output
bpy.data.meshes['Cube'].vertices
```

　この中身を見ていくと、

```Python
for i in bpy.data.meshes['Cube'].vertices:
    print(i)
```
```Python:Output
<bpy_struct, MeshVertex at 0x0000024A9FA28038>
<bpy_struct, MeshVertex at 0x0000024A9FA2804C>
<bpy_struct, MeshVertex at 0x0000024A9FA28060>
<bpy_struct, MeshVertex at 0x0000024A9FA28074>
<bpy_struct, MeshVertex at 0x0000024A9FA28088>
<bpy_struct, MeshVertex at 0x0000024A9FA2809C>
<bpy_struct, MeshVertex at 0x0000024A9FA280B0>
<bpy_struct, MeshVertex at 0x0000024A9FA280C4>
```

　とひたすらバイナリデータを返されます。ちゃんと8個あるのでキューブで間違いなさそうですが、できれば数値を返してほしいところです。[リファレンス](https://docs.blender.org/api/current/bpy.types.MeshVertex.html#bpy.types.MeshVertex)を見ると`co`という属性で数値が得られることがわかります。

```Python
for i in bpy.data.meshes['Cube'].vertices:
    print(i.co)
```
```Python:Output
<Vector (1.0000, 1.0000, 1.0000)>
<Vector (1.0000, 1.0000, -1.0000)>
<Vector (1.0000, -1.0000, 1.0000)>
<Vector (1.0000, -1.0000, -1.0000)>
<Vector (-1.0000, 1.0000, 1.0000)>
<Vector (-1.0000, 1.0000, -1.0000)>
<Vector (-1.0000, -1.0000, 1.0000)>
<Vector (-1.0000, -1.0000, -1.0000)>
```

　これでやっと頂点座標の配列が得られました。

#fbxに適用

　インポートしたfbxについても同じことをやっていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/53bd1454-e172-7ad2-ff16-66207a92e497.png)

　原理的には、すべてのメッシュデータに対して頂点を取り出せば上と同じ形が得られるはずです。

```Python
for i in bpy.data.meshes:
    print(i)
```
```Python:Output
<bpy_struct, Mesh("Cube")>
<bpy_struct, Mesh("Mesh")>
<bpy_struct, Mesh("Mesh.001")>
<bpy_struct, Mesh("Mesh.002")>
<bpy_struct, Mesh("Mesh.003")>
<bpy_struct, Mesh("Mesh.004")>
<bpy_struct, Mesh("Mesh.005")>
<bpy_struct, Mesh("Mesh.006")>
<bpy_struct, Mesh("Mesh.007")>
```

　……。謎の`Cube`があります。
　そう、**Blenderを立ち上げた時に最初に存在するキューブ**。あれはシーンから削除（正確にはアンリンク）しても中のデータは消えないのです。このままではキューブの頂点まで拾ってしまいます。消えてもらいましょう。

```Python
bpy.data.meshes.remove(bpy.data.meshes['Cube'])
for i in bpy.data.meshes:
    print(i)
```
```Python:Output
<bpy_struct, Mesh("Mesh")>
<bpy_struct, Mesh("Mesh.001")>
<bpy_struct, Mesh("Mesh.002")>
<bpy_struct, Mesh("Mesh.003")>
<bpy_struct, Mesh("Mesh.004")>
<bpy_struct, Mesh("Mesh.005")>
<bpy_struct, Mesh("Mesh.006")>
<bpy_struct, Mesh("Mesh.007")>
```
　
　消えました。
　後はこれらのメッシュデータすべてに対し頂点座標を抽出するようなスクリプトを書きます。

```Python
import bpy
import numpy as np

vts = []

for m in bpy.data.meshes:
    for v in m.vertices:
        vts.append(v.co)

vts = np.array(vts)

```

　`co`で得られる数字が`Vector`という型なので、扱いやすくするため`np.array`で変換しています。

　ここで得られた配列が、本当に元の形を表しているのかを検証します。頂点が3次元情報を正確に保持しているなら、単に散布図にするだけで元の形を表してくれるはずです。

　先ほど得られた配列`vts`を

```Python
np.savetxt(r'任意のパス\vts.txt', vts)
```

　でエクスポートします。別のPythonファイルで（Blenderの内蔵Pythonにはmatplotlibがないため……）散布図を描画します。

```Python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

data = np.loadtxt('vts.txt')

X = data[:,0]
Y = data[:,1]
Z = data[:,2]

fig = plt.figure()
ax = Axes3D(fig)
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.plot(X,Y,Z,marker="o",linestyle='None',ms=0.2)

plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/b3f6e89c-654b-e665-bced-35afccddce5f.png)

　元の3Dデータの頂点を正確に配列化できていることがわかりました。