TOPでUSDを扱ってみる
Houdini アドベントカレンダー 2023 16 日目は、普段書いている LOP...ではなく
TOP 内で USD を使用した処理を試していこうと思います。
TOP とは
まず、前提として Houdini の TOP について軽く触れておきます。
TOP とは TaskOperator の略で、様々なタスクをプロシージャルに実行を可能にするものです。
たとえば、あるフォルダ以下にある画像ファイルを指定サイズにリサイズし、1 ファイルに結合する...
といったように、A が終わった後に B をする、そして最後に C をする のように
連続した処理を実行したい場合があったとします。
TOP では、そうした連続した処理を構築して、実行できるようになります。
TOP と USD
そんな TOP という世界ですが、実は非常に USD(SOLARIS) と相性が良いです。
という記事でも書いたのですが、USDというのは非破壊な処理はおおむねPythonで可能なのですが 破壊的編集に関してはPythonでは難しいものの、SOLARISではそうした破壊的処理が簡単に構築できます。 この破壊的な処理をしながらファイルを複数出力しつつ...みたいなことをやろうとすると SOLARISだけではできず、TOPを使用したほうが便利なことが多いです。 また、USDという複数のファイルを合成して何か処理をする都合 バッチ処理的なことをするのに向いています。 そういった処理は、SOLARISだけで頑張るよりもTOP上でやるほうが良いです。 ので、今回はTOPを利用してUSDを操作する方法を試していこうと思います。
デフォルトで用意されているノードを確認する
USD に関連するノードは Houdini20 から増えているよう ですが( Cat Diff Stitch Zip 等は GenericGenerator)
その中で使いそうなのをいくつか試してみます。
USD Import Data
usdImportData は、ある USD のシーンから WorkItem を生成します。
ファイルを指定することも可能ですが(Input のファイルから WorkItem を作るなど)
LOP Path で指定したノードのステージを使用して WorkItem を作ることが可能です。
例えば、このようなステージを指定した場合、Primitives を * にしているので、
ステージ内に含まれている Prim だけ WorkItem を作ります。
ROPUSD
ROPUSD ノードは、その名の通り指定の LOP ノードを USD としてエクスポートできるノードです。
指定の LOP ノードを指定して、そのノードを Export します。
使い方としては、複数カメラを Switch で作っていたら
そのスイッチした数分だけ出力するとかなどが考えられます。
Export する Prim を指定する...みたいなことはできなさそう
USDModifyPaths
USDModifyPaths とは、USD 内にあるアセットパスを見つけて、パスを修正するノードです。
アセットパスとは、USD のアトリビュートタイプの 1 つです。
文字列とは違い、AssetPath の場合は
「AssetResolaver を使用して USD プラグイン内でパス解決を行う」ことができます。
例えば、このようなあるところへのファイルパスをもつアトリビュートがあるとします。
def "C"{
asset testPath = @D:/test.usda@
}
usda だとこのようになります。
このパスは、現在フルパスになっていますが、
例えばこれらのパスをすべて SEARCH_PATH からの相対扱いにしたいとします。
その場合は、PythonCode に条件と置き換える文字列処理(結果を return で返す)
するコードを書くか、 Find Prefix と Replace Prefix を使用して文字列置換します。
手元だとすべてローカルディレクトリを見るようにするが
リリースするときにはパスを一斉置換したい時などに使えそうです。
USDAnalyze
USDAnalyze ノードは、その名の通り、USDFile あるいは指定の LOP ノードのステージ情報を
解析し てアトリビュートとして取得します。
例として、totalPrimCount や、usedLayerCount などのようなシーンにまつわる情報を取得します。
このノード自体で何かをするわけではないですが、この情報をもとに Wedge で複数の WorkItem を生成したり
条件分岐に使用する(例として Model を含む場合は次の処理を実行する、など)等が考えられます。
今回紹介した以外だと、レンダリング関係のノードがありますが、
専門外なので今回は割愛します。
実際に使ってみる
実際に TOP で処理を作っていきます。
TOP 上でやったほうが良いこととしては、繰り返し処理をしつつ Export するようなことをしたい場合。
LOP だけで完結しようとすると、USDROP を通す都合 ForEach だけではうまくいきません。
ので HDA を作りつつ TOP 側で処理します。
今回は、ある 1 つの USD ファイル
/PrimA
/PrimB
/PrimC
のような階層を持つ単一の USD を、3 つの USD ファイルにばらして Reference 化する処理を作ります。
(意外とこの処理は Python でやるとめんどくさい)
あらかじめこのような USD シーンを 作っておきます。
TOP 側は、 USDImport と HDAProcessor を使用します。
Null ノードには、
処理をする USD を指定するアトリビュートだけ入れておきます。
USDImportData では、処理対象 USD から切り出したい Prim の情報を取得します。
対象は Import Primitives の条件にマッチする Prim になり、この数だけ Prim Path Attrib と
PrimNameAttrib を持つ WorkItem を生成します。
結果生成された WorkItem がこのようになります。
次に、指定した Prim で切り出す HDA を作ります。
切り出す Prim の Path 指定と元になる USD ファイルを受け取り、
LoadLayerForEditing で UsdPath の USD を開き、
SplitScene で分離して Output につないでおきます。
この時、このノード以下で USDROP を繋げなくても OK です。
作成した HDA を指定し、 OutputFileName に PrimPath を入れます。
HDAProcessor は、LOP ノードの HDA を使 用すると、HDA の Output から出力される Stage を
Processor 側で出力し、OutputFile に指定してくれます。
(USD 側で USDROP を使用しないのは、Output にしたいから)
あとは、対象の UsdPath と切り出す Prim の Path を渡して置けば終了です。
実行すると、このような 3 つの USD が作成されます。
一応これだけでも大丈夫ですが、Reference するときは DefaultPrim を作っておいたほうが良いので
階層を調整しつつ ConfigureLayer で DefaultPrim を指定します。
最終的にはこんな感じになります。
HDAProcessor を使用した場合、USDROP を指定しない場合は、Output で出力した Stage が
HDAProcessor 側で出力されます。
結果を Output で受け取ることができました。
(以前実験した時は bgeo になっていたような気がするけど、LOP の HDA になっていれば大丈夫っぽい)
これをまとめて Reference にしたいので、
WaitForAll で、Output をまとめます。
おそらく Houdini20 空だと思いますが、 Merge Output Files を All Files にしないと
Output の Attribute が取得できないようです。
ここで、あとは Python で処理をしようとしたのですが、
何故か work_item.inputFiles で、Input を受け取ろうとしたのですが
HDAProcessor > WaitForAll だと 表示上は Output があっても取得できないみたいで
Python ではなくマージ処理も HDA で作ることにしました。
HDA は、Reference したい USD ファイルを USD Paths で受け取り、 SavePath に保存するようにします。
先ほどの分割する HDA とは違い、こちらは USDROP を使用して Export します。
HDAProcessor を使用して USD を出力すると、どうやらコンポジションが全部消えて Flatten された
状態で出力されてしまうようです(何故...)
中の構造はとてもシンプルで、ForEach を使用して、USDPaths の数だけ繰り返します。
Reference ノードなどもあるのですが、ファイル名から USD の PrimName とかを決める処理を
書くのが面倒だったので、そのあたりは Python で書きます。
node = hou.pwd()
stage = node.editableStage()
import os.path
bn = os.path.splitext(os.path.basename(hou.contextOption("ITERATIONVALUE")))[0]
prim = stage.DefinePrim(f"/Root/{bn}")
prim.GetReferences().AddReference(hou.contextOption("ITERATIONVALUE"))
結果、意図した構造ができました。
あとは、Export を実行する Button をつけておきます。
def Export(kwargs):
usdrop = kwargs['node'].node("./usd_rop1")
usdrop.parm("execute").pressButton()
作るたびに書き方忘れるのでメモがてら...
あとは PDG 側を調整します。
HDA Processor で、上流の inputs を受け取り、 Export ボタンを実行します。
input のファイルは、 @pdg_input だと最初の 1 つしか受け取ることができないので
pdginputvals を使用して取得します。
そして、Button のチェックボックスを ON にしておきます。
無事出力できました。
若干腑に落ちないところ
HDAProcessor > WaitForAll だと inputs が Python で取得できない
HDAProcessor で USD を出力すると Flatten されてしまう
はありますが、USD のファイルを意図した形に加工することができました。
まとめ
もう 1 つのアドカレ記事 でも書いたのですが、USD は破壊的処理を使用すると非常にめんどくさくて
どうやってやったらいいかわからないことがあります。
ですが、PDG と SOLARIS と Python を組み合わせると何とかなることも多いので
アプローチとしては良いなぁと思います。
TOP側は、私もまだ理解できていないことも多いので
引き続きいろいろ検証してよい使い方を見つけていければなと思います。