🟠

Houdini: 複数のジオメトリをロードするHDAのご紹介

2024/12/13に公開

もうアドベントカレンダーの季節とか嘘でしょう…?(2回目)

気を取り直して皆さんいかがお過ごしでしょう。僕は今年もHoudini三昧の毎日でした。

今回は複数のジオメトリをロードするHDAの作り方についてお話をしたいと思います。

本記事はHoudiniアドベントカレンダー2024 13日目の記事です。

データ配布

HDAダウンロード

ツール制作の背景とその特徴

大量のジオメトリを読み込んで確認したいこと、頻繁にありますね。PDGなどを利用してバッチ処理でジオメトリを生成している場合などはなおさらです。

本ツールは下記特徴をもったツールとなります。

  • 任意のディレクトリにあるジオメトリを一括でロード
  • ルートディレクトリから子・孫…と再帰的にファイルを取得可能
  • 読み込んだファイルごとにPacked Primitiveとして読み込み可能
  • 連番は付与するコンポーネントをPoint/Primitiveに切り替え可能
  • Packed Primitive読み込み時には連番のAttributeを付与
  • ジオメトリの再読み込み可能
  • 読み込む拡張子を指定可能

動作は下記動画をご参考ください。

動作確認用動画

ツール構成の説明

ここから実際のネットワークを見ながらツール構成を順にご説明します。

パラメータ

最初に最終的なパラメータを確認しておきましょう。量も少なく簡単なHelpも書いていますので、上述の動画と合わせて使い方は簡単にわかるかと思います。

パラメータ

ツールのネットワーク

まずはAllow Editing of Contentsを実行せずHDAの中にダイブしてみましょう。PythonSOPのみノードが白くなっていてロックが解除されていますね。これが後ほどポイントとなります。

Editable Nodes

その理由は後ほど解説するとして、この状態の作り方はEdit Operator Type PropertiesウィンドウのNodeタブからEditable Nodespythonを指定してあげることで可能です。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の処理

Reloadボタンの挙動確認動画

実はリロードボタンを実装しなければ上述のPythonSOPのみでほぼ完成なのですが、書き出しされたデータを確認するというツールの特性上、これは省くわけにはいかない機能でした。しかし、Houdiniはジオメトリキャッシュを強く持って離さないという傾向があり、特にPacked Primitiveにはその傾向が顕著であるため、バッドノウハウ的な実装を追加して対処しています。ここを真っ当な方法で対処できるよ、というアイデアお持ちの方はぜひ教えて下さい。筆者が喜びます。

Reloadボタン

添付画像の通り、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つ。

  1. PythonSOPのスクリプトにアクセスし、文末に改行を入れた後元のコードに差し替え
  2. 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

参考

プログラム的にVerb(動詞)を使ったジオメトリ

Discussion