Skip to main content

EditTargetでLayerを操作する

Stage/Layer/Spec の説明にもあるとおり USD は1つの usd ファイルだけではなく、複数の usd ファイルが「コンポジションアーク」と呼ばれる 合成機能によって1つのファイルとして構築されている...と説明をしました。

つまりは、なにかの USD ファイルを Open してできあがった Stage は 複数の Usd ファイル(レイヤー)によって構成されていて その個別のファイル単位で Prim の元になっている「Spec(主張)」をもった 大量のファイルの塊でもあるわけです。

通常のファイルの場合は、ファイルを開いたら そのファイルを編集して保存すれば良いですが 上に書いたとおり USD は大量のファイルの塊なので 「どのファイル(レイヤー)」を「どのように編集するか」というのも 作成することができます。

なので、今回は USD の指定のレイヤーを Python から操作する方法を 説明しつつ、USD の Stage と Layer について改めて調べてみようと思います。

サンプル

サンプル USD ファイルとして、下の3つを作成します。

#usda 1.0

(
subLayers = [
@layerA.usda@
]
)

def "testPrim"
{
string hoge = "Root"
}

Root.usda

#usda 1.0

(
subLayers = [
@layerB.usda@
]
)

def "testPrim"
{
string hoge = "layerA"
int val = 20
}

layerA.usda

#usda 1.0

def "testPrim"
{
string hoge = "layerB"
int val = 10
}

def "addTest"
{
}

layerB.usda

構造はこんなかんじのシンプルな構造を用意します。

開く

from pxr import Usd, UsdGeom, Sdf
stage = Usd.Stage.Open(r"D:\work\usd_py36\usd\rootLayer.usda")

まずは、上の USD ファイルを開きます。

構造はこんなかんじで、

testPrim のアトリビュートは、

hoge = "Root" val = 20

という値を取得することができます。

ここで取得できるのは、コンポジションアーク(合成)されて構築された結果になります。

編集する

というわけで、開くことができたので中のファイルを編集してみます。

開いているファイルを編集する

UsdGeom.Xform.Define(stage, "/hoge")
stage.GetRootLayer().Save()

USD の編集はこのように Stage を経由して編集を行います。 これは今のステージに /hoge Prim を作る操作をしています。 そして保存時は「GetRootLayer」でレイヤーを取得して保存します。

この場合の「レイヤー」とは、usda ファイルのことになります。

図に表すとこういう状態です。 GetRootLayer は現在の Stage 上の RootLayer(開いた usda)を取得することができます。

指定のレイヤーを編集する

では、RootLayer ではないレイヤーを編集したい場合はどうすればいいでしょうか。 このあたりを理解するには「EditTarget」を理解する必要があります。

EditTarget は、現在の Stage 内の「編集対象のレイヤー」へのアクセスをできるようにするための クラスです。

target = stage.GetEditTarget()
# 編集ターゲットのusdaPathを確認
print(target.GetLayer().identifier)
# 保存
target.GetLayer().Save()

現在の編集ターゲットは GetEditTarget で取得することができます。 この場合、GetEditTarget = RootLayer になっています。

targetB = stage.GetEditTargetForLocalLayer(Sdf.Find('d:/work/usd_py36/usd/layerB.usda'))
stage.SetEditTarget(targetB)
# 試しに追加
stage.DefinePrim("/addTest")
# そして保存
targetB.GetLayer().Save()

では、RootLayer 以外を編集したい場合はどうするかというと SetEditTarget で編集したい Layer を指定してあげれば OK です。

上は DefinePrim で Prim を定義していますが、

print(targetBPrim.attributes['val'].default)
# 10
targetBPrim.attributes['val'].default = 20
print(targetBPrim.attributes['val'].default)
# 20

こうすると、指定のレイヤーのアトリビュートを書き換えることができます。

ざっくりと書くとこんな感じで stage の EditTarget を経由して、編集したい Layer を取得して Layer オブジェクトを利用して編集を行います。

注意点は、ここで書き換えたり編集しているものは「最終的な結果ではない」ということです。 あくまでも、最終的なコンポジションされた結果ではなく コンポジションされる前の「Spec(主張)」を書き換えているので、 自分よりも強い主張をもつレイヤーやプリム、アトリビュートがある場合は いくら書き換えても最終的な Prim には反映されないので 注意が必要です。

Spec とは

targetBPrim = targetB.GetLayer().GetPrimAtPath('/testPrim')

EditTarget の GetLayer を使用した場合、 通常の stage.GetRootLayer() とは違い GetPrimAtPath で Prim を取得すると

PrimSpec というオブジェクトになります。

help(stage.GetPrimAtPath("/testPrim"))

stage 経由の場合は、 Prim オブジェクトになります。

ここでいうところの Spec というのがなにかというと、 各レイヤーに記述された「合成される前の主張」です。

PrimSpec の場合は「こういう Prim を作りたい!!」という主張で この段階では他の PrimSpec との兼ね合い(コンポジション優先順位)によって未解決 の状態になります。

Sdf.Find のトラップ

ここで盛大にハマったのが「Sdf.Find」という関数です。 この関数、Pixar の API ドキュメント(C++)には存在しません。 print(dir(Sdf.Find(~~)))のようにしても、ものによって使える関数などが異なり こいつはいったいなんなんだよ!!!と思っていました。

が、 help 関数を使用してみるとなんとなく実態がわかります。

help(Sdf.Find('d:/work/usd_py36/usd/layerB.usda'))

試しに Help 関数を使用して Sdf.Find の Help を見てみます。

Sdf.Find 表示になっているものの実態はなにかというと、Layer だったり

print(targetB.GetLayer().GetPrimAtPath('/testPrim'))

こうしたときの このばあいは

help(targetB.GetLayer().GetPrimAtPath('/testPrim'))

こうすると

こうなったりと、オブジェクトの実体は別のものになっているようです。 わかりにくすぎるよ Pythonnnnnn!!!!!

なので、GetEditTargetForLocalLayer の引数として渡している

Sdf.Find('d:/work/usd_py36/usd/layerB.usda')

は、Sdf.Layer オブジェクトを渡しているということになります。

このアトリビュートはどこで「主張」されているのか?

各レイヤーが合成され、最終的な Prim が出来上がりますが ではその Prim が「どこのレイヤーで定義されているのか」というのを調べたくなることがあります。

サンプルの hoge は layerA と layerB で定義されていることがわかりますが これを Python で確認してみます。

prim = stage.GetPrimAtPath("/testPrim")
# Primのアトリビュートを取得
attr = prim.GetAttribute("hoge")
print(attr.Get())
# アトリビュートのStack(主張をつんだもの)
stack = attr.GetPropertyStack(Usd.TimeCode())

# 一番強い主張
print(stack[0].default)
print(stack[0].layer)

指定のプロパティがどのような主張を持っていて結果どうなったかを 見たい場合はp GetPropertyStack を確認します。

stack は、こんなかんじに Sdf.Find の配列になっていて この場合の Sdf.Find の中身は

AttributeSpec になります。 Spec なので、ここのアトリビュートは「主張」になります。

これを見ると、 rootLayer layerA layerB の3つに主張があり rootLayer が最も強い主張をもっていて、最終的にコンポジションされた結果になっているのが わかります。

GetPropertyStack で Usd.TimeCode() を渡しているのは、 アトリビュートは Prim と違いアニメーションのキーフレームのようにフレームごとに あるかどうかが異なります。 なので、タイムコードをわたして、指定のタイムコードでの Stack を取得しています。

レイヤーに「主張」があるかどうか

Prim から Spec を探すのはできましたが、今度は指定レイヤーに Spec があるかどうか 調べたいこともあります。

target = stage.GetEditTargetForLocalLayer(Sdf.Find('d:/work/usd_py36/usd/layerB.usda'))
layer = target.GetLayer()
primSpec = layer.GetPrimAtPath("/testPrim")
# このPrimのAttribute
# 定義があるか
print('hoge' in primSpec.attributes)
attrSpec = primSpec.attributes['hoge']
print(attrSpec)
# 値を確認
print(attrSpec.default)

ここのあたりも C++と Python とで全く挙動が違っていて API ドキュメントが全く参考にならず苦戦しました。

まずは、EditTarget を経由して 編集したいレイヤーを取得します。

取得したら、定義の有無を確認するために primSpec から AttributeSpec を取得します。 AttributeSpec の取得方法がかなり特殊で attributes は Dict 型になっているので、取得したいアトリビュートを引数とすることで AttributeSpec を取得できます。

つまりは、指定のレイヤーで主張があるかを確認するためには 'name' in spec.attributes のようにして、Dict にアトリビュート名が含まれているか 確認すれば OK です。

まとめ

いままでは EditTarget を知らずに、なんとなく開いているファイルを編集すれば OK という イメージを持っていましたが、 EditTarget を使用することで、開いている以外のレイヤーも PhotoShop のレイヤーを 編集するようにコントロールできることがわかりました。

いままでの GetRootLayer() での編集というのは、言ってしまえば PhotoShop の一番上に新規でレイヤーを作成して、そこに絵を書き足しているような 操作だったわけですね。

これで、できることの幅などが大きく変わりそうです。 USD すごい。

やはり日々研究していかないと USD を有効活用できないなーと思いました。 がんばろう。

あと、今回やったあたりは API ドキュメントが全く参考になりません。 ひたすら pprint.pprint(dir(hogehoge)) したりしながら関数をさがしてまわってた週末でしたが help(hogehoge)すればちょっとだけまともな Help を見れるという知見を得たので しばらくはこれを利用して Python での操作方法を全力で調べていこうと思います。