今回のゴール
- HDA で使う 外部 Python モジュール を、安全にリロードしながら開発できるようにする
- Detail の dict 型アトリビュート から、自動で Key-Value Dictionary パラメータ を生成・削除できるようにする
最終的には、
- HDA 生成時に自動で Key-Value Dictionary パラメータが追加される
- 辞書型アトリビュートを更新したら、ボタン 1 つでパラメータ側に反映できる
というワークフローを目指します。
(コードの更新をするたびPython Moduleにコピペするが面倒なのでなんとかしたい)
今回特に紹介したいPythonコード
全てのdetailアトリビュート名を取得
ポイント: intrinsicValue は他にもいろいろ取得できる。
全てのpointグループなど とても便利です。
attr_name_list :list[str] = geo.intrinsicValue('globalattributes')
Key-Value-Dictionaryのパラメータ作成
DataParmTemplate作成後に
setDataParmType(hou.dataParmType.KeyValueDictionary)のメソッドで指定します。
data_parm = hou.DataParmTemplate(
name=parm_name,
label=label,
num_components=1,
)
data_parm.setDataParmType(hou.dataParmType.KeyValueDictionary)
全体のフローイメージ
- 上側:外部モジュールと HDA の連携の流れ
- 下側:Detail の dict アトリビュートから Key-Value Dictionary パラメータを自動生成した結果
外部モジュールのパス設定
Houdini から外部モジュールを import できるように、packages ファイルでパスを設定します。
{
"enable": true,
"env": [
{
"HOUDINI_PATH": "D:/Development/Houdini_Tools"
},
{
"COMMON": "D:/Development/Houdini_Tools/packages/Houdini 21"
},
{
"HOUDINI_PYTHON_PANEL_PATH": "$COMMON/python_panel"
},
{
"HOUDINI_TOOLBAR_PATH" : "$COMMON/toolbar"
},
{
"HOUDINI_OTLSCAN_PATH":"$COMMON/hda"
},
{
"PYTHONPATH":"$COMMON/scripts/python/"
},
{
"HOUDINI_APEXGRAPH_PATH": "D:/Development/Houdini_Tools/apexgraph"
},
{
"EDITOR":"$LOCALAPPDATA/Programs/Microsoft VS Code/Code.exe"
},
{
"HOUDINI_EXTERNAL_HELP_BROWSER":"https://www.sidefx.com/docs/houdini21.0/"
}
]
}
ここではいろいろな環境変数を定義していますが、今回の主役は PYTHONPATH です。
"PYTHONPATH":"モジュールパス"
PYTHONPATH は、Houdini(というより Python 全体)が
「import するモジュールを探しに行く場所」を示す環境変数です。
そのため、
自分のモジュールを置いているディレクトリを
PYTHONPATHに追加しておく
ことが重要になります。
想定ディレクトリ構成
例として、次のようなパスを用意します。
-
HDA で使うモジュール用のディレクトリを作る
例:
D:/Development/Houdini_Tools/scripts/python/hda_pkg -
そのディレクトリを
PYTHONPATHに含めるように設定する。 -
ディレクトリ内に
__init__.pyを置いてパッケージとして扱えるようにする。
reload 処理付きパッケージを用意する
開発中にモジュールを書き換えたとき、Houdini を再起動せずに変更を反映させたいので、
__init__.py に reload 関数を用意しておきます。
#__init__.py
import importlib
import sys
def reload():
prefix = __name__ + "."
for name, module in list(sys.modules.items()):
if name == __name__ or name.startswith(prefix):
importlib.reload(module)
この reload() を呼ぶことで、
- パッケージ本体
- その配下のモジュール
をまとめて再読み込みできます。
動作確認用モジュール test_math.py
まずは挙動確認のため、簡単な計算クラスを作ります。
#test_math.py
class Math:
def __init__(self, a: float, b: float):
self.a = a
self.b = b
def sum(self) -> float:
c = self.a + self.b
return c
def multiply(self) -> float:
c = self.a * self.b
return c
def divide(self) -> float:
c = self.a / self.b
return c
外部エディタからテスト実行する
Windows メニューの Python Source Editor から外部エディタを開きます。
開いたエディタ上で、次のようなコードを実行して、
-
PYTHONPATHの設定が効いているか -
reloadが期待どおり動いているか
を確認します。
import hda_pkg
from hda_pkg.test_math import Math
hda_pkg.reload()
print(Math(2, 3).sum())
print(Math(3, 3).divide())
確認ポイント
-
PYTHONPATHに設定したディレクトリが正しく参照されているか -
test_math.pyに関数やメソッドを追加・削除したあと、hda_pkg.reload()で変更が反映されるか
を試して、パス設定と reload 処理が正しく機能しているかをチェックします。
HDA 内での Reload 確認フロー
HDA の PythonModule を経由して挙動を確認する場合の、現状のフローです。
Apply → Reload All Files → Apply → 実行ボタン → Apply
現状はやや手順が多く、ここは今後改善の余地ありです。
Detail クラスに dict 型アトリビュートを作る
次に、Key-Value Dictionary パラメータの元になる dict 型の Detail アトリビュートを作ります。
HDAの中でも外でもどこに作っても良いです。
// Run Over: Detail (only once)
dict animal;
animal["cat"] = "male";
animal["dog"] = "female";
setdetailattrib(0, "dict_animal", animal);
dict fruit;
fruit["apple"] = 2;
fruit["banana"] = 3;
fruit["cherry"] = 1;
setdetailattrib(0, "dict_fruit", fruit);
-
dict_animalとdict_fruitという 2 つの Detail dict アトリビュートを作成 - これらを元に、後で HDA パラメータとして Key-Value Dictionary を自動生成します
Key-Value Dictionary を自動生成する仕組み
Key-Value Dictionary とは?
Houdini のパラメータ上で、辞書型データをキー・バリューの組として管理できるパラメータタイプです。
ここでは、Detail の dict アトリビュートをスキャンして、対応する Key-Value Dictionary パラメータを自動で作る仕組みを整えます。
パッケージのディレクトリ構造(暫定)
現時点では、hda_pkg は次のような構成を想定しています。
.
└── hda_pkg
├── utils
│ └── hda_base.py
├── __init__.py
└── parameter.py
今後の運用の中で、必要に応じて構成を見直していきます。
HDA の基底クラス hda_base.py
HDA 全般で共通して使いたい処理をまとめるための基底クラスです。
import hou
class HDABase:
"""
hda_pkgの基底クラス
"""
def __init__(self,kwargs):
self.this_node :hou.SopNode =kwargs['node']
self.this_node_geo :hou.Geometry = self.this_node.geometry()
def get_current_node(self)->hou.SopNode :
"""
このHDAのノードを返す
"""
return self.this_node
def get_globalattributes_datatype(self) ->dict[str,hou.attribData] :
"""
グローバルアトリビュート名とdataTypeの辞書を返す
"""
attr_name_list :list[str] = self.this_node_geo.intrinsicValue('globalattributes')
datatype_dict : dict[str,hou.attribData] ={}
for attr_name in attr_name_list:
attr = self.this_node_geo.findGlobalAttrib(attr_name)
datatype_dict[attr_name] = attr.dataType()
return datatype_dict
ポイント:
-
get_globalattributes_datatype()で、Global Attribute 名とそのattribData型の対応表を dict で返す - 後で「Dict 型のアトリビュートだけを拾う」処理に使います
パラメータ生成用の基底クラス parametar.py
次に、パラメータ生成ロジックをまとめたクラスを用意します。
import hou
from hda_pkg.utils.hda_base import HDABase
class Parameter(HDABase):
"""
パラメータの基底クラス
"""
def __init__(self,kwargs):
super().__init__(kwargs) # 親の初期化を呼ぶ
def create_kv_parms(self)->None:
"""
key_value_dictionaryの作成
"""
datatype_dict = self.get_globalattributes_datatype()
dict_names: list[str] = [
name for name, dt in datatype_dict.items()
if dt == hou.attribData.Dict
]
ptg = self.this_node.parmTemplateGroup()
# dict attribute名 -> 作るparm名 の対応
dict_to_parm = {}
for dict_name in dict_names:
parm_name = f"kv_{dict_name}"
# 既に同名パラメータがあるならスキップ(または置き換え)
if ptg.find(parm_name) is not None:
continue
label = dict_name[:1].upper() + dict_name[1:]
data_parm = hou.DataParmTemplate(
name=parm_name,
label=label,
num_components=1,
)
data_parm.setDataParmType(hou.dataParmType.KeyValueDictionary)
ptg.append(data_parm)
dict_to_parm[dict_name] = parm_name
self.this_node.setParmTemplateGroup(ptg)
# ここから値の同期
for dict_name, parm_name in dict_to_parm.items():
parm = self.this_node.parm(parm_name)
if parm is None:
continue
# detail(dict) attribute の値を取得
try:
value = self.this_node_geo.attribValue(self.this_node_geo.findGlobalAttrib(dict_name))
except Exception:
value = {}
# KeyValueDictionary は {str: str} しか入らないので変換
kv_value = {str(k): str(v) for k, v in (value or {}).items()}
parm.set(kv_value)
def delete_created_kv_parms(self)->None :
"""
key_value_dictionaryの削除
"""
ptg =self.this_node.parmTemplateGroup()
# まず削除対象の名前を集める(走査しながら消すと壊れやすいのでリスト化)
targets = []
for parm_tmpl in ptg.entries():
# FolderParmTemplate も entries() に混ざるので name() が取れるものだけ
try:
name = parm_tmpl.name()
except Exception:
continue
if not name.startswith("kv_"):
continue
# KeyValueDictionary の DataParmTemplate だけを消したい場合は型で絞る
if isinstance(parm_tmpl, hou.DataParmTemplate):
if parm_tmpl.dataParmType() == hou.dataParmType.KeyValueDictionary:
targets.append(name)
# 削除
for name in targets:
ptg.remove(ptg.find(name))
self.this_node.setParmTemplateGroup(ptg)
やっていること:
- Global Attribute のうち Dict 型 (
hou.attribData.Dict) だけ を抽出 -
kv_プレフィックス付きの Key-Value Dictionary パラメータを自動生成 - Detail dict の中身を
{str: str}に変換してパラメータに流し込む - 後から消したくなったときのために、
kv_で始まる Key-Value Dictionary パラメータだけを削除するメソッドも用意
「生成」と「削除」を 1 クラスにまとめておくことで、HDA のボタンコールバックからシンプルに呼び出せます。
HDA の Python Module
作成したクラスを HDA 側から呼び出すための Python Module です。
import hda_pkg
from hda_pkg.parameter import Parameter
hda_pkg.reload()
def on_create_button_pressed(kwargs):
Parameter(kwargs).create_kv_parms()
def on_delete_button_pressed(kwargs):
Parameter(kwargs).delete_created_kv_parms()
ポイント:
- 冒頭で
hda_pkg.reload()を呼んでおくことで、外部モジュールの更新を HDA 側にすぐ反映 - HDA からは
on_create_button_pressed/on_delete_button_pressedという 2 つの関数だけを意識すればよい構成
HDA の onCreated
HDA 作成時に、自動で Key-Value Dictionary パラメータを生成するための設定です。
kwargs['node'].hdaModule().on_create_button_pressed(kwargs)
HDA の On Created スクリプトにこの 1 行を書いておくことで、
HDA 生成と同時に create_kv_parms() が実行されます。
追加と削除用のボタン
HDA のパラメータに、手動で 追加 / 削除 するためのボタンを用意します。
hou.phm().on_create_button_pressed(kwargs)
hou.phm().on_delete_button_pressed(kwargs)
- 各ボタンの Callback Script に上記のコードを設定
- これにより、HDA 作成後でも Detail dict を更新したタイミングでパラメータを再生成できます
まとめ:このワークフローでできること
この一連の仕組みにより、次のような流れが実現できます。
- HDA の中に 辞書型の Detail アトリビュート を追加する
- HDA 作成時、あるいはボタン押下時に、対応する Key-Value Dictionary パラメータ が自動で生成・同期される
- 外部モジュール側のロジックも
reload()を通じて安全に更新できる
結果として、
- 「ジオメトリ上の辞書」と「HDA パラメータ上の辞書」をセットで扱える
- ディティールアトリビュートにあるデータをわざわざパラメータ化する手間を省き、データを一元管理することができます
今後は、生成タイミングや UI 配置、Reload フローの簡略化など、運用しながらブラッシュアップしていく余地があります。







