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

PCPでコンポジションアークの構造を解析・編集対象を取得する

このサイトのほかページでも何度か説明している通り、USD は複数のファイルを合成して 1つのシーングラフに構築することができます。

たとえば、毎度おなじみキッチンセットでみてみます。

このキッチンセットは、 Kitchen_set.usd を開いた段階だとこのように1つのシーングラフに なっていますが、

この usd は1つの usd ではなく、複数の USD をコンポジションアークを使用して1つの usd に合成して 上のようなキッチンセットになっているわけです。 (この1つのアセットを構成する usd ファイルは 228 ファイルありました)

では、この複雑なファイルの組み合わせでできあがったシーングラフが

  1. どのようなファイルによって構成されているのか
  2. どのファイルを編集すればどこに書き込まれるのか

を調べるにはどうしたらよいのだろう???というのが今回の記事の内容です。

サブレイヤーの場合​

コンポジションが「サブレイヤー」によって合成されている場合は、

for spec in prim.GetPrimStack():
print(spec.layer)

指定の Prim の PrimStack を取得して、その Prim を構築するための Spec を取得する方法つかったり、 編集対象を取得して切り替える場合は、以前に説明を書いたEditTarget で Layer を操作するを利用して 編集レイヤーを取得して Target を切り替えて編集する...という手を使うことができます。

しかし、サブレイヤー以外のコンポジションの場合はこの手段を使うことができません。

では、この場合はどうしたらよいかというと 今まで使っていた Usd ネームスペースの API では実行できませんが、 PcpAPI を使うことで実現することができます。

PCP とは​

PCP とは 「Prim Cache Population」 の略で、これが何をするものかというと、 コンポジションアーク(シーン合成)を司る機構 になります。

ある Prim がシーンディスクリプション(usd ファイルすべて)がこの Prim にどのような影響を与えているか どのように構成されているのかを、PCP を使用することで知ることができます。

文章だけだとわかりにくいので具体的にみてみます。

サンプルのキッチンセットから、冷蔵庫アセットを選択します。 この Prim がどのようなファイルによって構成されているのか PCP をつかって調べてみます。

調べるには、DumpToIndex を使って Index 情報を Dump してみます。

Node 0:
Parent node: NONE
Type: root
DependencyType: root
Source path: </Kitchen_set/Props_grp/North_grp/FridgeArea_grp/Refridgerator_1>
Source layer stack: @d:/Kitchen_set/Kitchen_set.usd@,@anon:0000027614F5B360:Kitchen_set-session.usda@
Target path: <NONE>
Target layer stack: NONE
Map to parent:
/ -> /
Map to root:
/ -> /
Namespace depth: 0
Depth below introduction: 0
Permission: Public
Is restricted: FALSE
Is inert: FALSE
Contribute specs: TRUE
Has specs: TRUE
Has symmetry: FALSE
Node 1:
Parent node: 0
Type: reference
DependencyType: non-virtual, purely-direct
Source path: </Refridgerator>
Source layer stack: @d:/Kitchen_set/assets/Refridgerator/Refridgerator.usd@
Target path: </Kitchen_set/Props_grp/North_grp/FridgeArea_grp/Refridgerator_1>
Target layer stack: @d:/Kitchen_set/Kitchen_set.usd@,@anon:0000027614F5B360:Kitchen_set-session.usda@
Map to parent:
/Refridgerator -> /Kitchen_set/Props_grp/North_grp/FridgeArea_grp/Refridgerator_1
Map to root:
/Refridgerator -> /Kitchen_set/Props_grp/North_grp/FridgeArea_grp/Refridgerator_1
Namespace depth: 5
Depth below introduction: 0
Permission: Public
Is restricted: FALSE
Is inert: FALSE
Contribute specs: TRUE
Has specs: TRUE
Has symmetry: FALSE

長いので全部は貼りませんが、 Index を Dump するとこのような情報が表示されます。

この PrimIndex は、シーンディスクリプションのうち、ある特定の Prim に主張(Opinion)がある Index で、 「ある Prim に影響を与えている要素」を取得するために使用します。

これを見ると、この Prim がどのようにコンポジションされているのか。 コンポジションする要素(Opinion)がどうなっているのかを見ることができます。

例えば、 Type を見ればどのコンポジションを使用しているのか

備考

Type: reference

Source layer stack を見ればどのレイヤーかどうか、

備考

Source layer stack: @d:/Kitchen_set/assets/Refridgerator/Refridgerator.usd@

Target Path は、そのレイヤーのどの Prim が対象か

備考

Target path: </Kitchen_set/Props_grp/North_grp/FridgeArea_grp/Refridgerator_1>

など。

Map To Parent / Map To Rot​

または、 Map to Parent という情報もここから取得できます。

たとえば、Node1 のレイヤーを見てみると、冷蔵庫 Prim は /Refridgerator という Prim が あります。 それがリファレンスされると、

Target Path のネームスペースにマップされている... というのがわかります。

このように、PCP を見ることで、 複数のレイヤーをコンポジションして最終的な Prim が出来上がるまで どのようなコンポジションが行われているのか取得できているのがわかるわけです。

!!! info PcpPrimIndex はコンポジション機能だけを担うもので、シーングラフの親子関係(階層構造) は持ちません。 Pcp 自体は、ある Prim に誰が意見を持っているかを提供しているだけで 最終的な値はなにか とか、シーングラフのオブジェクトの型であったりスキーマ は UsdPrim の役割として分離されています。

PCPNodeRef​

この Dump した情報をみると「Node」というキーワードが出てきます。

この Node (PcpNodeRef)は、コンポジションの木構造を表現するためのノードで、

たとえば、

  1. どのレイヤーか
  2. どのようにコンポジションしていくか
  3. どのネームスペースにマップするか

という ある Prim を構成する要素を確認することができます。

この構造を視覚化してみます。

from pxr import Usd,Pcp
stage = Usd.Stage.Open(r"D:\Kitchen_set\Kitchen_set.usd")
prim = stage.GetPrimAtPath("/Kitchen_set/Props_grp/North_grp/FridgeArea_grp/Refridgerator_1")
index = prim.GetPrimIndex()
index.DumpToDotGraph("D:/graph.dot")

まず、構成をしらべたい Prim を選択して、PrimIndex を取得します。

Index の DumpToDotGraph を使用すると GraphViz のフォーマットで情報を出力できるので、 それを使用してグラフを表示してみると、

このようになります。 rootNode から複数のコンポジションアークが木構造で表されてるのがわかります。

コンポジションの構造が木構造になっているので、再帰を使用することで

def traverse(node):
# コンポジションタイプ
print(node.arcType) #CompositionArc
print(node.path) #SdfPath
print(node.site) #Layer + SdfPath
print(node.GetRootNode()) # RootNode
layer = node.layerStack.layers[0] # Layer取得
print(layer)
for child in node.children:
traverse(child)
traverse(rootRef)

Prim を構成する各レイヤーを検索することができます。

レイヤースタックとコンポジション​

上の Dump した結果をみると、コンポジションの中でも「サブレイヤー」が含まれていないことがわかります。

この Pcp でコンポジションを追いかけたときにサブレイヤーの扱いはどうなっているのかを確認してみます。

まずはこんな感じの構成の usda を用意します。

subLayerB.usda をサブレイヤーした subLayer.usda を root.usda にサブレイヤー そして subLayerB.usda で定義している Prim subLayerB を sublayerReference プリムに Reference します。

index = prim.GetPrimIndex()

def layerTraverse(node):
print(node.layer)
for child in node.childTrees:
layerTraverse(child)

def traverse(node):
print(f">> {node.mapToParent}")
layerTraverse(node.layerStack.layerTree)
for i in node.children:
traverse(i)

traverse(index.rootNode)

PcpNodeRef から LayerStack を使用することで、レイヤーを取得できます。

実行すると

>> / -> /
Sdf.Find('s:/fav/work/programming/python/JupyterUSD/pyDev/usd/root.usda')
Sdf.Find('s:/fav/work/programming/python/JupyterUSD/pyDev/usd/subLayer.usda')
Sdf.Find('s:/fav/work/programming/python/JupyterUSD/pyDev/usd/subLayerB.usda')
>> /subLayerB -> /sublayerReference
Sdf.Find('s:/fav/work/programming/python/JupyterUSD/pyDev/usd/root.usda')
Sdf.Find('s:/fav/work/programming/python/JupyterUSD/pyDev/usd/subLayer.usda')
Sdf.Find('s:/fav/work/programming/python/JupyterUSD/pyDev/usd/subLayerB.usda')

編集ターゲットを取得する​

では、この Pcp を使用してある Prim を構成するレイヤーの1つを取得して 変更できるようにしてみます。 基本的にはEditTarget を使用すると同じように、UsdEditTarget を作れればよい ので、PcpPrimIndex と PcpNodeRef を使用して EditTarget を取得します。

et = Usd.EditTarget(rootRef.layerStack.layers[0],rootRef)
stage.SetEditTarget(et)
stage.DefinePrim(~~~)

取得方法はこのとおり。 PcpNodeRef から layerStack を取得し、構成するレイヤーを取得します。 この layerStack は、サブレイヤーの場合はこの Stack にレイヤーが積まれた状態になっています。 そこから SdfLayer オブジェクトを取得することができます。

そして、レイヤーのネームスペースを取得するため PcpNodeRef を EditTarget に渡します。 あとは、SetEditTarget で、編集ターゲットをステージに渡せば OK です。

まとめ​

Usd の API を使用すれば、コンポジションアークを構築したり シーングラフを管理したりできましたが この UsdAPI からコンポジションを実際にどのように管理しているのかというのが Pcp を見ることで理解できました。

usdview で Prim を選択しているときに表示される この Composition や Layer Stack などの情報も 今までの自分の理解だと Prim から頑張って取得する。。。ぐらいしか思いつかなかったですが PrimIndex から PcpNodeRef を使用することで コンポジションの木構造を取得して表示してるんだなー というのがよく分かるようになりました。 (むしろここに表示されてる Tree は PcpPrimIndex から取得できる NodeRef のツリーだった)

これを利用すれば、より複雑なコンポジションであっても ターゲットレイヤーを選択して編集対象にして、usd を更新する...みたいなツールも 簡単に作ることができそうです。