😸

Pythonで始めるUSDの基本 - Stage/Layer

2021/07/04に公開

はじめに

さて。
Maya2022からデフォルトでUSDのExportができるようになりました。
そのため、本格的にUSDの検証を始めている人も多いのではないでしょうか。

そうしてUSDを触り始めると、今までにないUSD特有の概念で
わかりにくい、どういうことかわからない、、、ということが出てくるのではないかと思います。
そんな中から今回は、
https://fereria.github.io/reincarnation_tech/11_Pipeline/01_USD/04_layer_stage/
おそらく多くの人が最初混乱するであろうUSDの基本 ステージ/レイヤー
Pythonを利用しつつ詳しく解説していこうと思います。

レイヤー

まずは、サンプルのファイルを開いてみます。

import os
from pxr import Usd,Sdf

USD_ROOT = os.getcwd() + "/usd"
layer = Sdf.Layer.FindOrOpen(f"{USD_ROOT}/cube.usda")
print(layer)
print(layer.ExportToString())
>> Result
Sdf.Find('d:/work/zenn_ipynb/ipynb/usd/cube.usda')
#usda 1.0
(
    defaultPrim = "cube"
)

def Cube "cube"
{
    int testValue = 10
}



まず1つめが、なんのコンポジションも含まない、シンプルなキューブに、
testValueというアトリビュートに10が設定されているレイヤーです。

このレイヤーを開くには、Sdf.Layer.FindOrOpen を使用します。
FindOrOpenを使用すると、レイヤーオブジェクトを取得することができます。
このレイヤーから ExportToString() を使用すると、このレイヤーに
記述されているシーングラフを、アスキーで見ることができます。

refLayer = Sdf.Layer.FindOrOpen(f"{USD_ROOT}/reference.usda")
print(refLayer.ExportToString())
>> Result
#usda 1.0
(
    defaultPrim = "root"
)

def Xform "root"
{
    def "CubeA" (
        prepend references = @cube.usda@
    )
    {
        int testValue = 100
    }

    def "CubeB" (
        prepend references = @cube.usda@
    )
    {
    }
}



次に、もう一つのレイヤーを開いてみます。

このレイヤーには、「リファレンス」のコンポジションが含まれていて、

そして、CubeAには「testValue」というアトリビュートに100という値がセットされている
事がわかります。

ですが、このレイヤーだけでは
**「あくまでも現在開いているレイヤーの記述のみ」**がわかるだけで
「このリファレンスされている別のレイヤーと合成した結果どのようなシーングラフが出来上がるか」わかりません。

このように、レイヤーとは「最終的に合成されるシーングラフの元になるもの」のことです。

ステージ

レイヤーを開いただけでは、あくまでもそのレイヤーに記述された情報のみが
表示されるだけで、最終的に合成されたシーングラフはわかりません。
では、その複数のレイヤーから合成された「最終的な結果」がなにかというと
それが「ステージ」と呼ばれるものです。

stage = Usd.Stage.Open(refLayer)
print(stage.ExportToString())
>> Result
#usda 1.0
(
    defaultPrim = "root"
    doc = """Generated from Composed Stage of root layer d:\\work\\zenn_ipynb\\ipynb\\usd\\reference.usda
"""
)

def Xform "root"
{
    def Cube "CubeA"
    {
        int testValue = 100
    }

    def Cube "CubeB"
    {
        int testValue = 10
    }
}



先程のレイヤーを、ステージで開いてみます。
開いているのは reference.usda で、

このような構造になっています。
このステージの結果を、同じく ExportToString() してみるとどうなるかというと

結果はこのようになりました。
先ほどとは違い、Stage.Openで開いたレイヤー以下のレイヤーに書かれた情報(testValueの値や、Primタイプ)
が、全て合成された状態になっていることがわかります。

つまり、このステージというのは
ルートレイヤー(この場合は reference.usda)と、このルートレイヤー以下にあるコンポジションを
合成した結果できあがった結果のことを指しています。

そのため、USDで合成された結果のシーングラフを操作したい場合はUsd.Stageを使用するし、
そのステージ内のあるレイヤー(usdファイル)の情報を取得したい場合は
Sdf.Layer を使用するようにすれば、目的の情報にたどり着くことができます。

プリムとプリムスペック

レイヤーとステージの関係についてなんとなく分かってきたところで
プリムとプリムスペックについて説明します。

まず、プリムというのはなにかというとMayaでいうところのNodeにあたるものです。
上の例ならば、rootやCubeAなどがプリムにあたります。

primA = stage.GetPrimAtPath('/root/CubeA')
print(primA)
>> Result
Usd.Prim(</root/CubeA>)

ステージからGetPrimAtPathを使用することで、指定したパスのプリムを
取得することができます。

繰り返しになりますが、
「ステージ」とは「複数のレイヤーの合成した結果出来上がったシーングラフ」です。
つまり、ステージ上にあるプリムを GetPrimAtPathで取得した場合に
取得できるものは「プリム」であり、
いくつかのレイヤーによって合成された結果できあがったもの になります。

スペック

たとえばCubeAを例に上げると、
reference.usda の CubeA と cube.usda の cube という2つが合成された結果
CubeAが出来上がっています。

このCubeAというプリムがどのように構成されているかを分解してみると、

このようになっています。

それぞれのレイヤーの、シーングラフに組み立てられる前の
reference.usda の CubeA や、 cube.usda の cubeなどの要素をPrimSpecと呼びます。
これは、合成される前の「主張=オピニオン」です。

レイヤーから、Primを取得してそのPrimのAttributeを取得した例です。

for attr in primA.GetAttributes():
    print(attr)
>> Result
Usd.Prim(</root/CubeA>).GetAttribute('doubleSided')
Usd.Prim(</root/CubeA>).GetAttribute('extent')
Usd.Prim(</root/CubeA>).GetAttribute('orientation')
Usd.Prim(</root/CubeA>).GetAttribute('primvars:displayColor')
Usd.Prim(</root/CubeA>).GetAttribute('primvars:displayOpacity')
Usd.Prim(</root/CubeA>).GetAttribute('purpose')
Usd.Prim(</root/CubeA>).GetAttribute('size')
Usd.Prim(</root/CubeA>).GetAttribute('testValue')
Usd.Prim(</root/CubeA>).GetAttribute('visibility')
Usd.Prim(</root/CubeA>).GetAttribute('xformOpOrder')

対してPrimから取得した場合はこのようになります。

Primから取得した場合、コンポジションした結果から生まれるものなので
すべてのAttributeを取得した場合、UsdGeomCubeスキーマののアトリビュートと
reference.usda に記述されたアトリビュートの合成した結果のAttributeのリストが取得できます。

spec = refLayer.GetPrimAtPath('/root/CubeA')
for attr in spec.attributes:
    # このレイヤーに書かれているAttributeのみが表示される
    print(attr)
>> Result
Sdf.Find('d:/work/zenn_ipynb/ipynb/usd/reference.usda', '/root/CubeA.testValue')

対して、プリムスペックからアトリビュートを取得した場合はどうなるかというと

あくまでもこの レイヤーに記述された「意見(オピニオン)」 のみがリストされるので、結果このようになります。

まとめ

ここまで出た用語をまとめてみます。

レイヤーはUSDファイルのことで、シーングラフを構築するときの 「元情報」 です。
レイヤー単体では最終的なシーングラフはわかりません。

このレイヤーをコンポジションして組み立てられた結果できたものを 「ステージ」 と呼びます。

各レイヤーごとに書かれた 「このプリムはこうあってほしい」という Primの元になるPrim を プリムスペック と呼びます。
そして、そのプリムスペックに書かれた情報のことを オピニオン(=意見) と呼びます。

レイヤーからだとなにが取得できて、ステージからだと何が取得できるのか
というのは、USDを調べた直後はわかりにくいですが
この違いを理解することで、USDをPythonやSOLARISのなどで扱う場合の理解しやすくなるのではないかと思います。

PrimからPrimSpecを取得する(おまけ)

最後に応用編。
PrimとPrimSpec、レイヤーとステージを理解したところで
PrimからそのPrimを構成するレイヤーやSpecを取得してみます。

query =Usd.PrimCompositionQuery(primA)

# Compositionを取得する
for comp in query.GetCompositionArcs():
    print(comp.GetArcType())
    node = comp.GetTargetNode()
    # Target=されているNode、Introducing=しているNodeを取得できる
    # NodeRefとは、シーンディスクリプションを合成するためのノード(コンポジション)を取得できる
    if node.IsRootNode():
        # RootNodeの場合、LayerStackの先頭にアノニマスレイヤーが含まれるので
        # 2番目をRootLayerとして取得する。
        layer = node.layerStack.layers[1]
    else:
        layer = node.layerStack.layers[0]
    primSpec = layer.GetPrimAtPath(node.path)
    print(primSpec)
    # AttributeSpec
    for i in primSpec.attributes:
        print(i)
>> Result
Pcp.ArcTypeRoot
Sdf.Find('d:/work/zenn_ipynb/ipynb/usd/reference.usda', '/root/CubeA')
Sdf.Find('d:/work/zenn_ipynb/ipynb/usd/reference.usda', '/root/CubeA.testValue')
Pcp.ArcTypeReference
Sdf.Find('d:/work/zenn_ipynb/ipynb/usd/cube.usda', '/cube')
Sdf.Find('d:/work/zenn_ipynb/ipynb/usd/cube.usda', '/cube.testValue')

取得するには PrimCompositionQueryを使用します。
これは、関数名のとおり「あるPrimのコンポジションをすべて列挙」することができます。
コンポジション関係はPCPというモジュールを使用するのですが、
そのあたりは詳細は PCPでコンポジションアークの構造を解析・編集対象を取得する
詳しく書いてありますのでそちらを参照。

GitHubで編集を提案

Discussion