カスタムノードをUSDの世界に取り込んでみよう(OBJ編)



## はじめに
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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/0e6e725c-44b8-bbd6-96aa-130c9524805b.png)
気になる項目が！？ 読み進めてみる

>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で作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/b5100895-f9bd-5a02-6d76-9dfbc384d9fd.png)

パラメータに

* ファイル名(filename)
* アセット名(assetName)
* アセットバージョン(version)

を設定し、ファイルノードを内包した簡単なノードです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/ecba6753-8021-de08-2374-9d3cdbfbf4bb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/3cfeacbc-edb3-0e08-763d-f7585bd78a4d.png)

このノードでアセットファイルをインポートし、アセット情報が設定された状態を模倣した、ということで
>
* objレベルでアセットファイルを参照
* アセット情報を設定

の準備が完了しました。次はトランスレートプラグインなのですが、その前に USD へトランスレートされた状態はどういうものかを見ていきましょう。

まず ``ObjArchive`` ノードが持っている情報を今一度確認すると

>
* ファイル名(filename)
* アセット名(assetName)
* アセットバージョン(version)

USD へ変換された状態を テキスト でみると、情報が変換されているのを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/ef881274-f20b-888b-6561-d7cab50a1945.png)


### Scene Import LOP object translator plugin を書いてみよう

プラグインを実装してみましょう。以下のコードが完成したプラグインのコードです。要点を解説していきます。

```python
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`を継承して作成します。

```python
class ObjArchiveTranslator(husd.objtranslator.Translator):
```

肝心な関数は `primType` と `populatePrim` です。
`primType` は `Xform` と返していますが、`Xform` は USD の `Xform` を意味します。

```python
    def primType(self):
        return 'Xform'
```
例えば カメラであれば `Camera`、ライトであれば ライトタイプに応じて `UsdLuxSphereLight` や `UsdLuxDistantLight` 等を返すように実装します。

`populatePrim` はトランスレーターの核心となる関数です。

```python
    def populatePrim(self, prim, referenced_node_prim_paths, force_active):
```

`self._node` には objノードが設定されていて `self._node.parm`でパラメータを操作できます。 ここでは filenameパラメータにファイル名を指定されているかを調べ設定されていれば、PrimへPayloadを追加しファイル名を設定します。

```python
        if self._node.parm('filename').evalAsString():
            # ObjファイルをPayloadに登録する
            prim.GetPayloads().AddPayload(assetPath=self._node.parm('filename').evalAsString())
```
これでPrim（Xform）にファイルがPayloadされ、USD の世界にシェイプが表示されるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/43ea865e-cf52-fc3c-67a7-e143149a94dd.png)
続いて、残りの assetName, version アトリビュートを設定していきます。husd.objtranslator.Translator クラスには アトリビュート設定するユーティリティ関数`populateAttr`が用意されてますので、こちらの関数を使用して設定します。※USDの操作になれているからはこの関数を使用しないで実装しても問題ありません。

```python
            # 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 等の対応を行いトランスレートは完了します。

```python
        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`には、オペレータタイプ名 と トランスレータークラスを渡します。

```python
def registerTranslators(manager):
    manager.registerTranslator('ObjArchive', ObjArchiveTranslator)
```


## おわり
ざっくりとした解説で ？ な方もいると思いますが、 houdiniをpythonで操作できる方は、USD の Primまわりの Python API を覚えるだけで Scene Importプラグインを書けますよという話でした。
次回はマテリアルをなんとかしてみようと思います。次回までにHDAやらPythonコードはパッケージ化して github へアップしますのでお待ちくださいませ。