Houdini: 複数のジオメトリをロードするHDAのご紹介
もうアドベントカレンダーの季節とか嘘でしょう…?(2回目)
気を取り直して皆さんいかがお過ごしでしょう。僕は今年もHoudini三昧の毎日でした。
今回は複数のジオメトリをロードするHDAの作り方についてお話をしたいと思います。
本記事はHoudiniアドベントカレンダー2024 13日目の記事です。
データ配布
ツール制作の背景とその特徴
大量のジオメトリを読み込んで確認したいこと、頻繁にありますね。PDGなどを利用してバッチ処理でジオメトリを生成している場合などはなおさらです。
本ツールは下記特徴をもったツールとなります。
- 任意のディレクトリにあるジオメトリを一括でロード
- ルートディレクトリから子・孫…と再帰的にファイルを取得可能
- 読み込んだファイルごとにPacked Primitiveとして読み込み可能
- 連番は付与するコンポーネントをPoint/Primitiveに切り替え可能
- Packed Primitive読み込み時には連番のAttributeを付与
- ジオメトリの再読み込み可能
- 読み込む拡張子を指定可能
動作は下記動画をご参考ください。
ツール構成の説明
ここから実際のネットワークを見ながらツール構成を順にご説明します。
パラメータ
最初に最終的なパラメータを確認しておきましょう。量も少なく簡単なHelpも書いていますので、上述の動画と合わせて使い方は簡単にわかるかと思います。
ツールのネットワーク
まずはAllow Editing of Contents
を実行せずHDAの中にダイブしてみましょう。PythonSOP
のみノードが白くなっていてロックが解除されていますね。これが後ほどポイントとなります。
その理由は後ほど解説するとして、この状態の作り方はEdit Operator Type Properties
ウィンドウのNode
タブからEditable Nodes
でpython
を指定してあげることで可能です。python
という名前のノードはHDA化してもロックしないでねという設定となります。
PythonSOP
本HDAのほぼすべての機能はこのPythonSOP
が担っています。コードを見ていきましょう。
import hou
import glob
import os
from pathlib import Path
node = hou.pwd()
geo = node.geometry()
def load_geo(
directory: str,
extension: str = ".bgeo.sc",
ispacked: bool = True,
attrib: str = "variant",
recursive: bool = False,
) -> None:
"""ディレクトリからジオメトリファイルを読み込みマージする
Args:
directory (str): ジオメトリファイルが格納されているディレクトリパス。
extension (str, optional): 読み込むファイルの拡張子。デフォルトは '.bgeo.sc'
ispacked (bool, optional): パックドプリミティブとして読み込むかどうか。デフォルトは True
attrib (str, optional): バリアントを格納する属性名。デフォルトは 'variant'
recursive (bool, optional): サブディレクトリも再帰的に検索するかどうか。デフォルトは False
"""
node = hou.pwd()
geo = node.geometry()
# パスの正規化
directory = os.path.normpath(directory)
if not os.path.exists(directory):
raise ValueError(f"指定されたディレクトリが存在しません: {directory}")
# ファイルパターンの作成
pattern = "**/*" if recursive else "*"
search_pattern = str(Path(directory) / pattern) + extension
# ファイル検索
file_list = sorted(glob.glob(search_pattern, recursive=recursive))
if not file_list:
print(f"警告: {search_pattern} に一致するファイルが見つかりませんでした")
return
# ジオメトリ読み込み
loadedgeo = hou.Geometry()
sop = hou.sopNodeTypeCategory()
for i, filepath in enumerate(file_list):
try:
fileverb = sop.nodeVerb("file")
fileverb.setParms(
{
"file": filepath,
"loadtype": 4 if ispacked else 0,
"viewportlod": 0 if ispacked else 1,
}
)
fileverb.execute(loadedgeo, [geo])
loadedgeo.addAttrib(hou.attribType.Point, attrib, i)
geo.merge(loadedgeo)
except Exception as e:
print(
f"エラー: ファイル {filepath} の読み込み中にエラーが発生しました: {e}"
)
# パラメータの取得
directory = node.parm("path").eval()
extension = node.parm("filetype").evalAsString()
ispacked = node.parm("is_packed").eval()
attrib = node.parm("attrib").eval()
recursive = node.parm("is_recursive").eval()
# ジオメトリの読み込み実行
load_geo(directory, extension, ispacked, attrib, recursive)
ちょっと長そうに見えますが、基本的にload_geo
という関数を定義・実行しているだけです。ファイルやフォルダのパターンマッチはglob
モジュールにお任せしています。
コメントも多く入れていますし特に難しい部分はないため詳細の解説は割愛しますが、ポイントとしてはhou.NodeTypeCategory.nodeVerb
を利用したジオメトリ生成があげられます。
古き良きPythonSOP
の使用法を用いる場合、FileSOP
を動的に生成してMergeSOP
でまとめる必要がありますが、Verb
を利用すればノードの生成を行うことなくジオメトリのみを取得することが可能になります。便利ですね。
Reloadの処理
実はリロードボタンを実装しなければ上述のPythonSOP
のみでほぼ完成なのですが、書き出しされたデータを確認するというツールの特性上、これは省くわけにはいかない機能でした。しかし、Houdiniはジオメトリキャッシュを強く持って離さないという傾向があり、特にPacked Primitiveにはその傾向が顕著であるため、バッドノウハウ的な実装を追加して対処しています。ここを真っ当な方法で対処できるよ、というアイデアお持ちの方はぜひ教えて下さい。筆者が喜びます。
添付画像の通り、Reload Geometry
ボタンが押されるとコールバックとしてhou.phm().reload(kwargs)
が実行されます。そして実行される関数はEdit Operator Type Properties
ウィンドウのScripts
タブ、PythonModule
で定義しています。
コードの内容としては下記のとおりです。
def reload(kwargs):
node = kwargs["node"]
pythonsop = [c for c in hou.pwd().children() if c.name() == "python"][0]
code = pythonsop.parm("python").eval()
pythonsop.parm("python").set(code + "\n")
pythonsop.cook(force=True)
pythonsop.parm("python").set(code)
nullsop = [c for c in hou.pwd().children() if c.name() == "KICK"][0]
nullsop.cook(force=True)
やっていることは大きく2つ。
-
PythonSOP
のスクリプトにアクセスし、文末に改行を入れた後元のコードに差し替え -
KICK
という名前のNullSOP
を強制的にクック
このようになっています。
1に関してはPythonSOP
を強制的に再度評価させるためコードの書き換えを行っており、これを実現するために前述のEditable Nodes
設定をしていたわけですね。
続いて2に関してはForEach
ループ内のPacked PrimitiveをUnpack、ジオメトリのキャッシュクリア、再度Packという処理を実行させるため行っています。
ScriptSOP
ではgeocache -c
というコマンドを実行しており、これによってジオメトリキャッシュを開放していますが、Packされたままだとうまく動かないケースがあるのでUnpack、Packの間で実行しています。
残りは連番アトリビュートのPromoteやKICKを強制クックする際の処理の分岐なので難しいことはないでしょう。
まとめ
最後まで読んでくださった方、ありがとうございます。小さなネットワークかつ単純な機能のHDAですが、丁寧に説明しようと思うと長文になってしまいますね。またハックっぽいネットワーク・処理に関してはほめられたものではなく、もっといい方法を探していきたいものです。
これからもいろいろなHDAを作っていくことでしょう。便利そうなのがあればまたご紹介しますね!
ではでは、来年も素敵なHoudiniライフを!
開発環境
- Windows10
- Houdini 20.0.688
Discussion