[必携!?] Houdini 作業で必須級のshelfツール紹介！

こちらは、 [Houdini Apprentice Advent Calendar 2024](https://qiita.com/advent-calendar/2024/happrentice) の3日目の記事になります。


# Houdini 作業で必須級のShelfツール紹介！
Houdiniの作業中にPythonスクリプトを使うことはありますか？
ノードベースの操作では、膨大な数のノードを扱う必要があり、煩雑に感じることも多いですよね。
そこで今回は、少しでも作業を効率化できるshelfツールをいくつかご紹介します！

## shelfツール セットアップ
始めに、`Shelf`にツールを登録する方法について説明します。

Houdini画面上側の`ShelfTab`を見て、`+ボタン`→ `New Shelf Tab` 、`Name`と`Label`を適当に命名して、`Accept`を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1041568/6519c07b-55f4-688c-d5a3-953793be8138.png)

作成した`Shelf` の空白部分を左クリックして、`Edit tool` を出します。

**(1) Options タブ**
`Name`,`Label`,`Icon`,`Keywords(←これ何??)` を登録できます。
<font color="DarkGray">・`Icon`の右マークをクリックして、`hicon:/` → `SVGIcons.index` で任意の `Icon` を選択できます。
<font color="DarkGray">・任意ノードを右クリック、`Type Properties` → `Basicタブ` → `Icon` をコピペすると、そのノードの`Icon`にできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1041568/b7d6c7e6-de81-505e-1686-320745a4c068.png)

**(2) Script, Help タブ**
`Script` に pythonコード、`Help` に詳しい説明が書けます。
`Help`の書き方に関しては、[こちら](https://www.sidefx.com/ja/docs/houdini/help/nodes.html)をご参照ください。

**(3) Context タブ**
使用する`Contexts`,`Tab Submenu`を記入することで、`Tabメニュー` から `shelf` が呼び出せるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1041568/cf2a9c14-6697-a51c-796b-142878d2faaf.png)

**(4) Hotkeys タブ**
使う場面に応じて、ショートカットキーを登録できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1041568/36aed2cc-9139-43f3-1b39-77e0de8fdf18.png)

任意の`Editボタン`を押して `Hotkey Manager` が開き、 `緑の+ボタン` → `任意のショートカットキー` → `Accept` で登録できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1041568/4b3296ff-fdee-754a-5480-457fe33c2aa9.png)

以上で `Shelfツール` 登録完了です！
デフォルトの状態だと、`$HOME\houdini'version'\toolbar\default.shelf` に登録されます。

## 1. 任意ノードを選択ノード下に作成

<details><summary> [ createAnyNode ]</summary>

```py
##-- createAnyNode --##

# 任意ノードのタイプ(そのノードの Nodeinfo の上に書いてあるカッコ内の文字列)
NODETYPE = "null"

# 作成するノードの位置
ADDPOS = [0, -1.5]

##------------------------##

# itemから任意nodeを作成
def createNode(item, nodetype, addpos):
    addnode = item.parent().createNode(nodetype)
    
    # positionを設定(dotのみなぜかずれるので補正)
    pos = item.position()
    if item.networkItemType() == hou.networkItemType.NetworkDot:
        pos += hou.Vector2(-0.5, -0.155)
    addnode.setPosition(pos + hou.Vector2(addpos))
    
    # wireを繋ぐ(output=0のときスキップ)
    if item.networkItemType() == hou.networkItemType.Node and len(item.outputLabels()) == 0:
            return
    addnode.setInput(0, item)

##-----------------------##

# 選択ノードから任意ノードを作成
for selecteditem in hou.selectedItems():
    createNode(selecteditem, NODETYPE, ADDPOS)
```
</details>

この内容をscript欄にコピペしてご使用ください。コード上部にノードを指定できるようにしてあるので、任意のものに変えて下さい。
ちなみに、選択ノード以外に `NetworkDot`,`SubnetIndirectInput` にも対応しているところがミソです！


## 2. Inputを揃えて再接続

<details><summary> [ sortNodeInput ] </summary>

```py
import hou

# 接続のoutputIndexを取得
def outputIndex(connect):
    if connect.inputItem().networkItemType() is hou.networkItemType.Node:
        return connect.outputIndex()
    else:
        return 0
    
# 接続元ノードのX位置を取得
def itemPosX(connect):
    return connect.inputItem().position()[0]
    
# 接続元ノードのY位置を取得
def itemPosY(connect):
    return connect.inputItem().position()[1] * (-1)
    
##-------------------------##
    
# 選択ノードのinputをソートして再接続
for node in hou.selectedNodes():
    connects = node.inputConnections()
    if not connects: continue
    
    # X位置、Y位置、outputIndexの順でソート
    connects = sorted(connects, key=lambda 
               connect: (itemPosX(connect), itemPosY(connect), outputIndex(connect)))

    # ソートされた順に再接続
    for i, connect in enumerate(connects):
        node.setInput(i, connect.inputItem(), outputIndex(connect))
    
```
</details>

ぜひショートカットを設定して使ってください！
個人的にはこれがないと仕事ができないくらいよく使うコードです。

## 3. AutoUpdateを切り替え

<details><summary>[ toggleAutoUpdate ]</summary>

```py
##-- toggleAutoUpdate --##

import hou

def toggleAutoUpdate():
    # updateModeのリスト
    modelist = [hou.updateMode.AutoUpdate, 
                hou.updateMode.OnMouseUp, 
                hou.updateMode.Manual]

    # updateModeを取得
    mode = hou.updateModeSetting()


    # 次のモードを設定
    next_mode = modelist[(modelist.index(mode) + 1) % len(modelist)]
    hou.setUpdateMode(next_mode)
    
# 関数を実行
toggleAutoUpdate()
```
</details>

重い作業をしているとき、頻繫に`AutoUpdate`を切り替えたいときがあるのですが、これを使って切り替えることで、いちいちマウスでクリックするよりも全然楽になります。

## 4. 新しいGeoを作って任意ノードを参照

<details><summary> [ setNewGeometry ] </summary>

```py
##-- setNewGeometry --##

# 作成するノードの位置。
ADDPOS = [2.5, 0]


##------------------------##

import hou

# 選択ノードを取得
def customSelectedNodes():
    nodes = hou.selectedNodes()
    if len(nodes) == 0: print("[ set New Geometry ] Selected nodes not found.")
    return nodes

# 指定されたノードタイプの右下のBoundingBox座標を計算
def calcNodetypesBboxRB(nodes, typename):
    geos = [node for node in nodes if node.type().name() == typename]
    bboxrb = [max(node.position()[0] for node in geos), 
              min(node.position()[1] for node in geos)]
    return bboxrb
    
# 指定位置に任意のノードを作成
def createNodeFromPos(parent, nodetype, nodename, pos):
    addnode = parent.createNode(nodetype)
    addnode.setName(nodename, True)
    addnode.setPosition(hou.Vector2(pos))
    return addnode
    
##------------------------##
    
# メイン処理
selectednodes = customSelectedNodes()
if len(selectednodes):
    # geoノードを作成
    bboxrb = calcNodetypesBboxRB(hou.node("obj/").children(), "geo")
    geo_pos = [pos1 + pos2 for pos1, pos2 in zip(bboxrb, ADDPOS)]
    geo = createNodeFromPos(hou.node("obj/"), "geo", "geo", geo_pos)
    
    # obj_mergeノードを作成
    objm_pos = [0, 0]
    for selectednode in selectednodes:
        objmerge = createNodeFromPos(geo, "object_merge", "import_" + selectednode.name(), objm_pos)
        objmerge.parm("objpath1").set(selectednode.path())
        
        objm_pos = [pos1 + pos2 for pos1, pos2 in zip(objm_pos, ADDPOS)]

```

</details>

新しくGeoノードを作って、任意ノードをObjmergeSOPで参照する作業を自動化しました。
これもよくやる作業ですね。


## 5. Cache系ノードを自動セット

<details><summary> [ setCacheNodes ] </summary>

```py
##-- setCacheNodes --##

# テキストボックスのデフォルト値。よく使うものがあれば入れてください。
INITIAL_MESSAGE = ""

# 作成しない / TOP の切替スイッチ。
USETOP = 1

# 作成するノードの位置。
ADDPOS = [0, -1.5]


##------------------------##

import hou

# selecteditemsを返す
def customSelectedItems():
    items = hou.selectedItems()
    if len(items) == 0: print("[ set Cache nodes ] Seleted items not found.")
    return items

# メッセージウィンドウを呼び出す
def callMessage():
    message = "Cache Name : "
    buttons = ["Apply", "Cancel"]
    severity = hou.severityType.Message
    default_choice = 0
    close_choice = 1
    help = None
    title = "setCacheNodes"
    initial_contents = INITIAL_MESSAGE
    reply = hou.ui.readInput(message, buttons, severity, default_choice, close_choice,
            help, title, initial_contents)
    return reply

# Nodesから任意nodetypeのnodeを返す
def findNode(nodes, nodetype):
    return next((node for node in nodes if node.type().name() == nodetype), None)

# NodesのBoundingBoxの右下を取得
def calcNodesBboxRB(nodes):
    if not nodes:
        return [0, 0]
    bboxrb = [max(node.position()[0] for node in nodes), 
              min(node.position()[1] for node in nodes)]
    return bboxrb
    
    
# itemから任意nodeを作成
def createNodeFromItem(item, nodetype, nodename, addpos):
    addnode = item.parent().createNode(nodetype)
    addnode.setName(nodename, True)
    
    # positionを設定
    pos = item.position()
    if item.networkItemType() == hou.networkItemType.NetworkDot:
        pos += hou.Vector2(-0.5, -0.155) #dotの補正
    addnode.setPosition(pos + hou.Vector2(addpos))
    
    # wireを繋ぐ(outputがない場合スキップ)
    if item.outputs():  
        addnode.setInput(0, item)
    return addnode
    
# parentから任意nodeを作成
def createNodeFromPos(parent, nodetype, nodename, pos):
    addnode = parent.createNode(nodetype)
    addnode.setName(nodename, True)
    addnode.setPosition(hou.Vector2(pos))
    return addnode
    
##------------------------##

# メイン処理
selecteditems = customSelectedItems()
if len(selecteditems):
    reply = callMessage()

    if reply[0] == 0:
        cachename = reply[1]
    
        # filecacheノードを作成
        filecache = createNodeFromItem(hou.selectedItems()[0], "filecache::2.0", cachename, ADDPOS)
        
        # USETOP のときTOPノードを作成
        if USETOP:
            topnet = findNode(hou.node("tasks/").children(), "topnet")
            if not topnet: 
                print("[ set Cache Nodes ] topnet node not found.")
            else:
                ropfetch_pos = [pos1 + pos2 for pos1, pos2 in zip(calcNodesBboxRB(topnet.children()), ADDPOS)]
                ropfetch = createNodeFromPos(topnet, "ropfetch", cachename, ropfetch_pos)
                ropfetch.parm("roporder").set(0)
                ropfetch.parm("roppath").set(filecache.path() + "/render")
```

</details>

`cache`を取るとき`filecacheSOP`を作成し、`TOP`で`ropfetchTOP`に繋げる一連の作業を自動化しました。
オプションで`TOP`作成を`OFF`にすることもできるようになってます。


# 終わりに
以上、shelfツール紹介でした！
コードにどこか間違いや不具合などございましたら[TwitterDM](https://x.com/lethe_movie)にご連絡ください。
いくつかの関数についてはツールごとに重複しているものもあり、モジュール化した方がいいのは事実なんですが、一応python初学者の方のための参考にして頂きたいというのもあり、そのままコピペするだけで使えるようにしておきます。

個人的な話、こうして記事としてアウトプットすることで自分のツール周りの整理もできたので一石二鳥ではありましたが、久しぶりにpythonを書いたので疲れました...

ということで、このツールを上手いこと使ってみて良きHoudiniライフをお過ごし下さい～！