はじめに
Houdini から Solaris(以下、LOP) へのトランスレートは現状でもよく出来ていて多くの情報を LOP へ持っていくことができるようになっています。ですが、例えばゲーム開発では多くの独自のシーン情報を必要とするケースがしばしば存在します。USD は簡単にカスタムデータを設定可能で受け入れ順にはできていますが、問題は予約されていない情報をどうやって Houdini から USD へ引き渡すか、という事になります。本記事ではカスタム objノードを USD へトランスレートする方法を考えてみたいと思います。尚、Houdini19 を前提にしています。
HoudiniからSolaris(LOP)へ
これを実現する代表的なノードは
- SOP Import
- Scene Import(2.0)
となると思います。
SOP Import
このノードは SOP の情報を LOP へトランスレートするノードです。このノードは非常に強力で、例えば SOP の Primitive に pathアトリビュートを設定する事で階層構造もトランスレートすることができ、要するにSOPでプロシージャルに階層構造をオーサリングできることを意味します。またアトリビュートを Primアトリビュートへトランスレート可能でシェイプだけでなく、先に書いた階層構造、シェイプ、アトリビュートをトランスレートする事ができます。PackedPrimitive
を使用すればインスタンスも実現できます。このノードだけで数記事になりそうだボリュームですので興味ある方は是非深堀してみてください。以下にマニュアルのリンク:
https://www.sidefx.com/docs/houdini/nodes/lop/sceneimport.html
参考までにSOP→USDの例を挙げておきます
- SOP でモデリングする
- ノードかしたいシェイプ単位にPackする(Packしなくてもいいです。必要に応じて)
- 各Pack に path を設定する。この際、あとでマテリアルを設定しやすいように名前をルール化しておく
- マテリアルは別途USDで用意しておく
- LOD へ移動し SOPImport で SOP を取り込む
- マテリアルをリファレンスで取りこみ、アサインする
- USDファイルを出力する
このように流れを定型化しておけば、fbx/objなので書き出して、インポートして、もろもろ設定して・・・という流れから解放されるかもしれないです。
Scene Import(2.0)
Scene Import はモデル、マテリアル、ライトをオブジェクトレベルからLOPネットワークにインポートするノードです。SOP Import との大きな違いは SOP Import は SOP用、Scene Import は OBJ用である点です。さて、Scene Import のドキュメントをみてみましょう:
https://www.sidefx.com/docs/houdini/nodes/lop/sceneimport.html
Expert users can write Python plugins to customize how specific Houdini object node types are translated into USD. This is especially useful to translate custom node types, such as light and camera types associated with proprietary renderers. It may also be useful to modify or add to the translation of common object node types to handle custom data or workflows, or generate custom data on the USD side.
特定の objノードをトランスレートするプラグインをpythonで書ける?ような事がかかれています。詳細はこちら https://www.sidefx.com/docs/houdini/hom/sceneimport_object_translator.html と言う事なので潜ってみましょう。 完全に理解した!という方はもう大丈夫です。とはいえ、どうするの?という方は次の項から本題です。
カスタムノードをUSDの世界に取り込んでみよう
ここからが本題の始まります。 Scene Import のプラグインを作成して カスタムノードを USD へトランスレートしてみようという話です。
なにを作る?
LOPで出来るので、わざわざ obj 経由する必要は無いのですが、馴染のあるシンプルな例として:
- objレベルでアセットファイルを参照
- アセット情報を設定
- USDへ変換
を実現してみましょう
objレベルでアセットファイルを参照
ノードとして、ObjArchive
という名称のノードをHDAで作成しました。
パラメータに
- ファイル名(filename)
- アセット名(assetName)
- アセットバージョン(version)
このノードでアセットファイルをインポートし、アセット情報が設定された状態を模倣した、ということで
- objレベルでアセットファイルを参照
- アセット情報を設定
の準備が完了しました。次はトランスレートプラグインなのですが、その前に USD へトランスレートされた状態はどういうものかを見ていきましょう。
まず ObjArchive
ノードが持っている情報を今一度確認すると
- ファイル名(filename)
- アセット名(assetName)
- アセットバージョン(version)
USD へ変換された状態を テキスト でみると、情報が変換されているのを確認できます。
Scene Import LOP object translator plugin を書いてみよう
プラグインを実装してみましょう。以下のコードが完成したプラグインのコードです。要点を解説していきます。
import hou
import husd
from pxr import Usd, UsdGeom, Sdf
class ObjArchiveTranslator(husd.objtranslator.Translator):
def shouldTranslateNode(self):
if not self._node.isDisplayFlagSet():
return False
if self._node.parm('tdisplay').eval() and \
not self._node.parm('display').eval():
return False
return True
def primType(self):
return 'Xform'
def populatePrim(self, prim, referenced_node_prim_paths, force_active):
if self._node.parm('filename').evalAsString():
# ObjファイルをPayloadに登録する
prim.GetPayloads().AddPayload(assetPath=self._node.parm('filename').evalAsString())
# assetNameアトリビュートをPrimアトリビュートに登録する
self.populateAttr(prim.CreateAttribute('assetName', Sdf.ValueTypeNames.String), self._node.parm('assetName'))
# versionアトリビュートをPrimアトリビュートに登録する
self.populateAttr(prim.CreateAttribute('assetVersion', Sdf.ValueTypeNames.String), self._node.parm('version'))
if not force_active:
if not self._node.isDisplayFlagSet():
prim.SetActive(False)
elif self._node.parm('tdisplay').eval():
if self._node.parm('display').isTimeDependent():
t = Usd.TimeCode(hou.frame())
else:
t = Usd.TimeCode.Default()
api = UsdGeom.Imageable(prim)
if self._node.parm('display').eval():
api.MakeVisible(t)
else:
api.MakeInvisible(t)
def registerTranslators(manager):
manager.registerTranslator('ObjArchive', ObjArchiveTranslator)
トランスレーターはhusd.objtranslator.Translator
を継承して作成します。
class ObjArchiveTranslator(husd.objtranslator.Translator):
肝心な関数は primType
と populatePrim
です。
primType
は Xform
と返していますが、Xform
は USD の Xform
を意味します。
def primType(self):
return 'Xform'
例えば カメラであれば Camera
、ライトであれば ライトタイプに応じて UsdLuxSphereLight
や UsdLuxDistantLight
等を返すように実装します。
populatePrim
はトランスレーターの核心となる関数です。
def populatePrim(self, prim, referenced_node_prim_paths, force_active):
self._node
には objノードが設定されていて self._node.parm
でパラメータを操作できます。 ここでは filenameパラメータにファイル名を指定されているかを調べ設定されていれば、PrimへPayloadを追加しファイル名を設定します。
if self._node.parm('filename').evalAsString():
# ObjファイルをPayloadに登録する
prim.GetPayloads().AddPayload(assetPath=self._node.parm('filename').evalAsString())
これでPrim(Xform)にファイルがPayloadされ、USD の世界にシェイプが表示されるようになります。
続いて、残りの assetName, version アトリビュートを設定していきます。husd.objtranslator.Translator クラスには アトリビュート設定するユーティリティ関数populateAttr
が用意されてますので、こちらの関数を使用して設定します。※USDの操作になれているからはこの関数を使用しないで実装しても問題ありません。
# assetNameアトリビュートをPrimアトリビュートに登録する
self.populateAttr(prim.CreateAttribute('assetName', Sdf.ValueTypeNames.String), self._node.parm('assetName'))
# versionアトリビュートをPrimアトリビュートに登録する
self.populateAttr(prim.CreateAttribute('assetVersion', Sdf.ValueTypeNames.String), self._node.parm('version'))
最後の DisplayFlag 等の対応を行いトランスレートは完了します。
if not force_active:
if not self._node.isDisplayFlagSet():
prim.SetActive(False)
elif self._node.parm('tdisplay').eval():
if self._node.parm('display').isTimeDependent():
t = Usd.TimeCode(hou.frame())
else:
t = Usd.TimeCode.Default()
api = UsdGeom.Imageable(prim)
if self._node.parm('display').eval():
api.MakeVisible(t)
else:
api.MakeInvisible(t)
最後にトランスレータープラグインを登録します。manager.registerTranslator
には、オペレータタイプ名 と トランスレータークラスを渡します。
def registerTranslators(manager):
manager.registerTranslator('ObjArchive', ObjArchiveTranslator)
おわり
ざっくりとした解説で ? な方もいると思いますが、 houdiniをpythonで操作できる方は、USD の Primまわりの Python API を覚えるだけで Scene Importプラグインを書けますよという話でした。
次回はマテリアルをなんとかしてみようと思います。次回までにHDAやらPythonコードはパッケージ化して github へアップしますのでお待ちくださいませ。