Schemas
Schemas are a fundamental part in USD: a schema defines which properties (with fallback/default values) and which metadata a prim should have. A prim that uses the Cube
schema is expected to have a size
attribute and USD will also generated methods of the syntax Get[attr_name]Attr()/Create[attr_name]Attr
, i.e. GetSizeAttr()/CreateSizeAttr()
.
The UsdPhysics.usda
schema in the official pixar USD repo defines a collisionEnabled
attribute in the PhysicsCollisionAPI
class so that each prim which inherits from the PhysicsCollisionAPI
is going to have that attribute (which semantically controls whether the prim will participate or not in the physics engine simulation of collisions)
There are two types of schemas:
-
Typed Schemas
(also calledIsA-schemas
): these can impart a typeName to aUsdPrim
. An example isCube
: in the textusda
the defined prim as typecube
def Cube "Cube" # type is 'Cube' { float3[] extent = [(-50, -50, -50), (50, 50, 50)] double size = 100 }
The type of a prim can be inspected with
IsA
(which in C++ is a templated function for the inspecting type)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) .. cube_prim : Usd.Prim = cube.GetPrim() print(cube_prim.IsA(UsdGeom.Cube)) # True
By using the
Tf
module (recall that it’s used for type-related operations) we can do type manipulations as well and inspect the typed schema further (although this stuff is more advanced):# Query the typename and look it up in the internal USD registry of all known schemas up to # this point prim_type_name = cube_prim.GetTypeName() print(prim_type_name) # "Cube", this is the textual name of the prim type name # Get the Tf.Type (Tf is for internal type operations) from this 'Cube' type name prim_tftype_type : Tf.Type = Usd.SchemaRegistry.GetTypeFromName(prim_type_name) # Use it to define a new variable (get its python class with `pythonClass`) # which points to the same Cube prim instance myObj : prim_tftype_type.pythonClass = cube print(myObj.GetSizeAttr().Get()) # "100" # Get the textual representation of its class type receiving 'cube_prim' in its constructor prim_typed_schema = prim_tftype_type.pythonClass(cube_prim) print(prim_typed_schema) # "UsdGeom.Cube(Usd.Prim(</World/Cube>))"
Note that this kind of schemas can be
Concrete
orAbstract/non-concrete
which means that it can be instantiated directly (e.g.Cube
) or cannot be instantiated directly and you’ll have to either define or find a subclass which adds the missing required pieces (e.g.UsdGeom.Imageable
). -
API Schemas
: these do not define a prim type and do not contribute to a prim’s definition but rather add methods and properties/metadata to have the prim behave in a certain way. There are two types of API schemas:non-applied
schemas (we’ll take a look at thekind
non-applied schema later) which only provide an API to set and get data for a prim (and you usually just use that API to access it, e.g. thekind
non-applied schema which is basically a “is-this-the-topmost-parent-prim-of-a-large-and-complete-3d-model?” field)single-apply
schemas: adds properties to a prim’s definition, e.g. theUsdCollisionAPI
for physics collision behavior. These can be queried viaUsdPrim::HasAPI<..>()
or the equivalent PythonUsd.Prim.HasAPI("CollisionAPI")
method.multiple-apply
schemas: these can be applied to a prim more than once requiring an “instance name” to distinguish them, a typical example is theUsdShadeMaterialBindingAPI
multiple-apply schema which can be applied multiple times to a prim to bind different materials to different subsets of a geometry.
Here are some examples of what we just learned:
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) UsdPhysics.CollisionAPI.Apply(cube.GetPrim()) 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() # Get the typed schema of the cube prim prim_type_name = cube_prim.GetTypeName() prim_tftype_type : Tf.Type = Usd.SchemaRegistry.GetTypeFromName(prim_type_name) prim_typed_schema = prim_tftype_type.pythonClass # Here the type can be used, as we saw before, to define references or new cube variables ## API Schemas ## # Get the non-applied schema 'kind' non_applied_api_schema = Usd.ModelAPI(cube_prim) # use it non_applied_api_schema.SetKind(Kind.Tokens.subcomponent) # we'll see what this means later # Get the applied schema CollisionAPI applied_api_schema = UsdPhysics.CollisionAPI(cube_prim) # use it applied_api_schema.GetCollisionEnabledAttr().Set(True)
E.g. of
usda
for an API single-appy schemaPhysicsRigidBodyAPI
:#usda 1.0 () over "Kitchen_set" { over "FlowerPotA_5" ( prepend apiSchemas = ["PhysicsRigidBodyAPI"] ) {} }
And here is a table which intuitively maps the concepts that we’ve just learned about schemas to OOP (object oriented programming) concepts you might be familiar with:
USD Concept | Similar OOP Concept |
---|---|
Non-concrete typed schema | Abstract class (non-instantiable directly) |
Concrete typed schema | (Instantiable) class |
Non-applied API Schema | Provide methods to access/set non-defining properties (and you cannot manipulate its state directly) but does not contribute to type in any way |
Single applied API Schema | A member variable inside your class that you can use - it has its state and methods |
Multi-applied API Schema | An array of member variables inside your class that you can use - each element inside the array has its own state even though they all have the same methods |
Some of these can also be queried from the Usd.SchemaRegistry()
- a registry that contains the list of all schema names, types and fallback/default values for all known schemas
registry = Usd.SchemaRegistry()
print(registry.IsTyped(UsdGeom.Cube)) # True
print(registry.IsTyped(UsdGeom.Imageable)) # True
print(registry.IsAbstract(UsdGeom.Imageable)) # True
print(registry.IsAbstract(UsdGeom.Cube)) # False
print(registry.IsConcrete(UsdGeom.Imageable)) # False
print(registry.IsConcrete(UsdGeom.Cube)) # True
print(registry.IsTyped("UsdGeomImageable")) # True
print(registry.IsTyped("UsdGeomCube")) # True
print(registry.IsAppliedAPISchema("CollisionAPI")) # True
print(registry.IsMultipleApplyAPISchema("CollectionAPI")) # True
print(registry.GetSchemaKind("Cube")) # pxr.Usd.SchemaKind.ConcreteTyped
print(registry.GetSchemaKind("Imageable")) # pxr.Usd.SchemaKind.AbstractTyped
Lastly, let’s take a look at a more complex example that removes a typed API schema from a primspec and uses List Composition:
root_stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")
def removeAPI(prim, api_name):
if prim.IsInstanceProxy() or prim.IsInPrototype():
return # invalid prim
# Get a primspec on the root stage
editTarget = root_stage.GetEditTarget()
primSpec = editTarget.GetPrimSpecForScenePath(prim.GetPath())
listOp = primSpec.GetInfo(Usd.Tokens.apiSchemas)
# Look for the API in the prepended/appended/explicit lists
if api_name not in listOp.prependedItems:
if api_name not in listOp.explicitItems:
if api_name not in listOp.appendedItems:
return # not found, we're good
# Create a new list with whatever it was already present MINUS the api_name we want to remove
newPrepended = listOp.prependedItems
newPrepended.remove(api_name)
listOp.prependedItems = newPrepended
result = listOp.ApplyOperations([])
newListOp = Sdf.TokenListOp()
newListOp.prependedItems = result # Reassignment is needed here due to legacy reasons
# Write back the primspec again
primSpec.SetInfo(Usd.Tokens.apiSchemas, newListOp)
# 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")
cube_prim : Usd.Prim = cube.GetPrim()
extent = [(-50, -50, -50), (50, 50, 50)]
cube.GetExtentAttr().Set(extent)
cube.GetSizeAttr().Set(100)
# Apply a CollisionAPI
UsdPhysics.CollisionAPI.Apply(cube_prim)
environment_xform = UsdGeom.Xform.Define(root_stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(root_stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)
# Remove the CollisionAPI
removeAPI(cube_prim, "PhysicsCollisionAPI")
print(cube_prim.GetMetadata("apiSchemas")) # SdfTokenListOp(Explicit Items: [])
Custom schemas
As previously stated USD is extensible. This means that custom schemas can be defined. This is rather common when dealing with a custom pipeline from a DCC (digital content creation software) that involves USD: a developer defines his own schemas to create custom prim types/API so that the prims have sets of attributes relevant to the DCC in question or to the kind of workflow intended.
There is a pretty good tutorial on the OpenUSD official website regarding schema generations, but we’ll summarize the steps here for clarity:
-
One would usually first figure out what kind of schema he’s after (is it a multiple-apply schema? a single-apply one?). Then a
usda
schema would usually be defined, e.g. theUsdPhysics schema
. This is called the schema definition file. -
A schema definition file can be contained within a
USD plugin
, indicating that schema definitions and associated code (if not codeless) will be included in the resulting C++ and Python libraries. AUSD plugin
is a shared library object (e.g. .dll or .so) that USD applications can load via thePlugin registry
-
A script called
usdGenSchema
provided by the official pxr repo can be used to generate C++ classes (and/or python bindings)$ usdGenSchema schema.usda . Processing schema classes: SimplePrim, ComplexPrim, ParamsAPI Loading Templates Writing Schema Tokens: unchanged extras/usd/examples/usdSchemaExamples/tokens.h unchanged extras/usd/examples/usdSchemaExamples/tokens.cpp unchanged extras/usd/examples/usdSchemaExamples/wrapTokens.cpp Generating Classes: unchanged extras/usd/examples/usdSchemaExamples/simple.h unchanged extras/usd/examples/usdSchemaExamples/simple.cpp unchanged extras/usd/examples/usdSchemaExamples/wrapSimple.cpp unchanged extras/usd/examples/usdSchemaExamples/complex.h unchanged extras/usd/examples/usdSchemaExamples/complex.cpp unchanged extras/usd/examples/usdSchemaExamples/wrapComplex.cpp unchanged extras/usd/examples/usdSchemaExamples/paramsAPI.h unchanged extras/usd/examples/usdSchemaExamples/paramsAPI.cpp unchanged extras/usd/examples/usdSchemaExamples/wrapParamsAPI.cpp unchanged extras/usd/examples/usdSchemaExamples/plugInfo.json Generating Schematics: unchanged extras/usd/examples/usdSchemaExamples/generatedSchema.usda
-
Stuff is then compiled to build the plugin shared library object that can be loaded
$ cmake --build . --target install --config Release
-
Finally the environment variable
PXR_PLUGINPATH_NAME
can be used to indicate the location of the plugin’sresources
directory so it can be loaded from conforming USD-based applications (e.g. Kit for Omniverse apps).