難しく考えないShelf活用術

# はじめに
### 概要
この記事はHoudini Apprenticeアドベントカレンダー2024　8日目の記事です。

みなさんはShelfツール使ってますか？
なんかスクリプト使う場所だったり、エフェクト作るときのプリセットだなぁというイメージかと思います。
ではこれらを自前でカスタマイズしてHoudiniライフをもっとよくしてみませんか？

### 環境
Houdini20.5.410 py3.11
※こちらを使用していますが、もちろん別のバージョンでもある程度汎用的に使用できるTipsだと思います。
### 関連記事
https://qiita.com/Lethe_movie/items/6f61082d3deba24b5264

今回Shelf Editの基本的な部分は省略するので、上記の**shelfツール セットアップ**の項目の部分の説明を参考にしてみてください。

# まずは意識を低く使ってみよう
今回はこのようなノード群を作ってみましょう。
Attribtue DeleteとGroup Deleteでメッシュ以外のアトリビュートを削除して、Colorで赤色にしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613929/896c9cad-934f-2a5d-4227-fe9d47169ee0.png)
作成したら、下の3つをShelfにD&Dします。設定はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613929/5d436cb4-8412-2dff-cc30-17bf656ea9f2.png)
これで完成です。

クリックすると3つのノードが新しく追加されたのが分かります。
これが簡単なノードプリセットの完成です。

----
ここから少しアレンジを加えていきます。
Shelfを右クリックして、``Edit Tool``で改良しましょう。
まずはIcomを``BUTTONS_color``にします。
そしてContextタブからNetwork PaneでSOPにチェックをいれてましょう。
``TAB Submenu Path``は任意のカテゴリを追加します。ここが空白だとMoreというカテゴリになります。`/`で区切ることでサブカテゴリとして追加することも可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613929/b15e12c2-c46f-5ead-f57c-2ec8319c5f09.png)
ここまで設定したらAcceptで確定させます。

これによってShelfのツールをNetwork Viewから呼び出せるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613929/d645a2d8-0edd-7a94-d1bb-efdbe4359bbd.png)

わざわざHDAを作らなくてもカスタムプリセットとして呼び出せるようになりました！便利！
<details><summary>ちょっと余談</summary>

``Mountain``を呼び出したときに``AttributeNoise``が呼び出されるのって何でだろう？とかVellum系のノードを呼び出すと複数出てくるものとか、Tabから呼び出す``Cube``と``Box``の違いって何？など考えたことがあると思いますが、これらは全てShelfで設定されているノード群です。

</details>


SOPをD&DをしたScriptはかなり長いコマンドになっているのですが、HDAにするまでもないPresetや一時的に作成した便利なWrangleなどサクッと使用するには便利なので使いどころがあるのではないのなかぁと思ってます。
ただしSolverの中身のコネクションやExpressionのリレーションなど維持できないものも多いので、使いどころは難しいですが気軽に使えるものだと思ってます。
> 別PCからノードを急ぎ持っていきたいときにShelfにしてしまってそのスクリプトをコピペして持っていくなどにも使用できます。

# さてここからさらに発展させましょう。

HoudiniでPythonを呼び出すときに、諸々の情報を含んだ``kwargs``を活用することで発展した機能を追加することが可能です。
これを利用してさっきのShelfツールを改良しましょう。
①``Edit Tool``からScriptの5行目あたりに次の変数を追加します。
```Python
if kwargs["ctrlclick"]:
    custom_color = "( 1 1 0 )"
else:
    custom_color = "( 1 0 0 )"
```
②91行目当たりの``h_cmd = r'''``を``h_cmd=fr'''``に変更します。
③138行目あたりの``opparm $_obj_geo1_color1 color ( 1 0 0 ) ramp2pos ( 1 ) ramp2c ( 1 1 1 )``
を
``opparm $_obj_geo1_color1 color {custom_color} ramp2pos ( 1 ) ramp2c ( 1 1 1 )``
に変更します。
これをAcceptで確定させます。

<details><summary>一応全文</summary>

```Python:
import sys
import toolutils

if kwargs["ctrlclick"]:
    custom_color = "( 1 1 0 )"
else:
    custom_color = "( 1 0 0 )"

outputitem = None
inputindex = -1
inputitem = None
outputindex = -1

num_args = 1
h_extra_args = ''
pane = toolutils.activePane(kwargs)
if not isinstance(pane, hou.NetworkEditor):
    pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
    if pane is None:
       hou.ui.displayMessage(
               'Cannot create node: cannot find any network pane')
       sys.exit(0)
else: # We're creating this tool from the TAB menu inside a network editor
    pane_node = pane.pwd()
    if "outputnodename" in kwargs and "inputindex" in kwargs:
        outputitem = pane_node.item(kwargs["outputnodename"])
        inputindex = kwargs["inputindex"]
        h_extra_args += 'set arg4 = "' + kwargs["outputnodename"] + '"\n'
        h_extra_args += 'set arg5 = "' + str(inputindex) + '"\n'
        num_args = 6
    if "inputnodename" in kwargs and "outputindex" in kwargs:
        inputitem = pane_node.item(kwargs["inputnodename"])
        outputindex = kwargs["outputindex"]
        h_extra_args += 'set arg6 = "' + kwargs["inputnodename"] + '"\n'
        h_extra_args += 'set arg9 = "' + str(outputindex) + '"\n'
        num_args = 9
    if "autoplace" in kwargs:
        autoplace = kwargs["autoplace"]
    else:
        autoplace = False
    # If shift-clicked we want to auto append to the current
    # node
    if "shiftclick" in kwargs and kwargs["shiftclick"]:
        if inputitem is None:
            inputitem = pane.currentNode()
            outputindex = 0
    if "nodepositionx" in kwargs and             "nodepositiony" in kwargs:
        try:
            pos = [ float( kwargs["nodepositionx"] ),
                    float( kwargs["nodepositiony"] )]
        except:
            pos = None
    else:
        pos = None

    if not autoplace and not pane.listMode():
        if pos is not None:
            pass
        elif outputitem is None:
            pos = pane.selectPosition(inputitem, outputindex, None, -1)
        else:
            pos = pane.selectPosition(inputitem, outputindex,
                                      outputitem, inputindex)

    if pos is not None:
        if "node_bbox" in kwargs:
            size = kwargs["node_bbox"]
            pos[0] -= size[0] / 2
            pos[1] -= size[1] / 2
        else:
            pos[0] -= 0.573625
            pos[1] -= 0.220625
        h_extra_args += 'set arg2 = "' + str(pos[0]) + '"\n'
        h_extra_args += 'set arg3 = "' + str(pos[1]) + '"\n'
h_extra_args += 'set argc = "' + str(num_args) + '"\n'

pane_node = pane.pwd()
child_type = pane_node.childTypeCategory().nodeTypes()

if 'color' not in child_type:
   hou.ui.displayMessage(
           'Cannot create node: incompatible pane network type')
   sys.exit(0)

# First clear the node selection
pane_node.setSelected(False, True)

h_path = pane_node.path()
h_preamble = 'set arg1 = "' + h_path + '"\n'
h_cmd = fr'''
if ($argc < 2 || "$arg2" == "") then
   set arg2 = 0
endif
if ($argc < 3 || "$arg3" == "") then
   set arg3 = 0
endif
# Automatically generated script
# $arg1 - the path to add this node
# $arg2 - x position of the tile
# $arg3 - y position of the tile
# $arg4 - input node to wire to
# $arg5 - which input to wire to
# $arg6 - output node to wire to
# $arg7 - the type of this node
# $arg8 - the node is an indirect input
# $arg9 - index of output from $arg6

\set noalias = 1
set saved_path = `execute("oppwf")`
opcf $arg1

# Node $_obj_geo1_attribdelete1 (Sop/attribdelete)
set _obj_geo1_attribdelete1 = `run("opadd -e -n -v attribdelete attribdelete1")`
oplocate -x `$arg2 + 0` -y `$arg3 + 0` $_obj_geo1_attribdelete1
opparm $_obj_geo1_attribdelete1 ptdel ( * ) vtxdel ( * ) primdel ( * ) dtldel ( * )
opset -d off -r off -h off -f off -y off -t off -l off -s off -u off -F on -c on -e on -b off $_obj_geo1_attribdelete1
opexprlanguage -s hscript $_obj_geo1_attribdelete1
opuserdata -n '___Version___' -v '' $_obj_geo1_attribdelete1
opset -p on $_obj_geo1_attribdelete1
opcf $arg1

# Node $_obj_geo1_groupdelete1 (Sop/groupdelete)
set _obj_geo1_groupdelete1 = `run("opadd -e -n -v groupdelete groupdelete1")`
oplocate -x `$arg2 + 0.0034499950706958771` -y `$arg3 + -1.0000000119209287` $_obj_geo1_groupdelete1
opparm $_obj_geo1_groupdelete1  deletions ( 1 )
opparm -V 20.5.410 $_obj_geo1_groupdelete1 group1 ( * )
opset -d off -r off -h on -f off -y off -t off -l off -s off -u off -F on -c on -e on -b off $_obj_geo1_groupdelete1
opexprlanguage -s hscript $_obj_geo1_groupdelete1
opuserdata -n '___Version___' -v '20.5.410' $_obj_geo1_groupdelete1
opset -p on $_obj_geo1_groupdelete1
opcf $arg1

# Node $_obj_geo1_color1 (Sop/color)
set _obj_geo1_color1 = `run("opadd -e -n -v color color1")`
oplocate -x `$arg2 + 0.0034499950706958771` -y `$arg3 + -2.0000000238418574` $_obj_geo1_color1
opparm $_obj_geo1_color1  ramp ( 2 )
opparm $_obj_geo1_color1 color {custom_color} ramp2pos ( 1 ) ramp2c ( 1 1 1 )
opset -d on -r on -h off -f off -y off -t off -l off -s off -u off -F on -c on -e on -b off $_obj_geo1_color1
opexprlanguage -s hscript $_obj_geo1_color1
opuserdata -n '___Version___' -v '' $_obj_geo1_color1
opuserdata -n '___toolcount___' -v '2' $_obj_geo1_color1
opuserdata -n '___toolid___' -v 'sop_color' $_obj_geo1_color1
opset -p on $_obj_geo1_color1

opcf $arg1
opwire -n $_obj_geo1_testgeometry_pighead1 -0 $_obj_geo1_attribdelete1
opcf $arg1
opwire -n $_obj_geo1_attribdelete1 -0 $_obj_geo1_groupdelete1
opcf $arg1
opwire -n $_obj_geo1_groupdelete1 -0 $_obj_geo1_color1

set oidx = 0
if ($argc >= 9 && "$arg9" != "") then
    set oidx = $arg9
endif

if ($argc >= 5 && "$arg4" != "") then
    set output = $_obj_geo1_color1
    opwire -n $output -$arg5 $arg4
endif
if ($argc >= 6 && "$arg6" != "") then
    set input = $_obj_geo1_attribdelete1
    if ($arg8) then
        opwire -n -i $arg6 -0 $input
    else
        opwire -n -o $oidx $arg6 -0 $input
    endif
endif
opcf $saved_path
'''
hou.hscript(h_preamble + h_extra_args + h_cmd)

```
</details>

さてこれで実行すると赤色になっていますが、実行時にCtrlを押しながら呼び出すと黄色になります。
このような感じでCtrl/Cmd/Alt/Shiftなどのキー入力や、Shelfの実行がVeiw PortなのかNetwork Viewなのか、ネットワークのワイヤーに繋がっているノードなど様々な情報を取得できるので、ちょっと複雑な条件設定を組む時に活用すると便利です。

## kwargsについてもっと調べたい人向け
Shelfにこれを登録して、色々なところで実行することで返り値を調べることが出来る。
```Python
print(kwargs)
```
出力地の確認としてデフォルトだとHoudini Consoleに表示されるが、`Python Shell`ウィンドウを開いておくとそっちに出力されるようになるので、好きな方で確認すると良い。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613929/14b89197-98ff-59cd-219a-1df573fe0861.png)



# 実際に使っているShelfのおまけ
### Old Mountian
```Python:mountain
import soptoolutils
import toolutils

if kwargs['ctrlclick']:
    soptoolutils.genericTool(kwargs, 'mountain::2.0')
    
else :
    kwargs['parms'] = {
        'displace': 1,
        'attribs': 'P',
        'noiserange': 1,
        'amplitude': 0.25,
        'fractal': 3,
        'oct': 8,
        'rough': 0.4 }
    soptoolutils.genericTool(kwargs, 'attribnoise::2.0', force_filter=True, nodename = 'mountain1')
```
Deform ShelfのMountainツールの改良です。
Ctrlを押すと古い``MountainSOP``を実行できます。古い方に慣れていると組んでおくと便利。
### Reset Viewport
```Python:reset_viewport
from labsopui import reset_viewport
reset_viewport.resetSceneViewers()
```
LabsToolを入れている場合、上のメニューバーからLabs/Reset Viweportで呼び出せる機能をTabから呼び出せるように設定しているものです。
HoudiniはViewがエラー起きがちなので、Tabから呼び出せるようにした方が便利です。

# 参考ページ
Icon一覧確認用ツールめっちゃ使います。

https://github.com/TakashiKoide/HoudiniIconBrowser

HoudiniのPython Wiki

https://www.houpywiki.com/Shelf-Scripts#shelf-script-basics

# おわりに
難しく考えなくても使える、そんなところからでもShelfを活用してみてはいかがでしょうか？

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