USDのアセット構造の話
USD AdventCalendar2022 25 日目は「アセット構造について」です。
USD は、以前に比べるとだいぶ普及して知らないうちに実は使っていた なんてことも出てきているのではないかと思います。
しかし、何かしらのプロジェクトで実際に使おうとするとそう簡単ではなく パイプラインの設計などもきちんとやらないといけません。 そうなると、どのように使ったらよいかわからないことが多いのではないでしょうか。
今回は、 ASWF USD WG の usd-wg/assets リポジトリで公開されている Guidelines for Structuring USD Assets をベースにしつつ 過去に書いた記事や、公開されているサンプルなどをもとに解説していきたいと思います。
USD のアセットの構成を考える上で、いくつか考慮しなければいけないことがあります。 ざっくり分けると、以下の 5 つになります。
- コンポーネントとスコープ
- アセンブリ
- ディレクトリ構造
- USDA or USDC
- コンポジション構成
それぞれ詳しく見ていきます。
コンポーネントとスコープ
USD のアセットは、それ単体で使うというよりもアセットをリファレンスやペイロードし レイアウトして使用することになると思います。 そのため、作成するアセットはリファレンスして使うのを前提にして構築する必要があります。
defaultPrim
USD のアセットをリファレンス、またはペイロードする場合は USD レイヤーではなく レイヤー内のある 1 つの Prim を、指定の Prim に対して接ぎ木します。
そのため、たとえばこのようにルート以下に Prim が並んでいるようなデータをリファレンスしようとすると
Warning 扱いになり、
正しくリファレンスすることができません。
このため、USD でアセットを作る場合は、RootPrim を作成し これを defaultPrim(リファレンスするときに対象となる Prim)を指定します。
このようになります。
この defaultPrim は、リファレンスしてきた先の Prim に「接ぎ木」されるものです。 そのため、レイアウトシーンでアセット情報を付加する場合などは レイアウトシーンではなくアセット側でつけておく必要がありますが 付加する先はこの defaultPrim である必要があります。
Kitchen_set の Kitchen.usd を見ると、defaultPrim に対して assetInfo kind documenation が付いています。
Kitchen_set.usd をみてみると、ペイロードで読んでいる Prim に assetInfo kind documentation が指定されています。
AssetInfo や USD ファイルの依存関係図を作る 等、 アセット情報を defaultPrim に追加しているのは リファレンス・ペイロードをすることを前提に、シーンの構造を構成しているからになります。
スコープ
defaultPrim も重要ですが、もう 1 つシーングラフの構造を決めるうえで重要なのがスコープです。 USD は、シーンの階層構造は自由に構成できますが 自由にしすぎるとわかりにくいデータが出来上がってしまいます。 例えば、Mesh 階層下に Material があったり、Material 下に mesh があったりのようになっていると 「シーンに存在するマテリアルを検索したい」みたいなことをしたい場合に (できるけど)面倒だったり、余計な処理がふえることになります。
ので、そうならないように defaultPrim 以下には「スコープ」と呼ばれる階層を作り ある程度ルール化しておきます。
このスコープは、DCC ツールによって若干の差異はありますがおおむねこのような構造になります。 (Materials が Looks だったり materials だったり _materials だったりする)
Geom は Mesh データ、Materials は Material と Shader。 そして Render は
RenderSettings 等が配置されます(これは Karma の例)
Purpose
Geom 以下には proxy や render と呼ばれる構造が配置されます。
USD の Purpose と呼ばれる仕組みがあるのですが、これは OpenGL のビューポートで表示するときには 軽量な Proxy モデルを表示する機能です。
この機能で、アセットのモデルを切り替える場合の proxy 用(軽量なモデル)と render 用のモデルを Geom 以下に作成し、
Purpose を指定することで
前 半は Karma でプレビュー、このモデルを OpenGL に切り替えた時は Proxy を表示しています。 これをできるように、Geom 以下には render と proxy のスコープを作成しておきます。
Houdini での作り方などは ComponentBuilder で遊ぼう こちら参照。
Assembly
Assembly とは、個別に作成されたアセットを 1 つにまとめたアセットのことを指します。 たとえば、Kitchen_set アセットは、建物や冷蔵庫、椅子・机などのアセットを 1 つのシーンにレイアウトし 作られていますが、これが Assembly です。
複数の、多ければ数千・数万のアセットをレイアウトした場合、 ある程度ルールを設けなければ意図しない挙動が発生したりする可能性があります。 リファレンスしたアセットの Prim に対して、さらにリファレンスを追加する場合。 構造が変わったら破綻してしまったりする可能性もあります。
そうならないように、ある程度のルールは必要です。 このルールが Kind と Model と ModelHierarchy で説明している kind による component 指定です。
component とは
component とは、USD のアセットのうちもっとも基本的なアセットのことを指しています。 レイアウト用のアセット、と言い換えてもいいかもしれません。
レイアウトアセットの移動用の XformPrim が component 扱いになります。 そして component 以下は、上で説明したスコープによって各アセットの構造が構成されています。
Assembly するときの各アセットは、リファレンスでロードしたロード先 Prim を component そしてその component をまとめるのを group そしてルートプリム(defaultPrim ) が assembly という階層構造を持ちます。 階層は、 assembly > group > component と、component が末端にあたります。 component 以下には component 入れられません。 このようなルールで構成することで、複雑さを防ぐのが基本的な Assembly の構造になります。 これを Model Hierarchy といいます。
Reference するアセットのルートが Component 扱いになるので、 アセットの defaultPrim には kind = component を指定しておきます。
コンポジション(レイヤー)構成
最終的にどのような階層が出来上がっていればよいかが決まったら、それを適切な単位にレイヤーを分離します。 USD は にあるとおり、複数のレイヤーを合成して 1 つのシーングラフを構築できます。 アセットは 1 つのレイヤーにまとめても良いですが、決められた構造をセットアップすることで USD の機能を生かしたアセットにすることができます。
作成方法は、Houdini の ComponentEditor を使用したパターンと Python を使用したパターンの2つを 以前書いたのでそちらを参照してもらえればよいですが ここでは、いくつかの重要な要素についてまとめておこうと思います。
基本となる構成がこのようになります。 AssetName.usd がアセットとして実際に Reference する階層になります。 このレイヤーは、Mesh 等のアセットを構成する実データを持たず 各種メタデータを指定するためのレイヤーになります。
#usd 1.0
(
defaultPrim = "AssetName"
metersPerUnit = 1
timeCodesPerSecond = 30
upAxis = "Y"
)
def Xform "AssetName" (
prepend payload = @./payload.usd@
assetInfo = {
asset identifier = @./AssetInfo.usd@
string name = "AssetName"
string version = "1.0"
}
kind = "component"
)
{
}
スケールや upAxis、defaultPrim に対して は assetInfo や kind の設定など アセットに必要なメタデータをこのレイヤーに記述し、 頂点データなどの大きなデータは別のファイルに分離し、ペイロードにしておきます。
アセットの段階でこのようにメタデータと本体とを分けておくことで アンロード状態にしておいても、最低限必要な情報を取得できるようにします。
VariantSet を含む場合は、payload がこのようになります。 そして、variant の個別の usd は上の図の payload.usd と同じ構造になります。
geo.usd の中身が、Mesh データです。
Moana Island Scene をみてみる
ここまでが、Houdini の ComponentBuilder や Kitchen_set の例を参考にした場合の構造例ですが それ以外に Disney Animation Studio が出しているサンプルデータ Moana Island Scene の構造も、例としてみてみます。
アセット部分を切り出すと、構造はこのようになっています。 基本的にファイル名は異なりますが、レイアウト用の元になるコンポーネントが element.usda その次にペイロードの仕組みを入 れるレイヤーが存在し、マテリアルとジオメトリが別レイヤー (マテリアルのほうが強い状態で、同階層にリファレンス)
少し違うのが、geometry 部分がさらに model と分かれています。 geometry には、model(UsdGeomMesh)をペイロードで読み込む部分と vdb を OpenVDBAsset スキーマを使用してロードしている部分が書かれています。
USDA or USDC
IslandScene で注目したいところが、アセットのデータは わざわざ usda と usdc を使い分けているところにあります。
USD には、usda (アスキー)と usdc (バイナリー)の 2 種類が存在しています。 それぞれの特徴は、 USD は手書きするもの こちらの記事に書かれているので詳しくはこちらを見ていただきたいですが、 ざっくりいうと、テキストエディタで編集可能なデータが usda で、ファイルが小さく読み書きが早いのが usdc になります。
その特徴を生かした上で、コンポジションを構築していきたいわけです。
IslandScene では、多くの部分が usda によって構成されています。 (usdc を使用しているのは、 model.usd + vdb)
頂点データやカーブのデータ、キャッシュデータなどのような巨大なデータは極力バイナリーで扱いたいです。 それこそ、1 フレーム単位で分離しつつバイナリーで扱うくらいでしょう。 ですが、それに対してのメタデータや ValueClip ( )を使用するレイヤー マテリアルデータ、レンダーセッティング等はシーンの頂点データなどをわざわざ読まずに 編集して書き換えたりしたいはずです。
このようなことをするためにも、USD のアセットは 1 ファイルにデータを固めるのではなく 役割ごとにファイルを分離し、容易に編集可能な形で設計しておきます。
拡張子は、 usda usdc usd とありますが アスキーであってもバイナリーであっても usd にしておけばいい感じに取り計らってくれるので あとでバイナリーとアスキーを切り替えたくなっても楽なように、すべてを usd としておくのが良いです。
それ以外のデータの話
今回はモデル前提でしたが、エフェクトのデータだったりライティングデータが増えた場合も fx.usd や light.usd といった形で分離して、ペイロードでロードします。
こんな感じ。 この例だと「赤」くしている usd は usdc がよさそうです。 Light や FX に対してアスキーでメタデータを付加したい場合は、 payload.usd と各要素の USD の間に サブレイヤーを追加するなどの工夫をすると さらに柔軟に構成できるかもしれません。