コンテンツにスキップ

SOLARISでUSDアセットを作ろう

Info

現在(Houtini19)では、ComponentBuilderを使用することで
以下の内容をもう少し簡単につくることができます。
ComponentBuilderについては こちら

Houdini Advent Calendar 2020 12日目は SOLARISでUSDアセットを作ろう です。

ゴール

今回は、
「USDの汎用アセット」をSOLARISのネットワークでこねくり回して作ってみます。

どういうことかというと、USDのシーングラフは基本使う人間の設計によって
自由にシーングラフやレイヤー構造などを構築できます。
つまり、何でもできてしまうわけですね。

ですが、それだと何からどうしていいかもわかりにくいですし
どういうふうなアプローチを取っていいのかもわかりません。
何をどう組み合わせると使いやすい物ができるのかも想像しにくいのではないかと
ずっと思っていました。

ので、まずは割と汎用的に使える
SOLARIS上でレイアウトするもよし、ゲームエンジンに持っていくもよし
別のツールに持っていって使うもよし というような
汎用アセットの構造を、現在公開されている各種サンプルHip/USD・サンプルコード
をもとに整理して、それをHoudiniのSOLARIS上でセットアップしていきます。

このセットアップ方法を通じて、
USDのコンポジションの理屈と合わせてまとめていければと思います。

ここでいうアセットとは

これは分野や会社によっても定義は変わるかと思いますが、
ここではショットワークで使用する各種モデルなどの素材を指します。

ものすごいざっくりと書き出すとこういう関係になります。
映像作品などを作る場合は、各カットごとにアニメーションをつけてエフェクトをつけたりして
映像を作成します。
アセットはここで使用するキャラクターだったりBGだったり、小物等
あるいは汎用アニメーション・汎用エフェクト(共通素材)なども
アセットに含まれます。

なので、ここでは各Shotで使うアセット類をUSD化して管理できるようにします。

アセットには色々種類がありますが、今回は BG/Prop をベースにして構築します。
(キャラ、アニメーション、エフェクト周りは現状だと作りにくい故。。。)
ただし、基本的な考え方は共通で大丈夫なはずです。

Info

このあたりはプロジェクトや会社によって大きく扱いが変わるとはおもいますので
今回はあくまでこういうものとして見ていただけると。。。

基本構造を作る

では早速、やっていきましょう。
まずは、BGやプロップなどのアセットをどういうふうに作っていけばいいか
毎度おなじみ、Pixarのサンプル「Kitchen_set」を使用して確認していきます。

最小単位の構成を作る

まず、USDの大きな特徴として
USDはファイルを好きに分割することができます。
分割したファイルはコンポジションアークを利用して1つのシーングラフに合成することができます。
ので、まずUSDでアセットを構築する場合
どういうファイル単位にするのか、どういうふうにコンポジションを構築すると
扱いやすいかを考える必要があります。

この場合のセオリーは、個人的には2つあると思っていて

  1. 担当者が変わるところは分ける
  2. 共通素材になるところは分ける

だと思います。

キッチンセットは、 assetsフォルダ以下に複数のアセットが保存されています。
その中のKitchenTableを開いてみると

その下には KitchenTable.geom.usd KitchenTable_payload.usd KitchenTable.usd
という3つのモデルが入っています。

そのTableが、「キッチンセット」という1つのアセットにリファレンスで
読み込まれているのがわかります。
(payloadの階層などはこのあとで説明)

これから読み取れることは、
Kitchen_setというアセット(カテゴリ的にはこれはBGに当たる)の中には
数多くの別のアセット(小物等)がリファレンスで読み込まれています。

1つのBGを構築しようとしたときに、共通の小物
(家具、机の上の小物、街路樹、信号などなどBGを構成する要素)は
共通素材となるパーツ となります。
また、そういった小物を作るときは複数の担当者で同時並行して作業をすることが
可能でしょう。

なので、まずはアセットを構築する場合は
最小単位はまずほかのセット(BG)でも使い回せる小物類にしておくのが良さそうです。

SOLARISで構築開始

とりあえず、SOLARIS上で組み立ててみます。

いつものように豚さんを召喚します。

ネットワークはこちら。
召喚方法はいろいろありますが、今回は sopimportを使用して
指定のモデルを指定のネームスペース以下に生成します。

なにもしないとこうなります。
これじゃあ何がなんだかわからないので、ちゃんと整理してみます。

こうなりました。

これは、アセットを作る場合はPixarが提供しているサンプルをみるとわかりますが
トップノードはアセット名がルート以下に来るようにしています。
そしてこのアセット名Primを 「defaultPrim」 に指定します。

最近ずっと調べていた ConfigureLayerノードを作り

Default Primitive を、ルート以下につくった アセット名のPrimに指定します。

1
2
3
4
5
6
7
8
9
#sdf 1.4.32
(
    defaultPrim = "Pig"
    doc = """Generated from Composed Stage of root layer 
"""
    framesPerSecond = 24
    metersPerUnit = 1
    timeCodesPerSecond = 24
)

こうすると、レイヤーに defaultPrimが指定できました。
なぜこれを指定するかというと、この作成したusdファイルを
どこかのPrimに対してリファレンスしたい場合、このデフォルト指定をしておかないと
エラーになってしまう(リファレンス時にPrimを指定しなければいけなくなる)からです。

マテリアルの設定

これだけですと、メッシュがあるだけでマテリアルも何もないので
次にマテリアルを設定していきます。

マテリアルは、SOP側ですでに指定されている場合は、MaterialLibraryを使用して

shopnetのマテリアルをSOLARIS上に読み込みます。

MaterialLibraryはこんなかんじです。
SOLARIS側でマテリアルを作るのならば UsdPreviewSurfaceを使用して
作る方法もあります。

結果。
注意点として、デフォルトだとマテリアルは /materials に作られますが
それだとPig以下にならないので、 Material Path Prefix にアセットのPrim以下の
Material用の階層(この場合はMaya準拠のLooks)を作成します。

そして最後にUSD ROPを使用して USDファイルに出力します。

この段階だと、 pig.usda という USDファイルが1つあり
その中にアセットが入っているという状態になります。
(とくにコンポジションなどの処理もなにもされていない)

これが、最もシンプルなUSDアセットの構造になります。

マテリアルを別レイヤー化する例

上の構成でもOKですが、もう少しレイヤーを分割する例も軽く触れておきます。
それはマテリアルを分割したい場合。

豚のマテリアルを共有化したいケースはまぁさすがに無いとは思いますが
汎用的なマテリアルは他のモデルでも使いたい場合が出てくると思います。
そういう場合などは、マテリアルのUSDは別途出力しておいて、
それをアセットのレイヤーに合成して使用する...といった構造も作ることができます。

この、Materialを作る で作ったレイヤーは、
こんなふうにルート以下にMaterialがあるような構造になっています。
これをリファレンスでMeshのシーンに合成して
合成後にアサインすることで、Material部分を共通化することができました。

参考Hip

参考はこちらのHipのコメントなどを参考にしてください。

以降は、この基本構造をもとにして
さらにUSDのコンポジションを活用した構造をつくっていきます。

USD的な構造を追加する

一応ここまでの構造でも完結していましたが、
USD的に見ると今ひとつというか、もう少し改善する余地があります。
それが、上で軽くだけ触れましたが「ペイロード」の仕組みであったり
「バリアント」を使ったモデルのバリエーションの作成であったりの
コンポジションアークを駆使したUSDアセット です。

次からは、このコンポジションアークを使った構造を、キッチンセットと こちら
USDリポジトリ以下にあるサンプルPythonをベースに
SOLARIS上で構築していきつつ説明していきます。

ペイロード

ペイロードとは

まずは ペイロード について。
ペイロードとはなにかというと、基本的な動作はリファレンスと共通ですが
「アンロード」指定でUSDを開いた場合、ペイロード化しているレイヤーはロードされなくなります。

1
usdview D:\USDsample\Kitchen_set\Kitchen_set.usd --unloaded

例として、キッチンセットをアンロード状態で開いてみます。
usdviewの引数に --unloaded して実行します。

実行すると、開いてもモデルが何も表示されなくなりました。
そのかわり、USDViewがものすごい高速で起動します。

これは、キッチンセットで使用している大量のアセットは、かならずルートに
ペイロードの構造を持っているからです。

たとえば、この中のKitchen TableをLoadしてみます。

すると、Tableのみがロードされました。

この機能を使うと何が良いかというと、
たとえばあるショットが超巨大なビル群などの背景に大量のキャラクターが配置され、
エフェクトもバンバン乗っているようなものがあったとします。
ファイルサイズは合計何十ギガ。
これをまともに開こうとするととんでもない時間がかかってしまいます。
けど、直したいのはカメラ近くにある邪魔なオブジェクトを消したいだけだったり。

そういう場合は、 アンロードでシーンを開き(ロードされないので一瞬で開く)
作業したい場所のみピンポイントでロードし、
編集して保存 すれば、でかいシーンをロードしないですむのでとてもエコです。

なので、使用するアセットのルートにはペイロードの階層を
必ず入れることで、アンロードしたあとアセット単位でのロードが可能になります。

ペイロードの構造をつくる

以上を踏まえて、最初の基本サンプルを変更しました。

結果。
このレイヤー構成はまだまだ考える余地はあるのですが、
まず、最後にペイロードの階層をいれます。
ペイロード前までのレイヤーは pig.geom.usda として出力するようにしました。
そのため、この段階で

  • Pig.material.usda マテリアルのみのレイヤー
  • Pig.geom.usda MeshとそのMeshにマテリアルアサインをするレイヤー
  • Pig.usda ペイロードの構造を追加するレイヤー

という3つのレイヤーが作成されました。

別案。
先程のに+して、マテリアルアサインとモデル部分を分離した場合。
この場合は

  • Pig.geom.usda マテリアルがアサインされていないMeshのみのレイヤー
  • Pig.material.usda マテリアルのみのレイヤー
  • Pig.payload.usda マテリアルをアサインするレイヤー
  • Pig.usda ペイロードの構造を追加するロード用のレイヤー

という構成になります。

こうすると、テクスチャがアサインされていない素のメッシュだけのレイヤーが
作成できるので、例えばテクスチャとかは何もいらないガイドとしてだけ使いたい
みたいなケースがある場合は、この メッシュのみのレイヤーが使えるようになるので
ちょっと便利になります。

バリアント

次にバリアントの構造を作っていきます。
この参考には、こちらのコードを参考にします。

バリアントが入った場合も、基本的な考え方は同じです。
バリアントで切り替えるモデル自体も、最小単位のアセットとして構築しておきます。
(このサンプルの場合は、面倒なのでマテリアルは分離していません)

そして、切り替えしたいモデルを Add Variant につなぎます。

PrimitivePathは、このアセットの名前(HoudiniFriends)
バリアントの切り替えはこのルートPrimに対して作りたいので Variant Primitive は / にします。

結果、ルートの HoudiniFriendsPrim に対して、バリアントセットが追加されます。
これでモデルの切り替えができるようになりました。

あとは、このモデルに対してペイロードの構造を追加し、
USDROPでエクスポートします。

バリアントを使えば、複数のアセットを1つのアセットとして切り替えて使用できるように
できます。
たとえば、木だったり草だったり、あとは室内の本や小物などなど。
まずは大量に配置して切り替えて調整したりしたい場合など。
キャラクターだと、服装違いや髪型違いをバリアントとして保持すると
色々コントロールがしやすくなります。

今回は1つのHIP内でモデルの作成とセットアップ内でバリアントの構築をしていますが、
(SOPでプロシージャルモデリングで複数バリエーション作るならこの作り方が有効)
すでに別途作っておいたアセットを、LoadLayerなどでロードして
Add Variantにそのレイヤーをペイロードで入力することで
単独でも使えるし、バリアントセットで切り替え可能なアセットとしても
使用することができます。
(Houdiniと合わせて、別のDCCツールで作ったものをオーサリングするときに有効)

継承

最後がこの 継承(Inherits)
Houdiniのページでもあまり言及がないのと、いまいち使い方がピンとこない
(Referenceとの違いは?など)ところはあると思いますが、
個人的にはコンポジションアークで最も興味深い機能がこの継承です。

継承に関しては、Pixar公式のDownloads にある USD Composition の内容がわかりやすいですので、あわせて参考にしてもらえれば。

基本的な継承構造の作り方

まず、継承の特徴はあるPrimを対象のPrimに内容を「継承させる」ことができます。

USDの定義には Define 以外に Class が存在します。(あとはOver)
このClassは、いわゆるプログラミングのClasと同様で
あくまでも型であり実体ではありません。
ので、このClassで定義しただけではなにも作られません。

このClass定義は Primitive ノードで作成できます。

このClassを継承(Referenceノードの Reference Typeを Inherit From First Input にする)
で継承します。

すると、ClassのPrimを継承したPrimができあがりました。

BaseClassを複数継承してくる...のような使い方も可能です。
このとき、ベースクラスのPrimを編集すれば
継承先のPrimも変更されます。

USDアセット的 継承 の使い方

これだけだと、Referenceと代わりがありません。
ですが、他のコンポジションアークと組み合わせたとき、この継承は意味をなしていきます。

この継承も、こちらのコードを参考に組み立ててみます。

全貌はこちら。

アセットの構築部分はこのあたりになります。

その結果、こんなシーングラフができあがります。
この __class__Pig を、Pigが継承しています。

この作成したアセットを、Referenceで配置します。

ブタさん2匹が配置されました。

シーングラフはこのとおり。
先程作った __class__Pig は見当たりませんが
LayoutPrim以下にPigが2匹召喚されていることがわかります。

それとは別の系統で、このような構造を作ります。

この構造では、最初のPigアセットの構造と同じ
__class__Pig 以下に、 Pig以下にあるマテリアルの構造 Looks/Pig を作っています。
このMaterialは「赤くする」マテリアルです。
このマテリアルを、 __class__Pig 以下に作っているわけです。

これを、レイアウトした構造とサブレイヤーで合成してみます。

結果。
二匹とも赤くなってしまいました。

ルート以下の __class__Pig 1つに対して設定していたはずなのに
配置していたPigが両方とも変わってしまう...というのが継承の特徴です。

これだけだと何が起きたのが全くわかりません。
ので、図解してみます。

継承のしくみ

今の状況を図にするとこうなっています。

コンポジションが1つのPrimに対して複数ある場合は、 「LIVRPS」の原則 によって
順番が解決していきます。
それを踏まえて詳しく見ていきます。

Info

LIVRPSとは、コンポジションアークの解決順序の原則です。
L Local
I Inherits
V Variant
R Reference
P Payload
S Specialize
のそれぞれの頭文字をとって LIVRPS という。
1つのPrimに複数のコンポジションがある場合、 Lに近いほど優先されます。

まず、読み込んでいるアセットの構造は左上のようになっています。
PigPrimが Root 以下にあるPrimを「継承」しています。
しかし、この段階だと継承もとのPrimは何も定義されていないのでなにも起きません。

そして、サブレイヤーで合成された構造は右上です。

この合成を順番に(弱い順に)見ていくと、

まず、リファレンスで、「Pigアセット」が配置されます。

アセットの段階で、 __class__Pig という空のClassを defaultPrimの「Pig(リファレンス元)」
が継承していたので、このようになっています。
( Layout/__class__Pig ではないのがミソ)

この「リファレンス元で継承しておいた Root以下の __class__Pig に
サブレイヤーで「赤いマテリアルにする」というMaterialプリムを合成します。

すると、リファレンスの段階で仕込んでいた「継承」の構造は
サブレイヤーで合成された __class__Pig 以下のPrimを合成します。

Pig1とPig2というネームスペース以下にリファレンスされた「Pigアセット」は、
両方とも __class__Pig を継承しています。
そのため、両方に「赤いマテリアルのマテリアル」が継承されて「最終的に赤く」なっているのです。

この場合「継承」のほうが「リファレンス」より強いので
「赤くするマテリアル」のほうが「Pig.usdaのマテリアル」より強いので
結果Pigが赤くなっています。

「ローカル」のほうが「継承」より強いはずなのに、
なんでローカル(サブレイヤー)されたものが継承で使われるんだろう?となるかもしれませんが
あくまでもサブレイヤーで合成しているのは、
__class__Pig に対してであって /Layout/Pig2/Looks/Pig に対してサブレイヤーしているわけでは
ありません。
なので、この場合は、継承→継承した先のローカル (再帰的なコンポジションの解決)
という扱いになっています。

さらなる使いみち

見ての通り、この継承を使用すると
「1つのレイヤーに読み込んだ別のアセットをまとめて編集できる」
わけです。

例えば、あるシーンに本(Bookアセット)が1000冊おいあり、
この本は5色(赤・青・黃・緑・黒_のバリアントを持っていて、それがシーンにレイアウトされています。
この本のうち「赤い本」だけを調整したい
となった場合。
すべての赤い本だけをサブレイヤーで書き換えをしたいとすると、

/Layout/Book1/Looks/RedBooksMaterial
/Layout/Book2/Looks/RedBooksMaterial

のように すべてのネームスペース以下のマテリアルを編集しなければいけません。
これはさすがに冗長です。

ですが、

/__class__Books というClassを Bookアセットのルートプリムに対して継承しておけば

/__class__Books/Looks/RedBooksMaterial
に修正したマテリアルを定義すれば、すべての赤い本を調整することができるわけです。

このとき、マテリアルには 継承・バリアント・リファレンス が関係しているわけですが、
コンポジションの順序は、元の本の「リファレンス」より「赤い本」を選ぶバリアントが強く、
その「バリアント」よりも「継承」のほうが強いので
バリアントで選択された「赤い本」を継承によって編集することができるわけです。

これですべての赤い本が一律で調整できました。
しかし、これのうち1つの赤い本をピンポイントで調整したくなった場合はどうでしょうか。
その場合はサブレイヤー(Local)で値を編集すれば良いです。
そうすることで、この個別の編集が最も強いのでピンポイントの調整ができます。

直感的にわかりにくいかもしれませんが、わかるととても便利なので
ぜひとも活用してほしい機能です。

サンプルHip

Info

継承順が
Local -> Inherits -> Varint -> Reference -> Paylaods -> Specialize
なのは、考えれば考えるほどうまくできてるよなぁ...と思うのが
バリアントと継承の位置です。
バリアントよりリファレンスが強いと、リファレンスの切り替えができなくなるし
継承がリファレンスより弱いと一律で編集ができなくなります。
それが、このアセット構築をするだけでもとても良くわかります。

AssetInfo

一通りの構造は完成したので、最後にこの作成するアセットの情報を仕込みます。
この情報とは、例えば「名前」だったり「バージョン」だったり
それ以外にもアセットに紐づく様々な情報を、usda内に埋め込むことができます。

ペイロードの手前に ConfigurePrimitive と ConfigureLayer を追加します。

ConfigureLayerは、FlattenInputを Flatten Input Layers に変更します。
これを入れることで、メタ情報を入れるUSDファイルを pig.usda になるようにします。

次に ConfigurePrimitive

Primitivesは、メタ情報を入れたいPrimのPathです。
今回は /Pig (Root以下にあるアセット名のPrim)に指定します。

ここは必要な値を入れれば良いですが、今回はアセット名とバージョン、Kind(今回は割愛)
を指定します。
ここに無いパラメーターは、 Custom Data に仕込むことができて

こんなかんじに入れると

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def "Pig" (
    assetInfo = {
        string name = "Pig"
        string version = "1.0"
    }
    customData = {
        double Age = 1
        string whoAreYou = "HoudiniPig"
    }
    kind = "component"
    prepend payload = @anon:0000000126126B00:rootlayer@
)
{
}

usd のメタ情報が追加されます。
このようにメタ情報を入れるようにすれば、どのバージョンのファイルなのかを明確化
することができます。

完成

いろいろやってきましたが、こんなかんじになりました。
できあがった hip ファイルは こちら

これに必要に応じてバリアントで切り替えを追加すれば基本的なUSDの構造を含んだ
アセットができあがります。

重要な点は

  1. アセット名のPrim以下にすべてをまとめる
  2. 1のPrimをデフォルトPrimにする
  3. ルート以下に __class__アセット名 のクラスを定義して 1がこれを「継承」
  4. 1を「ペイロード」する構造をつくる
  5. メタ情報を入れておく

がワンセットです。
あとは必要に応じてこの構造を「バリアント」で切り替えできるようにまとめます。

このように作ったアセットは、
複数リファレンスでロードした場合でも「アンロード」で開けば高速に開いて
一部だけロードが必ずできるようになります。
また継承することで、大量にリファレンスされた場合でもまとめて調整が可能になります。

まとめ

長々とやっていきましたが、
これで一通り基本的なUSDの構造を活用したアセットが出来上がりました。

USDの構造は理解していても、じゃあ実際Houdini上でどうやってやったものかと
結構色々調べていたのですが
調べたおかげでようやく自分の思い描いた構造がSOLARIS上で構築できるようになりました。

ここ最近
EditLayerの記事とか LayerFlattenの記事とかPreviewSurfaceの記事とかは、
このUSDの構造を構築する方法を調べる過程で理解した内容をまとめたものになります。

一応できはしましたが、実際に運用する場合に考慮しなければいけないことや
まだまだ最適化できることとか、良さげなコンポジション構造をつくるとか
どういうレイヤー分割にするかだったり、工夫する点はたくさんあるとおもうので
今後は色々と試していきたいです。

とくに、マテリアル周りは正直まだ考える余地は多分にあると思います。

あとは、VEXも理解できたので
このあたりの処理をHDA化したりPDGで処理したいみたいなこともいずれは
試してみようかなと思います。

参考