packageファイルを使って自前環境を整えよう！

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

Houdiniのバージョンアップの際やプロジェクトごとに設定ファイルや自作HDAを移し替えたり、DLしたHDAと自作HDAがごちゃ混ぜになってしまったりと、管理が煩雑になっていませんか？
package機能を活用して、自前環境を整理する方法を紹介します。

### 環境
Houdini20.5.410　Py3.11
※20.0以降のPackageBrowserの紹介なども含みますが、基本的にはどのバージョンでも使用できるノウハウだと思います。

# packageとは？
[Houdini パッケージを使用して、チームで環境を共有するには](https://support.borndigital.co.jp/hc/ja/articles/26261060729625-Houdini-%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6-%E3%83%81%E3%83%BC%E3%83%A0%E3%81%A7%E7%92%B0%E5%A2%83%E3%82%92%E5%85%B1%E6%9C%89%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF)や
[Houdini Workflowについて](https://fxnomemo.blogspot.com/2021/04/houdini-workflow.html)などにもありますが、環境変数やツールを読み込むのに、デフォルトで設定されている`$HOME\houdini20.5`の中以外に環境を作成してそれを読み込むことでそれらを別フォルダで管理することが出来ます。
それってBatファイルとかEnvに書き込みでいいんじゃない？という意見もあるかと思いますが、複数のプロジェクトで分けたり更新の手間などを考えるとpackageの選択肢も良いと個人的には思っている。

https://qiita.com/takavfx/items/b58cae65984b15e5ad29

# packageを読み込ませる準備

まず`$HOME\houdini20.5`フォルダの中に`packages`フォルダを作成して、以下のJSONファイルを作成する。

```JSON:custom_tools_path.json
{
    "env":[
        {"$TOOLS_ROOT_PATH" :"your/tools/path" },
    ],
    "package_path":"$TOOLS_ROOT_PATH"
}
```
それから任意の`your/tools/path`の先にもJSONと用意したパッケージ用フォルダを配置する。JSONは以下のように設定。ここで必要に応じて環境変数とかその他の設定などを書き込む。
自分の場合はVSCodeと接続するためのエディタ変数とかCACHE用の変数とかを追加しているがここでは省略。
```JSON:my_package.json
{
    "env": [
        {"MY_PACKAGE_PATH" :"$TOOLS_ROOT_PATH/my_package"},
    ],
    "hpath": ["$MY_PACKAGE_PATH"]
}
```
これによって、my_packageの中にあるツール群や設定類をHoudiniの中に読み込むことが出来るようになった。

# my_packageの中身を作る
さて、いよいよpackageの中身を作っていきます。
個人的に使用している物を中心に紹介していきます。
## フォルダ編
### otls
自作のHDAを入れておくフォルダ。

### desktop
デスクトップレイアウトを保存しておくフォルダ。バージョン移行時の再設定をある程度しなくて済む便利。下記のpython_panelsと組み合わせて作業セクション毎の専用レイアウトを作成すると便利。

### python_panels
Pythonを使用して独自の作業用パネルを作成できる。Solaris用のアセットブラウザやデバック用の確認画面など。ちなみにHoudniでレイアウト出来る画面の1/3はこれで設定されている。

### pythonX.Ylibs
こちらはPythonのライブラリなどを保存しておく場所。

### scripts
ここは起動時に読み込む123.pyや456.pyなどの他に、`python`や`hscript`の子階層を作ることでHoudini上からスクリプトを読み込むことが出来る。
>一般的には、Houdiniは、HOUDINI_PATH/scripts/下でディスク上の“スクリプト”(通常はコールバックスクリプト)を探すのに対し、インポートされるモジュールはHOUDINI_PATH/scripts/python/またはHOUDINI_PATH/pythonX.Ylibsに置きます。

例えば456.pyの記入例として
```Python:456.py
#オートセーブを必ず有効にする
import hou
hou.hscript('autosave on')

#強制的にJOBパスを上書きする
import os
hou.allowEnvironmentToOverwriteVariable("JOB", True)
os.environ["JOB"] ="your/jobs/path"
```
など設定しておくと便利。
詳しくは[Houdini Startup Scripts](https://ikatnek.blogspot.com/2015/03/houdini-startup-scripts.html)を参照。

### toolbar
シェルフツールなどを格納する場所。実際にシェルフとして使用しなくても、自作ノードを含めたプリセットとして管理するのも便利。

### fonts
Houdini内で使用するフォントファイルを入れる場所。
UIのフォント変更に関しては[Japanese UI font](https://ikatnek.blogspot.com/2023/06/japanese-ui-font.html)を参照。

### config
UIの設定やHDA用のIconファイルを入れる場所。
UIの編集に関しては[Houdini Custom Desktop Settings](https://houdinifx.jp/blog/houdini-custom-desktop-settings/#3)を参照。

### apexgraph
APEX用のサブグラフを入れる場所。
APEXのグラフに関しては[APEX解体新書](https://qiita.com/amateras/items/e576dcb144390407b74b)を参照。

### ramp
ColorやFloatのRampプリセットを登録できる場所。記述はJSON形式。
以下はカラーランプのファイル例。
```JSON:Custom_rgb.ramp
{
    "aurora": {
        "type": "rgb",
        "label": "Aurora",
        "ramp": {
            "colortype": "RGB",
            "points": [
                4,
                {
                    "t": 0.0,
                    "rgba": [ 0.0, 0.0, 0.0, 1.0 ],
                    "basis": "linear"
                },
                {
                    "t": 0.333330005,
                    "rgba": [ 0.0, 1.0, 0.5, 1.0 ],
                    "basis": "linear"
                },
                {
                    "t": 0.66667002,
                    "rgba": [ 0.5, 0.0, 1.0, 1.0 ],
                    "basis": "linear"
                },
                {
                    "t": 1.0,
                    "rgba": [ 1.0, 1.0, 1.0, 1.0 ],
                    "basis": "linear"
                }
            ]
        }
    }
}
```
### その他
`gallery`:SolarisのGallaryプリセット
`soho`:レンダラーやシーン出力に関連する設定
`dso`:Houdiniプラグイン（C++で作成したDSOファイル）を配置
`vex`:includeする構造ファイルを配置できる。
などなど。

## ファイル編
### VEXpressions.txt
VEXのプリセットを記入しておける。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613929/d6c68eb8-6242-4e1c-4103-114837a5eb63.png)

### PythonScripts.txt
PythonSOPのプリセットを記入しておける。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613929/856af93f-5d48-8756-5f9b-17e0c0c0a90a.png)

### Expressions.txt
エクスプレッションのプリセットを記入できる。
例えば
```:Expressions.txt
Sop/Parm
    TestA
    AAA
Sop/Parm
    TestB
    BBB
```
というのを用意して、パラメータのMenu Scriptに
```Python:Menu Script
import expressionmenu
return expressionmenu.buildExpressionsMenu('Sop/Parm')
```
とすると、選択支に`TestA`と`TestB`が表示される。
LOPとかRigとかで複雑な設定仕込むのに便利。
※前の2つとは違い、受け入れ側のMenu Scriptを編集する必要があることに注意。

### UsdInlineLayers.txt
inlineusd LOP用のプリセット。
inlineusdに関しては[InlineUSDを使おう](https://fereria.github.io/reincarnation_tech/dcc/Houdini/SOLARIS/inline_usd)を参照。

### jump.pref
`Open…`や`Save As…`を開いたときの左下の項目を設定できます。
テクスチャアセット用フォルダとか、環境変数と組み合わせると便利です。
```:jump.pref
$HIP/tex
$JOB/../render
$MYASSETS/Materials
```

### OPmenu.xml
ノードを右クリックしたときに表示されるメニューをカスタマイズできます。
```XML:OPmenu.xml
<?xml version="1.0" encoding="UTF-8"?>

<menuDocument>
  <menu>
    <scriptItem id="test">
    <label>Test</label>
    <scriptCode><![CDATA[
print("test")
    ]]></scriptCode>
    </scriptItem>
  </menu>
</menuDocument>
```
これで右クリックにメニューが追加されて、スクリプトを実行することが出来ます。
### PARMmenu.xml
パラメーターを右クリックしたときに表示されるメニューを追加できます。

### その他もっと深く設定する
`PaneTabTypeMenu.xml`：python_panelを追加する際のカテゴリの設定
`MainMenuCommon.xml`：上のメニュータブの設定
`NetworkViewMenu.xml`：ネットワークビューのメニュータブの設定
`ParmGearMenu.xml`：パラメータビューの歯車部分のメニューの設定
`APEXmenu.xml`：APEX Network Viewの右クリックメニューの設定
その他にも大体の画面で右クリックメニューの作成が可能なので、是非色々試してほしい。

これを1フォルダにまとめておくとそれを管理するだけでカスタマイズが可能で、フォルダを増やすことでプロジェクトごとに分けることも出来るようになる。

# Package Browserの注意点
Houdini20.0から追加されたPackageBrowser、Packageで設定された変数の確認とかに便利です。
しかし、パッケージをDeleteすることも出来るので、運用には注意してほしい。
`Remove`は今のファイルから読み込まないようにして非表示にする（Houdiniを開きなおすと再度読み込まれる。）
`Delete`はJSONファイルが消えるから使わない方が良いと思う。

ここに表示しないようにするためには`package.json`の中に`"show":false`を追加することでここに表示されなくなる。

:::note info
Lock機能は$HFSから読み込まれたパスが保護される仕組みなので、こちらでカスタマイズなどは現状出来ない。
:::

:::note warn
Houdini20.0ではチェックを外すとpackage.jsonの中身が上書きされて、削除される怪しい挙動が一部であるので、運用には注意してください。20.5では治ってた気がする。
:::

# おまけ：HDAをサブフォルダで管理する方法
HDAを1つのフォルダにまとめると、多くなりすぎてサブフォルダにまとめたくなりますが、サブフォルダに入れると読み込まれなくなります。
これを解決するために、HDAの読み込みよりも前に実行されるpythonrc.pyでOTLSのサブフォルダを読みに行く仕組みを作ることでHDAを読み込むことが出来る。
```Python:pythonrc.py
import hou
import os

# ルートパスの取得
rootpath = hou.getenv("MY_PACKAGE_PATH")
if not rootpath:
    raise EnvironmentError("MY_PACKAGE_PATH environment variable is not set.")

# otls ディレクトリのルートパス
otls_rootpath = os.path.join(rootpath, "otls")

# otls ディレクトリ内のサブフォルダを収集
otls_paths = [
    os.path.join(otls_rootpath, d)
    for d in os.listdir(otls_rootpath)
    if os.path.isdir(os.path.join(otls_rootpath, d))
]

# サブフォルダをセミコロンで連結し、最後に "@/otls" を追加
otls_path_str = ";".join(otls_paths) + ";@/otls"

# 環境変数 HOUDINI_OTLSCAN_PATH を設定
hou.putenv("HOUDINI_OTLSCAN_PATH", otls_path_str)

# 確認用に出力
print(f"HOUDINI_OTLSCAN_PATH: {otls_path_str}")

```

# おまけ２：externaldragdrop.py
Houdiniの作業画面の中や外にノードやファイルを移動させたときのアクションとして`externaldragdrop.py`を実行することが出来る。

<details><summary>作例１</summary>

### 作例１ノードを画面外にドロップすることでObject Mergeノードに割り当てるスクリプト
```Python:externaldragdrop.py

# -*- coding: utf-8 -*-
import hou

def dropAccept(files):
    """
    ドラッグ＆ドロップされたノードリストを処理するエントリポイント関数。

    Args:
        files (list): ドロップされたノード名のリスト。
    """
    # 現在のネットワークエディタとその親ノードを取得
    pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
    if not pane:
        return  # ネットワークエディタが見つからない場合は終了

    network_node = pane.pwd()
    net_type_name = network_node.type().name()

    # ノードリストに基づいて処理
    createFromNodes(network_node, files, net_type_name)


def createFromNodes(network_node, files, net_type_name):
    """
    ドロップされたノードリストを元に新しいノードを作成。

    Args:
        network_node (hou.Node): 処理を行うネットワークノード。
        files (list): ドロップされたノード名のリスト。
        net_type_name (str): 現在のネットワークタイプ名（例: "geo"）。
    """
    if net_type_name == "geo":  # "geo"ネットワークでのみ処理を実行
        # 選択されたノードを優先的に使用
        selected_nodes = hou.selectedNodes()
        nodes = selected_nodes if selected_nodes else files

        # object_mergeノードのパラメータを設定
        parms = {f"objpath{i + 1}": node.path() for i, node in enumerate(nodes)}
        preparms = {"numobj": len(nodes)}

        # object_mergeノードを作成
        create_node(network_node, "object_merge", parms, is_good_pos=True, preparms=preparms)
        return True
    return False


def create_node(network_node, node_name, node_parms, is_good_pos=False, preparms=None):
    """
    指定されたパラメータで新しいノードを作成。

    Args:
        network_node (hou.Node): ノードを作成するネットワークノード。
        node_name (str): 作成するノードの名前。
        node_parms (dict): ノードに設定するパラメータ。
        is_good_pos (bool): ノードの位置を自動調整するかどうか。
        preparms (dict): ノード作成時に設定する追加パラメータ。

    Returns:
        hou.Node: 作成されたノード。
    """
    # 事前パラメータが指定されていない場合の初期化
    if preparms is None:
        preparms = {}

    # 新しいノードを作成
    new_node = network_node.createNode(node_name)

    # ノードの位置を調整（自動配置が有効な場合）
    if is_good_pos:
        new_node.moveToGoodPosition()

    # パラメータを設定
    new_node.setParms(preparms)
    new_node.setParms(node_parms)

    return new_node

```

</details>

<details><summary>作例２</summary>

### 作例２：OBJファイルをネットワーク内に投げ入れるとFileSOPに変換してくれるスクリプト
```Python:
# -*- coding: utf-8 -*-
import hou
import os
import re

def dropAccept(files):
    """
    ドラッグ＆ドロップされたファイルを処理して対応するノードを作成。

    Args:
        files (list): ドロップされたファイルパスのリスト。
    """
    # 現在のネットワークエディタとその親ノードを取得
    pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
    if not pane:
        return  # ネットワークエディタが見つからない場合は終了

    network_node = pane.pwd()
    net_type_name = network_node.type().name()

    # ドロップされたファイルを順に処理
    for i, file in enumerate(files):
        # ファイルごとに位置をずらしてノードを作成
        curpos = pane.cursorPosition() + hou.Vector2(2, -1) * i
        createFromFiles(file, network_node, curpos, net_type_name)


def createFromFiles(file_path, network_node, pos, net_type):
    """
    ドロップされたファイルに基づいてノードを作成。

    Args:
        file_path (str): ドロップされたファイルの絶対パス。
        network_node (hou.Node): ノードを作成するネットワークノード。
        pos (hou.Vector2): 作成するノードの位置。
        net_type (str): 現在のネットワークタイプ（例: "geo"）。

    Returns:
        bool: ノードの作成に成功した場合はTrue。
    """
    # ファイル拡張子と相対パスを取得
    file_ext = os.path.splitext(file_path)[1].lower()
    relpath = rel_path(file_path)

    if net_type == "geo":
        if file_ext == ".obj":
            # OBJファイルを処理
            create_node(network_node, "file", {"file": relpath}, pos)
            return True

    return False


def create_node(network_node, node_name, node_parms, node_pos=hou.Vector2(0, 0), is_good_pos=False, preparms=None):
    """
    指定されたパラメータで新しいノードを作成。

    Args:
        network_node (hou.Node): ノードを作成するネットワークノード。
        node_name (str): 作成するノードの名前。
        node_parms (dict): ノードに設定するパラメータ。
        node_pos (hou.Vector2): 作成するノードの位置（デフォルトは(0, 0)）。
        is_good_pos (bool): ノードの位置を自動調整するかどうか。
        preparms (dict): 作成時に設定する追加パラメータ（デフォルトはNone）。

    Returns:
        hou.Node: 作成されたノード。
    """
    # preparmsが指定されていない場合は空の辞書を初期化
    preparms = preparms or {}

    # 新しいノードを作成
    new_node = network_node.createNode(node_name)
    new_node.setPosition(node_pos)

    # 自動配置が有効な場合は位置を調整
    if is_good_pos:
        new_node.moveToGoodPosition()

    # パラメータを設定
    new_node.setParms(preparms)
    new_node.setParms(node_parms)

    return new_node


def rel_path(fullpath):
    """
    絶対パスをHoudiniの相対パス形式に変換。

    Args:
        fullpath (str): 絶対パス。

    Returns:
        str: Houdiniの相対パス形式（例: $HIP/...）。
    """
    hippath = hou.getenv("HIP")

    if fullpath.startswith(hippath):
        # $HIPディレクトリ内の場合
        return "$HIP" + fullpath[len(hippath):]
    else:
        # $HIP/../形式での相対パスを作成
        relative_dirs = "../" * (hippath.count("/") - fullpath.count("/") + 1)
        return f"$HIP/{relative_dirs}{os.path.basename(fullpath)}"

```

</details>


# おわりに
Houdiniを勉強していると様々なHDAやスクリプトをネットから拾ってきては検証することが増え、何がどこにあるか分からなくなることが多々あります。
その他にも小規模なチームなどでは環境設定などの個人でやる場合が多いです。
Packageを用いて運用することで環境が少しでも整う。そんな人たちの何かのきっかけになれればありがたいです。

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