Blender 2.8の低レベルPython APIを利用してオブジェクトを操作する

この記事は[Nextremer Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nextremer)の24日目の記事です(遅刻ですが)。

## あらまし

Blenderは3Dグラフィクス用のモデルデータを作成したりするためのソフトウェアです。ソースコードは公開されておりGPL-v3.0でライセンスされた自由なソフトウェアです。Blenderにはスクリプト言語としてPythonが組み込まれており、UIの操作等を自動化できるようです。

プログラムから操作できるということは、Processingでジェネラティブアートをやるように、オブジェクトをたくさん作って配置したりして遊ぶことができるのではないかしら？ そう思って調べると以下のような記事がでてきます。ありがたいですね。

- [Python for Blender クックブック ～はじめに～](https://qiita.com/KTakahiro1729/items/5de5f60982af37d23aa0) - 
@KTakahiro1729 
- [APIを見てみる - BlenderでPythonスクリプト](https://blender-python-api-tutorial.netlify.com/open_api/)

ところで、オブジェクトの追加方法としては、BlenderのScriptingタブのコンソールで以下のようにやる方法が紹介されているようです。

```py
# ファイルでやる場合は`import bpy`が必要
>>> bpy.ops.mesh.primitive_cube_add()
{'FINISHED'}
>>> 
```

戻り値が`{"FINISHED"}`。ちなみにこれを起動直後のBlenderで行うと、`Scene Collection`>`Collection`以下に`Cube.001`というオブジェクトが追加されます（すでに`Cube`というオブジェクトが置かれているため）。この`Cube.001`の情報を`primitive_cube_add()`の後に変更しようとしたらどうするかというと、こうします。

```py
# Scene Collection内のCollectionの子要素から"Cube.001"という名前で引いてくる
>>> bpy.data.collections.data.objects['Cube.001']
bpy.data.objects['Cube.001']
```

ちなみに`Cube.001`という名前は、もし`Cube.001`が存在する状況で`primitive_cube_add()`をやった後だと`Cube.002`になります。つまり、UIの状態によって名前が異なる。ううむ、なんというか、通常のPythonプログラミング的には、作ったオブジェクトの名前を`primitive_cube_add()`で返してほしい。

ちなみに調べるとどうも`bpy.obs`モジュールは**UIの操作を関数として提供した**モジュールのようです。

であるならば。
きっとUIの操作の中で呼ばれている、もっと低レベルなオブジェクトたちの生成・操作APIがあるに違いないです。

この記事では、そんなBlenderの低レベルなオブジェクト操作APIで遊んでみます。

## 動作環境

この記事の内容はBlenderの公式サイトで配布されているGNU/Linux版のBlender 2.81バイナリを利用しています。

```sh
$ blender -v
Blender 2.81 (sub 16)
        build date: 2019-12-04
        build time: 13:48:07
        build commit date: 2019-12-04
        build commit time: 11:32
        build hash: f1aa4d18d49d
        build platform: Linux
        build type: Release
        build c flags:  -Wall -Wcast-align -Werror=implicit-function-declaration -Werror=return-type -Werror=vla -Wstrict-prototypes -Wmissing-prototypes -Wno-char-subscripts -Wno-unknown-pragmas -Wpointer-arith -Wunused-parameter -Wwrite-strings -Wlogical-op -Wundef -Winit-self -Wmissing-include-dirs -Wno-div-by-zero -Wtype-limits -Wformat-signedness -Wnonnull -Wuninitialized -Wredundant-decls -Wshadow -Wno-error=unused-but-set-variable  -fuse-ld=gold -std=gnu11   -msse -pipe -fPIC -funsigned-char -fno-strict-aliasing -msse2 -D_GLIBCXX_USE_CXX11_ABI=0
        build c++ flags:  -Wredundant-decls -Wall -Wno-invalid-offsetof -Wno-sign-compare -Wlogical-op -Winit-self -Wmissing-include-dirs -Wno-div-by-zero -Wtype-limits -Werror=return-type -Wno-char-subscripts -Wno-unknown-pragmas -Wpointer-arith -Wunused-parameter -Wwrite-strings -Wundef -Wformat-signedness -Wuninitialized -Wundef -Wmissing-declarations  -fuse-ld=gold -std=c++11   -msse -pipe -fPIC -funsigned-char -fno-strict-aliasing -msse2 -D_GLIBCXX_USE_CXX11_ABI=0
        build link flags:  -Wl,--version-script='/home/sources/buildbot-x86_64-slave/linux_glibc217_x86_64_cmake/blender.git/source/creator/blender.map'
        build system: CMake
```

## オブジェクトを生成する

というわけでまずはBlenderのあの猿のSuzanneを低レベルAPIによって召喚しましょう。コンソールでこうやります。

```py
# blenderの生メッシュ情報を操作するモジュールをインポートしてBMeshオブジェクトを作成
>>> import bmesh
>>> bm = bmesh.new()

# Suzanneのメッシュデータをbmにセット
>>> bmesh.ops.create_monkey(bm)
... # なんか名前頂点データがぶわーって出る

# bmに設定された実際のBMeshオブジェクト
>>> bm
<BMesh(0x7f40f970ba08), totvert=507, totedge=1005, totface=500, totloop=1968>

# 表示用のメッシュオブジェクトをつくり、BMeshをmeshに変換して設定する
>>> mesh = bpy.data.meshes.new('suzanne')
>>> bm.to_mesh(mesh)
>>> bm.free()  # いっぱい作るなら解放したほうがいいっぽい

# 表示用の*オブジェクト*オブジェクトをmeshを指定して作成する
>>> obj = bpy.data.objects.new('suzanne', mesh)

# 現在のコンテキストのコレクションのオブジェクトリストにリンク（登録）する（と表示される）
>>> bpy.context.collection.objects.link(obj)
```

結果、こうなります（デフォルトで置かれていた箱は削除の上）。

![blender-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109205/914caa9e-c866-3d8a-299a-23b2fb8fc0a0.png)

ちなみにUIの操作としてのSuzanne召喚は`bpy.ops.mesh.primitive_monkey_add()`でできます([API doc](https://docs.blender.org/api/current/bpy.ops.mesh.html#bpy.ops.mesh.primitive_monkey_add)より)。

## 内部のことを若干想像

以上の操作とPython APIドキュメント( https://docs.blender.org/api/current/bpy.types.html や https://docs.blender.org/api/current/bmesh.html )などから、なんとなくBlenderのデータモデルっぽいものが推測できます。

- 頂点データ等を直接編集できるオブジェクト(BMesh)と、実際にBlenderで利用するオブジェクト(MeshやText)は分かれている
    - BMeshオブジェクトはBlender自身のメッシュ編集ツールが使うC APIに従う感じらしい…？
- オブジェクトは`bpy.data.objects`や`bpy.data.lights`など、`bpy.data` ([BlendDataのフィールド](https://docs.blender.org/api/current/bpy.types.BlendData.html))に`link(obj)`されるとBlenderから見えるようになる
- Blenderのアプリケーションの内部状態は`bpy.context`に保持されている
- Blenderのオブジェクトプールは`bpy.data`の各フィールド。種類別に入っている
- Blenderのオブジェクトのベースクラスは`bpy_struct`
- なんかたまに`bpy_struct`以外に`ID`クラスを継承したクラスがある
    - このクラスのインスタンスは`bpy.data.objects.new(name, object_data)`の第2引数に渡すことができる
    - たとえば`TextCurve` https://docs.blender.org/api/current/bpy.types.TextCurve.html
    - たとえば`Text` https://docs.blender.org/api/current/bpy.types.Text.html
    - しかしそのままインスタンスつくろうとしてもなんかエラーになる
    - 推測: Blenderの内部はEntity Component Systemアーキテクチャでつくられており、`ID`はEntityとコンポーネントに対応するのでは…？

どのようにつくられているのかソースコードを読んでみると、楽しいかもしれません。

## 文字を色をつけて表示する

```py
# フォントをロードする
font = bpy.data.fonts.load('/home/grey/.fonts/en/Nobile/Nobile-Regular.ttf')

# コレクションをつくる
col = bpy.data.collections.new('nils')
bpy.context.scene.collection.children.link(col)

# オブジェクトに設定するマテリアル(色=黒)を用意
mat = bpy.data.materials.new(name="black")
mat.diffuse_color = (0, 0, 0, 1.0)

# FontCurveオブジェクトを作成
text_curve = bpy.data.curves.new(type='FONT', name='fontcurve')
nil = bpy.data.objects.new('nil', text_curve)

# 文字列本体やフォントやマテリアルを設定
nil.data.body = 'nil'
nil.data.font = font
nil.data.materials.append(mat)

# 角度や場所を指定して文字を置く関数
def put_nil(x, y, z):
    obj = nil.copy()
    obj.location = (x, y, z)
    obj.rotation_euler = (0, 0, PI/2)
    col.objects.link(obj)

# 実際に文字を置く
put_nil(0, 0, 0)
```

その結果は[こちら](https://twitter.com/sin_clav/status/1206926781866860544)。

## たくさん`()` (空リスト)を置く

```py
# たくさんnilを並べるだけの関数
def nils(xr, yr, zr):
    for x in xr:
        for y in yr:
            for z in zr:
                put_nil(x, y, z)

# それはもうたくさん並べる空リスト
nils(
    [x/1.1 for x in range(-6, 6)],
    [y/1.8 for y in range(-5, 7)],
    [z/15 for z in range(-5, 5)]
)
```

`put_nil()`関数を呼ぶ部分をこのようにすると、ぼくが[技術書典8用につくったサークルカット用画像](https://twitter.com/sin_clav/status/1206954316470177792)ができあがります。