Houdini公式ドキュメントのPythonサンプルを触っていく～その1～

# はじめに
### 概要
HoudiniでPythonを使えるようになりたい。
そうだ！公式のサンプルを触ってみよう、第1回。
今回はこのページを読んでいく。

https://www.sidefx.com/ja/docs/houdini/hom/tool_script.html

公式ページに記載されているコードは基本的に転載せず、修正箇所のみ書いていくのでその点ご容赦。
※サンプルを触るので、基本的な説明の部分はある程度読み飛ばしていく。
※知識的にはツギハギなのでその点はご容赦。。。

### 環境
Houdini19.5.334
python3.9.10

# Pythonの実行方法
HoudiniにおけるPythonの実行場所は以下の種類がある
**1.Python Shell**
対話型で表示される。これが表示されていると、実行結果がコンソールではなくこちらに表示されるようになる。
**2.Python Source Editor**
記述した内容を実行したり、シーン内に関数を保存できる。その場で使用する場合はApply、呼び出すときはhou.session.[関数名]でシーンのどこからでも呼び出せるようになる。
**3.Python Panel Editor**
ワークスペースに独自のWindowを作成することが出来る。こちらに関しては今後深堀予定。
**4.シェルフツール**
シェルフでクリックすると起動するツールとして実行可能。めっちゃ使う。
**5.パラメーターフィールド**
HExpressionの代わりにPythonで記述可能。
**6.Pythonノード**
VEX/VOPで処理が難しいものなどはこちらで処理すると良い。こちらに関しては今後深堀予定。
**7.Hython**
コマンドライン等で自動化するときに使用される。

後は起動時に実行したり、右クリック時に実行したり、ショートカットで実行したり等々あるので、この辺は割愛。

基本的には外部ファイルとして保存しておくか、HDA内部に関数を記述しておく、または独自の保存場所（シェルフツールの場合は.shelfファイルの中に記述される）にファイルを保存しておくことが多い。

今回のサンプルでは基本的にシェルフツールから実行していく。

## シェルフツールの登録方法
シェルフを右クリックで[New Tool]でシェルフツールを作成して、Edit toolウィンドウのScriptタブからコードを記述する。
実行はシェルフツールをクリックだが、NetwotkViewやSceneViewにドラッグ＆ドロップでも実効出来る。



# サンプルを試していく
## 引数
シェルフからツールを実行したときにkwargsという辞書型の変数が追加される。詳しい種類に関してはページを確認。
このkwargs変数はこの先色んな場所で出てくる。
ちなみに参考の実行結果は以下の通り。パネルの名前やキー入力に関しては場合によって異なる。
```python:
#シェルフからの実行時
{'toolname': 'pythontester', 'panename': '',
'altclick': False, 'ctrlclick': False, 'shiftclick': False, 'cmdclick': False,
'pane': None, 'viewport': None, 'inputnodename': '',
'outputindex': -1, 'inputs': [], 'outputnodename': '', 'inputindex': -1, 'outputs': [],
'branch': False, 'autoplace': False, 'requestnew': False}
```
```python:
#NetWorkパネルにドラッグ&ドロップした時
{'toolname': 'pythontester', 'panename': 'panetab7',
'altclick': False, 'ctrlclick': False, 'shiftclick': False, 'cmdclick': False,
'pane': <hou.NetworkEditor panetab7>, 'viewport': None,
'inputnodename':'', 'outputindex': -1, 'inputs': [], 'outputnodename': '', 'inputindex': -1, 'outputs': [],
'branch': False, 'autoplace': False, 'requestnew': False,'nodepositionx': '-1.000', 'nodepositiony': '6.000'}
```
```python:
#SceneViewパネルにドラッグ&ドロップした時
{'toolname': 'pythontester', 'panename': 'panetab1',
'altclick': False, 'ctrlclick': False, 'shiftclick': False, 'cmdclick': False,
'pane': <hou.SceneViewer panetab1>, 'viewport': <hou.GeometryViewport persp1 of type Perspective>,
'inputnodename': '', 'outputindex': -1, 'inputs': [], 'outputnodename': '', 'inputindex': -1, 'outputs': [],
'branch': False, 'autoplace': False, 'requestnew': False}
```
## シーンビューを取得する
これに関しては、シーンビューを取得する方法がいくつかありますよ。ということを言っている。
**・stateutils.activePane(kwargs)**
他のスクリプトを見る限り、これが一番使用されている。
シェルフから実行した際にkwargsの'pane': Noneになっている。この場合3番目の**stateutils.findSceneViewer()** が実行されてその値が返ってくる、それ以外の場合はkwargs['pane']の値が返ってくる。
<details><summary>ソースコード</summary>

```python:
#utils.py
def activePane(kwargs={}):
    pane = kwargs.get("pane", None)
    if pane and isinstance(pane, hou.ContextViewer) and pane.sceneViewer():
        pane = pane.sceneViewer()
    if not pane:
        pane = findSceneViewer()
    return pane
```
</details>

**・stateutils.activeSceneViewer(kwargs['pane'])**
説明文にSceneViewerを返すとあるが、そんなことはなく、Netwotk viewで実行するとNetwotkViewが返ってくる。実行結果は**activePane**と変わらない。
<details><summary>ソースコード</summary>

```python:
#utils.py
def activeSceneViewer(pane):
    """
    Returns a `hou.SceneViewer`, based on the `pane` value passed to a tool
    script. If the tool script was called with a SceneViewer pane (or a
    ContextViewer pane that is viewing the scene), returns it, otherwise looks
    for any open scene viewer.
    """

    if pane and isinstance(pane, hou.ContextViewer):
        if pane.sceneViewer():
            pane = pane.sceneViewer()
    if not pane:
        pane = findSceneViewer()
    return pane
```
この書き方だと、普通にkwargs['pane']=NetwotkViewはスルーされるけど、合ってるのか...？
</details>

**・stateutils.findSceneViewer()**
書いてある通り、SceneViewerを返す。
複数ある場合は、作業スペースで可視(表示)状態→panelNameの名前が若い順→タブ等で隠れているSceneViewerの順で検索がかけられて、最初にヒットしたものが返ってくる。
ソースコードは長いので割愛。toolutils.pyのdef sceneViewer参照。

## ジェネレータSOPノードを配置する
これを使用してBOXを生成するスクリプトを作成する。"
＄HDA_NAME"/"$NODE_NAME"の部分を"box"と置き換える。
こうすることで、シェルフタブから実行orSceneViewにドロップすることで、位置を問われるタイプの実行に、NetwotkViewにドロップすると、ノードが作成される仕組みになる。
ちなみに＄HDA_NAME/$NODE_NAMEはHDA内部にジェネレーターを設定する際にそのHDAの名前を返すローカル変数として処理される。
ちなみに `「ユーザがCtrl/Cmdをクリックした場合は自動配置になります」` とあるが、**stateutils.createGeneratorSop**の方で参照している関数の変数名に不備があるため、Ctrl/Cmdをクリックするとエラーが発生する。サンプルとして残念。
<details><summary>エラーの詳細が気になる方へ</summary>
utils.pyのcreateGeneratorSop内でCtrl/Cmdをクリックしている時に位置を参照するdefaultPositionAndOrientation内の

```python:
#original Bad
cplane = scene_viewer.constructionPane()

#Good
cplane = scene_viewer.constructionPlane()
```
になれば正常に動く。
ソースを書き換えるのが怖い場合はif文のところを

```python:
if isinstance(pane, hou.SceneViewer) and not (kwargs['ctrlclick'] or kwargs['cmdclick']):
```
みたいな回避をしてあげるとそれっぽく動くがイマイチ。
</details>

## フィルターノードを配置する
Copy to Points SOPに対してみたいに書いてあるので、
"$HDA_NAME"を"copytopoints::2.0"に置き換えて実行してみる。
box2つ用意して、スクリプトを実行、
1つ目のboxを表示させてSceneViewからPrimを選択してEnter、その後2つ目boxを表示させてSceneViewからPointsを選択してEnter、そうすると、box1とbox2がCopyToPointsノードに接続されているのが確認できると思う。ただ残念なのは、この実行方法だとPrimやPointの部分選択が反映されないのでやはり一工夫必要らしい。
その辺りはstateutils.Selectorを深堀する際にできればいいなぁ。

もう1つも試す。
こちらは上の亜種版みたいな感じで選択が促されて、選択すればそのオブジェクトにノードを、選択が無ければ新しくGeometry階層を作成してそちらにノードを作成するという挙動らしい。
ちなみに、オブジェクトの中か外か（OBJ階層かSOP階層か）を識別しているので、OBJ階層であれば上記の挙動、SOP階層であれば選択せずに表示されているオブジェクトに対して自動的にノードが作成される。
今回は1つのオブジェクトに対してのノード作成なので、"$HDA_NAME"を"xform"に変更して実行してみる。
はい、動かない。サンプルのミスですね。
**stateutils.createFilterSop** は引数が4つ必要なのに3つしか設定されてないですね。containerの変数を追加してあげましょう。
```python:
#original Bad
newnode = stateutils.createFilterSop(kwargs, "$HDA_NAME", [])

#Good
newnode = stateutils.createFilterSop(kwargs, "$HDA_NAME", container, [])
```

これで動くようになりましたね。わーい。
## ユーザに位置、複数位置、経路を促す
今までSceneView上で位置を選択させてきたのですが、その部分だけを抽出したらどう書くかという部分のサンプルでる。
ifのところはジェネレータSOPの項目でもあるのですが、Ctrl/Cmdを押しながら実行すると原点の位置を返す部分の開設です。ちなみに上記項目でも書いたのですが、**stateutils.defaultPositionAndOrientation**の関数がミスがあるので、このままではエラーになります。残念。
else以降は位置選択の部分ですね。BoundingBoxとpositionTypeはhouモジュールなので、hou.BoundingBoxとhou.positionTypeに変更しておきましょう。デフォルト変数を少し深堀します。
```python:
positions = scene_viewer.selectPositions(
    prompt='Click to specify a position',#下に出す文字列
    number_of_positions=1,#選択する最大数
    min_number_of_positions=-1,#返り値の最小数。0だとエラーになるので、設定しない場合は-1とする。
    #これより選択数が少ないと不足分を、[0,0,0]で埋めて返ってくる。
    connect_positions=True,#複数選択の順番を線でつなぐ表示の有無
    show_coordinates=True,#選択時の座標表示の有無
    bbox=kwargs.get("bbox", hou.BoundingBox()),#選択時に四角い範囲を表示する
    #bbox=hou.BoundingBox(xmin,ymin,zmin,xmax,ymax,zmax)で設定すると任意のサイズに出来る。
    position_type=hou.positionType.WorldSpace,#返り値の座標のタイプ。
    #WorldSpace/ViewportUV/ViewportXYがある。
    icon=None,#多分使わない
    label=None#多分使わない
    )
```
## ノードのパラメータを変更する
パラメーターの取得方法。my_cam変数が指定されていないので、hou.node("/obj/hogehoge")のように取得すると使える。
get()関数が使用できないので、eval()関数を使用すると値が取得できる。
## ツールがコールされているネットワークコンテキストを検知する
Netwotk viewの階層を識別してしてくれるものです。
フィルターノードを配置での2つ目のサンプルで使用されているギミックです。
このサンプルはそのままではエラーが出るので修正します。
```python:
# ツールスクリプト内
import stateutils

scene_viewer = stateutils.activeSceneViewer(kwargs['pane'])
if not scene_viewer:
   raise hou.Error("The tool was not invoked in the scene viewer.")
child_type = scene_viewer.pwd().childTypeCategory()

if child_type == hou.objNodeTypeCategory():
    ...
elif child_type == hou.sopNodeTypeCategory():
    ...
elif child_type == hou.dopNodeTypeCategory():
    ..
```
## 現行ビューポートを制御する
ビューポートの設定を制御する方法です。関数名がそのままだと使えないので、
stateutils.find_scene_viewer→stateutils.findSceneViewer
と使えるようになります。
# おわりに
サンプルがそのまま動かないと大変ですね。誰かの何かの助けになれば幸いです。

次→

それでは、しゃーした～。

# おまけ
Netwotk viewにいちいちドロップするのが面倒なので、他のノードと同じようにTabキーから呼び出す方法です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613929/f7dcf25e-680c-c8fc-30aa-bc82f79f5cfc.png)
Edit Tool内のContextタブからnetwork contextsのチェックボックスをオンにすると、その階層のパネルでTabキーからノードと同じようにシェルフツールを呼び出すことが出来る。（画像のはOBJ階層とSOP階層から呼び出し可能）
同じくViewerPanelも設定可能。
TAB Submenu PathでTabキーを押したときの表示するカテゴリーを設定することが出来る。（カンマで複数設定可）