Siggraph Presentation

This guide will be officially introduced at Siggraph 2023 - Houdini Hive on Wednesday, 9. of August 2023 at 11:00 AM PST.

Loading & Traversing Data

Table of Contents

  1. Traversing & Loading Data In-A-Nutshell
  2. What should I use it for?
  3. Resources
  4. Overview
  5. Loading Mechanisms
    1. Layer Muting
    2. Prim Path Loading (USD speak: Prim Population Mask)
    3. Payload Loading
    4. GeomModelAPI->Draw Mode
  6. Traversing Data
    1. Traversing Stages
    2. Traversing Layers
    3. Traverse Sample Data/Profiling

TL;DR - Loading & Traversing Data In-A-Nutshell

Loading Mechanisms

When loading large scenes, we can selectively disabling loading via the following loading mechanisms: There are three ways to influence the data load, from lowest to highest granularity .

  • Layer Muting: This controls what layers are allowed to contribute to the composition result.
  • Prim Population Mask: This controls what prim paths to consider for loading at all.
  • Payload Loading: This controls what prim paths, that have payloads, to load.
  • GeomModelAPI->Draw Mode: This controls per prim how it should be drawn by delegates. It can be one of "Full Geometry"/"Origin Axes"/"Bounding Box"/"Texture Cards". It requires the kind to be set on the prim and all its ancestors. Therefore it is "limited" to (asset-) root prims and ancestors.
  • Activation: Control per prim whether load itself and its child hierarchy. This is more a an artist facing mechanism, as we end up writing the data to the stage, which we don't do with the other methods.

Traversing/Iterating over our stage/layer

To inspect our stage, we can iterate (traverse) over it:

When traversing, we try to pre-filter our prims as much as we can, via our prim metadata and USD core features(metadata), before inspecting their properties. This keeps our traversals fast even with hierarchies with millions of prims. We recommend first filtering based on metadata, as this is a lot faster than trying to access attributes and their values.

We also have a thing called predicate, which just defines what core metadata to consult for pre-filtering the result.

Another important feature is stopping traversal into child hierarchies. This can be done by calling `ìterator.PruneChildren()

Stage/Prim Traversal

# Standard
start_prim = stage.GetPrimAtPath("/") # Or stage.GetPseudoRoot(), this is the same as stage.Traverse()
iterator = iter(Usd.PrimRange(start_prim))
for prim in iterator:
    if prim.IsA(UsdGeom.Imageable): # Some condition as listed above or custom property/metadata checks
        # Don't traverse into the child prims
        iterator.PruneChildren()
# Pre and post visit:
start_prim = stage.GetPrimAtPath("/") # Or stage.GetPseudoRoot(), this is the same as stage.Traverse()
iterator = iter(Usd.PrimRange.PreAndPostVisit(start_prim))
for prim in iterator:
    if not iterator.IsPostVisit():
        if prim.IsA(UsdGeom.Imageable): # Some condition as listed above or custom property/metadata checks
            # Don't traverse into the child prims
            iterator.PruneChildren()
# Custom Predicate
predicate = Usd.PrimIsActive & Usd.PrimIsLoaded # All prims, even class and over prims.
start_prim = stage.GetPrimAtPath("/") # Or stage.GetPseudoRoot(), this is the same as stage.Traverse()
iterator = iter(Usd.PrimRange.PrimRange(start_prim, predicate=predicate))
for prim in iterator:
    if not iterator.IsPostVisit():
        if prim.IsA(UsdGeom.Imageable): # Some condition as listed above or custom property/metadata checks
            # Don't traverse into the child prims
            iterator.PruneChildren()

Layer traversal is a bit different. Instead of iterating, we provide a function, that gets called with each Sdf.Path representable object in the active layer. So we also see all properties, relationship targets and variants.

Layer Traversal

prim_paths = []
variant_prim_paths = []
property_paths = [] # The Sdf.Path class doesn't distinguish between attributes and relationships
property_relationship_target_paths = []
def traversal_kernel(path):
    print(path)
    if path.IsPrimPath():
        prim_paths.append(path)
    elif path.IsPrimVariantSelectionPath():
        variant_prim_paths.append(path)
    elif path.IsPropertyPath():
        property_paths.append(path)
    elif path.IsTargetPath():
        property_relationship_target_paths.append(path)
layer.Traverse(layer.pseudoRoot.path, traversal_kernel)

What should I use it for?

Tip

We'll be using loading mechanisms to optimize loading only what is relevant for the current task at hand.

Resources

Loading Mechanisms

Let's look at load mechanisms that USD offers to make the loading of our hierarchies faster.

Before we proceed, it is important to note, that USD is highly performant in loading hierarchies. When USD loads .usd/.usdc binary crate files, it sparsely loads the content: It can read in the hierarchy without loading in the attributes. This allows it to, instead of loading terabytes of data, to only read the important bits in the file and lazy load on demand the heavy data when requested by API queries or a hydra delegate.

When loading stages/layers per code only, we often therefore don't need to resort to using these mechanisms.

There are three ways to influence the data load, from lowest to highest granularity .

  • Layer Muting: This controls what layers are allowed to contribute to the composition result.
  • Prim Population Mask: This controls what prim paths to consider for loading at all.
  • Payload Loading: This controls what prim paths, that have payloads, to load.
  • GeomModelAPI->Draw Mode: This controls per prim how it should be drawn by delegates. It can be one of "Full Geometry"/"Origin Axes"/"Bounding Box"/"Texture Cards". It requires the kind to be set on the prim and all its ancestors. Therefore it is "limited" to (asset-) root prims and ancestors.
  • Activation: Control per prim whether load itself and its child hierarchy. This is more a an artist facing mechanism, as we end up writing the data to the stage, which we don't do with the other methods.

Stages are the controller of how our Prim Cache Population (PCP) cache loads our composed layers. Technically the stage just exposes the PCP cache in a nice API, that forwards its requests to the its pcp cache stage._GetPcpCache(), similar how all Usd ops are wrappers around Sdf calls.

Houdini exposes all three in two different ways:

  • Configue Stage LOP node: This is the same as setting it per code via the stage.
  • Scene Graph Tree panel: In Houdini, that stage that gets rendered, is actually not the stage of your node (at least what we gather from reverse engineering). Instead it is a duplicate, that has overrides in the session layer and loading mechanisms listed above.

More Houdini specific information can be found in our Houdini - Performance Optimizations section.

Layer Muting

We can "mute" (disable) layers either globally or per stage.

Globally muting layers is done via the singleton, this mutes it on all stages that use the layer.

from pxr import Sdf
layer = Sdf.Layer.FindOrOpen("/my/layer/identifier")
Sdf.Layer.AddToMutedLayers(layer.identifier)
Sdf.Layer.RemoveFromMutedLayers(layer.identifier)

Muting layers per stage is done via the Usd.Stage object, all function signatures work with the layer identifier string. If the layer is muted globally, the stage will not override the muting and it stays muted.

### High Level ###
from pxr import Sdf, Usd
stage = Usd.Stage.CreateInMemory()
layer_A = Sdf.Layer.CreateAnonymous("Layer_A")
layer_B = Sdf.Layer.CreateAnonymous("Layer_B")
layer_C = Sdf.Layer.CreateAnonymous("Layer_C")
stage.GetRootLayer().subLayerPaths.append(layer_A.identifier)
stage.GetRootLayer().subLayerPaths.append(layer_B.identifier)
stage.GetRootLayer().subLayerPaths.append(layer_C.identifier)
# Mute layer
stage.MuteLayer(layer_A.identifier)
# Unmute layer
stage.UnmuteLayer(layer_A.identifier)
# Or both MuteAndUnmuteLayers([<layers to mute>], [<layers to unmute>])
stage.MuteAndUnmuteLayers([layer_A.identifier, layer_B.identifier], [layer_C.identifier])
# Check what layers are muted
print(stage.GetMutedLayers()) # Returns: [layerA.identifier, layerB.identifier]
print(stage.IsLayerMuted(layer_C.identifier)) # Returns: False

Pro Tip | Layer Muting

We use layer muting in production for two things:

  • Artists can opt-in to load layers that are relevant to them. For example in a shot, a animator doesn't have to load the background set or fx layers.
  • Pipeline-wise we have to ensure that artists add shot layers in a specific order (For example: lighting > fx > animation > layout >). Let's say a layout artist is working in a shot, we only want to display the layout and camera layers. All the other layers can (should) be muted, because A. performance, B. there might be edits in higher layers, that the layout artist is not interested in seeing yet. If we were to display them, some of these edits might block ours, because they are higher in the layer stack.

Here is an example of global layer muting:

We have to re-cook the node for it to take effect, due to how Houdini caches stages.

Prim Path Loading Mask (USD speak: Prim Population Mask)

Pro Tip | Prim Population Mask

Similar to prim activation, the prim population mask controls what prims (and their child prims) are even considered for being loaded into the stage. Unlike activation, the prim population mask does not get stored in a USD layer. It is therefore a pre-filtering mechanism, rather than an artist facing "what do I want to hide from my scene" mechanism.

One difference to activation is that not only the child hierarchy is stripped away for traversing, but also the prim itself, if it is not included in the mask.

The population mask is managed via the Usd.StagePopulationMask class.

## Stage
# Create: 'OpenMasked',
# Get: 'GetPopulationMask',
# Set: 'SetPopulationMask', 'ExpandPopulationMask'
## Population Mask
# Usd.StagePopulationMask()
# Has: 'IsEmpty',  'Includes', 'IncludesSubtree'
# Get: 'GetIncludedChildNames', 'GetIntersection', 'GetPaths', 'GetUnion'
# Set:  'Add', 'Intersection',  'Union'
# Constants: 'All'
from pxr import Sdf, Usd
stage = Usd.Stage.CreateInMemory()
# Create hierarchy
prim_paths = [
    "/set/yard/biycle",
    "/set/yard/shed/shovel",
    "/set/yard/shed/flower_pot",
    "/set/yard/shed/lawnmower",
    "/set/yard/shed/soil",
    "/set/yard/shed/wood",
    "/set/garage/car",
    "/set/garage/tractor",
    "/set/garage/helicopter",
    "/set/garage/boat",
    "/set/garage/key_box",
    "/set/garage/key_box/red",
    "/set/garage/key_box/blue",
    "/set/garage/key_box/green",
    "/set/people/mike",
    "/set/people/charolotte"
]
for prim_path in prim_paths:
    prim = stage.DefinePrim(prim_path, "Cube")
    
population_mask = Usd.StagePopulationMask()
print(population_mask.GetPaths())
print(population_mask.All()) # Same as: Usd.StagePopulationMask([Sdf.Path("/")])
# Or stage.GetPopulationMask()
population_mask.Add(Sdf.Path("/set/yard/shed/lawnmower"))
population_mask.Add(Sdf.Path("/set/garage/key_box"))
stage.SetPopulationMask(population_mask)
print("<< hierarchy >>")
for prim in stage.Traverse():
    print(prim.GetPath())
"""Returns:
/set
/set/yard
/set/yard/shed
/set/yard/shed/lawnmower
/set/garage
/set/garage/key_box
/set/garage/key_box/red
/set/garage/key_box/blue
/set/garage/key_box/green
"""
# Intersections tests
print(population_mask.Includes("/set/yard/shed")) # Returns: True
print(population_mask.IncludesSubtree("/set/yard/shed")) # Returns: False (As not all child prims are included)
print(population_mask.IncludesSubtree("/set/garage/key_box")) # Returns: True (As all child prims are included)

What's also really cool, is that we can populate the mask by relationships/attribute connections.

stage.ExpandPopulationMask(relationshipPredicate=lambda r: r.GetName() == 'material:binding',
                           attributePredicate=lambda a: False)

Payload Loading

Pro Tip | Payload Loading

Payloads are USD's mechanism of disabling the load of heavy data and instead leaving us with a bounding box representation (or texture card representation, if you set it up). We can configure our stages to not load payloads at all or to only load payloads at specific prims.

What might be confusing here is the naming convention: USD refers to this as "loading", which sounds pretty generic. Whenever we are looking at stages and talking about loading, know that we are talking about payloads.

You can find more details in the API docs.

## Stage
# Has: 'FindLoadable', 
# Get: 'GetLoadRules', 'GetLoadSet'
# Set: 'Load', 'Unload', 'LoadAndUnload', 'LoadAll', 'LoadNone', 'SetLoadRules'
# Constants: 'InitialLoadSet.LoadAll', 'InitialLoadSet.LoadNone' 
## Stage Load Rules
# Has: 'IsLoaded', 'IsLoadedWithAllDescendants', 'IsLoadedWithNoDescendants'
# Get: 'GetRules', 'GetEffectiveRuleForPath',
# Set: 'LoadWithDescendants', 'LoadWithoutDescendants'
#      'LoadAll',  'LoadNone', 'Unload', 'LoadAndUnload', 'AddRule',  'SetRules'
# Clear: 'Minimize'
# Constants: StageLoadRules.AllRule, StageLoadRules.OnlyRule, StageLoadRules.NoneRule
from pxr import Sdf, Usd
# Spawn example data, this would be a file on disk
layer = Sdf.Layer.CreateAnonymous()
prim_path = Sdf.Path("/bicycle")
prim_spec = Sdf.CreatePrimInLayer(layer, prim_path)
prim_spec.specifier = Sdf.SpecifierDef
prim_spec.typeName = "Cube"
attr_spec = Sdf.AttributeSpec(prim_spec, "size", Sdf.ValueTypeNames.Double)
for frame in range(1001, 1010):
    value = float(frame - 1001)
    layer.SetTimeSample(attr_spec.path, frame, value)
# Payload data
stage = Usd.Stage.CreateInMemory()
ref = Sdf.Payload(layer.identifier, "/bicycle")
prim_path = Sdf.Path("/set/yard/bicycle")
prim = stage.DefinePrim(prim_path)
ref_api = prim.GetPayloads()
ref_api.AddPayload(ref)
# Check for what can be payloaded
print(stage.FindLoadable()) # Returns: [Sdf.Path('/set/yard/bicycle')]
# Check what prim paths are payloaded
print(stage.GetLoadSet()) # Returns: [Sdf.Path('/set/yard/bicycle')]
# Unload payload
stage.Unload(prim_path)
print(stage.GetLoadSet()) # Returns: []
# Please consult the official docs for how the rule system works.
# Basically we can flag primpaths to recursively load their nested child payloads or to only load the top most payload.

GeomModelAPI->Draw Mode

Pro Tip | Draw Mode

The draw mode can be used to tell our Hydra render delegates to not render a prim and its child hierarchy. Instead it will only display a preview representation.

The preview representation can be one of:

  • Full Geometry
  • Origin Axes
  • Bounding Box
  • Texture Cards

Like visibility, the draw mode is inherited downwards to its child prims. We can also set a draw mode color, to better differentiate the non full geometry draw modes, this is not inherited though and must be set per prim.

Important | Draw Mode Requires Kind

In order for the draw mode to work, the prim and all its ancestors, must have a kind defined. Therefore it is "limited" to (asset-)root prims and its ancestors. See the official docs for more info.

Here is how we can set it via Python, it is part of the UsdGeomModelAPI:

from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade
stage = Usd.Stage.CreateInMemory()
cone_prim = stage.DefinePrim(Sdf.Path("/set/yard/cone"), "Cone")
cone_prim.GetAttribute("radius").Set(4)
Usd.ModelAPI(cone_prim).SetKind("component")
sphere_prim = stage.DefinePrim(Sdf.Path("/set/yard/sphere"), "Sphere")
Usd.ModelAPI(sphere_prim).SetKind("component")
for ancestor_prim_path in sphere_prim.GetParent().GetPath().GetAncestorsRange():
    ancestor_prim = stage.GetPrimAtPath(ancestor_prim_path)
    ancestor_prim.SetTypeName("Xform")
    Usd.ModelAPI(ancestor_prim).SetKind("group")
# Enable on parent
set_prim = stage.GetPrimAtPath("/set")
set_geom_model_API = UsdGeom.ModelAPI.Apply(set_prim)
set_geom_model_API.GetModelDrawModeAttr().Set(UsdGeom.Tokens.bounds)
set_geom_model_API.GetModelDrawModeColorAttr().Set(Gf.Vec3h([1,0,0]))
# If we enable "apply" on the parent, children will not be drawn anymore,
# instead just a single combined bbox is drawn for all child prims.
# set_geom_model_API.GetModelApplyDrawModeAttr().Set(1)
# Enable on child
sphere_geom_model_API = UsdGeom.ModelAPI.Apply(sphere_prim)
# sphere_geom_model_API.GetModelDrawModeAttr().Set(UsdGeom.Tokens.default_)
sphere_geom_model_API.GetModelDrawModeAttr().Set(UsdGeom.Tokens.cards)
sphere_geom_model_API.GetModelDrawModeColorAttr().Set(Gf.Vec3h([0,1,0]))
# For "component" (sub-)kinds, this is True by default
# sphere_geom_model_API.GetModelApplyDrawModeAttr().Set(0)

Traversing Data

When traversing (iterating) through our hierarchy, we commonly use these metadata and property entries on prims to pre-filter what we want to access:

  • .IsA Typed Schemas (Metadata)
  • Type Name (Metadata)
  • Specifier (Metadata)
  • Activation (Metadata)
  • Kind (Metadata)
  • Purpose (Attribute)
  • Visibility (Attribute)

Pro Tip | High Performance Traversals

When traversing, using the above "filters" to narrow down your selection well help keep your traversals fast, even with hierarchies with millions of prims. We recommend first filtering based on metadata, as this is a lot faster than trying to access attributes and their values.

Another important feature is stopping traversal into child hierarchies. This can be done by calling ìterator.PruneChildren():

from pxr import Sdf, UsdShade
root_prim = stage.GetPseudoRoot()
# We have to cast it as an iterator to gain access to the .PruneChildren() method.
iterator = iter(Usd.PrimRange(root_prim))
for prim in iterator:
    if prim.IsA(UsdShade.Material):
        # Don't traverse into the shader network prims
        iterator.PruneChildren()

Traversing Stages

Traversing stages works via the Usd.PrimRange class. The stage.Traverse/stage.TraverseAll/prim.GetFilteredChildren methods all use this as the base class, so let's checkout how it works:

We have two traversal modes:

  • Default: Iterate over child prims
  • PreAndPostVisit: Iterate over the hierarchy and visit each prim twice, once when first encountering it, and then again when "exiting" the child hierarchy. See our primvars query section for a hands-on example why this can be useful.

We also have a thing called "predicate"(Predicate Overview), which just defines what core metadata to consult for pre-filtering the result:

  • Usd.PrimIsActive: Usd.Prim.IsActive() - If the "active" metadata is True
  • Usd.PrimIsLoaded: Usd.Prim.IsLoaded() - If the (ancestor) payload is loaded
  • Usd.PrimIsModel: Usd.Prim.IsModel() - If the kind is a sub kind of Kind.Tokens.model
  • Usd.PrimIsGroup: Usd.Prim.IsGroup() - If the kind is Kind.Tokens.group
  • Usd.PrimIsAbstract: Usd.Prim.IsAbstract() - If the prim specifier is Sdf.SpecifierClass
  • Usd.PrimIsDefined: Usd.Prim.IsDefined() - If the prim specifier is Sdf.SpecifierDef
  • Usd.PrimIsInstance: Usd.Prim.IsInstance() - If prim is an instance root (This is false for prims in instances)

Presets:

  • Usd.PrimDefaultPredicate: Usd.PrimIsActive & Usd.PrimIsDefined & Usd.PrimIsLoaded & ~Usd.PrimIsAbstract
  • Usd.PrimAllPrimsPredicate: Shortcut for selecting all filters (basically ignoring the prefilter).

By default the Usd.PrimDefaultPredicate is used, if we don't specify one.

Pro Tip | Usd Prim Range

Here is the most common syntax you'll be using:

# Standard
start_prim = stage.GetPrimAtPath("/") # Or stage.GetPseudoRoot(), this is the same as stage.Traverse()
iterator = iter(Usd.PrimRange(start_prim))
for prim in iterator:
    if prim.IsA(UsdGeom.Imageable): # Some condition as listed above or custom property/metadata checks
        # Don't traverse into the child prims
        iterator.PruneChildren()
# Pre and post visit:
start_prim = stage.GetPrimAtPath("/") # Or stage.GetPseudoRoot(), this is the same as stage.Traverse()
iterator = iter(Usd.PrimRange.PreAndPostVisit(start_prim))
for prim in iterator:
    if not iterator.IsPostVisit():
        if prim.IsA(UsdGeom.Imageable): # Some condition as listed above or custom property/metadata checks
            # Don't traverse into the child prims
            iterator.PruneChildren()
# Custom Predicate
predicate = Usd.PrimIsActive & Usd.PrimIsLoaded # All prims, even class and over prims.
start_prim = stage.GetPrimAtPath("/") # Or stage.GetPseudoRoot(), this is the same as stage.Traverse()
iterator = iter(Usd.PrimRange.PrimRange(start_prim, predicate=predicate))
for prim in iterator:
    if not iterator.IsPostVisit():
        if prim.IsA(UsdGeom.Imageable): # Some condition as listed above or custom property/metadata checks
            # Don't traverse into the child prims
            iterator.PruneChildren()

The default traversal also doesn't go into instanceable prims. To enable it we can either run pxr.Usd.TraverseInstanceProxies(<existingPredicate>) or predicate.TraverseInstanceProxies(True)

Within instances we can get the prototype as follows, for more info see our instanceable section:

# Check if the active prim is marked as instanceable:
# The prim.IsInstance() checks if it is actually instanced, this
# just checks if the 'instanceable' metadata is set.
prim.IsInstanceable()
# Check if the active prim is an instanced prim:
prim.IsInstance()
# Check if we are inside an instanceable prim:
prim.IsInstanceProxy()
# Check if the active prim is a prototype root prim with the following format /__Prototype_<idx>
prim.IsPrototype()
# For these type of prototype root prims, we can get the instances via:
prim.GetInstances()
# From each instance we can get back to the prototype via
prim.GetPrototype()
# Check if we are in the /__Prototype_<idx> prim:
prim.IsInPrototype()

# When we are within an instance, we can get the prototype via:
if prim.IsInstanceProxy():
    for ancestor_prim_path in prim.GetAncestorsRange():
        ancestor_prim = stage.GetPrimAtPath(ancestor_prim_path)
        if ancestor_prim.IsInstance():
            prototype = ancestor_prim.GetPrototype()
            print(list(prototype.GetInstances()))
            break

Let's look at some traversal examples:

Stage/Prim Traversal | Click to expand

from pxr import Sdf, Usd
stage = Usd.Stage.CreateInMemory()
# Create hierarchy
prim_paths = [
    "/set/yard/biycle",
    "/set/yard/shed/shovel",
    "/set/yard/shed/flower_pot",
    "/set/yard/shed/lawnmower",
    "/set/yard/shed/soil",
    "/set/yard/shed/wood",
    "/set/garage/car",
    "/set/garage/tractor",
    "/set/garage/helicopter",
    "/set/garage/boat",
    "/set/garage/key_box",
    "/set/garage/key_box/red",
    "/set/garage/key_box/blue",
    "/set/garage/key_box/green",
    "/set/people/mike",
    "/set/people/charolotte"
]
for prim_path in prim_paths:
    prim = stage.DefinePrim(prim_path, "Cube")

root_prim = stage.GetPseudoRoot()
# Standard Traversal
# We have to cast it as an iterator to gain access to the .PruneChildren()/.IsPostVisit method.
iterator = iter(Usd.PrimRange(root_prim))
for prim in iterator:
    if prim.GetPath() == Sdf.Path("/set/garage/key_box"):
        # Skip traversing key_box hierarchy
        iterator.PruneChildren()
    print(prim.GetPath().pathString)
"""Returns:
/
/set
/set/yard
/set/yard/biycle
/set/yard/shed
/set/yard/shed/shovel
/set/yard/shed/flower_pot
/set/yard/shed/lawnmower
/set/yard/shed/soil
/set/yard/shed/wood
/set/garage
/set/garage/car
/set/garage/tractor
/set/garage/helicopter
/set/garage/boat
/set/garage/key_box
/set/people
/set/people/mike
/set/people/charolotte
"""
# PreAndPostVisitTraversal
iterator = iter(Usd.PrimRange.PreAndPostVisit(root_prim))
for prim in iterator:
    print("Is Post Visit: {:<2} | Path: {}".format(iterator.IsPostVisit(), prim.GetPath().pathString))
"""Returns:
Is Post Visit: 0  | Path: /
Is Post Visit: 0  | Path: /set
Is Post Visit: 0  | Path: /set/yard
Is Post Visit: 0  | Path: /set/yard/biycle
Is Post Visit: 1  | Path: /set/yard/biycle
Is Post Visit: 0  | Path: /set/yard/shed
Is Post Visit: 0  | Path: /set/yard/shed/shovel
Is Post Visit: 1  | Path: /set/yard/shed/shovel
Is Post Visit: 0  | Path: /set/yard/shed/flower_pot
Is Post Visit: 1  | Path: /set/yard/shed/flower_pot
Is Post Visit: 0  | Path: /set/yard/shed/lawnmower
Is Post Visit: 1  | Path: /set/yard/shed/lawnmower
Is Post Visit: 0  | Path: /set/yard/shed/soil
Is Post Visit: 1  | Path: /set/yard/shed/soil
Is Post Visit: 0  | Path: /set/yard/shed/wood
Is Post Visit: 1  | Path: /set/yard/shed/wood
Is Post Visit: 1  | Path: /set/yard/shed
Is Post Visit: 1  | Path: /set/yard
Is Post Visit: 0  | Path: /set/garage
Is Post Visit: 0  | Path: /set/garage/car
Is Post Visit: 1  | Path: /set/garage/car
Is Post Visit: 0  | Path: /set/garage/tractor
Is Post Visit: 1  | Path: /set/garage/tractor
Is Post Visit: 0  | Path: /set/garage/helicopter
Is Post Visit: 1  | Path: /set/garage/helicopter
Is Post Visit: 0  | Path: /set/garage/boat
Is Post Visit: 1  | Path: /set/garage/boat
Is Post Visit: 0  | Path: /set/garage/key_box
Is Post Visit: 0  | Path: /set/garage/key_box/red
Is Post Visit: 1  | Path: /set/garage/key_box/red
Is Post Visit: 0  | Path: /set/garage/key_box/blue
Is Post Visit: 1  | Path: /set/garage/key_box/blue
Is Post Visit: 0  | Path: /set/garage/key_box/green
Is Post Visit: 1  | Path: /set/garage/key_box/green
Is Post Visit: 1  | Path: /set/garage/key_box
Is Post Visit: 1  | Path: /set/garage
Is Post Visit: 0  | Path: /set/people
Is Post Visit: 0  | Path: /set/people/mike
Is Post Visit: 1  | Path: /set/people/mike
Is Post Visit: 0  | Path: /set/people/charolotte
Is Post Visit: 1  | Path: /set/people/charolotte
Is Post Visit: 1  | Path: /set/people
Is Post Visit: 1  | Path: /set
Is Post Visit: 1  | Path: /
"""

Traversing Layers

Layer traversal is different, it only looks at the active layer and traverses everything that is representable via an Sdf.Path object. This means, it ignores activation and it traverses into variants and relationship targets. This can be quite useful, when we need to rename something or check for data in the active layer.

We cover it in detail with examples over in our layer and stages section.

Pro Tip | Layer Traverse

The traversal for layers works differently. Instead of an iterator, we have to provide a "kernel" like function, that gets an Sdf.Path as an input. Here is the most common syntax you'll be using:

prim_paths = []
variant_prim_paths = []
property_paths = [] # The Sdf.Path class doesn't distinguish between attributes and relationships
property_relationship_target_paths = []
def traversal_kernel(path):
    print(path)
    if path.IsPrimPath():
        prim_paths.append(path)
    elif path.IsPrimVariantSelectionPath():
        variant_prim_paths.append(path)
    elif path.IsPropertyPath():
        property_paths.append(path)
    elif path.IsTargetPath():
        property_relationship_target_paths.append(path)
layer.Traverse(layer.pseudoRoot.path, traversal_kernel)

Traverse Sample Data/Profiling

To test profiling, we can setup a example hierarchy. The below code spawns a nested prim hierarchy. You can adjust the create_hierarchy(layer, prim_path, <level>), be aware this is exponential, so a value of 10 and higher will already generate huge hierarchies.

The output will be something like this:

Houdini Traversal Profiling Hierarchy

import random
from pxr import Sdf, Usd, UsdGeom,UsdShade, Tf
stage = Usd.Stage.CreateInMemory()
layer = stage.GetEditTarget().GetLayer()

leaf_prim_types = ("Cube", "Cylinder", "Sphere", "Mesh", "Points", "RectLight", "Camera")
leaf_prim_types_count = len(leaf_prim_types)

def create_hierarchy(layer, root_prim_path, max_levels):
    def generate_hierarchy(layer, root_prim_path, leaf_prim_counter, max_levels):
        levels = random.randint(1, max_levels)
        for level in range(levels):
            level_depth = root_prim_path.pathElementCount + 1
            prim_path = root_prim_path.AppendChild(f"level_{level_depth}_child_{level}")
            prim_spec = Sdf.CreatePrimInLayer(layer, prim_path)
            prim_spec.specifier = Sdf.SpecifierDef
            # Type
            prim_spec.typeName = "Xform"
            # Kind
            prim_spec.SetInfo("kind", "group")
            # Seed parent prim specs
            hiearchy_seed_state = random.getstate()
            # Active
            random.seed(level_depth)
            if random.random() < 0.1:
                prim_spec.SetInfo("active", False)
            random.setstate(hiearchy_seed_state)
            if levels == 1:
                # Parent prim
                # Kind
                prim_spec.nameParent.SetInfo("kind", "component")
                # Purpose
                purpose_attr_spec = Sdf.AttributeSpec(prim_spec.nameParent, "purpose", Sdf.ValueTypeNames.Token)
                if random.random() < .9:
                    purpose_attr_spec.default = UsdGeom.Tokens.render
                else:
                    purpose_attr_spec.default = UsdGeom.Tokens.proxy
                # Seed leaf prim specs
                leaf_prim_counter[0] += 1
                hiearchy_seed_state = random.getstate()
                random.seed(leaf_prim_counter[0])
                # Custom Leaf Prim attribute
                prim_spec.typeName = leaf_prim_types[random.randint(0, leaf_prim_types_count -1)]
                prim_spec.assetInfo["is_leaf"] = True
                prim_spec.ClearInfo("kind")
                is_leaf_attr_spec = Sdf.AttributeSpec(prim_spec, "is_leaf", Sdf.ValueTypeNames.Bool)
                is_leaf_attr_spec.default = True
                # Active
                if random.random() < 0.1:
                    prim_spec.SetInfo("active", False)
                # Visibility
                visibility_attr_spec = Sdf.AttributeSpec(prim_spec, "visibility", Sdf.ValueTypeNames.Token)
                if random.random() < .5:
                    visibility_attr_spec.default = UsdGeom.Tokens.inherited
                else:
                    visibility_attr_spec.default = UsdGeom.Tokens.invisible
                    
                random.setstate(hiearchy_seed_state)
            else:
                generate_hierarchy(layer, prim_path, leaf_prim_counter, max_levels -1)
    random.seed(0)
    leaf_prim_counter = [0] # Make sure this is a pointer
    generate_hierarchy(layer, root_prim_path, leaf_prim_counter, max_levels)
                
with Sdf.ChangeBlock():
    prim_path = Sdf.Path("/profiling_grp")
    prim_spec = Sdf.CreatePrimInLayer(layer, prim_path)
    prim_spec.specifier = Sdf.SpecifierDef
    prim_spec.typeName = "Xform"
    prim_spec.SetInfo("kind", "group")
    create_hierarchy(layer, prim_path, 9)

Here is how we can run profiling (this is kept very simple, check out our profiling section how to properly trace the stats) on the sample data:

# We assume we are running on the stage from the previous example.
root_prim = stage.GetPrimAtPath("/profiling_grp")
leaf_prim_types = ("Cube", "Cylinder", "Sphere", "Mesh", "Points", "RectLight", "Camera")

def profile(func, label, root_prim):
    # The changeblock doesn't do much here as we are only querying data, but
    # let's keep it in there anyway.
    with Sdf.ChangeBlock():
        runs = 3
        sw = Tf.Stopwatch()
        time_delta = 0.0
        for run in range(runs):
            sw.Reset()
            sw.Start()
            matched_prims = []
            for prim in iter(Usd.PrimRange(root_prim)):
                if func(prim):
                    matched_prims.append(prim)
            sw.Stop()
            time_delta += sw.seconds
        print("{:.5f} Seconds | {} | Match {}".format(time_delta / runs, label, len(matched_prims)))

print("----")

def profile_boundable(prim):
    return prim.IsA(UsdGeom.Boundable)

profile(profile_boundable, "IsA(Boundable)", root_prim)
    
def profile_GetTypeName(prim):
    return prim.GetTypeName() in leaf_prim_types

profile(profile_GetTypeName, "GetTypeName", root_prim)

def profile_kind(prim):
    model_api = Usd.ModelAPI(prim)
    return model_api.GetKind() != Kind.Tokens.group
    
profile(profile_kind, "Kind", root_prim)
 
def profile_assetInfo_is_leaf(prim):
    asset_info = prim.GetAssetInfo()
    return asset_info.get("is_leaf", False)

profile(profile_assetInfo_is_leaf, "IsLeaf AssetInfo ", root_prim)

def profile_attribute_has_is_leaf(prim):
    if prim.HasAttribute("is_leaf"):
        return True
    return False

profile(profile_attribute_has_is_leaf, "IsLeaf Attribute Has", root_prim)

def profile_attribute_is_leaf(prim):
    is_leaf_attr = prim.GetAttribute("is_leaf")
    if is_leaf_attr:
        if is_leaf_attr.Get():
            return True
    return False

profile(profile_attribute_is_leaf, "IsLeaf Attribute ", root_prim)

def profile_attribute_extra_validation_is_leaf(prim):
    if prim.HasAttribute("is_leaf"):
        is_leaf_attr = prim.GetAttribute("is_leaf")
        if is_leaf_attr.Get():
            return True
    return False

profile(profile_attribute_extra_validation_is_leaf, "IsLeaf Attribute (Validation)", root_prim)

Here is a sample output, we recommend running each traversal multiple times and then averaging the results. As we can see running attribute checks against attributes can be twice as expensive than checking metadata or the type name. (Surprisingly kind checks take a long time, even though it is also a metadata check)

0.17678 Seconds | IsA(Boundable) | Match 38166
0.17222 Seconds | GetTypeName | Match 44294
0.42160 Seconds | Kind | Match 93298
0.38575 Seconds | IsLeaf AssetInfo  | Match 44294
0.27142 Seconds | IsLeaf Attribute Has | Match 44294
0.38036 Seconds | IsLeaf Attribute  | Match 44294
0.37459 Seconds | IsLeaf Attribute (Validation) | Match 44294