Siggraph Presentation

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

Layers & Stages

Layers and stages are the main entry point to accessing our data stored in USD.

Table of Contents

  1. Layers & Stages In-A-Nutshell
  2. What should I use it for?
  3. Resources
  4. Overview
  5. Layers
    1. Layer Singleton
    2. (Anonymous) Layer Identifiers
    3. Layers Creation/Import/Export
    4. Dependencies
    5. Layer Metrics
    6. Permissions
    7. Muting
    8. Composition
    9. Traversal and Prim/Property Access
    10. Time Samples
    11. Metadata
  6. Stages
    1. Configuration
      1. Asset Resolver
      2. Stage Metrics
      3. Stage Time Sample Interpolation
      4. Variant/Prim Type Fallbacks
      5. Color Management
      6. Metadata
    2. Composition
    3. Loading mechanisms
    4. Stage Layer Management (Creation/Save/Export)
    5. Traversal and Prim/Property Access

TL;DR - Layers & Stages In-A-Nutshell

Layers

  • Layers are managed via a singleton pattern: Each layer is only opened once in memory and is identified by the layer identifier. When stages load a layer, they point to the same data in memory.
  • Layers identifiers can have two different formats:
    • Standard identifiers: Sdf.Layer.CreateNew("/file/path/or/URI/identifier.<ext(.usd/.usdc/.usda)>"), these layers are backed by a file on disk
    • Anonymous identifiers: Sdf.Find('anon:<someHash(MemoryLocation)>:<customName>', these are in-memory only layers

Stages

  • A stage is a view of a set of composed layers. You can think of it as the viewer in a view--model design. Each layer that the stage opens is a data source to the data model. When "asking" the stage for data, we ask the view for the combined (composed) data, which then queries into the layers based on the value source found by our composition rules.
  • When creating a stage we have two layers by default:
    • Session Layer: This is a temp layer than doesn't get applied on disk save. Here we usually put things like viewport overrides.
    • Root Layer: This is the base layer all edits target by default. We can add sublayers based on what we need to it. When calling stage.Save(), all sublayers that are dirty and not anonymous, will be saved.

What should I use it for?

Tip

Stages and layers are what make USD work, it is our entry point to accessing our hierarchies.

Resources

Overview

This section will focus on what the Sdf.Layer and Usd.Stage classes have to offer. For an explanation of how layers work together, please see our compsition section.

Pro Tip | Advanced Concepts and Utility Functions for Layers/Stages

There are also utility methods available, that are not in the Sdf.Layer/Usd.Stage namespace. We cover these in our advanced concepts in production section.

Layers

flowchart LR
    layerSingleton(Layer Singleton/Registry) --> layer1([Layer])
    layer1([Layer]) --> prim1([Property])
    prim1([Prim]) --> property([Property])
    property --> attribute([Attribute])
    property --> relationship([Relationship])
    layerSingleton --> layer2([Layer])
    layer2([Layer]) --> prim2([...])

Layers are the data container for our prim specs and properties, they are the part of USD that actually holds and import/exports the data.

Layers - In-A-Nutshell

  • Layers are managed via a singleton pattern: Each layer is only opened once in memory and is identified by the layer identifier.
  • Layers identifiers can have two different formats:
    • Standard identifiers: Sdf.Layer.CreateNew("/file/path/or/URI/identifier.<ext(.usd/.usdc/.usda)>")
    • Anonymous identifiers: Sdf.Find('anon:<someHash(MemoryLocation)>:<customName>'
  • Layers store our prim and property specs, they are the data container for all USD data that gets persistently written to file. When we want to edit layers directly, we have to use the low-level API, the high level API edits the stage, which in return forwards the edits to the layer that is set by the active edit target.
  • The Sdf.FileFormat plugin interface allows us to implement plugins that convert the content of (custom) file format's to the USD's prim/property/metadata data model. This is how USD manages the USD crate (binary), alembic and vdb formats.
  • USD's crate (binary) format allows layers to be lazily read and written to. Calling layer.Save() multiple times, flushes the in-memory content to disk by appending it to the .usd file, which allows us to efficiently write large layer files. This format can also read in hierarchy data without loading property value data. This way we have low IO when opening files, as the property data gets lazy loaded on demand. This is similar to how we can parse image metadata without reading the image content.

Layer Singleton

Layers in USD are managed by a singleton design pattern. This means that each layer, identified by its layer identifier, can only be opened once. Each stage that makes use of a layer, uses the same layer. That means if we make an edit on a layer in one stage, all other stages will get changed notifications and update accordingly.

We get all opened layers via the Sdf.Layer.GetLoadedLayers() method.

for layer in Sdf.Layer.GetLoadedLayers():
    print(layer.identifier)
# Skip anonymous layers
for layer in Sdf.Layer.GetLoadedLayers():
    if layer.anonymous:
        continue
    print(layer.identifier)

If a layer is not used anymore in a stage and goes out of scope in our code, it will be deleted. Should we still have access the to Python object, we can check if it actually points to a valid layer via the layer.expired property.

As also mentioned in the next section, the layer identifier is made up of the URI(Unique Resource Identifier) and optional arguments. The layer identifier includes the optional args. This is on purpose, because different args can potentially mean a different file.

To demonstrate the singleton behavior let's try changing a layers content in Houdini and then view the layer through two different unrelated stages. (In Houdini every LOPs node is a separate stage):

The snippet from the video:

from pxr import Sdf
flippy_layer = Sdf.Layer.FindOrOpen("/opt/hfs19.5/houdini/usd/assets/rubbertoy/geo.usdc")
pig_layer = Sdf.Layer.FindOrOpen("/opt/hfs19.5/houdini/usd/assets/pig/geo.usdc")
flippy_layer.TransferContent(pig_layer)

"No flippyyyy, where did you go?" Luckily all of our edits were just in memory, so if we just call layer.Reload() or refresh the layer via the reference node, all is good again.

Should you ever use this in production as a way to broadcast an edit of a nested layer? We wouldn't recommend it, as it breaks the WYSIWYG paradigm. A better approach would be to rebuild the layer stack (this is what Houdini's "Edit Target Layer" node does) or we remap it via our asset resolver. In Houdini you should never use this method, as it can cause very strange stage caching issues.

(Anonymous) Layer Identifiers

Layer identifiers come in two styles:

  • Standard identifiers: Sdf.Layer.CreateNew("URI.<ext(.usd/.usdc/.usda)>")
  • Anonymous identifiers: Sdf.Find('anon:<someHash(MemoryLocation)>:<customName>'

We can optionally add file format args: Sdf.Layer.CreateNew("URI.<ext>:SDF_FORMAT_ARGS:<ArgNameA>=<ArgValueA>&<ArgNameB>=<ArgValueB>")

Anonymous layers have these special features:

  • They are in-memory layers that have no real path or asset information fields.
  • We can additionally give a custom name suffix, so that we can identify the layer better visually
  • The identifier is not run through the asset resolver (Edit: I have to verify this again, but I'm fairly certain)
  • They cannot be saved via layer.Save(), it will return an error
  • We can convert them to "normal" layers, by assigning a non-anonymous identifier (layer.identifier="/file/path/myIdentifier.usd"), this also removes the save permission lock.

When using standard identifiers, we use the URI not the absolute resolved path. The URI is then resolved by our asset resolver. We often need to compare the URI, when doing so be sure to call layer_uri, layer_args = layer.SplitIdentifier(layer.identifier) to strip out the optional args or compare using the resolve URI layer.realPath.

Danger

The layer identifier includes the optional args. This is on purpose, because different args can potentially mean a different file.

If we write our own file format plugin, we can also pass in these args via attributes, but only non animated.

# Add code to modify the stage.
# Use drop down menu to select examples.
#### Low Level ####
# Get: 'identifier', 'resolvedPath', 'realPath', 'fileExtension' 
# Set: 'identifier'
## Helper functions:
# Get: 'GetFileFormat', 'GetFileFormatArguments', 'ComputeAbsolutePath'
# Create: 'CreateIdentifier', 'SplitIdentifier'
### Anoymous identifiers
# Get: 'anonymous'
## Helper functions:
# Get: 'IsAnonymousLayerIdentifier'
import os
from pxr import Sdf
## Anonymous layers
layer = Sdf.Layer.CreateAnonymous() 
print(layer.identifier) # Returns: anon:0x7f8a1040ba80
layer = Sdf.Layer.CreateAnonymous("myCustomAnonLayer")
print(layer.identifier) # Returns: anon:0x7f8a10498500:myCustomAnonLayer
print(layer.anonymous, layer.resolvedPath or "-", layer.realPath or "-", layer.fileExtension) # Returns: True, "-", "-", "sdf"
print(Sdf.Layer.IsAnonymousLayerIdentifier(layer.identifier)) # Returns True
## Standard layers
layer.identifier = "/my/cool/file/path/example.usd"
print(layer.anonymous, layer.resolvedPath or "-", layer.realPath or "-", layer.fileExtension)
# Returns: False, "/my/cool/file/path/example.usd", "/my/cool/file/path/example.usd", "usd"
# When accesing an identifier string, we should always split it for args to get the URI
layer_uri, layer_args = layer.SplitIdentifier(layer.identifier)
print(layer_uri, layer_args) # Returns: "/my/cool/file/path/example.usd", {}
layer_identifier = layer.CreateIdentifier("/dir/file.usd", {"argA": "1", "argB": "test"})
print(layer_identifier) # Returns: "/dir/file.usd:SDF_FORMAT_ARGS:argA=1&argB=test"
layer_uri, layer_args = layer.SplitIdentifier(layer_identifier)
print(layer_uri, layer_args) # Returns: "/dir/file.usd", {'argA': '1', 'argB': 'test'}
# CreateNew requires the file path to be writable
layer_file_path = os.path.expanduser("~/Desktop/layer_identifier_example.usd")
layer = Sdf.Layer.CreateNew(layer_file_path, {"myCoolArg": "test"})
print(layer.GetFileFormat()) # Returns: Instance of Sdf.FileFormat
print(layer.GetFileFormatArguments()) # Returns: {'myCoolArg': 'test'}
# Get the actuall resolved path (from our asset resolver):
print(layer.identifier, layer.realPath, layer.fileExtension) # Returns: "~/Desktop/layer_identifier_example.usd" "~/Desktop/layer_identifier_example.usd" "usd"
# This is the same as:
print(layer.identifier, layer.resolvedPath.GetPathString())
# Compute a file relative to the directory of the layer identifier
print(layer.ComputeAbsolutePath("./some/other/file.usd")) # Returns: ~/Desktop/some/other/file.usd

Layers Creation/Import/Export

Here is an overview of how we can create layers:

Pro Tip | Saving and Reloading

  • We can call layer.Save() multiple times with the USD binary format (.usd/.usdc). This will then dump the content from memory to disk in "append" mode. This avoids building up huge memory footprints when creating large layers.
  • Calling layer.Reload() on anonymous layers clears their content (destructively). So make sure you can really dispose of it as there is no undo method.
  • To reload all (composition) related layers, we can use stage.Reload(). This calls layer.Reload() on all used stage layers.
  • Calling layer.Reload() consults the result of layer.GetExternalAssetDependencies(). These return non USD/composition related external files, that influence the layer. This is only relevant when using non USD file formats.
### Low Level ###
# Create: 'New', 'CreateNew', 'CreateAnonymous', 
# Get: 'Find','FindOrOpen', 'OpenAsAnonymous',  'FindOrOpenRelativeToLayer', 'FindRelativeToLayer',
# Set: 'Save', 'TransferContent', 'Import', 'ImportFromString', 'Export', 'ExportToString'
# Clear: 'Clear', 'Reload', 'ReloadLayers'
# See all open layers: 'GetLoadedLayers'
from pxr import Sdf
layer = Sdf.Layer.CreateAnonymous()
## The .CreateNew command will check if the layer is saveable at the file location and create an empty file.
layer_file_path = os.path.expanduser("~/Desktop/layer_identifier_example.usd")
layer = Sdf.Layer.CreateNew(layer_file_path)
print(layer.dirty) # Returns: False
## Our layers are marked as "dirty" (edited) as soon as we make an edit.
prin_spec = Sdf.CreatePrimInLayer(layer, Sdf.Path("/pig"))
print(layer.dirty) # Returns: True
layer.Save()
# Only edited (dirty) layers are saved, when layer.Save() is called
# Only edited (dirty) layers are reloaded, when layer.Reload(force=False) is called.
# Our layer.Save() and layer.Reload() methods also take an optional "force" arg.
# This forces the layer to be saved. We can also call layer.Save() multiple times,
# with the USD binary format (.usd/.usdc). This will then dump the content from memory
# to disk in "append" mode. This avoids building up huge memory footprints when
# creating large layers.
## We can also transfer layer contents:
other_layer = Sdf.Layer.CreateAnonymous()
layer.TransferContent(other_layer)
# Or we import the content from another layer
layer.Import(layer_file_path)
# This is the same as:
layer.TransferContent(layer.FindOrOpen((layer_file_path)))
# We can also import/export to USD ascii representations,
# this is quite usefull for debugging and inspecting the active layer.
# layer.ImportFromString(other_layer.ExportAsString())
layer = Sdf.Layer.CreateAnonymous()
prin_spec = Sdf.CreatePrimInLayer(layer, Sdf.Path("/pig"))
print(layer.ExportToString())
# Returns:
"""
#sdf 1.4.32
over "pig"
{
}
"""
# We can also remove all prims that don't have properties/metadata
# layer.RemoveInertSceneDescription()

Dependencies

We can also query the layer dependencies of a layer.

Pro Tip | Inspecting dependencies

The most important methods are:

  • layer.GetCompositionAssetDependencies(): This gets layer identifiers of sublayer/reference/payload composition arcs. This is only for the active layer, it does not run recursively.
  • layer.UpdateCompositionAssetDependency("oldIdentifier", "newIdentifier"): The allows us to remap any sublayer/reference/payload identifier in the active layer, without having to edit the list-editable ops ourselves. Calling layer.UpdateCompositionAssetDependency("oldIdentifier", "") removes a layer.

In our example below, we assume that the code is run in Houdini.

### Low Level ###
# Get: 'GetCompositionAssetDependencies', 'GetAssetInfo', 'GetAssetName', 'GetExternalAssetDependencies',
# Set: 'UpdateCompositionAssetDependency', 'UpdateExternalReference', 'UpdateAssetInfo'
import os
from pxr import Sdf
HFS_env = os.environ["HFS"]
layer = Sdf.Layer.FindOrOpen(os.path.join(HFS_env, "houdini","usd","assets","pig","payload.usdc"))
# Get all sublayer, reference and payload files (These are the only arcs that can load files)
print(layer.GetCompositionAssetDependencies()) # Returns: ['./geo.usdc', './mtl.usdc']
# print(layer.GetExternalReferences(), layer.externalReferences) # The same thing, deprecated method.
# Get external dependencies for non USD file formats. We don't use this with USD files.
print(layer.GetExternalAssetDependencies()) # Returns: [] 
# Get layer asset info. Our asset resolver has to custom implement this.
# A common use case might be to return database related side car data.
print(layer.GetAssetName()) # Returns: None
print(layer.GetAssetInfo()) # Returns: None
layer.UpdateAssetInfo() # Re-resolve/refresh the asset info. This just force requeries our asset resolver query.
# The perhaps most powerful method for dependencies is:
layer.UpdateCompositionAssetDependency("oldIdentifier", "newIdentifier")
# This allows us to repath any composition arc (sublayer/reference/payload) to a new file in the active layer.
# Calling layer.UpdateCompositionAssetDependency("oldIdentifier", ""), will remove the identifier from the
# list-editable composition arc ops.

Now there are also utility functions available in the UsdUtils module (USD Docs):

  • UsdUtils.ExtractExternalReferences: This is similar to layer.GetCompositionAssetDependencies(), except that it returns three lists: [<sublayers>], [<references>], [<payloads>]. It also consults the assetInfo metadata, so result might be more "inclusive" than layer.GetCompositionAssetDependencies().
  • UsdUtils.ComputeAllDependencies: This recursively calls layer.GetCompositionAssetDependencies() and gives us the aggregated result.
  • UsdUtils.ModifyAssetPaths: This is similar to Houdini's output processors. We provide a function that gets the input path and returns a (modified) output path.

Layer Metrics

We can also set animation/time related metrics, these are stored via metadata entries on the layer itself.

(
    timeCodesPerSecond = 24
    framesPerSecond = 24
    startTimeCode = 1
    endTimeCode = 240
    metersPerUnit = 0.01
    upAxis = "Z"
)

As this is handled via metadata, we cover it in detail our Animation (Time related metrics), Scene Unit Scale/UpAxis - FAQ and Metadata sections.

The metersPerUnit and upAxis are only intent hints, it is up to the application/end user to correctly interpret the data and change it accordingly.

The time related metrics should be written into all layers, as we can then use them to quickly inspect time related data in the file without having to fully parse it.

Permissions

We can lock a layer to not have editing or save permissions. Depending on the DCC, this is automatically done for your depending on how you access the stage, some applications leave this up to the user though.

Anonymous layers can't be saved to disk, therefore for them layer.permissionToSave is always False.

### Low Level ###
# Get: 'permissionToEdit', 'SetPermissionToEdit'
# Set: 'permissionToSave', 'SetPermissionToSave'
import os
from pxr import Sdf
layer = Sdf.Layer.CreateAnonymous()
print("Can edit layer", layer.permissionToEdit) # Returns: True
Sdf.CreatePrimInLayer(layer, Sdf.Path("/bicycle"))
# Edit permission
layer.SetPermissionToEdit(False)
try: 
    # This will now raise an error
    Sdf.CreatePrimInLayer(layer, Sdf.Path("/car"))
except Exception as e:
    print(e)
layer.SetPermissionToEdit(True)
# Save permission
print("Can save layer", layer.permissionToSave) # Returns: False
try:
    # This fails as we can't save anoymous layers
    layer.Save()
except Exception as e:
    print(e)
# Changing this on anoymous layers doesn't work
layer.SetPermissionToSave(True)
print("Can save layer", layer.permissionToSave) # Returns: False
# If we change the identifer to not be an anonymous identifer, we can save it.
layer.identifier = os.path.expanduser("~/Desktop/layerPermission.usd")
print("Can save layer", layer.permissionToSave) # Returns: True

Muting

Muting layers can be done globally on the layer itself or per stage via stage.MuteLayer(layer.identifier)/stage.UnmuteLayer(layer.identifier). When doing it globally on the layer, it affects all stages that use the layer. This is also why the mute method is not exposed on a layer instance, instead we call it on the Sdf.Layer class, as we modify muting on the singleton.

More info on this topic in our loading data section.

### Low Level ###
# Get: 'IsMuted', 'GetMutedLayers'
# Set: 'AddToMutedLayers', 'RemoveFromMutedLayers'
from pxr import Sdf
layer = Sdf.Layer.CreateAnonymous()
print(Sdf.Layer.IsMuted(layer)) # Returns: False
Sdf.Layer.AddToMutedLayers(layer.identifier)
print(Sdf.Layer.GetMutedLayers()) # Returns: ['anon:0x7f8a1098f100']
print(Sdf.Layer.IsMuted(layer)) # Returns: True
Sdf.Layer.RemoveFromMutedLayers(layer.identifier)
print(Sdf.Layer.IsMuted(layer)) # Returns: False

Composition

All composition arcs, excepts sublayers, are created on prim(specs). Here is how we edit sublayers (and their Sdf.LayerOffsets) on Sdf.Layers:

# For sublayering we modify the .subLayerPaths attribute on a layer.
# This is the same for both the high and low level API.
### High Level & Low Level ###
from pxr import Sdf, Usd
stage = Usd.Stage.CreateInMemory()
# Layer onto root layer
layer_a = Sdf.Layer.CreateAnonymous()
layer_b = Sdf.Layer.CreateAnonymous()
root_layer = stage.GetRootLayer()
# Here we pass in the file paths (=layer identifiers).
root_layer.subLayerPaths.append(layer_a.identifier)
root_layer.subLayerPaths.append(layer_b.identifier)
# Once we have added the sublayers, we can also access their layer offsets:
print(root_layer.subLayerOffsets) # Returns: [Sdf.LayerOffset(), Sdf.LayerOffset()]
# Since layer offsets are ready only copies, we need to assign a newly created 
# layer offset if we want to modify them. We also can't replace the whole list, as
# it needs to keep a pointer to the array.
layer_offset_a = root_layer.subLayerOffsets[0]
root_layer.subLayerOffsets[0] = Sdf.LayerOffset(offset=layer_offset_a.offset + 10, 
                                                scale=layer_offset_a.scale * 2)
layer_offset_b = root_layer.subLayerOffsets[1]
root_layer.subLayerOffsets[1] = Sdf.LayerOffset(offset=layer_offset_b.offset - 10, 
                                                scale=layer_offset_b.scale * 0.5)
print(root_layer.subLayerOffsets) # Returns: [Sdf.LayerOffset(10, 2), Sdf.LayerOffset(-10, 0.5)]

# If we want to sublayer on the active layer, we just add it there.
layer_c = Sdf.Layer.CreateAnonymous()
active_layer = stage.GetEditTarget().GetLayer()
root_layer.subLayerPaths.append(layer_c.identifier)

For more info on composition arcs (especially the sublayer arc) see our Composition section.

Default Prim

As discussed in more detail in our composition section, the default prim specifies the default root prim to import via reference and payload arcs. If it is not specified, the first prim in the layer is used, that is not abstract (not a prim with a class specifier) and that is defined (has a Sdf.SpecifierDef define specifier), unless we specify them explicitly. We cannot specify nested prim paths, the path must be in the root (Sdf.Path("/example").IsRootPrimPath() must return True), setting an invalid path will not error, but it will not working when referencing/payloading the file.

We typically use this in asset layers to specify the root prim that is the asset.

### Low Level ###
# Has: 'HasDefaultPrim'
# Get: 'defaultPrim'
# Set: 'defaultPrim'
# Clear: 'ClearDefaultPrim'
from pxr import Sdf
layer = Sdf.Layer.CreateAnonymous()
print(layer.defaultPrim) # Returns ""
layer.defaultPrim = "example"
# While we can set it to "/example/path",
# references and payloads won't use it.

Traversal and Prim/Property Access

Traversing and accessing prims/properties works a tad different:

  • The layer.Get<SpecType>AtPath methods return Sdf.Spec objects (Sdf.PrimSpec, Sdf.AttributeSpec, Sdf.RelationshipSpec) and not USD high level objects.
  • The traverse method doesn't return an iterable range, instead it is "kernel" like. We pass it a function that each path in the layer gets run through.
### Low Level ###
# Properties: 'pseudoRoot', 'rootPrims', 'empty'
# Get: 'GetObjectAtPath', 'GetPrimAtPath', 'GetPropertyAtPath', 'GetAttributeAtPath', 'GetRelationshipAtPath',
# Traversal: 'Traverse',
from pxr import Sdf
layer = Sdf.Layer.CreateAnonymous()
# Check if a layer actually has any content:
print(layer.empty) # Returns: True
print(layer.pseudoRoot) # The same as layer.GetPrimAtPath("/")
# Define prims
bicycle_prim_spec = Sdf.CreatePrimInLayer(layer, Sdf.Path("/set/yard/bicycle"))
person_prim_spec = Sdf.CreatePrimInLayer(layer, Sdf.Path("/characters/mike"))
print(layer.rootPrims) # Returns: {'set': Sdf.Find('anon:0x7ff9f8ad7980', '/set'),
                       #           'characters': Sdf.Find('anon:0x7ff9f8ad7980', '/characters')}
# The GetObjectAtPath method gives us prim/attribute/relationship specs, based on what is at the path
attr_spec = Sdf.AttributeSpec(bicycle_prim_spec, "tire:size", Sdf.ValueTypeNames.Float)
attr_spec.default = 10
rel_sec = Sdf.RelationshipSpec(person_prim_spec, "vehicle")
rel_sec.targetPathList.Append(Sdf.Path(bicycle_prim_spec.path))
print(type(layer.GetObjectAtPath(attr_spec.path))) # Returns: <class 'pxr.Sdf.AttributeSpec'>
print(type(layer.GetObjectAtPath(rel_sec.path))) # Returns: <class 'pxr.Sdf.RelationshipSpec'>
# Traversals work differently compared to stages.
def traversal_kernel(path):
    print(path)
layer.Traverse(layer.pseudoRoot.path, traversal_kernel)
print("---")
# Returns:
"""
/set/yard/bicycle.tire:size
/set/yard/bicycle
/set/yard
/set
/characters/mike.vehicle[/set/yard/bicycle]
/characters/mike.vehicle
/characters/mike
/characters
/
"""
# As we can see, it traverses all path related fields, even relationships, as these map Sdf.Paths.
# The Sdf.Path object is used as a "filter", rather than the Usd.Prim object.
def traversal_kernel(path):
    if path.IsPrimPath():
        print(path) 
layer.Traverse(layer.pseudoRoot.path, traversal_kernel)
print("---")
""" Returns:
/set/yard/bicycle
/set/yard
/set
/characters/mike
/characters
"""
def traversal_kernel(path):
    if path.IsPrimPropertyPath():
        print(path) 
layer.Traverse(layer.pseudoRoot.path, traversal_kernel)
print("---")
""" Returns:
/set/yard/bicycle.tire:size
/characters/mike.vehicle
"""
tire_size_attr_spec = attr_spec
tire_diameter_attr_spec = Sdf.AttributeSpec(bicycle_prim_spec, "tire:diameter", Sdf.ValueTypeNames.Float)
tire_diameter_attr_spec.connectionPathList.explicitItems = [tire_size_attr_spec.path]
def traversal_kernel(path):
    if path.IsTargetPath():
        print(">> IsTargetPath", path) 
layer.Traverse(layer.pseudoRoot.path, traversal_kernel)
""" Returns:
IsTargetPath /set/yard/bicycle.tire:diameter[/set/yard/bicycle.tire:size]
IsTargetPath /characters/mike.vehicle[/set/yard/bicycle]
"""

Time Samples

In the high level API, reading and writing time samples is handled via the attribute.Get()/Set() methods. In the lower level API, we use the methods exposed on the layer.

### Low Level ###
# Get: 'QueryTimeSample', 'ListAllTimeSamples', 'ListTimeSamplesForPath', 'GetNumTimeSamplesForPath', 
#      'GetBracketingTimeSamples', 'GetBracketingTimeSamplesForPath',
# Set: 'SetTimeSample', 
# Clear: 'EraseTimeSample',
from pxr import Sdf
layer = Sdf.Layer.CreateAnonymous()
prim_path = Sdf.Path("/bicycle")
prim_spec = Sdf.CreatePrimInLayer(layer, prim_path)
attr_spec = Sdf.AttributeSpec(prim_spec, "size", Sdf.ValueTypeNames.Double)
for frame in range(1001, 1005):
    value = float(frame - 1001)
    # .SetTimeSample() takes args in the .SetTimeSample(<path>, <frame>, <value>) format
    layer.SetTimeSample(attr_spec.path, frame, value)
print(layer.QueryTimeSample(attr_spec.path, 1005)) # Returns: 4
print(layer.ListTimeSamplesForPath(attr_spec.path)) # Returns: [1001.0, 1002.0, 1003.0, 1004.0]
attr_spec = Sdf.AttributeSpec(prim_spec, "width", Sdf.ValueTypeNames.Float)
layer.SetTimeSample(attr_spec.path, 50, 150)
print(layer.ListAllTimeSamples()) # Returns: [50.0, 1001.0, 1002.0, 1003.0, 1004.0]
# A typicall thing we can do is set the layer time metrics:
time_samples = layer.ListAllTimeSamples()
layer.startTimeCode = time_samples[0]
layer.endTimeCode = time_samples[-1]

See our animation section for more info about how to deal with time samples.

Metadata

Layers, like prims and properties, can store metadata. Apart from the above mentioned layer metrics, we can store custom metadata in the customLayerData key or create custom metadata root keys as discussed in our metadata plugin section. This can be used to track important pipeline related data without storing it on a prim.

from pxr import Usd, Sdf
layer = Sdf.Layer.CreateAnonymous()
layer.customLayerData = {"myCustomPipelineKey": "myCoolValue"}

See our Metadata section for detailed examples for layer and stage metadata.

Stages

Stages offer a view on a set of composed layers. We cover composition in its on section, as it is a complicated topic.

flowchart LR
    stage(Stage) --> layerRoot(Root Layer)
    layerRoot -- Sublayer --> layer1([Layer])
    layer1 -- Payload --> layer1Layer1([Layer])
    layer1 -- Sublayer--> layer1Layer2([Layer])
    layerRoot -- Sublayer --> layer2([Layer])
    layer2 -- Reference --> layer2Layer1([Layer])
    layer2Layer1 -- Payload --> layer2Layer1Layer1([Layer])
    layer2 -- Payload --> layer2Layer2([Layer])
    layerRoot -- Composition Arc --> layer3([...])
    layer3 -- Composition Arc --> layer3Layer1([...])

Unlike layers, stages are not managed via a singleton. There is the Usd.StageCache class though, that would provide a similar mechanism. We usually don't use this though, as our DCCs manage the lifetime cycle of our stages.

If a stage goes out of scope in our code, it will be deleted. Should we still have access the to Python object, we can check if it actually points to a valid layer via the stage.expired property.

When creating a stage we have two layers by default:

  • Session Layer: This is a temp layer than doesn't get applied on disk save. Here we usually put things like viewport overrides.
  • Root Layer: This is the base layer all edits target by default. We can add sublayers based on what we need to it. When calling stage.Save(), all sublayers that are dirty and not anonymous, will be saved.

Configuration

Let's first look at some configuration related options we can set on the stage.

Asset Resolver

The stage can be opened with a asset resolver context. The context needs to be passed in on stage open, it can be refreshed afterwards (if implemented in the resolver). The resolver context object itself is bound to the runtime of the the stage though. The asset resolver context is just a very simple class, that our custom asset resolver can attach data to to help with path resolution.

In terms of asset resolution there are only two methods exposed on the stage class:

  • stage.GetPathResolverContext(): Get the resolver context object.
  • stage.ResolveIdentifierToEditTarget(): Resolve an asset identifier using the stage's resolver context.

We cover how to use these in our asset resolver section, where we also showcase asset resolver reference implementations that ship with this guide.

Stage Metrics

As discussed in the above layer metrics section, we can set animation/time related metrics. The stage class also exposes methods to do this, which just set the metadata entries on the root layer of the stage.

The time related metrics should be written into all layers, as we can then use them to quickly inspect time related data in the file without having to fully parse it.

We cover it in detail our Animation (Time related metrics), Scene Unit Scale/UpAxis - FAQ and Metadata sections.

Stage Time Sample Interpolation

We can set how time samples are interplated per stage.

The possible stage interpolation types are:

  • Usd.InterpolationTypeLinear: Interpolate linearly (if array length doesn't change and data type allows it))
  • Usd.InterpolationTypeHeld: Hold until the next time sample
from pxr import Usd
stage = Usd.Stage.CreateInMemory()
print(stage.GetInterpolationType()) # Returns: Usd.InterpolationTypeLinear
stage.SetInterpolationType(Usd.InterpolationTypeHeld)

Checkout our animation section for more info on how animation and time samples are treated in USD.

Variant/Prim Type Fallbacks

We can also provide fallback values for:

Color Management

Still under construction!

This sub-section is still under development, it is subject to change and needs extra validation.

# Get: 'GetColorConfiguration', 'GetColorManagementSystem', 'GetColorConfigFallbacks'
# Set: 'SetColorConfiguration', 'SetColorManagementSystem', 'SetColorConfigFallbacks'

Metadata

Setting metadata on the stage, redirects the edits to the root layer. We discuss this in detail in our metadata section.

from pxr import Usd, Sdf
stage = Usd.Stage.CreateInMemory()
bicycle_prim = stage.DefinePrim("/bicycle")
stage.SetMetadata("customLayerData", {"myCustomStageData": 1})
# Is the same as:
layer = stage.GetRootLayer()
metadata = layer.customLayerData
metadata["myCustomRootData"] = 1
layer.metadata = metadata
# As with layers, we can also set the default prim
stage.SetDefaultPrim(bicycle_prim)
# Is the same as:
layer.defaultPrim = "bicycle"

Composition

We cover in detail how to inspect composition in our composition section.

Stages offer access to the Prim Cache Population cache via stage._GetPcpCache(). We almost never interact with it this way, instead we use the methods dicussed in our inspecting composition section.

We also have access to our instanced prototypes, for more info on what these are and how they can be inspected/used see our composition instanceable prims section.

Lastly we control the edit target via the stage. The edit target defines, what layer all calls in the high level API should write to.

When starting out with USD, you'll mostly be using it in the form of:

stage.SetEditTarget(layer)
# We can also explicitly create the edit target:
# Or
stage.SetEditTarget(Usd.EditTarget(layer))
# Or
stage.SetEditTarget(stage.GetEditTargetForLocalLayer(layer))
# These all have the same effect.

In Houdini we don't have to manage this, it is always the highest layer in the active layer stack. Houdini gives it to us via hou.node.activeLayer() or node.editableLayerin python LOP nodes.

More info about edit targets in our composition fundamentals section.

Loading mechanisms

Stages are the controller of how our Prim Cache Population (PCP) cache loads our composed layers. We cover this in detail in our Traversing/Loading Data section. 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.

Stages control:

  • Layer Muting: This controls what layers are allowd 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.

Stage Layer Management (Creation/Save/Export)

When creating a stage we have two layers by default:

  • Session Layer: This is a temp layer than doesn't get applied on disk save. Here we usually put things like viewport overrides.
  • Root Layer: This is the base layer all edits target by default. We can add sublayers based on what we need to it. When calling stage.Save(), all sublayers that are dirty and not anonymous, will be saved.

Let's first look at layer access, there are two methods of special interest to us:

  • stage.GetLayerStack(): Get all the layers in the active layer stack
  • stage.GetUsedLayers(includeClipLayers=True): Get all layers that are currently used by the stage. We can optionally exclude value clip layers. This is only a snapshot, as layers might be "varianted" away or in the case of value clips, we only get the active chunk file.

The following example is run in Houdini:

import os
from pxr import Sdf, Usd, UsdUtils
stage = Usd.Stage.CreateInMemory()
root_layer = stage.GetRootLayer()
# Create sublayers with references
bottom_layer_file_path = os.path.expanduser("~/Desktop/layer_bottom.usda")
bottom_layer = Sdf.Layer.CreateNew(bottom_layer_file_path)
top_layer_file_path = os.path.expanduser("~/Desktop/layer_top.usda")
top_layer = Sdf.Layer.CreateNew(top_layer_file_path)
root_layer.subLayerPaths.append(top_layer_file_path)
top_layer.subLayerPaths.append(bottom_layer_file_path)
stage.SetEditTarget(top_layer)
prim = stage.DefinePrim(Sdf.Path("/pig_1"), "Xform")
prim.GetReferences().AddReference("/opt/hfs19.5/houdini/usd/assets/pig/pig.usd", "/pig")
stage.SetEditTarget(bottom_layer)
prim = stage.DefinePrim(Sdf.Path("/pig_1"), "Xform")
prim.GetReferences().AddReference("/opt/hfs19.5/houdini/usd/assets/rubbertoy/rubbertoy.usd", "/rubbertoy")
# Save
stage.Save()
# Layer stack
print(stage.GetLayerStack(includeSessionLayers=False)) 
""" Returns:
[Sdf.Find('anon:0x7ff9f47b9600:tmp.usda'),
Sdf.Find('/home/lucsch/Desktop/layer_top.usda'),
Sdf.Find('/home/lucsch/Desktop/layer_bottom.usda')]
""" 
layers = set()
for layer in stage.GetLayerStack(includeSessionLayers=False):
    layers.add(layer)
    layers.update([Sdf.Layer.FindOrOpen(i) for i in layer.GetCompositionAssetDependencies()])
print(list(layers))
""" Returns:
[Sdf.Find('/opt/hfs19.5/houdini/usd/assets/rubbertoy/rubbertoy.usd'), 
Sdf.Find('/home/lucsch/Desktop/layer_top.usda'),
Sdf.Find('anon:0x7ff9f5677180:tmp.usda'),
Sdf.Find('/opt/hfs19.5/houdini/usd/assets/pig/pig.usd'),
Sdf.Find('/home/lucsch/Desktop/layer_bottom.usda')]
"""

As you might have noticed, when calling stage.GetLayerStack(), we didn't get the pig reference. Let's have a look how we can get all composition arc layers of the active layer stack:

import os
from pxr import Sdf, Usd, UsdUtils
stage = Usd.Stage.CreateInMemory()
root_layer = stage.GetRootLayer()
# Create sublayers with references
bottom_layer_file_path = os.path.expanduser("~/Desktop/layer_bottom.usda")
bottom_layer = Sdf.Layer.CreateNew(bottom_layer_file_path)
top_layer_file_path = os.path.expanduser("~/Desktop/layer_top.usda")
top_layer = Sdf.Layer.CreateNew(top_layer_file_path)
root_layer.subLayerPaths.append(top_layer_file_path)
top_layer.subLayerPaths.append(bottom_layer_file_path)
stage.SetEditTarget(top_layer)
prim = stage.DefinePrim(Sdf.Path("/pig_1"), "Xform")
prim.GetReferences().AddReference("/opt/hfs19.5/houdini/usd/assets/pig/pig.usd", "/pig")
stage.SetEditTarget(bottom_layer)
prim = stage.DefinePrim(Sdf.Path("/pig_1"), "Xform")
prim.GetReferences().AddReference("/opt/hfs19.5/houdini/usd/assets/rubbertoy/rubbertoy.usd", "/rubbertoy")
# Save
stage.Save()
# Layer stack
print(stage.GetLayerStack(includeSessionLayers=False)) 
""" Returns:
[Sdf.Find('anon:0x7ff9f47b9600:tmp.usda'),
Sdf.Find('/home/lucsch/Desktop/layer_top.usda'),
Sdf.Find('/home/lucsch/Desktop/layer_bottom.usda')]
""" 
layers = set()
for layer in stage.GetLayerStack(includeSessionLayers=False):
    layers.add(layer)
    layers.update([Sdf.Layer.FindOrOpen(i) for i in layer.GetCompositionAssetDependencies()])
print(list(layers))
""" Returns:
[Sdf.Find('/opt/hfs19.5/houdini/usd/assets/rubbertoy/rubbertoy.usd'), 
Sdf.Find('/home/lucsch/Desktop/layer_top.usda'),
Sdf.Find('anon:0x7ff9f5677180:tmp.usda'),
Sdf.Find('/opt/hfs19.5/houdini/usd/assets/pig/pig.usd'),
Sdf.Find('/home/lucsch/Desktop/layer_bottom.usda')]
"""

If you are confused what a layer stack, check out our composition layer stack section for a detailed breakdown.

Let's have a look at stage creation and export:

### High Level ###
# Create: 'CreateNew', 'CreateInMemory', 'Open', 'IsSupportedFile', 
# Set: 'Save', 'Export', 'Flatten', 'ExportToString', 'SaveSessionLayers',
# Clear: 'Reload'
import os
from pxr import Sdf, Usd
# The stage.CreateNew has multiple method signatures, these take:
# - stage root layer identifier: The stage.GetRootLayer().identifier, this is where your stage get's saved to.
# - session layer (optional): We can pass in an existing layer to use as an session layer.
# - asset resolver context (optional): We can pass in a resolver context to aid path resolution. If not given, it will call
#                                      ArResolver::CreateDefaultContextForAsset() on our registered resolvers.
# - The initial payload loading mode: Either Usd.Stage.LoadAll or Usd.Stage.LoadNone
stage_file_path = os.path.expanduser("~/Desktop/stage_identifier_example.usda")
stage = Usd.Stage.CreateNew(stage_file_path)
# The stage creation will create an empty USD file at the specified path.
print(stage.GetRootLayer().identifier) # Returns: /home/lucsch/Desktop/stage_identifier_example.usd
prim = stage.DefinePrim(Sdf.Path("/bicycle"), "Xform")
# We can also create a stage in memory, this is the same as Sdf.Layer.CreateAnonymous() and using it as a root layer
# stage = Usd.Stage.CreateInMemory("test")
# Or:
layer = Sdf.Layer.CreateAnonymous()
stage.Open(layer.identifier)
## Saving
# Calling stage.Save(), calls layer.Save() on all dirty layers that contribute to the stage.
stage.Save()
# The same as:
for layer in stage.GetLayerStack(includeSessionLayers=False):
    if not layer.anonymous and layer.dirty:
        layer.Save()
# Calling stage.SaveSessionLayers() will also save all session layers, that are not anonymous
## Flatten
# We can also flatten our layers of the stage. This merges all the data, so it should be used with care,
# as it will likely flood your RAM with large scenes. It removes all composition arcs and returns a single layer
# with the combined result
stage = Usd.Stage.CreateInMemory()
root_layer = stage.GetRootLayer()
prim = stage.DefinePrim(Sdf.Path("/bicycle"), "Xform")
sublayer = Sdf.Layer.CreateAnonymous()
root_layer.subLayerPaths.append(sublayer.identifier)
stage.SetEditTarget(sublayer)
prim = stage.DefinePrim(Sdf.Path("/car"), "Xform")
print(root_layer.ExportToString())
"""Returns:
#usda 1.0
(
    subLayers = [
        @anon:0x7ff9f5fed300@
    ]
)

def Xform "bicycle"
{
}
"""
flattened_result = stage.Flatten()
print(flattened_result.ExportToString())
"""Returns:
#usda 1.0
(
)

def Xform "car"
{
}

def Xform "bicycle"
{
}
"""
## Export:
# The export command calls the same thing we just did
# layer = stage.Flatten()
# layer.Export("/myFilePath.usd")
print(stage.ExportToString()) # Returns: The same thing as above
## Reload:
stage.Reload()
# The same as:
for layer in stage.GetUsedLayers():
    # !IMPORTANT! This does not check if the layer is anonymous,
    # so you will lose all your anon layer content.
    layer.Reload()
    # Here is a saver way:
    if not layer.anonymous:
        layer.Reload()

Traversal and Prim/Property Access

USD stage traversing and accessing prims/properties works via the high level API.

  • The stage.Get<SpecType>AtPath methods return Usd.Object objects (Usd.Prim, Usd.Attribute, Usd.Relationship).
  • The traverse method returns an iterable that goes through the prims in the stage.

We cover stage traversals in full detail in our Traversing/Loading Data (Purpose/Visibility/Activation/Population) section.

Here are the basics:

### High Level ###
# Get: 'GetPseudoRoot', 'GetObjectAtPath',
#      'GetPrimAtPath', 'GetPropertyAtPath','GetAttributeAtPath', 'GetRelationshipAtPath',
# Set: 'DefinePrim', 'OverridePrim', 'CreateClassPrim', 'RemovePrim'
# Traversal: 'Traverse','TraverseAll'
from pxr import Sdf, Usd, UsdUtils
stage = Usd.Stage.CreateInMemory()
# Define and change specifier
stage.DefinePrim("/changedSpecifier/definedCube", "Cube").SetSpecifier(Sdf.SpecifierDef)
stage.DefinePrim("/changedSpecifier/overCube", "Cube").SetSpecifier(Sdf.SpecifierOver)
stage.DefinePrim("/changedSpecifier/classCube", "Cube").SetSpecifier(Sdf.SpecifierClass)
# Or create with specifier
stage.DefinePrim("/createdSpecifier/definedCube", "Cube")
stage.OverridePrim("/createdSpecifier/overCube")
stage.CreateClassPrim("/createdSpecifier/classCube")
# Create attribute
prim = stage.DefinePrim("/bicycle")
prim.CreateAttribute("tire:size", Sdf.ValueTypeNames.Float)
# Get the pseudo root prim at "/"
pseudo_root_prim = stage.GetPseudoRoot()
# Or:
pseudo_root_prim = stage.GetPrimAtPath("/")
# Traverse:
for prim in stage.TraverseAll():
    print(prim)
""" Returns:
Usd.Prim(</changedSpecifier>)
Usd.Prim(</changedSpecifier/definedCube>)
Usd.Prim(</changedSpecifier/overCube>)
Usd.Prim(</changedSpecifier/classCube>)
Usd.Prim(</createdSpecifier>)
Usd.Prim(</createdSpecifier/definedCube>)
Usd.Prim(</createdSpecifier/overCube>)
Usd.Prim(</createdSpecifier/classCube>)
"""
# Get Prims/Properties
# The GetObjectAtPath returns the entity requested by the path (prim/attribute/relationship)
prim = stage.GetObjectAtPath(Sdf.Path("/createdSpecifier"))
prim = stage.GetPrimAtPath(Sdf.Path("/changedSpecifier"))
attr = stage.GetObjectAtPath(Sdf.Path("/bicycle.tire:size"))
attr = stage.GetAttributeAtPath(Sdf.Path("/bicycle.tire:size"))