Blenderを使って、FBXデータから頂点座標のみを抜き出し配列化するスクリプトを作成したので、解説がてら紹介します。
Blenderのデータ構造
まずは見慣れたこの図(初期状態)について。
さて、Blenderの3Dビューは以下のような形で管理されています。
Pythonスクリプトで言うと
bpy.context.scene.collection
にあたるところです。
GUIでいうとここですね。
大元にシーン → コレクションがあり、コレクションの中にオブジェクトがあります。シーンとコレクションの違いはよくわかりません。ただ、オブジェクト(いわゆるキューブやカメラ等)はデータとして存在するものを、リンクという紐づけによってコレクションに呼んでいます。つまり、ビューに現れるのはリンクされたオブジェクトだけで、見えなくともデータとして存在するオブジェクトもありえる、ということです。こうすることで同じデータを使いまわせたりするようです。操作する方としてはややこしいですけど。
そしてオブジェクトと同様に、それに付随するメッシュやマテリアルにもデータリストがあって、それをリンクすることによって必要な分だけ使う、ということになります。
例えばここで、
bpy.data.objects.new("hoge", bpy.data.meshes.new("fuga"))
という命令をすると、見かけ上のシーンには何も変化が起きませんが、
for i in bpy.data.objects:
print(i)
で、
<bpy_struct, Object("Camera")>
<bpy_struct, Object("Cube")>
<bpy_struct, Object("hoge")>
<bpy_struct, Object("Light")>
が返ってくるので、hoge
という謎のオブジェクトが追加されていることがわかります。また、
bpy.data.objects['hoge'].to_mesh()
で、
bpy.data.meshes['fuga']
と返ってくるので、このオブジェクトはfuga
というメッシュにリンクされていることがわかります。もちろんこれはただ名前をつけただけの空データですので、今ここでhoge
をコレクションにリンクしても、何も頂点を持たない名前だけの無が召喚されます。
メッシュについて詳しく見ています。メッシュの頂点情報は以下のような形で管理されています。
オブジェクトがメッシュをリンクしています。
GUIでいうとここですね。
メッシュデータが更に頂点データを格納しているので、頂点の情報が欲しい場合はそこにアクセスすることになります。今度はちゃんと形のあるキューブを対象にやっていきましょう。
bpy.data.objects['Cube'].to_mesh().vertices
これは上の絵をほぼそのままなぞったものです。
bpy.data.meshes['Cube'].vertices
この中身を見ていくと、
for i in bpy.data.meshes['Cube'].vertices:
print(i)
<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個あるのでキューブで間違いなさそうですが、できれば数値を返してほしいところです。リファレンスを見るとco
という属性で数値が得られることがわかります。
for i in bpy.data.meshes['Cube'].vertices:
print(i.co)
<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についても同じことをやっていきます。
原理的には、すべてのメッシュデータに対して頂点を取り出せば上と同じ形が得られるはずです。
for i in bpy.data.meshes:
print(i)
<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を立ち上げた時に最初に存在するキューブ。あれはシーンから削除(正確にはアンリンク)しても中のデータは消えないのです。このままではキューブの頂点まで拾ってしまいます。消えてもらいましょう。
bpy.data.meshes.remove(bpy.data.meshes['Cube'])
for i in bpy.data.meshes:
print(i)
<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")>
消えました。
後はこれらのメッシュデータすべてに対し頂点座標を抽出するようなスクリプトを書きます。
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
を
np.savetxt(r'任意のパス\vts.txt', vts)
でエクスポートします。別のPythonファイルで(Blenderの内蔵Pythonにはmatplotlibがないため……)散布図を描画します。
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()
元の3Dデータの頂点を正確に配列化できていることがわかりました。