Houdini(Python 3)でBlender Python API


<font color="Red">**※Blender 2.93以降ではPython 3.9が使用されていてこの記事の方法では動かなくなっています。**</font>
[Blender 2.93: Python API](https://wiki.blender.org/wiki/Reference/Release_Notes/2.93/Python_API)

#Houdini 18.5でPython 3がProduction Buildsに
[Production Buildsの項目](https://www.sidefx.com/download/daily-builds/old-installer/#category-gold)にHoudini 18.5 - Python 3が追加されました。
Python3が使えるようになったのでPython3系にしかないライブラリが使えるようになったはず！
なので今回はPython3.7で使用できるbpy(Blender Python)をHoudiniに入れてみます。

使用したのは以下のWindows版のHoudiniです。
houdini-py3-18.5.351-win64-vc141.exe

# bpyをビルド
Python Moduleとしてのbpyは現状配布されていないので以下を参考にソースコードからビルドする必要があります。
[Building Blender as a Python Module](https://wiki.blender.org/wiki/Building_Blender/Other/BlenderAsPyModule)

## ビルド環境を整備
Blenderをソースコードからビルドする日本語記事は検索すればすぐ見つかると思いますのでより詳しい情報が必要な場合はそちらを探してみてください。

 * Visual Studio 2019
 * CMake
 * Git
 * Subversion

あたりがインストールされていれば多分大丈夫なはず・・・？

## ソースコードを落としてくる
https://developer.blender.org/diffusion/B/ を参考にソースコードを落としてきます。
githubのミラーからだとzipで落とせると思います。

## ビルドする
ソースコードを展開すると以下の用のディレクトリ構成になっていると思います。
![blender_build.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/1e584dd3-2823-f9b3-8c95-7be28159d64c.png)

このディレクトリ(make.batがある)でコマンドプロンプトを開いて

```
make bpy
```

を実行するとビルドに必要な他のライブラリをSubversionで落としてきてビルドされます
(開始してすぐに必要な他のライブラリを落として良いか聞かれるので y を押して進めます)。
初回はすごく時間がかかります。

## ビルド成果物をHoudiniで読めるように配置
ビルド成果物は
(blenderのソースコードのディレクトリ)\..\build_windows_Bpy_x64_vc16_Release\bin\Release
にあります。

### bpy.pyd, libfftw3-3.dll
この２つはHoudiniのpythonが読めるディレクトリに配置します。
必要なdllはblenderのバージョンが変わると増減するかもしれないです。
ここでは以下に配置しました。
`C:\Users\(ユーザー名)\Documents\houdini18.5\python3.7libs`

![37libs.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/11d2f8df-5b91-01ce-3c54-8e49536f05e1.png)

### 2.90(ビルドしたblenderのバージョン)フォルダ
このフォルダはインストールしたHoudiniのexeがあるディレクトリに配置します。
環境変数を設定すると別の場所でも良くなるかもしれません(未検証)。
ここでは以下に配置しました。
`C:\Program Files\Side Effects Software\Houdini 18.5.351\bin`
![bin.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/067dba80-66a0-1c7c-ff82-99b97b7b17b3.png)

# Blender Python API を使う

## import bpyの確認
やっと使用準備が整いました。Python ShellやPython(SOP)などで`import bpy`がエラーなく実行できることを確認します。ビルド成果物や配置場所に問題があるとエラーを出したりHoudiniが落ちたりします。

![python_shell.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/72d10ab7-e499-145f-a5f0-d23f6746d6e8.png)

## .blendファイルを読み込む
Python(SOP)で.blendファイルを読み込む処理を作成します。

### テストデータの用意
キューブとスザンヌを配置した.blendを用意しました。
スザンヌは半分に切ってMirror, Subdivisionモディファイアを設定しています。
![test_data.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/ae4a27f2-416a-a948-a900-af2d17449d56.png)

###  Python(SOP)でPythonコードを書く
Primitiveアトリビュートでname
Vertexアトリビュートで N, uv
を読み込むコードを書きました。

パラメータは以下の通りです。

| Label | Name | 説明 |
|:----------:|:------------:|:------------:|
| Blender File      | blend_file     | 読み込む.blendの指定         |
| Object Name       | objects        | 読み込むオブジェクトの指定         |
| Apply Modifier    | apply_modifier | モディファイアを適用するかの指定         |



![houdini.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/8fa3bb14-3354-d520-ff6c-e1a266034ae9.png)

```python
import os
import bpy
from bpy_extras.io_utils import axis_conversion

node = hou.pwd()
geo = node.geometry()

# 設定するアトリビュートを準備 
name_attrib = geo.addAttrib(hou.attribType.Prim, 'name', '')
normal_attrib = geo.addAttrib(hou.attribType.Vertex, 'N', (0.0, 0.0, 0.0))
uv_attrib = geo.addAttrib(hou.attribType.Vertex, 'uv', (0.0, 0.0, 0.0))

# パラメータの読み取り 
blend_file = node.parm('blend_file').evalAsString()
apply_modifier = node.parm('apply_modifier').evalAsInt()
object_names = [ s.strip() for s in node.parm('objects').evalAsString().split() ]

if len(blend_file)>0:
    # .blendを開く
    bpy.ops.wm.open_mainfile(filepath=blend_file)
    # 名前が設定されてなければすべてのオブジェクトを読む 
    if len(object_names)==0:
        object_names = bpy.data.objects.keys()
else:
    # blenderの初期ファイルを開いてスザンヌを出す
    bpy.ops.wm.read_homefile(app_template='')
    bpy.ops.mesh.primitive_monkey_add()
    object_names = ['Suzanne']

depsgraph = bpy.context.evaluated_depsgraph_get()
# blenderの軸の向きをHoudiniの軸の向きに変換する行列
axis_conv_mat = axis_conversion(
    from_forward='-Y', from_up='Z',  
    to_forward='Z', to_up='Y'
    ).to_4x4()
    
# 指定された名前のオブジェクトを開く 
for obj_name in object_names:
    obj = bpy.data.objects[obj_name]
    if obj.type!='MESH':
        continue
    
    # モディファイアを必要なら適用
    ob_for_convert = obj.evaluated_get(depsgraph) if apply_modifier else obj.original

    # オブジェクトからメッシュを取り出す
    try:
        me = ob_for_convert.to_mesh()
    except:
        me = None
    if me is None:
        continue
        
    # 軸変換とオブジェクトのトランスフォームをメッシュに適用
    me.transform( axis_conv_mat @ obj.matrix_world )
    
    # マイナスのスケールがかかっている場合の対応
    if obj.matrix_world.determinant() < 0.0:
        me.flip_normals()
    
    # Vertex Normalを計算する
    me.calc_normals_split()

    # UVのデータを取得
    uv_layer = me.uv_layers.active.data[:] if len(me.uv_layers) > 0 else None

    # ポイントを作成 
    points = [ hou.Vector3(v.co) for v in me.vertices ]
    pt_list = geo.createPoints(points)

    # blenderとhoudiniでポリゴンの頂点順が異なるので変換する 
    loop_indices_list = list()
    for mpoly in me.polygons:
        count = len(mpoly.loop_indices)
        loop_indices_list.append( [ mpoly.loop_indices[(count-i)%count] for i in range(0, count) ] )

    for loop_indices in loop_indices_list:
        poly = geo.createPolygon()
        poly.setAttribValue(name_attrib, obj_name)
        for i in loop_indices:
            # ポリゴンを作る 
            v = poly.addVertex( pt_list[ me.loops[i].vertex_index ] )
            # N attribute 
            v.setAttribValue(normal_attrib,  me.loops[i].normal)
            # uv attribute 
            if uv_layer:
                uv = uv_layer[i].uv
                v.setAttribValue(uv_attrib, (uv[0], uv[1], 0.0))
```

Object Nameのパラメータは以下のようにMenu Scriptで▽から選べるようにしました。

```python
import os
import bpy

name_list = list()

node = hou.pwd()
blend_file = node.parm('blend_file').evalAsString()
# ファイルの存在チェック 
if not os.path.exists(blend_file):
    return name_list

# Meshのオブジェクトの名前を列挙
objects = bpy.data.objects
for obj in objects:
    if obj.type == 'MESH':
        name_list.extend( [obj.name]*2 )

return name_list
```

コードにコメントを書いているので詳細な説明は省略します。
Blender Python APIでメッシュ情報を取得するコードは
`(Blenderインストールディレクトリ)\(Blenderバージョン)\scripts\addons`
以下でio_で始まるものがImporter/Exporterのアドオンなのでそちらが参考になります。
↑のコードはio_scene_objを参考に書きました。

## その他
bpyで.blendファイルを読み込んだ際に以下のようなログが出力されます。
これを外から止める方法は分かりませんでした。
![houdini_console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/e467c167-5130-a83c-0138-fa3d32723c4f.png)

どうしても邪魔だという時はbpyをビルドする際にこのログ出力コードをコメントアウトすれば出力されなくなります。

```c:source\blender\blenkernel\intern\blendfile.c
int BKE_blendfile_read(bContext *C,
                       const char *filepath,
                       const struct BlendFileReadParams *params,
                       ReportList *reports)
{
  BlendFileData *bfd;
  bool success = false;

  /* Don't print startup file loading. */
  if (params->is_startup == false) {
    printf("Read blend: %s\n", filepath);
  }
  ...
}

```