メインコンテンツまでスキップ

Stage/Layer/Spec

Universal Scene Description 8 日目は、 USD の Stage と Layer、そして Spec とはなにか?というのを説明していこうと思います。

Stage とは

USD に触れていると、この「Stage(ステージ)」という言葉はあらゆるところで見られます。 アドカレ2日目の記事曰く

しかしちょっと待ってください。USD では厳密に言うと、 usda ファイルとは「私はこうしようと思うんだけどな」という 計画の記述 なのです。 従ってこの時点ではまだ誕生はしていません。そうなんです。実はあなたの他にも神がいるのです。 八百万の神々が集まり本当に世界を生み出すところは出雲(Stage)と呼ばれますが、 その話はこのアドカレの別の記事 で @fereria さんが明らかにしてくれるでしょう。

ということで、今回はその Stage と現在開こうとしている USD ファイルとの違い 八百万の神々が集まり本当に世界を生み出すところは出雲(Stage)について詳しく説明したいとおもいます。

USD と今までのフォーマットとの違い

多くのフォーマット(FBX だったり、OBJ だったり、GLTF だったり)では、 1つのファイルに1つのシーングラフが保存されています。

なので、たとえば複数に分かれた FBX をレンダリングする場合などに Maya でインポートなどをした場合も、それはあくまで1つの Maya シーンになる(統合される) だけで、Maya シーンが複数の FBX を持っているわけではありません。 (追いかけようとした場合は、別途なにかしらの情報を持っておく必要がある)

ですが、USD の場合はコンポジションアークによって 複数の USD ファイルが合成されて、その結果1つのシーングラフになります。 そのため、 開いた USD ファイルと実際に出来上がったシーングラフは一致しません。

このあたりは具体的な例を見ていったほうがわかりやすいので、Kitchen_set をみていきます。

Kitchen_set でみる Stage そして Layer

Kitchen_set をダウンロードして解凍したフォルダ内には Kitchen_set.usd というファイルがあります。

usdview D:\Kitchen_set\Kitchen_set.usd

このファイルを、 usdview で開くとこのように Kitchen_set が表示されます。 開くと、モデルが配置されていて1つの完成されたシーンになっています。

ですが、この Kitchen_set.usd を、 を使用してアスキーファイルでどうなっているかを見てみます。

usdcat D:\Kitchen_set\Kitchen_set.usd

実行すると、このような usda で表示されます。 見てわかる通り、ここにあるのは Xform(Maya でいうところの Group ノード)と リファレンスをしている Prim(Kitchen_1 等)と、その配置情報(xformOp:translate)のみが 書かれているだけで、Mesh の情報などは見当たりません。

つまりは、開いているファイルは Kitchen_set.usd ではあるものの このファイルに書かれているのは「私はこうしようとおもうんだよな」という主張(オピニオンと呼ぶ)が書かれているだけで、 これが最終的なシーングラフの形ではありません。

この計画の記述が書かれているファイルのことを、USD では Layer(レイヤー)と呼び このレイヤーがコンポジションによって組み合わされた結果出来上がったものを Stage と呼びます。

実際に配置されているオブジェクトは、 add references = @./assets/Kitchen/Kitchen.usd@ とか書かれている通り Kitchen_set/assets/Kitchen フォルダ以下にあります。

このアセットフォルダ以下も、1つのファイルではなく複数ファイルで構成されています。 このあたりの詳しい事は別途記事にする予定です。

Layer と Spec

Kitchen_set だと複雑なので、もっとシンプルな usd ファイルで確認してみます。

このような、samplePrim に sampleValue = false を設定したレイヤーと そのレイヤーをサブレイヤーしているレイヤーを用意します。

この2つの Prim を usdcat を使用して Flatten(コンポジションした結果)を表示してみると このようになります。

書き表すとこのようになります。

Stage は、コンポジションアークによって(この場合 サブレイヤー によって合成された結果出来上がったものです。 今回のサンプルならば、 subLayer.usda と root.usda が合成された結果出来上がったシーングラフが Stage です。

Layer と Stage、というのの何が違うのか?というのは、 たとえるなら PhotoShop のレイヤーがわかりやすいです。

このような赤いレイヤーと青いレイヤーがあった場合、最終的に表示される色は何色になるでしょうか。 上にあるレイヤーが赤なのだから、赤!とも思えるかもしれませんが

合成方法が乗算なら

黒になります。

今は2つのレイヤーだけでしたが、これがもっとレイヤーが増えて、フォルダでまとめられていたりしたら? 重ね方が複雑になっていたら? レイヤーにある絵がなんであろうと、最終的にすべてが合成されるまでどんな絵(どんな色)になるかはわかりません。

これと同じ考え方が USD にもあります。

PhotoShop のレイヤーが、すなわち USD の Layer = usd ファイルにあたりますし、 キャンバス上に表示されている最終的な絵が Stage です。

この合成前の USD の Layer 内にも Prim や Property の記述はあります。 ですが、これは最終的な形と同じとは限りません。 なぜならば、ほかのレイヤーによって上書きされている可能性があるからです。 PhotoShop の別のレイヤーに別の何かが書かれていて絵が上書きされるように、 Prim や Prim に指定されている Property は別のレイヤーによって上書きされる可能性があります。

そのため、USD では合成前のレイヤーに書かれている記述を「Prim」とは別に 「PrimSpec」

A PrimSpec can be thought of as an “uncomposed prim in a layer”. 引用: https://graphics.pixar.com/usd/release/glossary.html#usdglossary-primspec

レイヤー内にある 「合成されていない Prim」 として区別して呼びます。 Property に関しても同様に「PropertySpec」と呼ばれ、合成後とは区別されます。

図に書き出すと、Layer 内に記述されている内容はこのようになり、

すべての Layer が合成された結果、出来上がったものが Stage であり Prim であり、Property です。

言い換えるならば、 各 Stage にある Prim や Property とは多くのレイヤーに記述されている PrimSpec あるいは PropertySpec の寄せ集められた(合成された)結果とも言えます。

usdview で確認したい場合

この Layer や PrimSpec、PropertySpec は usdview の Layer Stack と Composition で確認することができます。

samplePrim を選択すると、この Prim がどのレイヤーにどのように定義されていていたのか 結果どのような SdfPath になったかがわかりますし、

Property の場合は、

Composition を見ると、sampleValue の Composition には2つのレイヤーに主張(オピニオンと呼ぶ)があり レイヤー内に Spec (合成されていない「合成予定のもの」)があると示す Has Spec が yes になっています。

さらには、 Layer Stack を見ると、それぞれのレイヤーにある Value(PropertySpec)が書かれていて どのような順番で合成されて、結果なにになったか(上のほうが強い)がわかります。

これは、これよりも多くのレイヤーで、複数のコンポジションが絡んできても同様で、たとえば Kitchen_set を見ると

どのレイヤーで定義されているのか、 そのレイヤー内のどの PrimSpec(LayerStack に書かれている Path が、PrimSpec の Path)がもとになっていて

それら PrimSpec が、 どのようなコンポジション順序によって、結果この Prim が出来上がったのかがわかります。 (Composition の ArcPath が PrimSpec の Path)

Python から取得する場合

これまでの Stage と Layer、そして Spec についてを頭に入れたうえで Python から取得してみます。

Layer で取得

# レイヤーを取得
layer = Sdf.Layer.FindOrOpen('D:/root.usda')
# primSpecを取得
primSpec = layer.GetPrimAtPath('/samplePrim')
# AttributeSpecを取得
attrSpec = primSpec.attributes['sampleValue']
print(attrSpec.default)
print(layer.identifier)

まず、Layer から値を取得する場合。 この場合は Stage ではなく SdfLayer と各 Spec のオブジェクトとして扱います。

Layer から値を取得する場合、Python からだと非常に大きな罠があって C++の API ドキュメントとは関数もなにもかも異なります。

Python の場合はどれも Sdf.Find という形で値が取得されますがこれは Layer または~~ Spec といった 合成される前の情報のいずれかです。 上のサンプルコードの場合は、Layer から PrimSpec を取得し、そこからさらに AttributeSpec を 取得しています。

これらはあくまで合成前の「今のレイヤーに書かれている記述」なので コンポジション情報やほかのレイヤーへの情報は持ちません。

Stage から取得

stage = Usd.Stage.Open("D:/root.usda")

prim = stage.GetPrimAtPath('/samplePrim')

# PrimSpecを取得する
for primSpec in prim.GetPrimStack():
print(primSpec)
# primSpecからattribute取得
attributeSpec = primSpec.attributes['sampleValue']
print(attributeSpec.default)
# primSpecから、そのprimSpecが記述されたレイヤーを取得
layer = primSpec.layer
print(layer.identifier)

次に Stage から、Layer・各種 Spec を取得した例。

これは、指定の Prim の由来や成り立ちを知るための方法とも言えて GetPrimStack は、この Prim が出来上がるまでに合成された PrimSpec を強い順に取得します。

実行するとこのようになります。 GetPrimStack で取得できるのは PrimSpec なので、そこから AttributeSpec や それが記述されているレイヤーなどを知ることができます。

まとめ

usda ファイルと Stage との違いが理解できたでしょうか。

LayerStack と Composition の違いはなんだろうとかもろもろ出てくるかもしてませんが さらに詳しいことが気になった方は、以前PCP でコンポジションアークの構造を解析・編集対象を取得するという ここまでいけば USD 1級というディープな記事を以前書きましたので もしさらに詳しい事を知りたい!!という方はぜひとも合わせて読んでください。