Python in Houdini入門|houモジュールでノード操作を自動化

Python in Houdini入門|houモジュールでノード操作を自動化

Pythonで横断パイプライン|Maya/Houdini/Painterを繋ぐ設計では、Houdini の Python API を hou モジュールとして紹介し、「ノードを組む・並べるのは Python、点ごとの高速な計算は VEX」という分担を整理しました。本記事はその続編として、Houdini の中で Python を書く場所ごとに、実際のコードを解説します。hou モジュールによるノードの生成とパラメータ操作、シェルフツール、Python SOP、HDA のコールバック、hython によるバッチ処理までを順に扱います。

クラス名・メソッド名・引数は、執筆時点で最新の Houdini 22.0(同梱の Python は 3.13)の公式ドキュメントで確認しています。Houdini 20 以降、parm() などパラメータを扱うメソッドは、hou.Node ではなく hou.OpNode のページに載っています。メニュー名などの表記はバージョンによって異なります。Maya 側の同じ位置づけの記事はMaya OpenMaya API入門|API 2.0でメッシュ処理を高速化です。

夕宮たいだ

ふぁ……みんな〜、今日は Houdini の中で Python を書くおはなしだよぉ。ノードを作るところから、夜のうちに書き出しを済ませてくれるバッチまで、順番に見ていこ〜。

目次

Houdini で Python を書く場所

ひとことで:同じ hou モジュールでも、書く場所によって役割と使える情報が変わります。

Houdini には Python を書ける場所がいくつもあり、向いている用途がそれぞれ違います。

書く場所開き方・置き場所向いている用途
Python ShellWindows > Python Shell1行ずつ試す・調べる
Python Source EditorWindows > Python Source Editorhip ファイルと一緒に保存したい関数
シェルフツールシェルフの背景を右クリック > New Toolボタン1つで繰り返す操作
Python SOPネットワークの Tab メニューから Pythonジオメトリの加工
HDA の Scripts タブHDA を右クリック > Type Propertiesボタンの処理、作成時などのイベント
hythonインストール先の bin フォルダGUI を開かないバッチ処理
Houdini で Python を書く6つの場所と役割
図1:どこに書いても hou モジュールは同じ。書く場所ごとに役割が違う

試すときは Python Shell が便利です。ネットワークエディタのノードを Python Shell にドラッグすると、そのノードを指す hou.node() の式が入力されます。また、既存のノードで print(node.asCode(brief=True)) を実行すると、そのノードを作り直す Python コードが表示されるので、知らないパラメータの書き方を調べる手がかりになります。

hou でノードを作ってつなぐ

ひとことで:hou.node() で親を取り、createNode() で作り、setInput() でつなぎます。

次のコードは、/obj に Geometry ノードを作り、中で Box と Null をつないで、表示フラグとレンダーフラグを立てます。

import hou

obj = hou.node("/obj")
geo = obj.createNode("geo", "crate")             # ノードの種類, ノード名
box = geo.createNode("box")
box.parmTuple("size").set((2.0, 1.0, 1.0))
box.parmTuple("t").set((0.0, 0.5, 0.0))          # 中心を上げて、底面を地面にそろえる

out = geo.createNode("null", "OUT")
out.setInput(0, box)                             # OUT の0番目の入力に box をつなぐ
out.setDisplayFlag(True)
out.setRenderFlag(True)
geo.layoutChildren()                             # ネットワーク内を自動で整列する

createNode() の1つ目の引数はノードの種類(内部名)、2つ目はノード名です。同じ名前のノードがすでにあると、Houdini が末尾に番号を付けた名前で作ります。setInput() の1つ目は入力の番号(0から数える)、2つ目はつなぐノードです。

hou.node() は、パスが間違っていても例外にはならず None を返します。その次の行で 'NoneType' object has no attribute というエラーになるので、パスを変数で組み立てるときは、None でないことを確かめてから使います。

パラメータを読む・書く・式を入れる

ひとことで:1つなら parm()、XYZ のような組なら parmTuple()、まとめて設定するなら setParms() を使います。

geo.setParms({"tx": 3.0, "sy": 2.0})             # 複数のパラメータをまとめて設定
print(geo.parm("tx").eval())                     # 3.0
print(box.parmTuple("size").eval())              # (2.0, 1.0, 1.0)

geo.parm("ry").setExpression("$F * 3", language=hou.exprLanguage.Hscript)   # フレームごとに回る
geo.parm("ty").setExpression("hou.frame() * 0.01", language=hou.exprLanguage.Python)

for p in box.parms():                            # パラメータの内部名と値を一覧する
    print(p.name(), p.eval())
  • parm() と parmTuple() も、名前が間違っていると None を返します
  • eval() は現在のフレームで評価した値を返します。文字列として受け取りたいときは evalAsString()、変数を展開する前の文字列が欲しいときは unexpandedString() を使います
  • setExpression() の言語は language で指定します。省略した場合、パラメータにまだ式がなければ、ノードに設定された式の言語になります

シェルフツールにして使い回す

ひとことで:Python Shell で動いたコードは、シェルフに登録すればボタン1つで何度でも使えます。

シェルフツールは次の手順で作ります。

1. シェルフの背景(ツールが並んでいない所)を右クリックして、New Tool を選ぶ

2. Options タブで Name(内部名。英字で始め、読み込まれているすべてのツールの中で重複できない)と Label(表示名)を入れる

3. Script タブで Script Language を Python にして、コードを貼る

4. Accept で確定する

次のコードは、選んだ SOP の後ろに、書き出し用の Null(OUT_EXPORT)を追加するツールです。Shift を押しながらクリックしたときは、レンダーフラグも立てます。

import hou

nodes = hou.selectedNodes()
if not nodes or not isinstance(nodes[-1], hou.SopNode):
    hou.ui.displayMessage("SOP ノードを選んでから実行してください")
else:
    src = nodes[-1]                                  # 最後に選んだノード
    null = src.parent().createNode("null", "OUT_EXPORT")
    null.setInput(0, src)
    null.moveToGoodPosition()
    null.setDisplayFlag(True)
    if kwargs["shiftclick"]:
        null.setRenderFlag(True)

シェルフから実行されたコードには、クリックしたときの状況が入った kwargs という辞書が渡されます。shiftclick・ctrlclick・altclick で修飾キーの状態が分かります。pane には実行されたペインが入りますが、シェルフから実行したときは None になります。キーの一覧は公式のTool scripts のページにまとまっています。

作ったツールは、Edit Tool ウィンドウの Save to に指定した .shelf ファイル(既定は $HOME/houdiniX.Y/toolbar/default.shelf)に保存されます。チームで共有するときは、HOUDINI_PATH に含まれるフォルダの中の toolbar フォルダ(HOUDINI_TOOLBAR_PATH を設定している場合はそのフォルダ)に .shelf ファイルを置きます。

夕宮たいだ

ボタン1つでいつもの手順が終わるの、便利だねぇ。チームのシェルフにしとけば、みんな同じ道具で作業できるよぉ。

Python SOP でジオメトリを加工する

ひとことで:SOP のジオメトリを書き換えられるのは、Python SOP のように、そのノード自身が計算(クック)されている間だけです。

ネットワークエディタの Tab メニューから Python を選ぶと、Python SOP を作れます。パラメータの Python Code に書いたコードが、ノードが計算されるたびに実行されます。1つ目の入力につないだ SOP のジオメトリは、コードの実行前に Python SOP 側へ複製されるので、hou.pwd().geometry() でそれを受け取って書き換えます。

一方、Python Shell などから node.geometry() で取得したジオメトリは読み取り専用です。変更しようとすると hou.GeometryPermissionError になります。

次のコードは、全ポイントの高さ(Y 座標)を0〜1に正規化して、height01 という新しいポイントアトリビュートに書き込みます。

node = hou.pwd()
geo = node.geometry()

def cook():
    ys = geo.pointFloatAttribValues("P")[1::3]    # x, y, z の並びから y だけを取り出す
    if not ys:
        raise hou.NodeWarning("入力にポイントがありません")
    lo, hi = min(ys), max(ys)
    span = (hi - lo) or 1.0
    if geo.findPointAttrib("height01") is None:
        geo.addAttrib(hou.attribType.Point, "height01", 0.0)
    geo.setPointFloatAttribValues("height01", [(y - lo) / span for y in ys])

cook()

pointFloatAttribValues() は、全ポイントの値を1つの平たいタプルで返します。P は3つの値を持つので、x, y, z, x, y, z…の順に並びます。geo.points() でループして1点ずつ値を読むこともできますが、公式ドキュメントでは、pointFloatAttribValues() でまとめて読むほうが速いと説明されています。書き込みも setPointFloatAttribValues() で一度に行います。同じ名前のアトリビュートがすでにあると addAttrib() は失敗するので、findPointAttrib() で確かめてから作っています。

点ごとの重い計算は VEX(Attribute Wrangle)に任せ、Python SOP は外部ファイルの読み込みなど、Python が得意な処理に使うのが基本です。エラーを出したいときは hou.NodeError、警告なら hou.NodeWarning を raise すると、ノードにそのメッセージが表示されます。公式のPython で SOP を定義するページでは、Python SOP の中でノードのパラメータを設定しないよう注意されています。依存関係が壊れ、さまざまな不具合の原因になるためです。

Python SOP のコードは hip ファイルの中に保存されます。複数のシーンで使い回すなら、File > New Asset で Operator Definition を Python、Network type を Geometry にしてアセットを作ると、Python で定義した SOP のノードタイプとして配れます。

夕宮たいだ

読み取り専用って言われても、どこなら書けるんだっけ……むずかしいねぇ。「SOP のジオメトリを書き換えるのは Python SOP の中」って覚えとけば、迷わないんだぁ。

HDA のボタンとイベントに Python をつなぐ

ひとことで:処理の本体は HDA の Python Module に書き、ボタンやイベントからは短い1行で呼び出します。

HDA(デジタルアセット)に「書き出し」ボタンを付ける例で説明します。HDA の中には、書き出したいジオメトリを出す OUT という Null がある前提です。

1. HDA のノードを右クリックして、Type Properties を開く

2. Parameters タブで、ファイルパス用の File パラメータ(名前 export_path)と Button パラメータ(名前 export)を追加する

3. export を選び、Callback Script 欄の右にあるメニューで言語を Python にして、このあと示す1行を書く

4. Scripts タブの Event Handler メニューから Python Module を選び、このあと示す関数を書いて Accept で確定する

Callback Script に書く1行です。

kwargs["node"].hdaModule().export_geo(kwargs)

Python Module に書く関数です。

import os

import hou

def export_geo(kwargs):
    node = kwargs["node"]                              # ボタンを押された HDA
    path = node.parm("export_path").evalAsString()
    out = node.node("OUT")                             # HDA の中の出力用 Null
    if out is None:
        raise hou.OperationFailed("中に OUT ノードがありません")
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    out.geometry().saveToFile(path)                    # 拡張子で形式が決まる
    if hou.isUIAvailable():                            # hython から押したときは UI がない
        hou.ui.displayMessage(f"書き出しました:{path}")

コールバックに渡される kwargs には、ボタンを押されたノード(node)、パラメータ(parm)、その値(script_value)などが入っています。hou.phm() は hou.pwd().hdaModule() の短縮形で、Callback Script は hou.phm().export_geo(kwargs) とも書けます。

作成時や読み込み時の処理は、Scripts タブの Event Handler で On Created・On Loaded などを選んで書きます。イベントの処理では、kwargs["node"] で対象のノード、kwargs["type"] でノードタイプを受け取れます。次の例は、HDA を置いたときにノードの色を変えます。On Created に書くのは1行だけです。

kwargs["type"].hdaModule().on_created(kwargs)

Python Module には、次の関数を足します。

def on_created(kwargs):
    kwargs["node"].setColor(hou.Color((0.2, 0.6, 1.0)))
HDA のボタンから Python Module の関数が呼ばれる流れ
図2:処理の本体は Python Module に1か所。ボタンからもバッチからも同じ関数を呼ぶ

Parm.pressButton() を使うと、スクリプトからボタンを押したのと同じ処理を呼べます。次の章の hython と組み合わせれば、GUI で使っている書き出しボタンを、そのままバッチから押せます。hython では hou.ui が使えないため、UI を出す処理は hou.isUIAvailable() で分けておきます。

hython でバッチ処理する

ひとことで:GUI を開かずに hip ファイルを次々に開いて処理するなら、hython でスクリプトを実行します。

hython は Houdini に同梱されている Python で、起動時に hou モジュールを自動で読み込みます。Windows では、インストール先の bin フォルダ(C:\Program Files\Side Effects Software\Houdini 22.0.xxx\bin、xxx はビルド番号)にあります。

次のスクリプトは、フォルダ内の hip ファイルを順に開き、名前が OUT_EXPORT の SOP をすべて探して、ジオメトリを .bgeo.sc で書き出します。

# batch_export.py  使い方: hython batch_export.py <hip のフォルダ> <出力フォルダ>
import sys
from pathlib import Path

import hou

def export_hip(hip_path, out_dir):
    hou.hipFile.load(str(hip_path), ignore_load_warnings=True)
    count = 0
    for node in hou.node("/obj").allSubChildren():
        if isinstance(node, hou.SopNode) and node.name() == "OUT_EXPORT":
            out_path = out_dir / f"{hip_path.stem}_{node.parent().name()}.bgeo.sc"
            node.geometry().saveToFile(str(out_path))
            count += 1
    return count

def main():
    hip_dir, out_dir = Path(sys.argv[1]), Path(sys.argv[2])
    out_dir.mkdir(parents=True, exist_ok=True)
    failed = []
    for hip_path in sorted(hip_dir.glob("*.hip")):
        try:
            print(f"[OK] {hip_path.name}: {export_hip(hip_path, out_dir)} 件")
        except hou.Error as e:                         # hou の例外はすべて hou.Error を継承
            failed.append(hip_path.name)
            print(f"[NG] {hip_path.name}: {e}")
    sys.exit(1 if failed else 0)

if __name__ == "__main__":
    main()

コマンドプロンプトからは、次のように実行します。

"C:\Program Files\Side Effects Software\Houdini 22.0.xxx\bin\hython.exe" batch_export.py D:\proj\hip D:\proj\export
  • hou.hipFile.load() は、アセットが見つからないなどの警告が出ると hou.LoadWarning を投げます。ignore_load_warnings=True を付けないと、1つの警告でバッチ全体が止まります。hython では保存確認のダイアログは出ません
  • hou の例外はすべて hou.Error を継承しています。ファイルごとに except hou.Error で受けて記録し、残りのファイルの処理を続けます
  • 終了コードを 0 と 1 で分けておくと、CI やタスクスケジューラーから成否を判定できます
  • hou を読み込んだ時点で、Houdini のライセンスを1つ使います。公式のコマンドラインでの使い方のページでは、既定で Houdini Batch ライセンスを使い、ない場合は Houdini FX ライセンスを使うと説明されています。ライセンスの種類によって扱いが変わるので、運用する前に自分の環境で hython が起動するか確かめておきます
  • hou の読み込み時には、123.py や 456.py などの起動スクリプトも実行されます。GUI では問題ないのにバッチだけ挙動が変わるときは、ここも確認します
夕宮たいだ

寝てるあいだに全部の hip を書き出してくれるの、ほら、便利でしょ? おやすみ中に働いてもらえるの、いいよねぇ。

よくある失敗と対処

ひとことで:None の見落とし、読み取り専用のジオメトリ、実行する場所による違いの3つが特に多い失敗です。

'NoneType' object has no attribute が出る

hou.node() や parm() は、パスや名前が間違っていると None を返します。エラーが出た行ではなく、その値を取得した行を疑います。パラメータの内部名は node.parms() や asCode() の出力で確かめられます。

GeometryPermissionError でジオメトリを変更できない

Python SOP の外で node.geometry() から取得したジオメトリは読み取り専用です。変更する処理は、Python SOP か Python で定義した SOP の中に移します。

シェルフのコードを Python Shell で試すと kwargs がないと言われる

kwargs は、シェルフやコールバックから呼ばれたときだけ用意される変数です。処理の本体を関数にまとめ、シェルフ側では kwargs から取り出した値を関数に渡す形にしておくと、Python Shell からも同じ関数を試せます。

Python SOP の中でパラメータを変えたら挙動がおかしくなった

公式ドキュメントで避けるよう書かれている使い方です。パラメータの変更は、シェルフツールやコールバックなど、ノードの計算の外で行います。

ロックされた HDA の中のノードを変更できない

ロックされたアセットの中では、つなぎ替えや削除が hou.PermissionError や hou.OperationFailed になります。中身を直すのは定義そのものを更新する作業なので、スクリプトからロックを外す allowEditingOfContents() は、その作業のときだけ使います。

バッチが途中で止まる

hou.hipFile.load() が投げる hou.LoadWarning と、UI が前提の hou.ui の呼び出しがよくある原因です。ignore_load_warnings=True を付け、UI を出す処理は hou.isUIAvailable() で分けます。

夕宮たいだ

ぁぅ……None が返ってきてたのに気づかないの、みんな一度はやるんだぁ。エラーの行より前を、落ち着いて見てねぇ。

チェックリスト

  • [ ] 新しい処理は、まず Python Shell で1行ずつ確かめた
  • [ ] hou.node() と parm() の戻り値が None でないか確かめている
  • [ ] 繰り返す操作はシェルフツールにして、共有の toolbar フォルダに置いた
  • [ ] ジオメトリの変更は Python SOP の中だけで行い、パラメータは書き換えていない
  • [ ] 多数のポイントは、まとめて読み書きするメソッドで扱っている
  • [ ] HDA の処理本体は Python Module に置き、コールバックは短い呼び出しにした
  • [ ] hou.ui を使う処理は hou.isUIAvailable() で分けている
  • [ ] バッチは ignore_load_warnings=True で開き、失敗したファイルを記録している

次に読む記事

夕宮たいだ

ふぁ……hou モジュール、けっこう素直でしょ? まずは Python Shell で1行、そこからシェルフ、バッチって、少しずつ広げていこうねぇ。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次