NetworkEditorをPythonで操作する

この記事は、[Houdiniアドベントカレンダー2017](https://qiita.com/advent-calendar/2017/houdini) 3日目の記事です。

## 前置き

本屋の技術書コーナーで、たまに見かける分厚いHoudini本が気になっていたのですが、手に取ったが最後、どっぷりHoudiniの世界にはまってしまいました。

さて、Houdiniはノードエディタ（NetworkEditor）の使い勝手がいいものの、ノードを手で配置していくとレイアウトが気になり始めます。ノードの量が増えてきて、ノード間の接続が込み入ってくると、ノードをあちこちに動かすのにストレスを感じるようになります。

そんなわけで、NetworkEditor内をPython使って制御してみようというのが、本記事の趣旨になります。


## NetworkEditorとは
とりあえず英語でマニュアル検索する時に、特定の部分の正式名称とかが結構大事になってきたりします。
「えーと、あのノードを操作する部分の名前って、ノードエディタだっけなんだっけ」という状態では、Google先生に聞いてもまともな回答が返ってこないのです。

今回調べた所、ノードを追加して操作する部分（赤で囲った部分）の正式名称は、Network Editorということでした。
![all_2.png](https://qiita-image-store.s3.amazonaws.com/0/32778/2f8cdc08-6069-88f8-b6bb-be59749275ad.png)

正式名称がわかれば、Pythonのマニュアルも検索できます。「Houdini NetworkEditor」で検索すると、公式のPythonマニュアルが出てきます。
[hou.NetworkEditor class](http://www.sidefx.com/docs/houdini/hom/hou/NetworkEditor.html)　というページのようですね。

## この記事で伝えたい内容
1. 階層の移動
1. フラッシュメッセージ表示
1. 背景画像表示
1. 座標系とノードの配置操作


## 動作させた環境
Houdini FX 16.5.268 (HOUDINI APPRENTICE)
OS: Windows 7

## 念のため
これからの記事は、ドキュメントを適当に斜め読みして、動かして得られた結果から勝手に考察しているだけですので、正しいかどうかの保証はありませんのでご注意ください。

## 1. 階層の移動
まずは、誰もが通る基本操作をPythonで実現してみます。

+ File>Newで新規シーンを開く
+ Geometryオペレーターを作成する
+ ダブルクリックでSOPレベルに移る
+ SOPレベルの不要なノードを全部削除する

```python:basic.py
import hou  # この行がないと、「houなんて名前は知らん」とエラーが出る
ne = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor) # Network Editorでの操作に使う

hou.hipFile.clear() # 「File>Newで新規シーンを開く」の操作（セーブしていないシーンがあれば、ここでダイアログが出る）
geonode = hou.node('/obj/').createNode('geo', 'geo') # 「Geometryオペレーターを作成する」の操作
ne.cd('/obj/' + geonode.name()) # 「ダブルクリックでSOPレベルに移る」の操作
geonode.deleteItems(geonode.children()) # 「SOPレベルの不要なノードを全部削除する」の操作
```

Pythonスクリプトの実行は、Python Source Editorを使ってください。メニューの、Windows>Python Source Editorから開くことができます。

![script_1.png](https://qiita-image-store.s3.amazonaws.com/0/32778/1ba298a3-7bf3-fca3-5b52-9e04e4618ea6.png)

実行すると、「SOPレベルの不要なノードを全部削除する」の後の状態になります。実行後のNetwork Editorのスクリーンショットですが、赤丸の部分がSOPレベルに移動後であることがわかります。

![script_1_result.PNG](https://qiita-image-store.s3.amazonaws.com/0/32778/07ed82cd-4a1e-6698-8d46-28c319454234.png) 


一点注意があって、ApplyボタンでPythonスクリプトが動くのですが、hou.hipFile.clear()を実行すると、処理が完了するものの、Python Source Editorからコードが消えてしまいます。それが嫌な場合は、hou.hipFile.clear()をコメントアウトしておきましょう。

## 2. フラッシュメッセージ表示
次は小ネタです。
Network Editor内に、フラッシュメッセージ（何秒間か出て、消えるメッセージ）を表示させることができます。



```python:flashMessage1.py
import hou
ne = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)

ne.flashMessage(None, "このようなメッセージが出て消えます！", 2) # 2秒間メッセージを表示する
```

Pythonスクリプトの実行すると、下のスクリーンキャプチャのようにメッセージが出て消えます。

![art_2.gif](https://qiita-image-store.s3.amazonaws.com/0/32778/57b00caf-9c48-6aad-9022-b437ab30d875.gif)

もう一つ、画像も表示できるので、やってみましょう。

```python:flashMessage2.py
import hou
ne = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)

ne.flashMessage("F:/houdini/icon.png", "画像も出るよ！", 2) # 2秒間メッセージを表示する（画像ファイルはフォルダから直接指定）
```

![art_3.gif](https://qiita-image-store.s3.amazonaws.com/0/32778/c72f18c1-407b-df74-3411-52b2de9a4ad3.gif)


テキストメッセージとは違い、画像はパッと消える感じですが、使う機会があればどうぞ使ってみてください。

※画像ファイルは適当に用意して、適当なフォルダに格納して、上記パスは適宜変更してください。

## 3. 背景画像表示
もう１つ小ネタです。
Network Editorの背景画像を表示させることができます。
公式で案内されているサンプルはこちら[hou.NetworkImage class](http://www.sidefx.com/docs/houdini/hom/hou/NetworkImage.html)です。

機能としては、Network Editorの、「Create a new background image」ですね。

```python:backgroundimage.py
import hou
ne = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)

image = hou.NetworkImage() # networkimageのインスタンスを作成
image.setPath('F:/houdini/article/icon.png') # 画像ファイルのパスを指定
image.setRect(hou.BoundingRect(-2,-2,1,1)) # Network Editor内の座標を指定
ne.setBackgroundImages([image]) # 画像をセット
```
座標位置については次の座標系の項で紹介しますので、こちらでは割愛します。

![all_3_4.PNG](https://qiita-image-store.s3.amazonaws.com/0/32778/580a6f31-77b8-7e5e-aa82-33e5c3215e3a.png)


背景画像が表示されました。

## 4. 座標系とノードの配置操作

では次に、大物のNetwork Editorにノードを自動で配置するにはどうするかを見ていきましょう。

### 座標系を調査する

まず、配置するための座標系について確認しておきます。
さきほどの「階層の移動」で作ったPythonスクリプトを実行した後の状態から、適当にboxオペレーターを２つほど配置して、その後座標の数値を取得するためのプログラムを実行してみましょう。

```python:positioncheck1.py
import hou
ne = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)

geonode = hou.node('/obj/geo') # geometryオペレータがある前提
geonode.deleteItems(geonode.children()) # お掃除しておく
box1 = hou.node('/obj/geo').createNode("box") # geometryの下にboxオペレーターを作る
box2 = hou.node('/obj/geo').createNode("box")

print ne.visibleBounds()  # ネットワークスペース内の、画面に見えている部分の範囲のBoundingRectを返す
print box1.position() # Network Editor内の現在のpositionを返す
print box2.position()
```

Pythonスクリプトを実行すると、Network Editor内にboxオペレーターが作られるとともに、print文の結果を表示するための、Houdini Consoleが出て来ます。

![console.PNG](https://qiita-image-store.s3.amazonaws.com/0/32778/885059d0-04d1-809a-51db-0d08ff5e400c.png)

![all_3.PNG](https://qiita-image-store.s3.amazonaws.com/0/32778/714a2231-39af-2f70-4bc2-27deee7d68b8.png)

表示内容を見てみると、visibleBoundsの出力結果は、マイナスのx,y座標(-2.87838, -1.48518)と、プラスのx.y(3.87838, 1.87518)座標が表示されていることがわかります。

※ne.visibleBounds()の戻り値は、個人の環境によって差があると思います。

また、Pythonスクリプトでnode(オペレーター)を作成すると、座標としては、どちらも(0,0)に配置されているようです。

これらの結果から、OSが持っている画面の座標系とは関係なく、原点からマイナス方向にもプラス方向にも無限に広がる座標系を使っているようです。画面上、(0,0)に配置されているオペレーターが真ん中に表示されていることと、画面に見えている範囲をvisibleBoundsの結果として得られることがわかりました。

(0,0)だと、マイナス方向が上下左右のどこかわかりませんので、配置場所を動かしてみます。

```python:positioncheck2.py
import hou
ne = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)

geonode = hou.node('/obj/geo') # geometryオペレータがある前提
geonode.deleteItems(geonode.children()) # お掃除しておく
box1 = hou.node('/obj/geo').createNode("box") # geometryの下にboxオペレーターを作る
box2 = hou.node('/obj/geo').createNode("box")

box1.setPosition(hou.Vector2(-2,-2)) # 座標を(-2,-2)にセット
box2.setPosition(hou.Vector2(1,1)) # 座標を(1,1)にセット
```
![all_3_2.PNG](https://qiita-image-store.s3.amazonaws.com/0/32778/fa32947d-3f4a-1801-fc25-4a6a0f6bb641.png)

あれ、右上のbox2は表示されていますが、左下には微妙に青い丸の一部だけが見えている状態ですね。どうやら、右上がプラスになる座標系であることがわかりました。

### Home Allっぽいことをやってみる

Network Edtior上で、キーボードショートカットがデフォルトの状態だと、キーボードの「H」キーを押すと、自動的に拡大縮小して、全ノードが画面上に入るようになります（キーボードショートカットでは、Home Allという名前になっています）

全く同じ機能ではないのですが、setVisibleBounds()というメソッドを使うと、表示範囲を変えられるので、こちらを使って試してみましょう。



```python:setvisible1.py
import hou
ne = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)

geonode = hou.node('/obj/geo') # geometryオペレータがある前提
geonode.deleteItems(geonode.children()) # お掃除しておく
box1 = hou.node('/obj/geo').createNode("box") # geometryの下にboxオペレーターを作る
box2 = hou.node('/obj/geo').createNode("box")

box1.setPosition(hou.Vector2(-2,-2)) # 座標を(-2,-2)にセット
box2.setPosition(hou.Vector2(1,1)) # 座標を(1,1)にセット

pos1 = box1.position() # 座標を取得
pos2 = box2.position()

# 表示領域を設定する。配置した位置ぎりぎりからマージンを取っている。最後の0.2はアニメーションのスピード
ne.setVisibleBounds(
    hou.BoundingRect(
        pos1 - hou.Vector2(1,1),
        pos2 + hou.Vector2(1,1)), 
        0.2)
```

Pythonスクリプトを実行すると、いい感じにアニメーションしながら、配置範囲が変更されましたね！

![art_4.gif](https://qiita-image-store.s3.amazonaws.com/0/32778/4650019a-8c80-7f3b-8a11-dddc70d733b9.gif)


### 配置して繋げて動かしてみる
では、せっかく座標もわかったことですし、ノードを配置して繋げて動かしてみましょう。
動かす必要は特にないのですが、setAdjustments()というメソッドがあったので使ってみました。

```python:placeconnectmove.py
import hou
ne = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)

geonode = hou.node('/obj/geo') # geometryオペレータがある前提
geonode.deleteItems(geonode.children()) # お掃除しておく

merge = geonode.createNode("merge") # mergeオペレータ作成
merge.setPosition(hou.Vector2(1,-2)) # (-1,2)に配置

# boxオペレーターを4つ作って配置する
nodes = [] # boxオペレーター格納用配列
posarray = [(0,0),(0,2),(2,2),(2,0)] # 座標値の配列
for i in xrange(4): # 4回繰り返し
    nodes.append(geonode.createNode("box")) # boxオペレーターを作成
    nodes[i].setPosition(hou.Vector2(posarray[i])) # 位置をセット
    merge.setInput(i,nodes[i]) # mergeオペレーターとboxオペレーターをつなげる

# boxオペレーターの配置位置を隣のboxオペレーターの位置にする
# posarrayの位置を一つずらす ※(1,2,3,4)を、(2,3,4,1)にする
posarray2 = posarray[:]
posarray2.append(posarray2[0])
posarray2.pop(0)

anival = [] # 移動前と移動後の位置情報の配列
for i in xrange(4):
    # 各オペレーターの移動前と移動後の位置情報を格納する。1.0はアニメーション速度
    anival.append(
        hou.NetworkAnimValue(
            1.0, 
            nodes[i].position(), 
            hou.Vector2(posarray2[i])))


ne.setVisibleBounds(hou.BoundingRect(-3,-3,6,3),0.1) # 表示範囲を調整
ne.setAdjustments(nodes, anival, auto_remove=True) # 移動アニメーション

# 移動アニメーション後、位置を再度設定しなおす
# ref) http://www.sidefx.com/docs/houdini/hom/hou/NetworkAnimValue.html
for i in xrange(4):  # 4回繰り返し
    nodes[i].setPosition(hou.Vector2(posarray2[i])) # 位置をセット
```
無駄にヌルヌルした動きをお楽しみください。

![art_5.gif](https://qiita-image-store.s3.amazonaws.com/0/32778/f0bf7906-eec5-065a-562a-f7b77929c33e.gif)



## 最後に

3日目もあまりHoudiniっぽくない内容ですいません！