Attributes

As we’ve already stated there are two kind of properties: attributes and relationships.

Let’s get back to /World/Cube:size and similar attributes that we’ve quickly covered in the previous sections and add a few more words on how to access them via code.

Attributes can be provided via a schema or be user-defined (to be precise even relationships can be schema-provided or user-defined, therefore it is more correct to state that properties can either be provided via a schema or be user-defined).

Tip

We will revisit schemas later, for now it suffices to think of them as “inherited prim rules to define which properties are present on a prim which implements that schema, and what values are allowed for those properties” E.g. a faceVertexIndices attribute for a prim which implements the UsdGeomMesh schema must have type VtArray<int>, i.e. be an array of integer values. This type constraint (and the existence of that value which must be present) is enforced by the UsdGeomMesh schema definition.

In the UsdGeomCube schema, the attribute size is schema-provided and can be accessed in code as follows

cube : UsdGeom.Cube = UsdGeom.Cube.Define(root_stage, "/World/Cube")
attr = cube.GetSizeAttr()
attr_value = attr.Get()
attr.Set(200)  # change attribute value

in python USD there are always getters for attributes defined in schemas of the form .Get[ATTRIBUTENAME]Attr() and they return a UsdAttribute whose value can be extracted via Get() and set via Set() as shown in the code above. For time-varying attributes you can also query which timecode you’re interested in: .Get(Usd.TimeCode.EarliestTime()). Whether an attribute is time-varying can be queried via GetVariability().

Let’s see an example of a cube prim where a custom attribute is checked for existence in various ways and created/set.

from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf
import omni.usd
import carb

BASE_DIRECTORY = "/tmp"  # This is where the .usda files will be saved

root_stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")

# Add a cube to the scene

xform : UsdGeom.Xform = UsdGeom.Xform.Define(root_stage, Sdf.Path("/World"))
cube : UsdGeom.Cube = UsdGeom.Cube.Define(root_stage, "/World/Cube")
extent = [(-50, -50, -50), (50, 50, 50)]
cube.GetExtentAttr().Set(extent)
cube.GetSizeAttr().Set(100)
environment_xform = UsdGeom.Xform.Define(root_stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(root_stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)

cube_prim : Usd.Prim = cube.GetPrim()

# Check if a custom myAttribute attribute already exists
my_custom_attr : Usd.Attribute = cube_prim.GetAttribute("myCustomFloatAttribute")
if my_custom_attr.IsValid():
    carb.log_warn("Attribute exists!")
    # my_custom_attr.Set(42.8)
else:
    carb.log_warn("Attribute does not exist yet")

attr = cube_prim.CreateAttribute("myCustomFloatAttribute", Sdf.ValueTypeNames.Float)
attr.Set(42.8)

my_custom_attr : Usd.Attribute = cube_prim.GetAttribute("myCustomFloatAttribute")
if my_custom_attr:  # use the boolean override to check for existence as well
    carb.log_warn("Attribute exists!")
else:
    carb.log_warn("Attribute does not exist yet")

# Access a non-custom attribute defined in the UsdGeomCube schema
cube = UsdGeom.Cube(cube_prim)
size_value = cube.GetSizeAttr().Get()
carb.log_warn(f"cube has size attr of value: {size_value}")

# Export root stage to file
root_stage.GetRootLayer().Export(BASE_DIRECTORY + "/RootLayer.usda")

# Issue an 'open-stage' command to avoid doing this manually and free whatever stage
# was previously owned by this context
omni.usd.get_context().open_stage(BASE_DIRECTORY + "/RootLayer.usda")

As already noted custom attributes are not part of a schema and marked as custom in USDA:

$ cat /tmp/RootLayer.usda
#usda 1.0

def Xform "World"
{
    def Cube "Cube"
    {
        float3[] extent = [(-50, -50, -50), (50, 50, 50)]
        custom float myCustomFloatAttribute = 42.8  # <---------
        double size = 100
    }

    def Xform "Environment"
    {
        def DomeLight "DomeLight"
        {
            float inputs:intensity = 1000
        }
    }
}

Whether an attribute is custom can be queried via IsCustom().

Primspecs

A Primspec is the part of a prim which resides in a layer only: if a layer only has an override for a prim’s attribute, the Primspec will just be a view over that over specifier with an AttributeSpec inside that redefines that attribute. Think of a Primspec as a git delta for a specific prim, on a specific layer.

Enumerating all the attributes of a prim by querying the UsdPrim on the composed (i.e. where all of the hard work of resolving opinions, etc. has already been done) stage is performed via

cube_prim = cube.GetPrim()
for attr in cube_prim.GetAttributes():
    print(attr)

and yields all of the attributes applied by schemas and the primvars attributes (used by the Hydra renderer usually):

Usd.Prim(</World/Cube>).GetAttribute('doubleSided')
Usd.Prim(</World/Cube>).GetAttribute('extent')
Usd.Prim(</World/Cube>).GetAttribute('orientation')
Usd.Prim(</World/Cube>).GetAttribute('primvars:displayColor')
Usd.Prim(</World/Cube>).GetAttribute('primvars:displayOpacity')
Usd.Prim(</World/Cube>).GetAttribute('purpose')
Usd.Prim(</World/Cube>).GetAttribute('size')
Usd.Prim(</World/Cube>).GetAttribute('testValue')
Usd.Prim(</World/Cube>).GetAttribute('visibility')
Usd.Prim(</World/Cube>).GetAttribute('xformOpOrder')

But if we enumerate the attributes in a Sdf.PrimSpec, we will find only the attributes that are effectively defined or authored in that specific layer: in the case of just an override for a radius attribute for a sphere in a another_layer, here’s what we would get:

$ cat /tmp/another_layer.usda
#usda 1.0
()

over "World"
{
    over "Sphere"
    {
        double radius = 200
    }
}
another_layer = Sdf.Layer.FindOrOpen("/tmp/another_layer.usda")
primSpec : Sdf.PrimSpec = another_layer.GetPrimAtPath("/World/Sphere")
for attr in primSpec.attributes:
    print(attr.GetAsText())  # prints "double radius = 200"

PrimSpecs allow us to enumerate the contributions of a layer without composing the stage first (which can be quite expensive due to the LIVRS and opinion resolution); they also allow us to create definitions, overrides, etc. in a single layer.

Here’s a rather involved example which defines a RootLayer which references AnotherLayer as a sublayer. RootLayer overrides the Sphere defined in the other layer and overrides the radius attribute. The sphere has a CollisionAPI applied, just to showcase how PrimSpecs can enumerate those as well. Finally also a new Cube prim is defined in the RootLayer through a newly created PrimSpec.

from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf, UsdPhysics
import omni.usd
import carb

BASE_DIRECTORY = "/tmp"  # This is where the .usda files will be saved

root_stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")
another_stage : Usd.Stage = Usd.Stage.CreateInMemory("AnotherLayer.usda")

# Create a Xform "World" and a Sphere "Sphere" prim in the "another stage"

xform : UsdGeom.Xform = UsdGeom.Xform.Define(another_stage, Sdf.Path("/World"))
sphere : UsdGeom.Sphere = UsdGeom.Sphere.Define(another_stage, "/World/Sphere")
extent = [(-30, -30, -30), (30, 30, 30)]
sphere.GetExtentAttr().Set(extent)
radius = sphere.GetRadiusAttr()
radius.Set(50)
environment_xform = UsdGeom.Xform.Define(another_stage, "/Environment")
dome_light = UsdLux.DomeLight.Define(another_stage, "/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)

# Apply a physics CollisionAPI (defined in a schema) to the Sphere in the "another stage"
UsdPhysics.CollisionAPI.Apply(sphere.GetPrim())

# Set "another stage" as a sublayer in the "root layer"
another_stage.GetRootLayer().Export(BASE_DIRECTORY + "/AnotherLayer.usda")
root_stage.GetRootLayer().subLayerPaths.append(BASE_DIRECTORY + "/AnotherLayer.usda")

# Also add an override in the "root layer" for the radius defined in "another layer"
sphere_override_prim = root_stage.OverridePrim("/World/Sphere")
sphere_override : UsdGeom.Sphere = UsdGeom.Sphere(sphere_override_prim)
radius = sphere_override.CreateRadiusAttr()
radius.Set(200)

# Now for the interesting part: enumerate the primspecs in the root layer

ref_layer = root_stage.GetRootLayer()
primSpec : Sdf.PrimSpec = ref_layer.GetPrimAtPath("/World/Sphere")
print(ref_layer.GetObjectAtPath("/World/Sphere"))
for attr in primSpec.attributes:
    # attr is Sdf.AttributeSpec
    # Usd.Attribute and customAttributes have a GetTypeName() function,
    # while Sdf.AttributeSpec has a typeName field
    # print(attr.typeName)
    print(attr.GetAsText())  # pretty-print whatever this attributespec contains


# Create another primspec in this very same root layer - this defines a cube
cube = Sdf.PrimSpec(ref_layer.GetPrimAtPath("/World"), "Cube", # this is the name
    Sdf.SpecifierDef,
    "Cube") # this is the type
size = Sdf.AttributeSpec(cube, "size", Sdf.ValueTypeNames.Double)
size.default = 250.0

# Enumerate the primspecs for sphere in the "another layer" and get the 'apiSchemas'
ref_layer = another_stage.GetRootLayer()
prim_spec: Sdf.PrimSpec = ref_layer.GetObjectAtPath("/World/Sphere")
api_schemas_metadata : Sdf.ListOpType = prim_spec.GetInfo("apiSchemas")
for api in api_schemas_metadata.prependedItems:
    print(api)  # this should print the 'CollisionAPI' schema

# Export root layer to file and open it
root_stage.GetRootLayer().Export(BASE_DIRECTORY + "/RootLayer.usda")
omni.usd.get_context().open_stage(BASE_DIRECTORY + "/RootLayer.usda")

USDA is provided for completeness

$ cat /tmp/RootLayer.usda
#usda 1.0
(
    subLayers = [
        @/tmp/AnotherLayer.usda@
    ]
)

over "World"
{
    over "Sphere"
    {
        double radius = 200
    }

    def Cube "Cube"
    {
        double size = 250
    }
}

$ cat /tmp/AnotherLayer.usda
#usda 1.0

def Xform "World"
{
    def Sphere "Sphere" (
        prepend apiSchemas = ["PhysicsCollisionAPI"]
    )
    {
        float3[] extent = [(-30, -30, -30), (30, 30, 30)]
        double radius = 50
    }
}

def Xform "Environment"
{
    def DomeLight "DomeLight"
    {
        float inputs:intensity = 1000
    }
}

Primspecs are an advanced USD concept but they allow for quite powerful manipulations of the scene graphs.