In USD, What Prims are Affected by Layer X?
A Network Snapshot of USD Stage Composition
When working with Pixar's USD stages, sometimes we want to know what prims are affected by some layers. This entry is a summary of the approach I've taken to help answer those questions in a GUI.
Disclaimer
This does not aim to be an official guide or a tutorial, it is the key notes of what continues to be a learning exercise. There are a lot of areas of opportunity to improve and fix possible errors, which I'll aim at addressing as soon as I come across them (and time permits).
Context
USDView's prim composition tab is great but it's mainly useful when inspecting a single prim and when you already know what prim you're looking for.
USD comes with robust APIs for inspecting prim composition.
Before UsdPrimCompositionQuery was available, some answers were possible via UsdPrim.GetPrimStack, while others could be achieved by inspecting the internals of UsdPrim.GetPrimIndex. Let's have a quick overview of both.
* Note: The below code snippets ran under Python-3.9, which USD-21.08 added support for.
Given a prim with a reference:
>>> from pxr import Usd
>>> stage = Usd.Stage.CreateInMemory()
>>> a = stage.DefinePrim("/a")
>>> b = stage.DefinePrim("/b")
>>> b.GetReferences().AddInternalReference(a.GetPath())
True
We could inspect prim /b via GetPrimStack, which returns PrimSpecs (the authored scene description on USD layers) in strength order:
>>> for spec in b.GetPrimStack():
... print(f"{spec.layer=}")
... print(f"{spec.hasReferences=}")
... print(spec.GetAsText())
...
spec.layer=Sdf.Find('anon:0000024570298C00:tmp.usda')
spec.hasReferences=True
def "b" (
prepend references = </a>
)
{
}
spec.layer=Sdf.Find('anon:0000024570298C00:tmp.usda')
spec.hasReferences=False
def "a"
{
}
We could also inspect /b via GetPrimIndex, which returns an object containing cached Pcp.NodeRef objects (The nodes composing the actual scene description):
>>> index = b.GetPrimIndex()
>>> type(index)
<class 'pxr.Pcp.PrimIndex'>
>>> index.rootNode
<pxr.Pcp.NodeRef object at 0x000002456F717350>
>>> def tell(node):
... print(f"\nChecking {node=}")
... print(f"{node.arcType=}")
... print(f"{node.layerStack.identifier=}")
... for child in node.children:
... tell(child)
...
>>> tell(index.rootNode)
Checking node=<pxr.Pcp.NodeRef object at 0x000002456F717270>
node.arcType=Pcp.ArcTypeRoot
node.layerStack.identifier=Pcp.LayerStackIdentifier(Sdf.Find('anon:0000024570298C00:tmp.usda'), Sdf.Find('anon:0000024570298500:tmp-session.usda'), Ar.ResolverContext())
Checking node=<pxr.Pcp.NodeRef object at 0x000002456F7174A0>
node.arcType=Pcp.ArcTypeReference
node.layerStack.identifier=Pcp.LayerStackIdentifier(Sdf.Find('anon:0000024570298C00:tmp.usda'), Sdf.Find('anon:0000024570298500:tmp-session.usda'), Ar.ResolverContext())
What I found extremely useful when diagnosing USD prims, was that the PrimIndex object has methods for inspecting all nodes via:
- Pcp.PrimIndex.DumpToString
Which reminds me a bit to the dis.dis python function:>>> print(index.DumpToString()) Node 0: Parent node: NONE Type: root DependencyType: root Source path: </b> Source layer stack: @anon:0000024570298C00:tmp.usda@,@anon:0000024570298500:tmp-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: </a> Source layer stack: @anon:0000024570298C00:tmp.usda@,@anon:0000024570298500:tmp-session.usda@ Target path: </b> Target layer stack: @anon:0000024570298C00:tmp.usda@,@anon:0000024570298500:tmp-session.usda@ Map to parent: /a -> /b Map to root: /a -> /b Namespace depth: 1 Depth below introduction: 0 Permission: Public Is restricted: FALSE Is inert: FALSE Contribute specs: TRUE Has specs: TRUE Has symmetry: FALSE
>>> import dis >>> dis.dis(tell) 2 0 LOAD_GLOBAL 0 (print) 2 LOAD_CONST 1 ('\nChecking node=') 4 LOAD_FAST 0 (node) 6 FORMAT_VALUE 2 (repr) 8 BUILD_STRING 2 10 CALL_FUNCTION 1 12 POP_TOP 3 14 LOAD_GLOBAL 0 (print) 16 LOAD_CONST 2 ('node.arcType=') 18 LOAD_FAST 0 (node) 20 LOAD_ATTR 1 (arcType) 22 FORMAT_VALUE 2 (repr) 24 BUILD_STRING 2 26 CALL_FUNCTION 1 28 POP_TOP 4 30 LOAD_GLOBAL 0 (print) 32 LOAD_CONST 3 ('node.layerStack.identifier=') 34 LOAD_FAST 0 (node) 36 LOAD_ATTR 2 (layerStack) 38 LOAD_ATTR 3 (identifier) 40 FORMAT_VALUE 2 (repr) 42 BUILD_STRING 2 44 CALL_FUNCTION 1 46 POP_TOP 5 48 LOAD_FAST 0 (node) 50 LOAD_ATTR 4 (children) 52 GET_ITER >> 54 FOR_ITER 12 (to 68) 56 STORE_FAST 1 (child) 6 58 LOAD_GLOBAL 5 (tell) 60 LOAD_FAST 1 (child) 62 CALL_FUNCTION 1 64 POP_TOP 66 JUMP_ABSOLUTE 54 >> 68 LOAD_CONST 0 (None) 70 RETURN_VALUE
- Pcp.PrimIndex.DumpToDotGraph
This dumps the content to a file as a DOT graph. If you have dot installed on your system, you can turn that into an image like so:>>> import tempfile >>> path = tempfile.mkstemp()[-1] >>> path 'C:\\Users\\CHRIST~1\\AppData\\Local\\Temp\\tmpw9a0owhl' >>> index.DumpToDotGraph(path)
The created image looks like this:>>> import shutil >>> shutil.which("dot") 'C:\\Users\\Christian\\.conda\\envs\\py39usd2108\\Library\\bin\\dot.BAT' >>> import subprocess >>> subprocess.run([shutil.which("dot"), path, "-Tpng", "-o", f"{path}.png"])
The Approach
UsdPrimCompositionQuery, added in USD-19.11, facilitates some of these queries since it provides filtering mechanisms to get only what you need (e.g. based on arc type, or if there are specs):
>>> query = Usd.PrimCompositionQuery(b)
>>> for arc in query.GetCompositionArcs():
... print(f"{arc.GetArcType()=}")
... print(f"{arc.GetIntroducingLayer()=}")
...
arc.GetArcType()=Pcp.ArcTypeRoot
arc.GetIntroducingLayer()=None
arc.GetArcType()=Pcp.ArcTypeReference
arc.GetIntroducingLayer()=Sdf.Find('anon:0000024570298C00:tmp.usda')
>>> query_filter = Usd.PrimCompositionQuery.Filter()
>>> query_filter.arcTypeFilter = query.ArcTypeFilter.Reference
>>> query.filter = query_filter
>>> for arc in query.GetCompositionArcs():
... print(f"{arc.GetArcType()=}")
... print(f"{arc.GetIntroducingLayer()=}")
...
arc.GetArcType()=Pcp.ArcTypeReference
arc.GetIntroducingLayer()=Sdf.Find('anon:0000024570298C00:tmp.usda')
This, combined with the above, inspired me to try to answer the original question:
What prims are being affected by layers X, Y in the current USD stage?
Mixing this with dot, SVG and some Qt, allows for taking a "snapshot" of a USD stage for inspection of the different layer stacks and how they connect to others via composition arcs and the prims they have opinions on.
In the above example, we're inspecting Animal Logic's USD ALab.
- On the upper left, all currently used layers are listed.
- On the upper right are all prims affected by the selected layers.
- On the bottom section, a network of the selected layers is displayed.
- Nodes in the network represent layerStacks.
- Edges are the composition arcs between them (it follows the same color scheme as the ones provided by Pcp.PrimIndex.DumpToDotGraph)
- Options to filter composition arcs are provided on the up left corner of the network view.
- An additional option to display "Precise Source Layer" (off by default) exists to draw the edge source from the layer from the stack that introduces it. This allows to go from this:To (note books_magazines01_surfacing and books_magazines01_modelling):
Documentation for this can be found on The Grill's LayerStack Composition page.
Trying it out
Source code can be found on the main grill repository, or it can be installed via pypi with:
pip install grill
Convenience launcher menus for USDView, Maya-2022 and Houdini-18.5 are included.
Final Notes
Having available scenes like Pixar's Kitchen and Animal Logic's USD ALab has allowed me to test this LayerStack Composition widget against real assets, so get those downloads if you haven't already!
I've learnt a lot from this exercise, hopefully these notes will be useful for others. Feel free to reach out for any questions, though I'd suggest to join the usd-interest google group for an open USD discussion.