What is USD
USD stands for “Universal Scene Description” and at this point is an umbrella term that includes an open standard https://openusd.org/ to collaboratively edit and create 3D scenes and assets, apply physically correct properties to the 3D assets, describe the materials to apply, define the hierarchies (e.g. which asset goes under which other asset to create the 3D model of a car), define how properties vary with time (e.g. for an animation of a 3D model) and much more.
Among other things the USD standard also describes a serialization file format (in the simplest case to describe how data is laid out in .usd files) which defines how to save that information to a single or multiple files that can be shared with other users locally or across a network to collaborate on authoring (i.e. ‘editing’) and visualizing 3D scenes.
A consortium called the Alliance for OpenUSD has recently been created and among other tasks they’re working on creating a formal specification (much like the C++’s standard) for OpenUSD and its file formats.
What is the goal of USD
There used to be no consensus among modeling software programs (e.g. Blender, 3D Studio Max or Maya) and programs operating on assets needed to compose 3D scenes (e.g. programs to aid in material designing like Adobe Substance Painter) on how to communicate with each other (especially if from different vendors). These programs are commonly referred to as DCC tools, i.e. “Digital Content Creator” tools.
USD aims at filling that gap and providing a single source of truth to exchange files seamlessly and let multiple people work on the same 3D scene/model at the same time.
If a 3D content creator program like Blender or 3D Studio Max or Maya has a USD connector available (be it a plugin, an addon or whatever integrated or external piece of code usually developed by NVIDIA), it means the program is able to export its content (be it a 3D model, a texture, or even just plain text data) to a USD file. And that file can be later read and imported by another program which has a USD connector. Plus there are tools to visualize those USD files, to edit them, to compose them and so on.
USD is complex but comprehensive
USD is a very descriptive, complex and powerful 3D representation format, some of its key points:
-
It was initially developed by Pixar and later open-sourced (free to use under Apache 2.0), so it’s rather battle-tested for large-scale usage
-
It is often addressed as the ‘Photoshop of 3D graphics’ since it uses layers to compose a final component of a scene (e.g. the color of an asset can be overridden in a stronger layer, but the other colors are retained as well - one can always go back to it or compose them together) and it allows for multiple people editing the same 3D scene collaboratively and/or in real-time (if the underlying software allows it). E.g. see this image where layer2 (a USD file itself) which gives the cube a red color with a stronger opinion is muted and the renderer transitions the cube to a blue color (given by layer1 which has a weaker opinion on the color given by the layers’ hierarchy)
-
It is extensible. USD allows for new schemas to be defined: a schema is a textual document of rules. E.g. there could be a ‘light schema’ which defines light properties like the light’s color, intensity and direction. The schema should afterwards be parsed and interpreted by a software (like Omniverse) which can later load a USD file with primitives (for now just think of ‘primitives’ as 3D assets) which have that schema applied to them (much like fulfilling an interface contract in programming languages or inheriting from a pure virtual class) and have the properties defined by that schema defined and instantiated into them. So to make it simple a software can read a ‘LightBulb’ prim (3D asset) which has the ‘Light’ schema applied on it, and therefore it must have
color
,intensity
anddirection
defined in order for the renderer to actually render it on the scene. This is the real light schema in usda format (USD format in ASCII - i.e. human readable, not binary nor compressed), as you can see it is pretty complex but there is no need to grasp that complexity right now.Schemas are a very powerful mechanism which allows the USD format to expand, evolve and represent complex 3D objects properties (e.g. physics properties!).
Pixar has also open-sourced with the same license a reference implementation (i.e. nothing stops you from conforming to the specifications and writing a USD implementation yourself) of the USD standard, i.e. they provide a rather large C++ library hosted at github.com/PixarAnimationStudios/USD that users can download, compile, contribute to, etc.. the library is written in C++ but python bindings are also available in that same repository to call USD APIs from Python programs.
It has to be noted that this library doesn’t provide any renderer in itself, i.e. even if you clone the repository, build it, generate the python bindings and write code to generate a cube on an empty stage and save it to a USD file as follows
from pxr import Usd, UsdGeom
# Create a new stage
stage = Usd.Stage.CreateNew('helloWorld.usd')
# Define a new Cube primitive
cube = UsdGeom.Cube.Define(stage, '/HelloWorldCube')
# Set the size of the cube
cube.GetExtentAttr().Set([(1.0, 1.0, 1.0)])
# Save the stage
stage.GetRootLayer().Save()
you will be able to execute this python USD code and get a USD output file with a stage with a cube on it, but you will not have any way to visually inspect and render that cube. USD defines another rather complex specification for a rendering architecture called Hydra that renderer programmers can abide by to have their own renderer integrate with USD scenes. There is however a small tool called usdView in the same official USD repo based on PyQt that allows you to quickly render USD stages (mostly for debugging purposes and to understand how USD works).
Performance considerations
Even though USD was originally created by Pixar for its own filmmaking needs, USD is a great choice even for more complex realtime applications (in fact it is also used in Omniverse physical large-scale simulations).
Advanced and performance-intensive graphical applications in Omniverse would use something called USDRT and its underlying library Fabric. The goal of Fabric is to act as a fast runtime cache for USD data and it enables massive performance gains by still leveraging all the capabilities of the USD format. USDRT can be thought as a wrapper on top of Fabric with an API that mimicks USD’s one (so that you can just plug in Fabric under the hood without changing your USD code).
So bottom line is: USD is still a great choice for any performance intensive application even though it was originally conceived as an offline scene composing format thanks to the work that NVIDIA has been putting into it.
In the next pages we will introduce a powerful tool that we’ll be using to render the contents of USD files and to execute USD python commands dynamically on a stage.
Using Omniverse to learn USD
Dabbling with USD is the quickest and easiest way to learn it, much like copy-pasting and changing your first C++ hello world source code. One of the best ways to do this is to use NVIDIA Omniverse for this. It is highly recommended that you download and install Omniverse Standard (specifically Omniverse USD Composer) for free from the NVIDIA website.
Let’s spend some words introducing what is Omniverse exactly. Feel free to skip this page if you know that already.
What is Omniverse
Omniverse is a platform and a series of technologies developed by NVIDIA around the USD standard (although they’re rapidly evolving in other directions as well).
Omniverse comprises technologies and applications to work with 3D graphics, collaborating on creating 3D assets and scenes, using AI to create stunning visual effects or improve the process of creating 3D contents, adding real-time and physically correct physics behaviors to 3D contents, rendering in a physically-correct way with ray tracing or path tracing in real-time, etc.
NVIDIA doesn’t impose any workflow or dictate how Omniverse tools and technologies should be used (they can be used to create photorealistic render images that you later use commercially, they can be used to let multiple 3D artists work on a 3D scene simultaneously without interfering with each other’s modifications, they can be used to ‘predict’ the mechanical ‘wear’ in a ‘digital twin’ 3D representation of a mechanical part in 3D with accurate physics after many simulation steps, they can be used to create a server-side web service which renders something complex and streams the result as a video back to the user’s browser, etc.).
The foundation of many Omniverse applications is called Kit and it’s an extensible framework and SDK developed by NVIDIA which provides a series of libraries and APIs to let users write extensions (i.e. kit-based libraries written in Python, C++, both or in other languages as well) so that they can use NVIDIA’s best-in-breed technologies (e.g. RTX raytracers, PhysX, AI integrations, etc.) to do useful graphical work for them. Official docs for Kit can be found here.
An example: a user can write a Kit extension (here’s a good list of extensions used in Omniverse) which acts as a web service: whenever a HTTP request is received, the extension can fire up a complex 3D scene, generate the assets or the scene requested via the HTTP request with chatGPT, render it and send the rendered result back to the user. Another extension could show up a small UI panel in the Omniverse Composer application (more on this later) which allows a 3D artist to click a button, process the scene geometries with an AI application and apply effects, shaders and much more. Extensions are the core of Omniverse as we will later discover.
Composer & Presenter
Two of the most famous reference applications (and some of the very first ones a newcomer might try out) in Omniverse are USD Composer (formerly Create) and USD Presenter (formerly View).
The former is a 3D authoring program which allows users to compose complex scenes from 3D assets, applying physical properties to them, simulating and rendering, applying photorealistic materials and much more
Composer/Create is usually not equipped with 3D creation tools to model single 3D assets (think Blender) but rather orchestrates composition of a USD scene from external assets (although it could even become a modeling tool with the right extensions).
Presenter/View instead focuses on visualizing already composed environments and inspecting USD scenes (it doesn’t feature advanced authoring tools as Composer).
There is some overlap in the UI elements of the two applications, as there is in some of the core extensions used for a simple reason: both Composer and Presenter are just a bunch of highly complex Kit based extensions. Kit is both a SDK that programmers can leverage to build their own extensions and also a portable cross-platform executable that can ‘bootstrap’ their extensions with a bare minimum skeleton environment. Composer and Presenter are both instances of the Kit platform executable but they load different extensions. These extensions can also be altered (and dependencies broken, if the user so desires..), changed or custom ones loaded. This is the beauty of Omniverse: you can compose it in any way you want. You’re not satisfied with a particular workflow? Create your own extension and fire it up from a Kit CLI command or from the Composer UI panel.
Nucleus
Omniverse isn’t just a collection of Kit extensions though but much more. For instance: to foster collaboration between lots of 3D artists working together on a complex 3D scene (think Pixar..), together with the USD specification and file format (which features “photoshop layer-like behavior” for 3D contents) Omniverse also provides Nucleus which is a distributed object storage optimized for graphical assets.
With nucleus users can reference omniverse://asset_paths
from external repositories or internal organization repos, use or modify those assets and seamlessly have them streamed back to other users which might be using them. Dynamically.
Pricing and requirements
Two things newcomers usually care a lot about: pricing and requirements.
Please check this page for a detailed explanation of pricing and requirements for Omniverse.
Support, learning, official resources
Official channels to learn more about Omniverse, post questions regarding its official applications and main extensions (e.g. related to omni.physx) and get in touch with the great NVIDIA Omniverse community (friendly and available, NVIDIA is doing its best to foster a good community) are the Omniverse discord server, the YouTube Omniverse channel, the developer blog articles and, for critical bugs/issues, the official Omniverse forum (less chatty, more support-y).
Any non-Omniverse related question should not be asked in the above channels but rather in the NVIDIA customer support forum.
Hello USD
Let’s start right away and let us visualize a very simple cube mesh and a distant white light (to actually see the cube in the dark of the scene) in a very minimal setup for Omniverse Composer.
We assume that you have already installed Omniverse Composer on your local system.
Hello USD(a)
This is the usda
file that we’ll be using
#usda 1.0
(
metersPerUnit = 0.01
upAxis = "Y"
)
def Mesh "Cube"
{
int[] faceVertexCounts = [4, 4, 4, 4, 4, 4]
int[] faceVertexIndices = [0, 1, 3, 2, 4, 6, 7, 5, 6, 2, 3, 7, 4, 5, 1, 0, 4, 0, 2, 6, 5, 7, 3, 1]
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0)] (
interpolation = "faceVarying"
)
point3f[] points = [(-50, -50, 50), (50, -50, 50), (-50, 50, 50), (50, 50, 50), (-50, -50, -50), (50, -50, -50), (-50, 50, -50), (50, 50, -50)]
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1), (1, 0), (1, 1), (0, 1), (0, 0), (0, 1), (0, 0), (1, 0), (1, 1), (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1), (1, 0), (1, 1), (0, 1), (0, 0)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
double3 xformOp:rotateXYZ = (0, 0, 0)
double3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (0, 50, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
}
def DistantLight "DistantLight" (
apiSchemas = ["ShapingAPI"]
)
{
float inputs:angle = 1
float inputs:intensity = 3000
float inputs:shaping:cone:angle = 180
float inputs:shaping:cone:softness
float inputs:shaping:focus
color3f inputs:shaping:focusTint
asset inputs:shaping:ies:file
double3 xformOp:rotateXYZ = (315, 0, 0)
double3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (0, 0, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
}
some elements might already be familiar to computer graphics programmers: normals, point coordinates, UV coords, etc.
USD extensions can be confusing at times: the following file extensions and formats are defined in USD:
Extension | Format of the data inside the file |
---|---|
.usda (or .usd) | ASCII human readable file. It can get quite large with complex ‘flattened’ scenes (i.e. scenes containing everything they reference outside collected into one single file). This can be written in a text editor. |
.usdc (or .usd) | ‘c’ stands for ‘crate’, denotes compressed binary files. Designed for minimal parsing on file load (structure is set up at the top). Uses LZ4 in some parts. Uses memory mapping for fast load. |
.usdz | Uncompressed zip archive that can contain other usda/usdc/usd or image or audio files inside. Meant for publishing only and should not be used for editing but as a read-only format. Again: it’s uncompressed, the zip aspect is just for bundling purposes. |
USDA files always start with #usda 1.0
at the beginning while usdc
have a magic number.
.usd
is an alias extension: internally it can be either a usda
or usdc
file.
Remember that there are various tools that you can use to inspect USD files and operate on them, e.g. usdview, usdcat, usddiff and a complete toolset from OpenUSD. We’ll be using Omniverse tools instead.
If we save the previous text file as hello_cube.usda
on disk with a simple text editor and open it with OV Composer, it will display a simple lit cube mesh.
A usdview clone with OV Composer
As we previously said Omniverse allows you to develop your own extensions and write small and self-contained Kit applications for your own graphical needs.
Let’s figure out on our local system where OV composer was installed (if we installed it via the official NVIDIA launcher of course). In our OV Composer install folder there are two important directories: apps
and kit
. The kit
folder contains the Kit executable while the apps
folder contains some .kit
files that will be fed to the kit
executable as command line parameters, for example:
~/.local/share/ov/pkg/create-2023.2.0$ ./kit/kit ./apps/omni.create.kit
The multiple omni.app.full.sh
or omni.app.full.bat
files are usually just to set up directories and launch the right kit
files. The NVIDIA documentation has great resources on kit files here.
To launch a full OV Composer app, a omni.create.kit
file is usually used, more or less of the form (remember that create
is the former name of composer
):
[package]
title = "USD Composer"
description = "An Authoring application for USD and Omniverse Content"
version = "2023.2.0"
keywords = ["experience", "app", "usd"]
# All extensions needed to show the viewport, the menu bars, the splash screen, etc..
[dependencies]
"omni.kit.uiapp" = {}
"omni.kit.renderer.core" = {}
# Status Bar
"omni.kit.window.status_bar" = {}
"omni.create.app.resources" = {}
"omni.create.app.setup" = { order = 1000 } # we are running that at the end
# Splash runner
"omni.kit.splash.carousel" = {}
"omni.kit.menu.utils" = {}
"omni.kit.menu.file" = {}
"omni.kit.menu.edit" = {}
"omni.kit.menu.create" = {}
"omni.kit.menu.common" = {}
"omni.kit.menu.stage" = {}
"omni.kit.window.file" = {}
"omni.kit.context_menu" = {}
"omni.kit.selection" = {}
"omni.kit.stage_templates" = {}
"omni.kit.stage.mdl_converter" = {}
## PhysX stuff
"omni.physx.bundle" = {}
"omni.physx.zerogravity" = {}
... lots of other extensions ...
Let us create a new text file in this apps
directory with name omni.minimal.kit
:
[package]
title = "Kit Simple App"
description = "Minimal USD viewer."
version = "1.0.0"
keywords = ["app"]
[dependencies]
# The Main UI App. Bring everything needed for UI application.
"omni.kit.uiapp" = {}
"omni.hydra.pxr" = {}
"omni.kit.window.viewport" = {}
"omni.kit.window.stage" = {}
[settings]
# Open a stage on startup
app.content.emptyStageOnStart = true
app.window.title = "Kit Custom"
renderer.enabled='pxr'
pxr
is the Pixar Storm renderer, a simple OpenGL hydra renderer (much simpler than the RTX renderer by NVIDIA which allows for far more powerful effects).
Now let’s create a USD python file to load our usda
file at startup and save it somewhere with name main.py
:
import omni.usd
import carb
import asyncio
usd_file_path = "/home/alex/Downloads/hello_cube.usda"
async def _open_usd(usd_file_path):
await omni.usd.get_context().open_stage_async(usd_file_path)
carb.log_warn(f"Stage opened from {usd_file_path}")
asyncio.ensure_future(_open_usd(usd_file_path))
This assumes that we saved our textual usda
file in the path /home/alex/Downloads/hello_cube.usda
. The code is rather simple:
- Schedules a python coroutine in background without waiting for it to finish
- The coroutine waits for the USD context provided by Kit to load a stage from file
- As soon as the stage is loaded, the coroutine resumes and prints a warning on the console (
log_info
would have been more appropriate but info-level logs are by default not shown unless a verbose parameter is passed)
Let’s put everything together and execute a simple kit app which loads the absolute default essential extensions to visualize in Omniverse our USD hello world file:
~/.local/share/ov/pkg/create-2023.2.0$ ./kit/kit ./apps/omni.minimal.kit --exec ./main.py
As you can see the same widgets that are available in OV Composer were used (i.e. viewport, stage hierarchy viewer, etc.). Omniverse Composer and View and virtually any other Omniverse Kit-based application only differ in the set of extensions loaded. You can load any extension which might suit you or create your own for your workflows.
Context, Stage and Layers
A stage
in USD is the final composed result of opening one or more layers. Again on the Photoshop analogy: all image layers compose a final image. A stage references one or more layers and use them to compose a 3D scene.
Stage objects are owned by a USD context
: a USD context
is usually provided by the application you’re working in (e.g. python or C++ code running in an Omniverse extension can use something like omni.usd.get_context()
to get the current active context) and is a container for resources and internal states needed by the stages. Multiple contexts can be created at the same time (or a context can be manually destroyed via code):
# Check if we already have a valid context when this code executes, or create one if there's none
usd_context = omni.usd.get_context()
if not usd_context:
print("Context was not ready, creating one..")
usd_context = omni.usd.create_context()
This snippet uses APIs provided by Kit and not available outside of Omniverse Kit (i.e. contexts are usually managed by applications which use OpenUSD libraries), in particular this API is provided by the omni.usd extension.
In the Hello USD section we used asynchronous routines to make sure a stage would be ready after we loaded up a .usda
file, if we had executed a python script at startup with the same shell invocation
~/.local/share/ov/pkg/create-2023.2.0$ ./kit/kit ./apps/omni.minimal.kit --exec ./main.py
with just a stage check like this:
import omni.usd
import carb
usd_context = omni.usd.get_context()
stage = usd_context.get_stage()
if not stage:
carb.log_error("stage was NOT ready")
else:
carb.log_error("stage was ready")
we would have found out that by the time our script is executed (i.e. after Kit is loaded and all of the extension dependencies have been loaded), there should already be a context provided by Kit but probably no stage is loaded yet (a blank stage or a blank template stage is loaded shortly afterwards, but it might not be ready by the time we execute the code above).
We would have had both (a valid context and a valid stage) if we had executed the script above in USD Composer directly by using the Script Editor
extension (you can activate this in Window->Script Editor
- if you can’t find this make sure the extension is downloaded and activated from Window->Extensions
.. in Omniverse everything is an extension).
Executing code in Kit by providing a .py file from the command line and/or using the script editor means you can make a lot of assumptions which are no longer valid when you’re developing your own Omniverse Kit extension. Extensions must pay attention to the availability of the resources they intend to use (e.g. do not assume there’s already a stage you can play with).
Ownership of USD stages
USD contexts are also the sole owners of a stage created in it or attached to it. Everything else referencing a stage should be a weakref (non-owning reference), this is by design to prevent leaking stages (which can get quite big).
Let’s see an example of this:
from pxr import Sdf, Usd, UsdUtils
import omni.usd
# 1
usd_context = omni.usd.get_context()
old_stage = usd_context.get_stage() # Note: this is a weakref
print(old_stage) # Valid stage
# 2
layer = Sdf.Layer.CreateAnonymous()
new_stage = Usd.Stage.CreateInMemory(layer.identifier)
print(old_stage) # Stage is still valid here
# 3
cache = UsdUtils.StageCache.Get()
stage_id = cache.Insert(new_stage).ToLongInt()
usd_context.attach_stage_with_callback(stage_id, None)
print(old_stage) # Old stage is NO LONGER valid here
Executing the code above in the Script Editor
prints something like the following
(19:06:08)> /tmp/carb.VUw057/script_169285425.py
Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x107afbd0:World0.usd'), sessionLayer=Sdf.Find('anon:0x107afe00:World0-session.usda'), pathResolverContext=<invalid repr>)
Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x107afbd0:World0.usd'), sessionLayer=Sdf.Find('anon:0x107afe00:World0-session.usda'), pathResolverContext=<invalid repr>)
invalid null stage
plus you should see OV Composer reloading a blank stage.
The code does the following:
- The current context is obtained via
omni.usd
API (Kit proprietary - this is not standard OpenUSD), the current stage is obtained from the current context (this assumes there’s a stage already, just like the blank scene you’re presented in OV Composer at startup) and printed for debugging purposes - A new anonymous layer is created. A layer is usually backed by a USD file, except for anonymous layers: these are layers used for debugging and/or storing runtime data for extensions. In this case we’re using an anonymous layer (so we avoid having it backed by a file) to create an in-memory stage and using that anonymous layer as the
root layer
(think of it as the starting layer for a stage, we’ll revisit this later). The gist of this part is that we’re creating a stage entirely in memory and ephemerally. The old stage reference is printed and it’s still valid (the context still has the old stage attached). - Now we use an OpenUSD facility called the
StageCache
: this is a cache exposed by USD since stages can get pretty expensive after loading tons of resources and computing the final rendering results of lots and lots of layers, therefore stages can be cached and reused. In this demonstrative example we use this facility to insert the new stage we manually created into the fold and we finally attach it to the current USD context provided by Kit: this causes Kit to switch to the new empty stage. Finally we print theold_stage
reference: now we get thenull stage
. Keep in mind that theStageCache
API is exposed by USD as meant to be used by higher-level facilities (like Kit) to manage multiple stages themselves (i.e. you probably shouldn’t dabble with this unless you know what you’re doing).
It has to be noted that stages which are created in your own Python code (Usd.Stage.Open
for instance) return a strong
ref. Stages which weren’t created in your Python code but just passed to your Python code (as in usd_context.get_stage()
) return a weak
ref. This is by design to prevent dangling references to stages owned by the context.
The behavior we just observed is in accordance to what we wrote at the beginning:
If we had kept another reference to the old stage, we would have seen the following:
from pxr import Sdf, Usd, UsdUtils
import omni.usd
usd_context = omni.usd.get_context()
old_stage = usd_context.get_stage()
old_stage = Usd.Stage.Open(old_stage.GetRootLayer().identifier) # Keep a reference to the old stage alive, this is a strong ref
print(old_stage) # Valid stage
layer = Sdf.Layer.CreateAnonymous()
new_stage = Usd.Stage.CreateInMemory(layer.identifier)
print(old_stage) # Stage is still valid here
cache = UsdUtils.StageCache.Get()
stage_id = cache.Insert(new_stage).ToLongInt()
usd_context.attach_stage_with_callback(stage_id, None)
print(old_stage) # Still valid here!
(19:06:08)> /tmp/carb.VUw057/script_169285425.py
Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x107afbd0:World0.usd'), sessionLayer=Sdf.Find('anon:0x107afe00:World0-session.usda'), pathResolverContext=<invalid repr>)
Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x107afbd0:World0.usd'), sessionLayer=Sdf.Find('anon:0x107afe00:World0-session.usda'), pathResolverContext=<invalid repr>)
Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x107afbd0:World0.usd'), sessionLayer=Sdf.Find('anon:0x107afe00:World0-session.usda'), pathResolverContext=<invalid repr>)
even though we attached another stage to the USD context to own. This is usually not recommended as USD contexts should be the objects delegated to managing stages.
Layers
We will revisit layers later on multiple times, but for now a quick introduction linked to what we’ve just learned can be beneficial.
Layers in USD are collections of prims
(short for primitive in USD, for instance: a cube mesh) and their properties
(for instance: the cube mesh’ display color) that can be saved to or loaded from disk or memory (book of USD defines them as “saveable hierarchy” which is an apt term for it).
Much like Photoshop layers, each layer can add to or override some properties defined in the layers below it. Each scene, as we’ve already seen, has a Root Layer
, i.e. a main layer (backed by a USD file as well) on which the stage is initially opened. It is also the strongest override layer (except for a special layer called session layer
which is like a scratch space for Kit and it’s by default hidden to the users), i.e. the layer where the ‘opinions’ on a prim property count more than all of its sublayers.
You can view the session layer contents in Omniverse Composer by checking the checkmark Session Layer
from the hamburger menu in the Layer
pane
Prims can be def
ined in a layer and have over
ridden properties in another:
Note the small white triangle on World
and Cube
under layer3.usda
: that symbol is a delta
and indicates that only changed properties were registered in that layer and the entire assets were not completely duplicated
$ cat layer2.usda
#usda 1.0
(
customLayerData = {
...
}
)
def Xform "World"
{
def Mesh "Cube"
{
double3 xformOp:translate = (-255.80417243728027, 0, 0)
...
}
}
$ cat layer3.usda
#usda 1.0
(
upAxis = "Y"
)
over "World"
{
over "Cube"
{
double3 xformOp:translate = (177.0296449279361, 0, 0) # <---- overrides this property for the prim /World/Cube
...
}
}
Also note that in the hierarchy above layer3
is positioned above layer2
and therefore has a stronger opinion on the xformOp:translate
property (which corresponds to the position of the prim) - layer3
is gonna win and place the cube at (177.0296449279361, 0, 0)
.
The idea would be that layer2
defines a 3D model created by an artist who is still working out some parts of it, the Omniverse Nucleus service has already synchronized the file with the local OV Composer copy of another scene artist who decided that the 3D model looks better moved from (-255.80417243728027, 0, 0)
to (177.0296449279361, 0, 0)
and authored this property in layer3
. This is an example of a non-destructive workflow: properties are changed, added and overridden without destroying the underlying layers. Artists, programmers, simulation engineers and whatnot can still work together within a USD scene by ensuring only their deltas are applied (and, eventually at the end of the work, maybe all changes flattened in the root layer, i.e. re-unifying all deltas and just getting everything together in the single layer which is the root layer).
Layers allow to create a gigantic amount of scenes without eating up a lot of space by reusing assets and just operating on changing properties via deltas.
In the next sections we’ll take a deeper look at layers and their internal representations.
Foundational classes
You might have noticed from the few python code listings in the previous sections that the USD API is somewhat scattered throughout different classes/submodules with different prefixes from the pxr module:
from pxr import Usd, Sdf, UsdGeom, Vt, Gf
# Some examples of modules into the pxr
Sdf.Layer.FindOrOpen("/tmp/usd_file.usda")
Usd.Stage.Open(..)
xformable = UsdGeom.Xformable(prim)
xformable.AddTranslateOp().Set(Gf.Vec3d(0.0, 180.0, 0.0))
positions = Vt.Vec3fArray(positions_list)
material = UsdShade.Material(material_prim)
Each prefix represents a different module with a specific purpose and functionality. Here is a brief overview of the main ones (there are many others though):
Module | Meaning of acronym | Description |
---|---|---|
Sdf | Scene Description Format | Low-level core scene description format and data model API for USD. It provides classes for representing layers, paths, schemas, attributes, references, variants, and more. Sdf is the foundation of USD and is used by all other modules. These routines come in handy when you want to reason on specific parts (opinions) coming from layers that end up composing a prim on the stage (and on how they interact and are combined together). |
Gf | Graphics Foundation | Basic geometric types and operations, such as vectors, matrices, transforms, colors, quaternions, etc. Gf is used by other modules to perform computations and manipulations on geometric data. |
Tf | Tools Foundation | Low-level utilities and foundation classes, such as python module initialization, string manipulation, error handling, debugging, type traits, smart pointers, etc. mostly OS-independent. Tf is used by other modules to implement common functionality and patterns. |
Vt | Value Types | Classes useful to abstract away types and have collections of values, such as arrays, dictionaries, etc. Vt is used by other modules to store and access data in a generic way. |
Usd | Universal Scene Description (Core) | Core Usd module: high-level scenegraph API that exposes USD’s composition features to application code. It provides classes for accessing and editing stages, prims, properties, etc. Usd is the main entry point for most USD applications. |
UsdGeom | USD Geometry Schema | Core geometry schemas for USD (we’ll revisit schemas later): it provides classes for representing common geometric primitives, such as meshes, curves, points, cameras, etc. UsdGeom also defines concepts such as transformation spaces, visibility, purpose, etc. |
UsdShade | USD Shading Schema | Shading schema for USD: it provides classes for representing materials, shaders, textures, etc. UsdShade also defines a network of connectable nodes for describing shading effects. |
UsdLux | USD Lighting Schema | Lighting schema for USD: it provides classes for representing lights, light filters, light links, etc. UsdLux also defines a set of built-in light types and filters that can be used by renderers. |
Many other modules in the USD C++ API provide additional functionality and domain-specific schemas, you can take a look at them here.
Knowing at least the module which owns a method or a class can help understanding what areas that API is operating on.
C++ and Python in USD
As we’ve already stated the USD API has a reference implementation in C++ with a quite comprehensive API documentation. USD was born C++-first and Python bindings were added later. This means sometimes there are small differences between the C++ and Python APIs:
// Get the current stage (use weakrefs, the context owns a strong reference to the stage)
pxr::UsdStageWeakPtr stage = omni::usd::UsdContext::getContext()->getStage();
if (!stage) {
return 1;
}
// Traverse the stage and print out all paths for the found prims
pxr::UsdPrimRange range = stage->Traverse();
for (pxr::UsdPrimRange::iterator iter = range.begin(); iter != range.end(); ++iter) {
// Get the current prim
pxr::UsdPrim prim = *iter;
if (!prim)
continue;
pxr::SdfPath path = prim.GetPath();
CARB_LOG_INFO("Found a prim with path: %s", path.GetText()); // Carbon API: Omniverse-specific
}
# Get the current stage (weakref)
stage = omni.usd.get_context().get_stage()
if not stage:
return False
# Traverse the stage and print out all paths for the found prims
for prim in self.stage.Traverse():
# Get the current prim
if not prim:
continue
path = prim.GetPath()
carb.log_info(f"Found a prim with path {str(path)}") # Carbon API: Omniverse-specific
If you don’t remember the reason why we’re always getting a weakref to the stage, read up on the previous context and stages section.
There is a chapter dedicated to Translating Between the OpenUSD C++ and Python APIs in the official Omniverse documentation which might provide more insights when working with porting C++ and/or Python USD code. Omniverse supports both native C++ extensions and Python extensions (together with extensions having both Python and C++ in them, a common pattern is for example writing UI code in Python and core processing logic in C++ for performance reasons). In this book however we’ll mainly focus on Python APIs to dive into learning OpenUSD. All the concepts are easily applicabile to the C++ API as well.
Prims and Properties
Prims
A prim
(short for primitive) is a dictionary of key-value pairs. Prims are the main components of a USD scene graph which gets composed from a stage after assembling all of the layers, resolving their opinions and deciding which final value a prim gets for e.g. its scaling or translation or rotation values.
Prims are hierarchical in the scene so you can have a wheel
prim and a car_chassis
prim and each one of those meshes containing: in their key field points
the XYZ coordinates of each vertex for the mesh, in the faceVertexCounts
the number of points composing each face, in faceVertexIndices
the indices for each vertex so the face can have a face-up face-down orientation, the normals
for each vertex, etc..
def Xform "World"
{
def Xform "Car"
{
double3 xformOp:rotateXYZ = (0, 0, 0)
double3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (0, 0, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
def Mesh "Wheel"
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
int[] faceVertexCounts = [3, 3, ...a lot of values...]
int[] faceVertexIndices = [0, 1, 2, 0,....]
normal3f[] normals = [(0, -50, 0),...](
interpolation = "faceVarying"
)
point3f[] points = [(0, -50, 0), ...]
texCoord2f[] primvars:st = [(1, 0), (1, 0.0625), ...]
(
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
double3 xformOp:rotateXYZ = (0, 0, 0)
double3 xformOp:scale = (1, 1, 1)
double3 xformOp:translate = (46.61611128680149, -2.08943043408906, 63.14526081950993)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
}
def Mesh "Car_Chassis"
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
int[] faceVertexCounts = [4, 4, ...]
int[] faceVertexIndices = [0, 1, 3, ...] (
interpolation = "faceVarying"
)
point3f[] points = [(-50, -50, 50), ...]
texCoord2f[] primvars:st = [(0, 0), ...] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
double3 xformOp:rotateXYZ = (0, 0, 0)
double3 xformOp:scale = (1, 1, 3.1101341016510786)
double3 xformOp:translate = (0, 50, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
}
}
}
Prims can have types and much more too:
def Mesh "Car_Chassis" (apiSchemas = ["GiveThisPrimPropertiesToBehaveAsAMetalPiece"])
#1 #2 #3 #4
specifier
(whether this is a prim definition, an OVERride to override properties of another prim, etc.)type
, this is the type of the prim (e.g. Mesh, Xform, DomeLight, etc..)name
- this is the name (part of the SdfPath so it cannot contain spaces) of the prim- prim
metadata
, API schemas, references, variantsets, etc. are specified here, together with user-defined metadata in a predefined dictionary here (i.e.customData
)
A mesh is a type of prim meant to store render-able data (points, normals, maybe UV coords, etc.), an xform prim stores a transform matrix that applies to its child prims (that can be the identity prim - in that case the xform prim name can be useful to group other prims and have a name that is meaningful to human readers/artists, much like the Car
xform prim that we used in the image above to group together the wheel and the car chassis), etc.
Properties
Properties are the key+value pairs that are contained in prims (e.g. normals
is a property in the prims above, or maybe a radius
can be a property of a sphere prim).
There are two types of properties:
-
Attributes:
normals
andradius
fall in this category: key-value pairs with well-defined values (e.g. afloat3
or adouble
). Attributes can be time-sampled, i.e. they can vary over time (so USD supports the concept of animations as well).def Sphere "Sphere" { float3[] extent = [(-30, -30, -30), (30, 30, 30)] # an attribute (not time-sampled) double radius = 50 # another attribute double radius.timeSamples = { # a time-sampled attribute 1: 1, # set the sphere with a radius of 1 at frame time 1 50: 50, # and with a radius of 50 at frame time 50 } # the above values will be linearly interpolated by default }
-
Relationships: the classical example here is a material binding where a prim has a relationship property (currently not time sampled)
def Xform "World" { def Mesh "Cube" ( prepend apiSchemas = ["MaterialBindingAPI"] ) { float3[] extent = [(-50, -50, -50), (50, 50, 50)] int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] int[] faceVertexIndices = [0, 1, 3, 2, 4, 6, 7, 5, 6, 2, 3, 7, 4, 5, 1, 0, 4, 0, 2, 6, 5, 7, 3, 1] rel material:binding = </World/Materials/SimpleRedSurface> ( # This is a relationship property! bindMaterialAs = "weakerThanDescendants" ) } def Scope "Materials" { def Material "SimpleRedSurface" { token outputs:displacement.connect = </World/Materials/SimpleRedSurface/Shader.outputs:displacement> token outputs:surface.connect = </World/Materials/SimpleRedSurface/Shader.outputs:surface> def Shader "Shader" { uniform token info:id = "UsdPreviewSurface" color3f inputs:diffuseColor = (1, 0, 0) ( # Red color customData = { float3 default = (0.18, 0.18, 0.18) } hidden = false renderType = "color" ) } } } }
The
material:binding
relationship is defined in theUsdShadeMaterialBindingAPI
so it’s part of a schema as well for all those prims which are meant to have a material applied to them.
Sometimes “properties” and “attributes” are used somewhat interchangeably in USD code and documentations, but it’s important to know the difference (properties include relationships as well).
Prims can also have custom properties too (something that maybe holds meaning for a 3D modeling program) and those properties can (or can not) be part of a Schema
(e.g. a prim with a CollisionAPI
schema will have physical properties like contactOffset
or similar physics-specific ones). In this example we have a prim with a MyCustomSchemaAPI
applied (a schema that might be owned by our own Omniverse extension), a custom attribute and some custom metadata applied to the generated prim as well
def Mesh "Wheel" (
prepend apiSchemas = ["PhysicsRigidBodyAPI", ... , "MyCustomSchemaAPI"]
customData = {
string thisIsMyCustomOmniverseExtensionMetadata = "This great prim was created by MyExtension v1.0"
}
)
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
int myCustomProperty = 22
...
}
Both prims and properties are identified by unique SdfPaths: examples for the above are
/World/Car/Wheel
/World/Car/Wheel.myCustomProperty
/World/Car/Wheel.material:binding
OV Composer has a handy property visualization pane which summarizes all properties of a prim and allows you to also copy the full SdfPaths by simply right-clicking on a visualized property
Sublayers: authoring time-sampled attributes
Before we proceed further let’s take a look at an example of what we’ve learned so far: we will generate through Python code multiple USD layers, save them to three different .usda
files, compose them on a stage and use different time-sampled attributes to demonstrate how one property in a layer can be overridden in another stronger layer.
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
# Create a temporary stage in memory for the root layer, the scale layer and the translate layer
root_stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")
scale_stage : Usd.Stage = Usd.Stage.CreateInMemory("ScaleLayer.usda")
translate_stage : Usd.Stage = Usd.Stage.CreateInMemory("TranslateLayer.usda")
# Add stage metadata to inform OV Composer that it should set up a timeline from timecode 0 to timecode 50,
# roughly at 30 fps
root_stage.SetStartTimeCode(0)
root_stage.SetEndTimeCode(50)
root_stage.SetTimeCodesPerSecond(30)
# Create a Xform "World" and a Sphere "Sphere" prim in the scale stage
xform : UsdGeom.Xform = UsdGeom.Xform.Define(scale_stage, Sdf.Path("/World"))
# Overly verbose way of getting "/World" SdfPath to demonstrate API usage
sphere : UsdGeom.Sphere = UsdGeom.Sphere.Define(scale_stage, Sdf.Path(str(xform.GetPrim().GetPath()) + "/Sphere"))
# Set the extent of the sphere
extent = [(-30, -30, -30), (30, 30, 30)]
sphere.GetExtentAttr().Set(extent)
# Set the radius of the sphere
radius = sphere.GetRadiusAttr()
radius.Set(50) # Set as not timesampled
radius.Set(1, 1) # Set as timesampled at timesample 1
radius.Set(50, 50) # Set as timesampled at timesample 50
# Also create a DomeLight on the scale stage to be actually able to see the cube in OV Composer
# (otherwise it's gonna be pitch black)
# Create an Xform named "Environment"
environment_xform = UsdGeom.Xform.Define(scale_stage, "/Environment")
# Create a DomeLight named "DomeLight" within "Environment"
dome_light = UsdLux.DomeLight.Define(scale_stage, "/Environment/DomeLight")
# Set DomeLight attributes
dome_light.CreateIntensityAttr(1000)
# Now create some overrides in the translate stage
sphere_override_prim = translate_stage.OverridePrim("/World/Sphere")
sphere_override : UsdGeom.Sphere = UsdGeom.Sphere(sphere_override_prim) # Treat this as a UsdGeom.Sphere
# Define radius.timeSamples as not varying (always the same) - note that an override has no type
# so we cannot assume that the "type Sphere" schemas are applied - we have to create those attributes
# ourselves so they can override whatever attribute will be found when composing these layers
radius = sphere_override.CreateRadiusAttr()
radius.Set(10, 1)
radius.Set(10, 50)
# Define xformOp:translate.timeSamples as varying!
translate_op = UsdGeom.Xform(sphere_override).AddTranslateOp()
translate_op.Set(Gf.Vec3d(0.0, 0.0, 0.0), 0)
translate_op.Set(Gf.Vec3d(150.0, 0.0, 0.0), 50)
# Export scale and translate layers to file
scale_stage.GetRootLayer().Export(BASE_DIRECTORY + "/ScaleLayer.usda")
translate_stage.GetRootLayer().Export(BASE_DIRECTORY + "/TranslateLayer.usda")
# Add the translate stage and the scale stage as sublayers to the root layer in the root stage
# Note: this would be wrong since it'll make a reference to the in-memory anonymous layer
# '@anon:0x2a0052e0:TranslateLayer.usda@' which will be freed when this interpreter exits
# root_stage.GetRootLayer().subLayerPaths.append(scale_stage.GetRootLayer().identifier)
# This is instead correct to reference a serialized layer on file (i.e. '@/tmp/TranslateLayer.usda@')
root_stage.GetRootLayer().subLayerPaths.append(BASE_DIRECTORY + "/TranslateLayer.usda")
# also add the scale stage AFTER the translate layer - ORDER IS IMPORTANT HERE!
root_stage.GetRootLayer().subLayerPaths.append(BASE_DIRECTORY + "/ScaleLayer.usda")
# Optional for demonstrative purposes: set the default prim to a prim in another layer
root_stage.SetDefaultPrim(xform.GetPrim())
# Export root layer to file
root_stage.GetRootLayer().Export(BASE_DIRECTORY + "/RootLayer.usda")
This rather verbose code listing can be executed in the Script Editor
in OV Composer. If everything works correctly, you should have three .usda
files in your /tmp
directory:
RootLayer.usda
- this will be our root layer, we will open this file with OV composer and reference the others through this.TranslateLayer.usda
- this will be the first (ORDER IS IMPORTANT) sublayer and it will contain a time-sampled override of theradius
property (which will not change from timecodes 0 to 50) and a time-sampled override of thetranslate
property (which will change continuously moving the sphere on the X axis when time flows from timecode 0 to 50).ScaleLayer.usda
- this will be the second sublayer and it will contain the definitions of the prims in the scene (i.e. theWorld
xform, theSphere
sphere and theDomeLight
light prim will be defined in this layer), plus there will be a time-sampled definition of theradius
property which will make the sphere grow from a small radius to a huge radius in 50 timecodes.
The opinion in a containing layer is always stronger than any opinions in its sublayers. E.g. the opinion for a property in the root layer is always stronger than any opinion for the same property in any sublayers of that root layer.
If you now try to open RootLayer.usda
in OV composer you will see three layers and a very small ball on the white-lit viewport:
Note that the Root Layer
is set as the Authoring Layer
, i.e. if you try to make modifications to the scene (e.g. changing the scale of the sphere), a delta will be added to the root layer (and written to the RootLayer.usda
file if you save it). The reason for this is that the /World/Sphere
prim is defined (i.e. there is a def
specifier directive in the ASCII text .usda
file) in the ScaleLayer.usda
: it is not defined in the current root layer, so if we modify anything in the scene and we force modifications to go to the Root Layer
, they will be written there as a delta, i.e. the root layer will have a stronger opinion on those properties which will be either defined or defaulted to their default values in the defining ScaleLayer
.
If we play the simulation in OV Composer we can see that the sphere slides on the X axis (we might have to move the camera to see it clearly or focus the view on the sphere prim by selecting it and pressing F
in OV Composer) from a timecode from 0 to 50 (you can better visualize this in the Window->Animation->Timeline
pane, you can even right-click on properties in the Property
pane and select Set Key
to “store” that value in the currently selected keyframe).
One side thing to note here: a Sphere
prim type is different than a prim sphere of Mesh
type, similarly a Cube
prim type is different than a cube prim of Mesh
type: a Mesh
is just a collection of points rendered together while Sphere
, Torus
, Cube
and some others are primitive prim types provided by USD and that have built-in properties that make sense for the shape in question (e.g. the radius
property for the Sphere
type). We could have structured our code above with a sphere of Mesh
type but it would have been consideraly harder and more verbose because of all the points
, normals
, etc. boilerplate. For clarity reasons, we chose to use a Sphere
prim type.
What’s interesting to see at this point is that if we run the simulation continuously and we mute the TranslateLayer
(which is higher in the layers hierarchy under the root layer), the muted opinions disappear and USD recomposes the scene graph by updating all of the overriden properties: now the ScaleLayer
has the winning opinion for the translate
and, most importantly, radius
property!
At this point you should have developed a pretty good sense of what can be accomplished with USD: non-destructive workflows, multiple scenes editing and a strongly typed and extensible system which enables applications to leverage graphical capabilities that weren’t possible before.
Composition Arcs
We already saw in the previous chapter how multiple sublayers each one with different opinions are composed together when referenced by another layer to compose a final stage (Sublayers: authoring time-sampled attributes): that is just one of the many ways USD places at your disposal to compose a scene graph and resolve which attribute value should be used amongst many.
There are other ways and operators that can be used to determine how layer stacks and their opinions are combined together, here’s an intuitive overview:
- Sublayers - we’ve seen these already in the previous section, similar to Photoshop layers. Each one can be backed by a USD file on disk and provide - according to their order in the layer stack - different opinions.
- References - they allow to put entire layers under prims to maximize prims reuse
- VariantSet - switchable states for a prim: think of multiple colors, materials and even different wheel rims for a car in a configurator scene
- Payload - lazily loaded references: these can be loaded or unloaded at runtime on user request (think LoD - Level of Detail - a high resolution model can be avoided to be loaded entirely if it makes sense on an already-quite-heavy USD scene)
- Inherits - similar to inheritance in object oriented programming: prims can inherit from other prims. Changes in the base prim reflect immediately on the prims that derive from it.
- Specializes - e.g. a
Metal
material can have some properties specialized to create aRustyMetalMaterial
. Very similar toRustyMetalMaterial
inheriting fromMetal
, but specialized properties are always the winning ones even if in some layer/other_composition we override thoseMetal
’s base properties with a stronger opinion: a bit similar to CSS’s!important
a specialization of a property cannot be overridden. Withinherit
you’re able to override a property multiple times instead. That’s the main difference.
Even if Specializes
wins over other overrides, this composition arc is the last one and weakest to be scanned. This is by design.
In this chapter we will take a deeper coding look at each of these USD composition techniques, but first an overview into their evaluation order.
LIVRPS
This acronym means L
ocal(and Sublayers), I
nherits, V
ariantSets, R
eferences, P
ayloads and S
pecializes.
This is the order in which the USD engine evaluates opinions.
When evaluating the value of an attribute on a prim or a metadata value, we iterate over PrimSpecs
(which can be thought as a partial view of a prim in a layer or rather: the part of a prim which resides in a specific layer and, together with all the other PrimSpec
s for that prim in the other layers, contributes in composing the final prim on the stage with all the ‘winning’ opinions for all attributes and metadata) in the LIVRPS order.
The attribute radius
for a type Sphere
sphere prim needs to be evaluated on the final composed stage.
- First we search for an attribute opinion in the Local/Sublayers stack (same rules as before: root layer wins, otherwise sublayers in the order in which they appear under the root layer, etc.). If we find a winning one, we abort the search and use it.
- No opinion was found? We continue by scanning the
inherits
and recursively start again from 1 with the new added layers. Specializations are ignored though. - No opinion was found? We scan
variants
and recursively start from 1. Specializations are ignored though. - No opinion was found? We scan
References
and recursively start from 1. Specializations are ignored though. - No opinion was found? We even scan the optional
Payload
s and recursively start from 1. Specializations are ignored though. - Still no opinion was found? We scan
Specializes
for an opinion.
If after point 6 still no opinion could be found, we just take the default value for that attribute/metadata.
References
In the same fashion as the sublayers example we explored before, let’s create a prim with a references
metadata that references another layer in a different USD file and open it in OV Composer (this script can be run in the Script Editor
in OV Composer directly)
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")
cube_stage : Usd.Stage = Usd.Stage.CreateInMemory("CubeLayer.usda")
# Create a Cube "Cube" prim in the cube stage and a light
xform : UsdGeom.Xform = UsdGeom.Xform.Define(cube_stage, Sdf.Path("/World"))
cube : UsdGeom.Cube = UsdGeom.Cube.Define(cube_stage, "/World/Cube")
extent = [(-50, -50, -50), (50, 50, 50)]
cube.GetExtentAttr().Set(extent)
cube.GetSizeAttr().Set(100)
# This time put the light under the "/World" prim - by referencing "/World" we will also
# import the lights as well since they're under "/World"
environment_xform = UsdGeom.Xform.Define(cube_stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(cube_stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)
# Export to file
cube_stage.GetRootLayer().Export(BASE_DIRECTORY + "/CubeLayer.usda")
# A pseudo-root prim (usually the '/' prim) exists solely to namespace all prims' paths under
# this prefix (e.g. "/World"). A UsdStage always has a pseudo-root prim (unless there was an
# error opening it)
root_prim : Usd.Prim = root_stage.GetPseudoRoot()
# Verbose to showcase API usage for "/RefPrim". Creates a generic UsdPrim (it will default
# to being an Xform)
ref_prim : Usd.Prim = root_stage.DefinePrim(str(root_prim.GetPath()) + "RefPrim")
# This would NOT work because as soon as this script finishes executing, the temporary stages
# would be destroyed (they're weakrefs). Let's open and reference the usd file for those
# layers instead
# nope: ref_prim.GetReferences().AddReference(cube_stage.GetRootLayer().identifier)
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/CubeLayer.usda")
ref_prim.GetReferences().AddReference(
loaded_layer.identifier, # which in this case it's just the relative file path string
"/World") # The prim which needs to be mapped at the ref_prim also needs to be specified
# 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")
Here’s the result in OV Composer: a prim is added to the root prim that references the outside layer (OV Composer adds a small orange arrow to the icon of the xform to indicate that the prim has a reference
metadata and that references something during the stage composition)
If we inspect the generated USD files we can see the reference
metadata
$ cat /tmp/CubeLayer.usda
#usda 1.0
def Xform "World"
{
def Cube "Cube"
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
double size = 100
}
def Xform "Environment"
{
def DomeLight "DomeLight"
{
float inputs:intensity = 1000
}
}
}
$ cat /tmp/RootLayer.usda
#usda 1.0
def "RefPrim" (
prepend references = @/tmp/CubeLayer.usda@</World> # Here it is
)
{
}
Note what happens if we save the USD file with OV Composer as flattened:
$ cat /tmp/Flattened.usda
#usda 1.0
(
doc = """Generated from Composed Stage of root layer /tmp/RootLayer.usda
"""
)
def Xform "RefPrim"
{
def Cube "Cube"
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
double size = 100
}
def Xform "Environment"
{
def DomeLight "DomeLight"
{
float inputs:intensity = 1000
}
}
}
i.e. only the composed elements are retained and the composition arcs are resolved into a single, flattened, hierarchy. This is very useful when dealing with e.g. huge Nucleus USD files which reference multiple-networks-scattered USD files and you want to save a copy (albeit probably huge) of the USD scene for debugging purposes on your local workstation: you flatten it out and save it to disk.
Note that overriding attributes works exactly as before: even if the cube
had an opinion in the original CubeLayer
# Give it a red color (quick way to apply a 'debug' color without shaders or materials - accesses primvar variables)
cube.GetPrim().CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray).Set([(1.0, 0.0, 0.0)])
# Export to file
cube_stage.GetRootLayer().Export(BASE_DIRECTORY + "/CubeLayer.usda")
we could have overridden that attribute with an override (that would appear as a delta on the referenced layer) in the root layer
# Give it a blue color override in the root layer
override_cube_prim : Usd.Prim = root_stage.OverridePrim("/RefPrim/Cube")
override_cube_attr = override_cube_prim.CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray)
override_cube_attr.Set([(0.0, 0.0, 1.0)])
# Export root stage to file
root_stage.GetRootLayer().Export(BASE_DIRECTORY + "/RootLayer.usda")
In the code above we apply a displayColor
in the original cube
stage by accessing that attribute via the UsdGeomPrimvar
schema. This is a schema that allows to access and modify attributes which are specific to geometry prims (i.e. prims like cameras and such do not implement such schema), stuff like visibility
or interpolation
are under control of this schema. It’s a quick way to visualize a simple shaded color for a geometric prim without setting a material.
The final color of the cube would have been blue: root layer wins in this case. Had the blue opinion come from a variant (lower in the LIVRPS
ordering), the reference opinion would have won.
VariantSet
Now let’s see an example of VariantSet
attribute override in action together with a reference
attribute override and let’s remember the LIVRPS
order of opinions evaluation:
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")
cube_stage : Usd.Stage = Usd.Stage.CreateInMemory("CubeLayer.usda")
# Create a Cube "Cube" prim in the cube stage and a light
xform : UsdGeom.Xform = UsdGeom.Xform.Define(cube_stage, Sdf.Path("/World"))
cube : UsdGeom.Cube = UsdGeom.Cube.Define(cube_stage, "/World/Cube")
extent = [(-50, -50, -50), (50, 50, 50)]
cube.GetExtentAttr().Set(extent)
cube.GetSizeAttr().Set(100)
environment_xform = UsdGeom.Xform.Define(cube_stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(cube_stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)
# In the cube layer (the one where the cube is defined and that will be referenced by RootLayer),
# the cube has originally a red color
cube.GetPrim().CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray).Set([(1.0, 0.0, 0.0)])
# Export to file
cube_stage.GetRootLayer().Export(BASE_DIRECTORY + "/CubeLayer.usda")
# Now as before set up a "/ReferencedCube" prim which references the "/World/Cube" in the CubeLayer
ref_prim : Usd.Prim = root_stage.DefinePrim("/ReferencedCube")
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/CubeLayer.usda")
ref_prim.GetReferences().AddReference(
loaded_layer.identifier, # which in this case it's just the relative file path string
"/World/Cube") # The prim which needs to be mapped at the ref_prim also needs to be specified
# At this point if graph composition were to end, the cube would just have an opinion in the referenced layer
# and would be a red colored one.
# This is where things get interesting: set up a variant set with sub-variants in the root layer
# (a VariantSet is like a new combo box to switch something in the prim, e.g. "colors for the car" or
# "level of damage of the car" or "types of wheel rims")
variant_set = ref_prim.GetVariantSets().AddVariantSet("differentColorsVariantSet")
# A variant is owned by a variant set, think of these as the different items that you can choose from the
# combo box (which is the VariantSet)
variant_set.AddVariant("blueColor")
variant_set.AddVariant("greenColor")
# Set the active variant
variant_set.SetVariantSelection("blueColor")
with variant_set.GetVariantEditContext(): # Set up an edit context (this is just like changing the authoring layer)
# Give the cube in the root layer a blue color
override_cube_prim : Usd.Prim = root_stage.OverridePrim("/ReferencedCube")
override_cube_attr = override_cube_prim.CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray)
override_cube_attr.Set([(0.0, 0.0, 1.0)])
variant_set.SetVariantSelection("greenColor")
with variant_set.GetVariantEditContext():
# Give the cube in the root layer a green color
override_cube_prim : Usd.Prim = root_stage.OverridePrim("/ReferencedCube")
override_cube_attr = override_cube_prim.CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray)
override_cube_attr.Set([(0.0, 1.0, 0.0)])
# Select the active variant after editing them
variant_set.SetVariantSelection("blueColor")
# 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")
If you execute this code, you should get a reference prim but with a blue color: even though the red color is authored and there’s an opinion in the referenced layer, the Variant
opinion is stronger in the LIVRPS
ordering and therefore wins (note that OV Composer has a very handy combo box in the Properties
pane that shows the VariantSet
along with its current active Variant
)
USDA code is also provided for completeness
$ cat /tmp/RootLayer.usda
#usda 1.0
def "ReferencedCube" (
prepend references = @/tmp/CubeLayer.usda@</World/Cube>
variants = { # Active variants for multiple VariantSets are stored in the 'variants' metadata
string differentColorsVariantSet = "blueColor"
}
prepend variantSets = "differentColorsVariantSet" # the additional VariantSets
)
{
variantSet "differentColorsVariantSet" = { # Definition for the VariantSet which will override displayColor
"blueColor" {
color3f[] primvars:displayColor = [(0, 0, 1)]
}
"greenColor" {
color3f[] primvars:displayColor = [(0, 1, 0)]
}
}
}
$ cat /tmp/CubeLayer.usda
#usda 1.0
def Xform "World"
{
def Cube "Cube"
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
color3f[] primvars:displayColor = [(1, 0, 0)] # This opinion will be weaker due to LIVRPS composition
double size = 100
}
def Xform "Environment"
{
def DomeLight "DomeLight"
{
float inputs:intensity = 1000
}
}
}
Payload
Let’s explore an example for payloads: reference-like arcs which can be loaded on demand and meant to be used with optional or resources-heavy assets.
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
# Create the usual cube stage, let's pretend this is a VERY HEAVY USD stage
# full of high-poly high-textured huge assets
cube_stage : Usd.Stage = Usd.Stage.CreateInMemory("CubeLayer.usda")
xform : UsdGeom.Xform = UsdGeom.Xform.Define(cube_stage, Sdf.Path("/World"))
cube : UsdGeom.Cube = UsdGeom.Cube.Define(cube_stage, "/World/Cube")
extent = [(-50, -50, -50), (50, 50, 50)]
cube.GetExtentAttr().Set(extent)
cube.GetSizeAttr().Set(100)
# This time put the light under the "/World" prim - by referencing "/World" we will also
# import the lights as well since they're under "/World"
environment_xform = UsdGeom.Xform.Define(cube_stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(cube_stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)
# Do not define a defaultPrim - read more about this later
# cube_stage.SetDefaultPrim(xform.GetPrim())
# Export to file
cube_stage.GetRootLayer().Export(BASE_DIRECTORY + "/CubeLayer.usda")
# Create the root stage with a payload prim referencing the "heavy" cube stage
root_stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")
payload_prim : Usd.Prim = root_stage.DefinePrim("/PayloadPrim")
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/CubeLayer.usda")
# Note that if we don't specify the target prim for the payload (i.e. "/World"), it will
# try to look for the 'defaultPrim' metadata in the loaded_layer (or fail to set a target if
# that metadata isn't even present). Layer-level metadata are just like prim metadata and are
# set at the beginning of a USD file between parentheses, e.g.
#
# $ cat ./usd_file.usda
# #usda 1.0
# (
# defaultPrim = "/World"
# )
# ..
payload_prim.GetPayloads().AddPayload(loaded_layer.identifier, "/World")
# 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
# Note the 'LOAD_NONE' which instructs Kit NOT to open Payloads.
omni.usd.get_context().open_stage(BASE_DIRECTORY + "/RootLayer.usda", load_set=omni.usd.UsdContextInitialLoadSet.LOAD_NONE)
Pay attention to the named parameter load_set=omni.usd.UsdContextInitialLoadSet.LOAD_NONE
that we added to the kit open_stage
invocation: that instructs Kit to NOT load stage payloads during opening (otherwise it would have been loaded and resolved automatically).
If we run the code above in the Script Editor
we’ll see a blank stage with a payload prim. Loading the payload can happen either via code
root_stage.Load("/PayloadPrim")
root_stage.Unload("/PayloadPrim")
or directly in the OV Composer UI through a checkbox which will triger the payload prim loading
Inherits
Let’s take a look at an example where a prim with a class
specifier (you can find this in the .usda
text file in the same prim definition lines where you used to read def
or over
: the class
specifier indicates that a prim is to be considered as a base class for other prims) is inherited by other prims. A prim can inherit from multiple other prims at the same time.
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 and a sphere 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)
sphere : UsdGeom.Sphere = UsdGeom.Sphere.Define(root_stage, "/World/Sphere")
sphere.GetExtentAttr().Set(extent)
# Just to beautify the scene: move the sphere a bit and scale it
UsdGeom.Xformable(sphere.GetPrim()).AddTranslateOp().Set(Gf.Vec3d(100, 0, 0))
UsdGeom.Xformable(sphere.GetPrim()).AddScaleOp().Set(Gf.Vec3d(50, 50, 50))
environment_xform = UsdGeom.Xform.Define(root_stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(root_stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)
# The base class for all red prims: adds a red color `primvars:displayColor` attribute
red_prims : Usd.Prim = root_stage.CreateClassPrim("/_red_prims")
red_prims.CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray).Set([(1.0, 0.0, 0.0)])
# Make both the sphere and the cube inherit from the _red_prims class prim
inherits: Usd.Inherits = cube.GetPrim().GetInherits()
inherits.AddInherit(red_prims.GetPath())
inherits: Usd.Inherits = sphere.GetPrim().GetInherits()
inherits.AddInherit(red_prims.GetPath())
# 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")
This will make both the cube and the sphere prims inherit from the _red_prims
class prim, causing them both to inherit the opinion for the color attribute
This is the generated USDA for the stage, note the inherits
metadata for the derived prims and the class
specifier for the _red_prims
$ cat /tmp/RootLayer.usda
#usda 1.0
def Xform "World"
{
def Cube "Cube" (
prepend inherits = </_red_prims>
)
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
double size = 100
}
def Sphere "Sphere" (
prepend inherits = </_red_prims>
)
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
float3 xformOp:scale = (50, 50, 50)
double3 xformOp:translate = (100, 0, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
}
def Xform "Environment"
{
def DomeLight "DomeLight"
{
float inputs:intensity = 1000
}
}
}
class "_red_prims"
{
custom color3f[] primvars:displayColor = [(1, 0, 0)]
}
Specializes
Understanding specializes
requires a bit more work to build intuition. We’ve already said that it behaves similarly to inherits
and that it has the weakest opinion strength in the LIVRPS
ordering, but that it can prevent override of its attributes if the attributes in a base class are overridden.
To demonstrate this, let’s take a look at a more complex example where RootLayer.usda
defines a /ReferencedWorld
prim which references entirely another USD file CubeAndSphereLayer.usda
. Inside CubeAndSphereLayer.usda
there are a cube and a sphere and they inherit from two different prims: _base_prims
which gives all prims a base red color and _rusty_prims
which give all prims a rust-like color (light brown-orange-ish). _rusty_prims
is not a derived class of _base_prims
but rather a specializes
of it: this means that if the _base_prims
class changes (e.g. by switching to a green color instead of a red color), the properties authored in _rusty_prims
will override those in _base_prims
(but any non-authored property will still be inherited). And that’s exactly what happens in RootLayer.usda
: the _base_prims
class prim is overridden and the color set to green. This will cause the cube, which inherits from _base_prims
, to have the opinion overridden and change color to green. The sphere instead, which inherited from _rusty_prims
, will not care for the green color and remain rusty.
After executing the code below, inspecting the USDA files will probably be a lot more intuitive in understanding what’s happening
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
# Add a cube and a sphere to a CubeAndSphereLayer stage
cube_and_sphere_stage : Usd.Stage = Usd.Stage.CreateInMemory("CubeAndSphereLayer.usda")
xform : UsdGeom.Xform = UsdGeom.Xform.Define(cube_and_sphere_stage, Sdf.Path("/World"))
cube : UsdGeom.Cube = UsdGeom.Cube.Define(cube_and_sphere_stage, "/World/Cube")
extent = [(-50, -50, -50), (50, 50, 50)]
cube.GetExtentAttr().Set(extent)
cube.GetSizeAttr().Set(100)
sphere : UsdGeom.Sphere = UsdGeom.Sphere.Define(cube_and_sphere_stage, "/World/Sphere")
sphere.GetExtentAttr().Set(extent)
UsdGeom.Xformable(sphere.GetPrim()).AddTranslateOp().Set(Gf.Vec3d(100, 0, 0))
UsdGeom.Xformable(sphere.GetPrim()).AddScaleOp().Set(Gf.Vec3d(50, 50, 50))
environment_xform = UsdGeom.Xform.Define(cube_and_sphere_stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(cube_and_sphere_stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)
# The base class for all prims: adds a red color `primvars:displayColor` attribute
base_prims : Usd.Prim = cube_and_sphere_stage.CreateClassPrim("/World/_base_prims")
base_prims.CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray).Set([(1.0, 0.0, 0.0)])
# Make the cube inherit from the _base_prims class prim (so it gets a red color)
inherits: Usd.Inherits = cube.GetPrim().GetInherits()
inherits.AddInherit(base_prims.GetPath())
# Define a 'rusty_prims` prim which has an opinion for the color of rusty metal and SPECIALIZES (*not* inherits)
# the _base_prims prim
rusty_prims : Usd.Prim = cube_and_sphere_stage.DefinePrim("/World/_rusty_prims")
rusty_prims.CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray).Set([(0.718, 0.255, 0.055)])
specializes: Usd.Specializes = rusty_prims.GetSpecializes()
specializes.AddSpecialize(base_prims.GetPath())
# Make the sphere inherit from the _rusty_prims specialization prim
inherits: Usd.Inherits = sphere.GetPrim().GetInherits()
inherits.AddInherit(rusty_prims.GetPath())
# Export stage to file
cube_and_sphere_stage.GetRootLayer().Export(BASE_DIRECTORY + "/CubeAndSphereLayer.usda")
# Now create the RootLayer stage where the final scene will be composed: this will reference the entire CubeAndSphereLayer
# with ONE difference: it will have an override for the color of _base_prims-inheriting prims. This will cause all prims
# to get a green color EXCEPT for those inheriting from the rusty specialization!
root_stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")
ref_prim : Usd.Prim = root_stage.DefinePrim("/ReferencedWorld")
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/CubeAndSphereLayer.usda")
ref_prim.GetReferences().AddReference(
loaded_layer.identifier, # which in this case it's just the relative file path string
"/World") # The prim which needs to be mapped at the ref_prim also needs to be specified
_base_prim_override = root_stage.OverridePrim("/ReferencedWorld/_base_prims")
_base_prim_override.CreateAttribute("primvars:displayColor", Sdf.ValueTypeNames.Color3fArray).Set([(0.0, 1.0, 0.0)])
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")
And here’s the generated .usda
files:
$ cat /tmp/CubeAndSphereLayer.usda
#usda 1.0
def Xform "World"
{
def Cube "Cube" (
prepend inherits = </World/_base_prims>
)
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
double size = 100
}
def Sphere "Sphere" (
prepend inherits = </World/_rusty_prims>
)
{
float3[] extent = [(-50, -50, -50), (50, 50, 50)]
float3 xformOp:scale = (50, 50, 50)
double3 xformOp:translate = (100, 0, 0)
uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
}
def Xform "Environment"
{
def DomeLight "DomeLight"
{
float inputs:intensity = 1000
}
}
class "_base_prims"
{
custom color3f[] primvars:displayColor = [(1, 0, 0)]
# Added to showcases that specializes inherits whatever it does *not* override
int someCustomProperty = 42;
}
def "_rusty_prims" (
prepend specializes = </World/_base_prims>
)
{
custom color3f[] primvars:displayColor = [(0.718, 0.255, 0.055)]
# Note that this inherits someCustomProperty, and if that changes in _base_prims,
# so it will here as well since we do _not_ have an overriding opinion on it.
}
}
$ cat /tmp/RootLayer.usda
#usda 1.0
def "ReferencedWorld" (
prepend references = @/tmp/CubeAndSphereLayer.usda@</World>
)
{
over "_base_prims"
{
custom color3f[] primvars:displayColor = [(0, 1, 0)]
}
}
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).
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 over
ride 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 over
ride 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"
PrimSpec
s 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 PrimSpec
s 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.
Traversing
Traversing means ‘scanning’ all (or a subset of) the prims on a stage.
The simplest form of traversal is:
def traverse_and_print_prim_paths(stage):
for prim in stage.Traverse():
print(prim.GetPath())
traverse_and_print_prim_paths(stage)
You can test this out by generating a random scene hierarchy of prims (this is mostly boilerplate code)
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf
import omni.usd
import carb
import random
import string
# Function to generate a random identifier and make the sibling prim names unique
def generate_random_identifier(length=5):
return ''.join(random.choice(string.ascii_letters) for _ in range(length))
BASE_DIRECTORY = "/tmp" # This is where the .usda files will be saved
stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")
xform : UsdGeom.Xform = UsdGeom.Xform.Define(stage, Sdf.Path("/World"))
environment_xform = UsdGeom.Xform.Define(stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)
# Function to generate a random scene hierarchy
def generate_random_hierarchy(parent_xform, depth):
if depth <= 0:
return
for _ in range(random.randint(1, 4)): # Randomly choose 1 to 4 children
child_name = "Child_" + generate_random_identifier() # Add a random identifier
child_xform = UsdGeom.Xform.Define(stage, parent_xform.GetPath().AppendChild(child_name))
if random.choice([True, False]): # Randomly choose to add cube or sphere
shape_name = "Cube_" + generate_random_identifier() # Add a random identifier
cube = UsdGeom.Cube.Define(stage, child_xform.GetPath().AppendChild(shape_name))
extent = [(random.uniform(-50, 50), random.uniform(-50, 50), random.uniform(-50, 50)),
(random.uniform(-50, 50), random.uniform(-50, 50), random.uniform(-50, 50))]
cube.GetExtentAttr().Set(extent)
cube.GetSizeAttr().Set(random.uniform(10, 100))
# Add random small offsets from the center
offset = Gf.Vec3d(random.uniform(-100, 100), random.uniform(-100, 100), random.uniform(-100, 100))
cube_center = Gf.Vec3d(0, 0, 0) # Center of the scene
cube_center += offset
UsdGeom.Xformable(cube.GetPrim()).AddTranslateOp().Set(cube_center)
else:
shape_name = "Sphere_" + generate_random_identifier() # Add a random identifier
sphere = UsdGeom.Sphere.Define(stage, child_xform.GetPath().AppendChild(shape_name))
extent = [(random.uniform(-50, 50), random.uniform(-50, 50), random.uniform(-50, 50)),
(random.uniform(-50, 50), random.uniform(-50, 50), random.uniform(-50, 50))]
sphere.GetExtentAttr().Set(extent)
# Add random small offsets from the center
offset = Gf.Vec3d(random.uniform(-100, 100), random.uniform(-100, 100), random.uniform(-100, 100))
sphere_center = Gf.Vec3d(0, 0, 0) # Center of the scene
sphere_center += offset
UsdGeom.Xformable(sphere.GetPrim()).AddTranslateOp().Set(sphere_center)
UsdGeom.Xformable(sphere.GetPrim()).AddScaleOp().Set(Gf.Vec3d(random.uniform(10, 100),
random.uniform(10, 100),
random.uniform(10, 100)))
generate_random_hierarchy(child_xform, depth - 1)
# Generate the random hierarchy with a maximum depth of 5-6
generate_random_hierarchy(xform, random.randint(5, 6))
# Traversal!
####################################
def traverse_and_print_prim_paths(stage):
for prim in stage.Traverse():
print(prim.GetPath())
traverse_and_print_prim_paths(stage)
####################################
stage.GetRootLayer().Export(BASE_DIRECTORY + "/RootLayer.usda")
omni.usd.get_context().open_stage(BASE_DIRECTORY + "/RootLayer.usda")
Other common ways of scanning prims is by searching for specific types of prims (e.g. of Sphere
type)
from typing import List, Type
def find_prims_by_type(stage: Usd.Stage, prim_type: Type[Usd.Typed]) -> List[Usd.Prim]:
found_prims = [x for x in stage.Traverse() if x.IsA(prim_type)]
return found_prims
prims: List[Usd.Prim] = find_prims_by_type(stage, UsdGeom.Sphere)
print(prims)
There are also UsdPrim::GetChild
functions and UsdPrim::GetChildren() and UsdPrim::GetAllChildren() (the former returns the active, loaded, defined and non-abstract children of a prim, the latter returns any child).
Instance traversals
Recall that normal traversals don’t work with instances, in case you need to traverse instance proxies, you should use Usd.TraverseInstanceProxies
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
parking_lot_stage : Usd.Stage = Usd.Stage.CreateInMemory("ParkingLot.usda")
car_stage : Usd.Stage = Usd.Stage.CreateInMemory("Car.usda")
# Car stage
xform : Usd.Prim = car_stage.DefinePrim(Sdf.Path("/Car"))
xform.GetPrim().CreateAttribute("color", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0, 0, 0))
body : UsdGeom.Mesh = UsdGeom.Mesh.Define(car_stage, Sdf.Path("/Car/Body"))
body.GetPrim().CreateAttribute("color", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0, 0, 0))
Door : UsdGeom.Mesh = UsdGeom.Mesh.Define(car_stage, Sdf.Path("/Car/Door"))
car_stage.GetRootLayer().Export(BASE_DIRECTORY + "/Car.usda")
# Parking Lot stage
xform : Usd.Prim = parking_lot_stage.DefinePrim(Sdf.Path("/ParkingLot"))
car1_prim : Usd.Prim = parking_lot_stage.DefinePrim("/ParkingLot/Car_1")
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/Car.usda")
car1_prim.GetReferences().AddReference(loaded_layer.identifier, "/Car")
car2_prim : Usd.Prim = parking_lot_stage.DefinePrim("/ParkingLot/Car_2")
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/Car.usda")
car2_prim.GetReferences().AddReference(loaded_layer.identifier, "/Car")
car3_prim : Usd.Prim = parking_lot_stage.DefinePrim("/ParkingLot/Car_3")
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/Car.usda")
car3_prim.GetReferences().AddReference(loaded_layer.identifier, "/Car")
# Mark with metadata Car_1 and Car_2 as instanceable, i.e. "these reference prims can be reused"
# while Car_3 is not marked
car1_prim.SetInstanceable(True)
car2_prim.SetInstanceable(True)
# Traversal!
####################################
# This will only return non-instance prims
# def traverse_and_print_prim_paths(stage):
# for prim in stage.Traverse():
# print(prim.GetPath())
# traverse_and_print_prim_paths(parking_lot_stage)
# This will allow you to traverse instanced prims thanks to instance proxies!
def traverse_and_print_all_prim_paths(stage):
for prim in stage.Traverse(Usd.TraverseInstanceProxies()):
print(prim.GetPath())
traverse_and_print_all_prim_paths(parking_lot_stage)
####################################
# Export root layer to file
parking_lot_stage.GetRootLayer().Export(BASE_DIRECTORY + "/ParkingLot.usda")
omni.usd.get_context().open_stage(BASE_DIRECTORY + "/ParkingLot.usda")
Kind
The kind
metadata can be authored via UsdModelAPI and it is often used to tag a prim:
def Xform "CameraMesh" (
hide_in_stage_window = true # some metadata
kind = "subcomponent" # kind metadata
no_delete = true # some more metadata
)
{
# usual rest of prim properties
bool primvars:doNotCastShadows = 1
bool primvars:omni:kit:isGizmo = 1
...
}
and is mainly used for two things:
-
During traversals you might want to detect where a complete 3D model hierarchy starts (i.e. the topmost parent of the self-contained 3D asset model), or where a custom-tagged sub-hierarchy of prims starts
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf, Kind import omni.usd import carb # Function to generate a random identifier and make the sibling prim names unique def generate_random_identifier(length=5): return ''.join(random.choice(string.ascii_letters) for _ in range(length)) BASE_DIRECTORY = "/tmp" # This is where the .usda files will be saved stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda") xform : UsdGeom.Xform = UsdGeom.Xform.Define(stage, Sdf.Path("/World")) environment_xform = UsdGeom.Xform.Define(stage, "/World/Environment") dome_light = UsdLux.DomeLight.Define(stage, "/World/Environment/DomeLight") dome_light.CreateIntensityAttr(1000) model_prim = stage.DefinePrim("/World/ThisIsAModel", "Xform") model_API = Usd.ModelAPI(model_prim) model_API.SetKind(Kind.Tokens.model) custom_prim = stage.DefinePrim("/World/ThisIsACustomKind", "Xform") model_API = Usd.ModelAPI(custom_prim) model_API.SetKind("custom") """ prim /World/ThisIsAModel has kind: model prim /World/ThisIsACustomKind has kind: custom """ for prim in stage.Traverse(): kind = Usd.ModelAPI(prim).GetKind() if kind: print(f"prim {str(prim.GetPath())} has kind: {kind}") stage.GetRootLayer().Export(BASE_DIRECTORY + "/RootLayer.usda") omni.usd.get_context().open_stage(BASE_DIRECTORY + "/RootLayer.usda")
There are also
UsdPrim::IsModel
andUsdPrim::IsGroup
predicates that can be used during stage traversals (since they’re very commonly used). -
DCCs (digital content creation programs) that use USD might have a custom UI selection mode to facilitate the user in selecting something in their viewport. OV Composer uses this to switch between a
select_any_prim_under_the_cursor
and aselect_only_model_prims_under_the_cursor
This is better documented in the selection modes documentation for OV Composer; the same applies for any other DCC which uses USD and the kind selection mode switch.
If you’re developing a USD Omniverse connector or working in USD within a DCC you can define your own tags and register them via the plugin system. The default ones in OV Composer have usually the following meaning
Kind | Description |
---|---|
model | This shouldn’t be used directly as it’s the base kind for all models |
assembly | In a USD file representing a scene this is usually the kind of the topmost prim. This kind should be used for published (i.e. shared with other artists) assets which aggregate other published components or assemblies together. Assemblies can sometimes override materials on individual components to better visually integrate them together. |
group | A group of 3D models. These are usually the intermediate Xforms under an assembly. It shouldn’t be used on the root of published assets since they’re helper kinds. |
component | The most basic asset published in a USD pipeline: it can be at the root prim of an asset and should not reference external geometries or models. The main difference between an assembly and a component is that an assembly can be made of more components and reference outside stuff as well. A component is self-contained. |
subcomponent | A subcomponent is usually a Xformable prim within a component model intentionally made available to apply some transformation. |
This is a ten-thousand feet overview but if you’re interested into learning more there are in-depth documents in the usd-wg repo on the subject.
Instancing
Instancing allows you to reuse parts of a USD hierarchy so that those parts get loaded into memory only once (reducing memory usage and increasing performances of your USD stage scene).
The official OpenUSD documentation has a pretty good chapter on instancing
Let us create an example similar to the one found in the official docs: a ParkingLot.usda
file and a Car.usda
file: the idea is to only load the (pretend expensive) assets of Car
once and reuse them for multiple cars in the ParkingLot
scene.
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
parking_lot_stage : Usd.Stage = Usd.Stage.CreateInMemory("ParkingLot.usda")
car_stage : Usd.Stage = Usd.Stage.CreateInMemory("Car.usda")
# Car stage
xform : Usd.Prim = car_stage.DefinePrim(Sdf.Path("/Car"))
xform.GetPrim().CreateAttribute("color", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0, 0, 0))
body : UsdGeom.Mesh = UsdGeom.Mesh.Define(car_stage, Sdf.Path("/Car/Body"))
body.GetPrim().CreateAttribute("color", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0, 0, 0))
Door : UsdGeom.Mesh = UsdGeom.Mesh.Define(car_stage, Sdf.Path("/Car/Door"))
car_stage.GetRootLayer().Export(BASE_DIRECTORY + "/Car.usda")
# Parking Lot stage
xform : Usd.Prim = parking_lot_stage.DefinePrim(Sdf.Path("/ParkingLot"))
car1_prim : Usd.Prim = parking_lot_stage.DefinePrim("/ParkingLot/Car_1")
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/Car.usda")
car1_prim.GetReferences().AddReference(loaded_layer.identifier, "/Car")
car2_prim : Usd.Prim = parking_lot_stage.DefinePrim("/ParkingLot/Car_2")
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/Car.usda")
car2_prim.GetReferences().AddReference(loaded_layer.identifier, "/Car")
car3_prim : Usd.Prim = parking_lot_stage.DefinePrim("/ParkingLot/Car_3")
loaded_layer = Sdf.Layer.FindOrOpen(BASE_DIRECTORY + "/Car.usda")
car3_prim.GetReferences().AddReference(loaded_layer.identifier, "/Car")
# Mark with metadata Car_1 and Car_2 as instanceable, i.e. "these reference prims can be reused"
# while Car_3 is not marked
car1_prim.SetInstanceable(True)
car2_prim.SetInstanceable(True)
# Export root layer to file
parking_lot_stage.GetRootLayer().Export(BASE_DIRECTORY + "/ParkingLot.usda")
omni.usd.get_context().open_stage(BASE_DIRECTORY + "/ParkingLot.usda")
$ cat /tmp/Car.usda && cat /tmp/ParkingLot.usda
#usda 1.0
def "Car"
{
custom color3f color = (0, 0, 0)
def Mesh "Body"
{
custom color3f color = (0, 0, 0)
}
def Mesh "Door"
{
}
}
#usda 1.0
def "ParkingLot"
{
def "Car_1" (
instanceable = true
prepend references = @/tmp/Car.usda@</Car>
)
{
}
def "Car_2" (
instanceable = true
prepend references = @/tmp/Car.usda@</Car>
)
{
}
def "Car_3" (
prepend references = @/tmp/Car.usda@</Car>
)
{
}
}
Let’s take a look at the stage in OV Composer
The icons in USD Composer help us identifying what a prim is made of: the orange/brown arrow on Car_1
indicates that the prim has a reference to another prim (just as we saw before). The new part is the I
icon which is present for Car_1
and Car_2
but not Car_3
: this is the I
nstanceable flag which is also exposed as a checkbox in the Property
panel.
The user marks prims as instanceable which means “everything referenced and below this prim can be reused if there are multiple prims that are resolved to use that very same location”. The grayed out color means that no property can be changed for an instanceable prim (while prims under Car_3
are normal non-instance prims so they can have their properties edited as usual). If you want to change some properties of instance prims you’ll have to de-instanceable the parent prim (that will make sure that you have a USD prim you can write to, but it’ll cost more in terms of performances and memory, of course).
There are APIs to check if a prim is an instance or not (IsInstance
) and even to query the Prototypes
: a prototype prim is a special prim that gets created by the USD scene composition engine when there are instanceable prims in the stage. It is invisible to the normal OV Composer stage panel since it’s an internal USD detail and users aren’t supposed to dabble with them and it gets usually created in root-paths like /__Prototype_1
with internal names. They are treated specially because they represent, in this example, the /Car
hierarchy which is read-only copied to all instanceable prims referencing it. Prototypes are considered ‘siblings’ to the pseudo-root and have no metadata nor properties.
One could use the code below to query prototypes and perform other manipulations on instanceable prims (you can paste this in the Script Editor
once the scene above has been created and opened)
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf
import omni.usd
import carb
stage = omni.usd.get_context().get_stage()
car_1 = stage.GetPrimAtPath('/ParkingLot/Car_1')
print(car_1.IsInstance()) # True
# Consumers can query the instance's prototype for its child prims.
# this prints '[Usd.Prim(</__Prototype_1/Body>), Usd.Prim(</__Prototype_1/Door>)]'
print(car_1.GetPrototype().GetChildren())
# this prints '[Usd.Prim(</__Prototype_1>), Usd.Prim(</__Prototype_1/Body>), Usd.Prim(</__Prototype_1/Door>)]'
print(list(Usd.PrimRange(car_1.GetPrototype())))
Instance proxies
An instance proxy is a prim that doesn’t really exist in the scenegraph but is created when some APIs are used to allow users to traverse the hierarchy and reason on the scenegraph in an easier way rather than dealing with prototype prims.
A normal prims traversal for the entire stage would be
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf
import omni.usd
import carb
stage = omni.usd.get_context().get_stage()
for prim in stage.Traverse():
print(prim)
and this would pick up Car_1
and Car_2
which are prims marked as instanceable, but nothing below those prims since it’s instanced from the invisible prototype prim (which is also not traversed with a normal traversal)
Usd.Prim(</ParkingLot>)
Usd.Prim(</ParkingLot/Car_1>) # nothing below this
Usd.Prim(</ParkingLot/Car_2>) # nothing below this
Usd.Prim(</ParkingLot/Car_3>) # this is not instanceable and everything below is NOT an instance
Usd.Prim(</ParkingLot/Car_3/Body>) # so they get printed
Usd.Prim(</ParkingLot/Car_3/Door>) # normally
# this is session layer stuff added by OV Composer
Usd.Prim(</OmniverseKit_Persp>)
Usd.Prim(</OmniverseKit_Front>)
Usd.Prim(</OmniverseKit_Top>)
Usd.Prim(</OmniverseKit_Right>)
Usd.Prim(</OmniKit_Viewport_LightRig>)
Usd.Prim(</OmniKit_Viewport_LightRig/Lights>)
Usd.Prim(</OmniKit_Viewport_LightRig/Lights/DomeLight>)
Usd.Prim(</OmniKit_Viewport_LightRig/Lights/DistantLight>)
Usd.Prim(</Render>)
Usd.Prim(</Render/RenderProduct_omni_kit_widget_viewport_ViewportTexture_0>)
Usd.Prim(</Render/Vars>)
Usd.Prim(</Render/Vars/LdrColor>)
to enable instance proxies traversal the following code can be used
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf
import omni.usd
import carb
stage = omni.usd.get_context().get_stage()
for prim in stage.Traverse(Usd.TraverseInstanceProxies(Usd.PrimIsActive and Usd.PrimIsDefined and Usd.PrimIsLoaded)):
print(prim)
Usd.Prim(</ParkingLot>)
Usd.Prim(</ParkingLot/Car_1>)
Usd.Prim(</ParkingLot/Car_1/Body>) # now the instance proxies are visible
Usd.Prim(</ParkingLot/Car_1/Door>) # all of them
Usd.Prim(</ParkingLot/Car_2>)
Usd.Prim(</ParkingLot/Car_2/Body>) # ditto
Usd.Prim(</ParkingLot/Car_2/Door>) # ditto
Usd.Prim(</ParkingLot/Car_3>)
Usd.Prim(</ParkingLot/Car_3/Body>)
Usd.Prim(</ParkingLot/Car_3/Door>)
Usd.Prim(</OmniverseKit_Persp>)
Usd.Prim(</OmniverseKit_Front>)
...
There are similar APIs to query the children prims of a specific prim only and not traverse all of the stage in depth-first order and they similarly accept predicates that indicate whether they should iterate over instance proxies or not, e.g.
car_1.GetFilteredChildren(Usd.TraverseInstanceProxies())
[Usd.Prim(</ParkingLot/Car_1/Body>), Usd.Prim(</ParkingLot/Car_1/Door>)]
Point Instancers
There is also another kind of instancing in USD: PointInstancer
s. The UsdGeomPointInstancer
class receives a variable number of geometries (classes derived by UsdGeom
), creates a prototype prim (similarly to the instanceable method) and sets USD rel
relationships (which are the other kind of properties, together with attributes) to point to those geometries. It also receives an attribute array of integers which are the indices (starting from 0) for which ones of those geometries should be rendered: this attribute also gives the order in which the relationships and another attribute that specifies different positions for each geometry, are to be combined.
Here is a simple explicative example from the developer-office-hours repository of USD code samples (the reader is highly encouraged to take a look at the listings contained therein as well):
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf
import omni.usd
import carb
stage = omni.usd.get_context().get_stage()
# Create a point instancer with three targets: a cube, a sphere and a cone.
# A 'UsdGeom.Scope' container is provided for clarity and better visual grouping in the hierarchy.
prim_path = Sdf.Path("/World/MyInstancer")
instancer: UsdGeom.PointInstancer = UsdGeom.PointInstancer.Define(stage, prim_path)
proto_container = UsdGeom.Scope.Define(stage, prim_path.AppendPath("Prototypes"))
shapes = []
shapes.append(UsdGeom.Cube.Define(stage, proto_container.GetPath().AppendPath("Cube")))
shapes.append(UsdGeom.Sphere.Define(stage, proto_container.GetPath().AppendPath("Sphere")))
shapes.append(UsdGeom.Cone.Define(stage, proto_container.GetPath().AppendPath("Cone")))
instancer.CreatePositionsAttr([Gf.Vec3f(0, 0, 0), Gf.Vec3f(2, 0, 0), Gf.Vec3f(4, 0, 0)])
instancer.CreatePrototypesRel().SetTargets([shape.GetPath() for shape in shapes])
instancer.CreateProtoIndicesAttr([0, 1, 2])
$ cat /tmp/PointInstancer.usda
#usda 1.0
()
def Xform "World"
{
def PointInstancer "MyInstancer"
{
point3f[] positions = [(0, 0, 0), (2, 0, 0), (4, 0, 0)]
int[] protoIndices = [0, 1, 2]
rel prototypes = [
</World/MyInstancer/Prototypes/Cube>,
</World/MyInstancer/Prototypes/Sphere>,
</World/MyInstancer/Prototypes/Cone>,
]
def Scope "Prototypes"
{
def Cube "Cube"
{
}
def Sphere "Sphere"
{
}
def Cone "Cone"
{
}
}
}
def DomeLight "DomeLight" ()
{
float inputs:intensity = 1000
}
}
List Composition
In USD there is a concept called List Editing which can be seen in action when defining metadata (e.g. apiSchemas
for a prim):
$ cat ./MyUSDLayer.usda
#usda 1.0
()
over "Kitchen_set"
{
over "FlowerPotA_5" (
prepend apiSchemas = ["PhysicsRigidBodyAPI"] # List-Editing in action
)
{}
}
We’ve already seen the prepend
keyword but let’s take a deeper look at its meaning.
A list can make use of append
and/or prepend
and/or delete
and/or explicit
actions when defining its items to change the order of the items (remember that in composition, just like in the layers stack, the order is important: in the layer stack, the higher the layer, the stronger the opinions - the root layer usually wins in fact). The same applies to these lists in USD: ["/cube", "/sphere", "/cone"]
here in this list /cube
has a stronger opinion than /cone
. If you keep order in mind, the official documentation explanation makes sense:
append another value or values to the back of the resolved list; if the values already appear in the resolved list, they will be reshuffled to the back. An appended composition arc in a stronger layer of a LayerStack will therefore be weaker than all of the arcs of the same type appended from weaker layers, by default; however, the Usd API’s for adding composition arcs give you some flexibility here.
delete a value or values from the resolved list. A “delete” statement can be speculative, that is, it is not an error to attempt to delete a value that is not present in the resolved list.
prepend another value or values on the front of the resolved list; if the values already appear in the resolved list, they will be shuffled to the front. A prepended composition arc in a weaker layer of a LayerStack will still be stronger than any arcs of the same type that are appended from stronger layers.
reset to explicit , which is an “unqualified” operation, as in references = @myFile.usd@. This causes the resolved list to be reset to the provided value or values, ignoring all list ops from weaker layers.
To summarize in simpler words: if you have a stronger layer (think the root layer at the top of the layer stack) and it defines some list property in a append
field, it means that the items must go to the end of the list and therefore the root layer wants that value to be weaker in the hierarchy (not deleted, but weaker than any other value of the same type).
If the bottom layer (the weakest) adds an item to a list in the prepend
field it means that weakest layer wants that item to be as strong as possible. But since it comes from the weakest layer, only if no other layer has any other opinion on it that is going to happen. If a higher layer (which is stronger) re-defines it in the same prepend
, the game is already lost for the weakest layer: its item will not be at the forefront of the list because another layer added something else (unless it added the same exact value). delete
does exactly what you think: it deletes an item from the list, and this is still subject to whatever precedence the layer has when applying it on a list. explicit
causes the list to be reset to whatever values are explicitly provided in there - but still: this is only as good as the precedence of the currently applying layer.
Here’s a coding example:
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
# Create a temporary stage in memory for the root layer and another sublayer
top_layer : Usd.Stage = Usd.Stage.CreateInMemory("TopLayer.usda")
middle_layer : Usd.Stage = Usd.Stage.CreateInMemory("MiddleLayer.usda")
bottom_layer : Usd.Stage = Usd.Stage.CreateInMemory("BottomLayer.usda")
sphere : UsdGeom.Sphere = UsdGeom.Sphere.Define(top_layer, Sdf.Path("/World/Sphere"))
sphere_prim = sphere.GetPrim()
middle_override_prim = middle_layer.OverridePrim("/World/Sphere")
bottom_override_prim = bottom_layer.OverridePrim("/World/Sphere")
# Get a edit target for the top layer, get a prim spec for the /World/Sphere and
# *low-level* and *manually* add some items to the apiSchemas metadata
editTarget = top_layer.GetEditTarget()
primSpec = editTarget.GetPrimSpecForScenePath("/World/Sphere")
newListOp = Sdf.TokenListOp()
newListOp.deletedItems = ["PhysicsRigidBodyAPI"]
primSpec.SetInfo(Usd.Tokens.apiSchemas, newListOp)
# The above is conceptually equivalent to this (higher level) but only to _add_ an API:
#
# edit_target = top_layer.GetEditTargetForLocalLayer(top_layer.GetRootLayer())
# top_layer.SetEditTarget(edit_target)
# UsdPhysics.RigidBodyAPI.Apply(sphere_prim)
#
# but we get to modify deletedItems directly with the code above.
# Now add some prependedItems to the middle layer
editTarget = middle_layer.GetEditTarget()
primSpec = editTarget.GetPrimSpecForScenePath("/World/Sphere")
newListOp = Sdf.TokenListOp()
newListOp.prependedItems = ["PhysicsCollisionAPI", "PhysicsMassAPI"]
primSpec.SetInfo(Usd.Tokens.apiSchemas, newListOp)
# And finally add some appendedItems and a deletedItem to the bottom layer
editTarget = bottom_layer.GetEditTarget()
primSpec = editTarget.GetPrimSpecForScenePath("/World/Sphere")
newListOp = Sdf.TokenListOp()
newListOp.prependedItems = ["PhysicsRigidBodyAPI"]
newListOp.appendedItems = ["PhysicsMassAPI", "PhysicsArticulationRootAPI"]
primSpec.SetInfo(Usd.Tokens.apiSchemas, newListOp)
# Export all layers to file and set the sublayers relationships
top_layer.GetRootLayer().subLayerPaths.append(BASE_DIRECTORY + "/MiddleLayer.usda")
top_layer.GetRootLayer().subLayerPaths.append(BASE_DIRECTORY + "/BottomLayer.usda")
# Now query the COMPOSED final stage, not just the primspecs
# output:
# SdfTokenListOp(Explicit Items: [PhysicsCollisionAPI, PhysicsMassAPI, PhysicsArticulationAPI])
print(sphere_prim.GetMetadata("apiSchemas"))
# it basically went on like this:
#
# top_layer: "I'm the most important, make sure that PhysicsRigidBodyAPI is DELETED"
# middle_layer: "I'm halfway important, it's imperative for me that PhysicsCollisionAPI and PhysicsMassAPI
# are added as soon as possible to the list"
# bottom_layer: "I'm not important.. anyway if I have any voice in the matter I'd like PhysicsRigidBodyAPI
# to be present and at the front of the list. Oh and also PhysicsMassAPI and
# PhysicsArticulationRootAPI should be present but at the bottom of the list (both not very
# important in my opinion).
#
# and the USD engine resolves things according to each layer's importance.
middle_layer.GetRootLayer().Export(BASE_DIRECTORY + "/MiddleLayer.usda")
bottom_layer.GetRootLayer().Export(BASE_DIRECTORY + "/BottomLayer.usda")
top_layer.GetRootLayer().Export(BASE_DIRECTORY + "/TopLayer.usda")
omni.usd.get_context().open_stage(BASE_DIRECTORY + "/TopLayer.usda")
The above is equivalent to (taken from here):
from pxr import Sdf
### Merging basics ###
path_list_op_layer_top = Sdf.PathListOp.Create(deletedItems = [Sdf.Path("/cube")])
path_list_op_layer_middle = Sdf.PathListOp.Create(prependedItems = [Sdf.Path("/disc"), Sdf.Path("/cone")])
path_list_op_layer_bottom = Sdf.PathListOp.Create(prependedItems = [Sdf.Path("/cube")], appendedItems = [Sdf.Path("/cone"),Sdf.Path("/sphere")])
result = Sdf.PathListOp()
result = result.ApplyOperations(path_list_op_layer_top)
result = result.ApplyOperations(path_list_op_layer_middle)
result = result.ApplyOperations(path_list_op_layer_bottom)
# Notice how on merge it makes sure that each sublist does not have the values of the other sublists, just like a Python set()
print(result) # Returns: SdfPathListOp(Deleted Items: [/cube], Prepended Items: [/disc, /cone], Appended Items: [/sphere])
# Get the flattened result. This does not apply the deleteItems, only ApplyOperations does that.
print(result.GetAddedOrExplicitItems()) # Returns: [Sdf.Path('/disc'), Sdf.Path('/cone'), Sdf.Path('/sphere')]
List composition can sometimes be daunting to newcomers but if explained properly it should be quite straightforward to grasp.
Transformations
Transformations in USD are represented with the UsdGeomXformOp class, e.g. xformOp:translate
, xformOp:scale
, xformOp:orient
(to represent a rotation with a quaternion) or xformOp:rotateYXZ
(to represent a rotation with euler angles applied in this order: Y
, X
and then finally Z
). Note the property namespace xformOp
where all operations are defined.
The transformations are stored along a prim and can be overridden in stronger layers and/or composed in the usual USD fashion. The class responsible for managing transformation on a prim is called UsdGeomXformable
.
As we’ve already seen USD uses inheritance to model which functionalities a prim owns: in the following graph the green boxes represent classes from the UsdGeomImageable
base class that provides attributes and functions for all prims that require rendering or visualization, to UsdGeomXformable
which provides a stack of operations (translate/rotate/etc.) that will be applied to the prim before visualizing it, to UsdGeomGprim
which is the base class for all geometric primitives (and encodes stuff like doubleSided
or orientation
and where the displayColor
primvar also resides) to the UsdGeomCube
class which we’ve already seen many times to create a Cube
prim.
Let’s take a look at a simple example of an Xform
with a child Cube
prim: the Xform
will have a scale and a translation transformations in its local stack and therefore those transformations will be inherited by the Cube
as well.
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf, Kind
import omni.usd
import carb
BASE_DIRECTORY = "/tmp" # This is where the .usda files will be saved
stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")
xform : UsdGeom.Xform = UsdGeom.Xform.Define(stage, Sdf.Path("/World"))
environment_xform = UsdGeom.Xform.Define(stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)
# Create an Xform and a Cube as child of the Xform
xform : UsdGeom.Xform = UsdGeom.Xform.Define(stage, "/World/Xform")
cube : UsdGeom.Cube = UsdGeom.Cube.Define(stage, "/World/Xform/Cube")
if not xform.GetPrim().IsA(UsdGeom.Xformable):
carb.log_error("Unexpected! A Xform derives from Xformable, as gprims do!")
# Define a scaling operation and a translation on the Xform
# These will be inherited by the cube's transformation
scale = Gf.Vec3f(50, 50, 50)
# Try to get the scale attribute or create it in case it doesn't exist yet
dstOp = UsdGeom.XformOp(xform.GetPrim().GetAttribute("xformOp:scale"))
if not dstOp:
# Create this scale op
xformable : UsdGeom.Xformable = UsdGeom.Xformable(xform)
dstOp = xformable.AddXformOp(UsdGeom.XformOp.TypeScale, UsdGeom.XformOp.PrecisionFloat)
dstOp.Set(Gf.Vec3f(scale))
translate = Gf.Vec3f(20, 0, 0)
dstOp = UsdGeom.XformOp(xform.GetPrim().GetAttribute("xformOp:translate"))
if not dstOp:
# Create this translate op
xformable : UsdGeom.Xformable = UsdGeom.Xformable(xform)
dstOp = xformable.AddXformOp(UsdGeom.XformOp.TypeTranslate, UsdGeom.XformOp.PrecisionFloat)
dstOp.Set(Gf.Vec3f(translate))
stage.GetRootLayer().Export(BASE_DIRECTORY + "/RootLayer.usda")
omni.usd.get_context().open_stage(BASE_DIRECTORY + "/RootLayer.usda")
If you inspect the generated .usda
, you’ll find that the order of the XformOp
s as we added them was also stored:
$ cat /tmp/RootLayer.usda
#usda 1.0
def Xform "World"
{
def Xform "Environment"
{
def DomeLight "DomeLight"
{
float inputs:intensity = 1000
}
}
def Xform "Xform"
{
float3 xformOp:scale = (50, 50, 50)
float3 xformOp:translate = (20, 0, 0)
uniform token[] xformOpOrder = ["xformOp:scale", "xformOp:translate"] # Order is stored
def Cube "Cube"
{
}
}
}
In case we wanted to apply first the translate operation and then the scale operation, the order could also have been changed by re-assigning those operations to the Xform
local stack with SetXformOpOrder
.
Reset Xform Stack
There is one particular operation called !resetXformOp!
which acts like a boolean flag: if added to a prim
- it is always added as first op in the
xformOpOrder
- it causes the prim not to inherit its parent’s transformation
Let’s see the same example as before but with a !resetXformOp!
applied to the Cube
(not to the Xform
- it would mean that we don’t want the Xform
to inherit transformations by /World
, but that’s not what we’re trying to do here):
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf, Kind
import omni.usd
import carb
BASE_DIRECTORY = "/tmp" # This is where the .usda files will be saved
stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")
xform : UsdGeom.Xform = UsdGeom.Xform.Define(stage, Sdf.Path("/World"))
environment_xform = UsdGeom.Xform.Define(stage, "/World/Environment")
dome_light = UsdLux.DomeLight.Define(stage, "/World/Environment/DomeLight")
dome_light.CreateIntensityAttr(1000)
# Create an Xform and a Cube as child of the Xform
xform : UsdGeom.Xform = UsdGeom.Xform.Define(stage, "/World/Xform")
cube : UsdGeom.Cube = UsdGeom.Cube.Define(stage, "/World/Xform/Cube")
if not xform.GetPrim().IsA(UsdGeom.Xformable):
carb.log_error("Unexpected! A Xform derives from Xformable, as gprims do!")
# Define a scaling operation and a translation on the Xform. But also set a
# resetXformStack op: the transformations will *NOT* be inherited by the cube
scale = Gf.Vec3f(50, 50, 50)
# Try to get the scale attribute or create it in case it doesn't exist yet
dstOp = UsdGeom.XformOp(xform.GetPrim().GetAttribute("xformOp:scale"))
if not dstOp:
# Create this scale op
xformable : UsdGeom.Xformable = UsdGeom.Xformable(xform)
dstOp = xformable.AddXformOp(UsdGeom.XformOp.TypeScale, UsdGeom.XformOp.PrecisionFloat)
dstOp.Set(Gf.Vec3f(scale))
translate = Gf.Vec3f(20, 0, 0)
dstOp = UsdGeom.XformOp(xform.GetPrim().GetAttribute("xformOp:translate"))
if not dstOp:
# Create this translate op
xformable : UsdGeom.Xformable = UsdGeom.Xformable(xform)
dstOp = xformable.AddXformOp(UsdGeom.XformOp.TypeTranslate, UsdGeom.XformOp.PrecisionFloat)
dstOp.Set(Gf.Vec3f(translate))
# Set the resetXformStack on the cube (NOT to the Xform.. otherwise the cube would have been affected)
xformable : UsdGeom.Xformable = UsdGeom.Xformable(cube.GetPrim())
xformable.SetResetXformStack(True)
stage.GetRootLayer().Export(BASE_DIRECTORY + "/RootLayer.usda")
omni.usd.get_context().open_stage(BASE_DIRECTORY + "/RootLayer.usda")
$ cat /tmp/RootLayer.usda
#usda 1.0
def Xform "World"
{
def Xform "Environment"
{
def DomeLight "DomeLight"
{
float inputs:intensity = 1000
}
}
def Xform "Xform"
{
float3 xformOp:scale = (50, 50, 50)
float3 xformOp:translate = (20, 0, 0)
uniform token[] xformOpOrder = ["xformOp:scale", "xformOp:translate"]
def Cube "Cube"
{
uniform token[] xformOpOrder = ["!resetXformStack!"] # Note the resetXformStack
}
}
}
Note how the !resetXformStack!
was declared as a uniform
token: tokens are types of attributes in USD which can encode string values, paths or - as in this case - concepts that do not vary over time or across different instances of a prim (think of uniform
pretty much like a GLSL shader’s uniform
).
In this second example the cube will not inherit any scaling nor translation from its parent’s hierarchy and therefore remain with its default dimensions at origin (0;0;0)
.
Stage Population Masks
There’s an interesting technique to only load and compose parts of a stage instead of loading the entire stage and composing everything (and remember that for the same stage, rendering it is infinitely more expensive than just composing it and resolving opinions and references..): population masks.
For example let’s suppose we have a very large USD scene: we’ll use the IsaacWarehouse
USD Composer showcase for this code sample. One can download all of the non-flattened USD assets to disk by loading it in Composer and then selecting File->Collect As..
and specifying a local temporary directory where to save a large quantity of data for that scene
Let’s suppose we want to find out the material associated with the main gate prim /World/Warehouse01/SM_Gate_C1
, this involves a lookup for the direct material-binding relationship. From the Property Window UI of the loaded scene we can see that we’re looking for /World/Warehouse01/Looks/Gate_C
Let’s do this in code in two different ways:
-
In this first way we’ll load the huge
IsaacWarehouse.usd
scene withUsd.Stage.Open()
and compose it entirely before querying the gate for its directly associated materialfrom pxr import Sdf, UsdGeom, Usd, UsdLux, Gf, UsdShade import omni.usd import carb import time # Record the start time start_time = time.time() ################################ ### composition happens here ### ### POTENTIALLY VERY SLOW ### ################################ stage : Usd.Stage = Usd.Stage.Open("/tmp/big_usd_scene/Collected_IsaacWarehouse/IsaacWarehouse.usd") # Record the end time and print the elapsed time in seconds end_time = time.time() elapsed_time = end_time - start_time print(f"Elapsed time: {elapsed_time} seconds") # Continue with gathering the material bound to the gate prim gate_prim : Usd.Prim = stage.GetPrimAtPath("/World/Warehouse01/SM_Gate_C1") material_binding = UsdShade.MaterialBindingAPI(gate_prim) # Do not specify a specific 'purpose' in GetDirectBindingRel() # (a material can be bound in 'preview' and another one, heavier, for 'full' scene rendering), # just get any of them relationship : Usd.Relationship = material_binding.GetDirectBindingRel() direct_binding : UsdShade.MaterialBindingAPI.DirectBinding = UsdShade.MaterialBindingAPI.DirectBinding(relationship) if not direct_binding.GetMaterial(): carb.log_error("No material directly associated") material_path : Sdf.Path = direct_binding.GetMaterialPath() prim : Usd.Prim = stage.GetPrimAtPath(material_path) material_bound_to_gate : UsdShade.Material = UsdShade.Material(prim) print(material_bound_to_gate.GetPath()) # /World/Warehouse01/Looks/Gate_C
Output:
Elapsed time: 0.02084561 seconds /World/Warehouse01/Looks/Gate_C
-
Now let’s see an alternative way of getting the same data, but this time let’s use
Usd.Stage.OpenMasked()
which accepts a list of prim paths to load (all of their children will be loaded as well) so we can ‘prune’ the stage hierarchy and avoid loading and composing unnecessary stufffrom pxr import Sdf, UsdGeom, Usd, UsdLux, Gf, UsdShade import omni.usd import carb import time # Record the start time start_time = time.time() ################################################# ### composition happens here ### ### but only for the requested prim paths! ### ################################################# primpaths_to_load = ["/World/Warehouse01/SM_Gate_C1", "/World/Warehouse01/Looks"] population_mask = Usd.StagePopulationMask(primpaths_to_load) stage : Usd.Stage = Usd.Stage.OpenMasked("/tmp/big_usd_scene/Collected_IsaacWarehouse/IsaacWarehouse.usd", population_mask) # Record the end time and print the elapsed time in seconds end_time = time.time() elapsed_time = end_time - start_time print(f"Elapsed time: {elapsed_time} seconds") # Continue with gathering the material bound to the gate prim gate_prim : Usd.Prim = stage.GetPrimAtPath("/World/Warehouse01/SM_Gate_C1") material_binding = UsdShade.MaterialBindingAPI(gate_prim) # Do not specify a specific 'purpose' in GetDirectBindingRel() # (a material can be bound in 'preview' and another one, heavier, for 'full' scene rendering), # just get any of them relationship : Usd.Relationship = material_binding.GetDirectBindingRel() direct_binding : UsdShade.MaterialBindingAPI.DirectBinding = UsdShade.MaterialBindingAPI.DirectBinding(relationship) if not direct_binding.GetMaterial(): carb.log_error("No material directly associated") material_path : Sdf.Path = direct_binding.GetMaterialPath() prim : Usd.Prim = stage.GetPrimAtPath(material_path) material_bound_to_gate : UsdShade.Material = UsdShade.Material(prim) print(material_bound_to_gate.GetPath()) # /World/Warehouse01/Looks/Gate_C
Output:
Elapsed time: 0.00132393 seconds /World/Warehouse01/Looks/Gate_C
Even though opening the stage and composing it only took ~20 milliseconds with Usd.Stage.Open()
, Usd.Stage.OpenMasked()
took ~1 millisecond: this technique can be quite effective to avoid loading and composing unnecessary parts of the stage.
A lower level technique would have been opening the layers manually (either by opening the .usd files or following reference
s via code) and inspecting primspecs in those layers and looking for material binding relationships and resolving them to the final material path. Properly optimized code could have been even faster with low-level primspec parsing, but population masks are substantially easier to use and quite effective too.
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).
Events and Listeners
USD features an event system to notify and take action in case of added prims, removed prims, attribute changes, a stage was just loaded, etc.
Your callback functions registered as listeners should be fast: think of ISVs (interrupt service vectors) - their actions should either be fast or another thread/entity should be delegate to take heavy actions later. This is by design since they run in the thread where the listener was created and that’s often the main thread.
The basic structure of a USD event listener is as follows (most of the facilities are in the Tf
module - see Tools Foundations):
def ObjectsChanged_callback(notice, sender):
print(notice.GetResyncedPaths())
print(notice.GetChangedInfoOnlyPaths())
listener = Tf.Notice.RegisterGlobally(Usd.Notice.ObjectsChanged, ObjectsChanged_callback)
# do some USD manipulations here..
listener.Revoke() # cleanup
-
ResyncedPaths are structural changes that can invalidate entire subtrees of UsdObjects (both prims and properties). These happen when you delete or add a prim, or even if you add a property or metadata. Anything that relates to composition is considered a structural change.
-
ChangedInfoOnly are non-structural changes, e.g. when you change an attribute’s or metadata’s value.
Here is a code example of registering a listener and spewing out a long list of information regarding what just happened. At a high level overview, the following is going to happen:
- A listener is registered
- A
/World
Xform is added to the scene, this will cause a resync (it’s a structural change) /World/Cube
is added, structural- Some attributes (as mandated by the
Cube
typed schema) are changed in/World/Cube
, this is non-structural /World/Environment
and/World/Environment/DomeLight
are created.. same as before..- The attribute
/World/Cube.myCustomFloatAttribute
is created. Structural change. - The default value of attribute
/World/Cube.myCustomFloatAttribute
is changed. Non-structural change. - The listener is revoked
Here’s the full code and output with explicative comments on what’s happening
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf, Tf
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")
def ObjectsChanged_callback(notice, sender):
stage = notice.GetStage()
print("---")
print(">", notice, sender)
print(">> (notice.GetResyncedPaths) - Updated paths", notice.GetResyncedPaths())
print(">> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes", notice.GetChangedInfoOnlyPaths())
prim = stage.GetPrimAtPath("/World/Cube")
if prim:
# path #1
# Check if a specific UsdObject was affected
print(">> (notice.AffectedObject) - Something changed for", prim.GetPath(), notice.AffectedObject(prim))
print(">> (notice.ResyncedObject) - Updated path for", prim.GetPath(), notice.ResyncedObject(prim))
print(">> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly", prim.GetPath(), notice.ChangedInfoOnly(prim))
print(">> (notice.HasChangedFields) - Attribute/Metadata HasChanges", prim.GetPath(), notice.HasChangedFields(prim))
print(">> (notice.GetChangedFields) - Attribute/Metadata ChangedFields", prim.GetPath(), notice.GetChangedFields(prim))
attr = stage.GetAttributeAtPath("/World/Cube.myCustomFloatAttribute")
if attr:
# path #2
# Check if a specific UsdObject was affected
print(">> (notice.AffectedObject) - Something changed for", attr.GetPath(), notice.AffectedObject(attr))
print(">> (notice.ResyncedObject) - Updated path for", attr.GetPath(), notice.ResyncedObject(attr))
print(">> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly", attr.GetPath(), notice.ChangedInfoOnly(attr))
print(">> (notice.HasChangedFields) - Attribute/Metadata HasChanges", attr.GetPath(), notice.HasChangedFields(attr))
print(">> (notice.GetChangedFields) - Attribute/Metadata ChangedFields", attr.GetPath(), notice.GetChangedFields(attr))
listener = Tf.Notice.RegisterGlobally(Usd.Notice.ObjectsChanged, ObjectsChanged_callback)
# Add some prims 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)
# Create an attribute
cube_prim : Usd.Prim = cube.GetPrim()
attr = cube_prim.CreateAttribute("myCustomFloatAttribute", Sdf.ValueTypeNames.Float)
attr.Set(42.8)
listener.Revoke()
# 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")
Output:
# listener is registered here
# /World Xform is created
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths [Sdf.Path('/World')]
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes []
# /World/Cube is created, path #1 begins to always be executed from now on
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths [Sdf.Path('/World/Cube')]
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes []
>> (notice.AffectedObject) - Something changed for /World/Cube True
>> (notice.ResyncedObject) - Updated path for /World/Cube True
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube True
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube ['specifier', 'typeName']
# /World/Cube.extent attribute is created (this is defined in the typed schema)
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths [Sdf.Path('/World/Cube.extent')]
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes []
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
# /World/Cube.extent attribute has its value changed
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths []
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes [Sdf.Path('/World/Cube.extent')]
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
# /World/Cube.size is created
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths [Sdf.Path('/World/Cube.size')]
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes []
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
# /World/Cube.size has its value changed
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths []
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes [Sdf.Path('/World/Cube.size')]
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
# /World/Environment is created
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths [Sdf.Path('/World/Environment')]
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes []
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
# /World/DomeLight is created... we'll skip all this part since it's similar to what we just explained
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths [Sdf.Path('/World/Environment/DomeLight')]
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes []
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths [Sdf.Path('/World/Environment/DomeLight.inputs:intensity')]
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes []
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths []
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes [Sdf.Path('/World/Environment/DomeLight.inputs:intensity')]
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
# /World/Cube.myCustomFloatAttribute is created - path #2 is also executed from now on
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths [Sdf.Path('/World/Cube.myCustomFloatAttribute')]
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes []
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
>> (notice.AffectedObject) - Something changed for /World/Cube.myCustomFloatAttribute True
>> (notice.ResyncedObject) - Updated path for /World/Cube.myCustomFloatAttribute True
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube.myCustomFloatAttribute False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube.myCustomFloatAttribute True
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube.myCustomFloatAttribute ['custom']
# /World/Cube.myCustomFloatAttribute has its default value changed
---
><pxr.Usd.ObjectsChanged object at 0x7feb381c4ea0> Usd.Stage.Open(rootLayer=Sdf.Find('anon:0x1db9dcf0:RootLayer.usda'), sessionLayer=Sdf.Find('anon:0xfb39dd0:RootLayer-session.usda'), pathResolverContext=<invalid repr>)
>> (notice.GetResyncedPaths) - Updated paths []
>> (notice.GetChangedInfoOnlyPaths) - Attribute/Metadata value changes [Sdf.Path('/World/Cube.myCustomFloatAttribute')]
>> (notice.AffectedObject) - Something changed for /World/Cube False
>> (notice.ResyncedObject) - Updated path for /World/Cube False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube False
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube False
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube []
>> (notice.AffectedObject) - Something changed for /World/Cube.myCustomFloatAttribute True
>> (notice.ResyncedObject) - Updated path for /World/Cube.myCustomFloatAttribute False
>> (notice.ChangedInfoOnly) - Attribute/Metadata ChangedInfoOnly /World/Cube.myCustomFloatAttribute True
>> (notice.HasChangedFields) - Attribute/Metadata HasChanges /World/Cube.myCustomFloatAttribute True
>> (notice.GetChangedFields) - Attribute/Metadata ChangedFields /World/Cube.myCustomFloatAttribute ['default']
# Listener is revoked here
As you can see the USD event system is quite comprehensive and allows you to inspect carefully what’s happening on the scene.
Here’s a link to a more complex example involving creating an Omniverse extension printing out prim paths in the viewport directly: this also uses a Usd.Notice.ObjectsChanged
listener to be notified if anything changes in the selected prim so it can react and update accordingly: How to make an extension to display Object Info.
One last thing to pay attention to: stage callbacks are usually handled by USD contexts, therefore the right place to find such callbacks would be taking a look at USD contexts documentations (usually provided by the application you’re using USD in), in Omniverse that would be omni.usd.USDContext:
stage: Usd.Stage = Usd.Stage.Open(some_usd_url)
cache = UsdUtils.StageCache.Get()
# Retrieve a long int id from the singleton cache for all local USD clients
stage_id = cache.Insert(stage).ToLongInt()
def on_stage_opened(result, err):
if result is True:
print(f"There were errors opening the stage: {err}")
else:
print("no errors, stage opened!")
omni.usd.get_context().attach_stage_with_callback(stage_id=stage_id, on_finish_fn=stage_opened_fn)
Debugging
Debugging USD can be challenging, there are some Tf
debugging facilities that can be used to make this job easier and print detailed log in composition phases and inspect what’s going on within multiple plugins
Here’s an excerpt from USD-Cookbook
from pxr import Sdf, UsdGeom, Usd, UsdLux, Gf, Tf
import omni.usd
import carb
stage : Usd.Stage = Usd.Stage.CreateInMemory("RootLayer.usda")
# Redirect debug output to stdout, it can be redirected to a file as well
Tf.Debug.SetOutputFile(sys.__stdout__)
# Actual symbols are defined in C++ across many files.
# You can query them using `Tf.Debug.GetDebugSymbolNames()` or by
# searching for files that call the `TF_DEBUG_CODES` macro in C++.
# (Usually this is in files named "debugCodes.h").
symbols = Tf.Debug.GetDebugSymbolNames()
# Check if debug symbols are enabled
# (on my machine, they're all False by default)
for symbol in symbols:
print(symbol)
print(Tf.Debug.IsDebugSymbolNameEnabled(symbol))
# A more detailed full description of everything
print("Descriptions start")
print(Tf.Debug.GetDebugSymbolDescriptions())
print("Descriptions end")
# Enable change processing so we can see something happening
# You can also use glob matching. Like "USD_*" to enable many flags
# at once.
Tf.Debug.SetDebugSymbolsByName("USD_CHANGES", True)
stage.DefinePrim("/SomePrim") # This line will print multiple messages to stdout
Note that per-plugin debugging should be enabled if you’re doing this in OV Composer. To enable all of the debug codes that start with the prefix PLUG_
the following can be used
$ TF_DEBUG="PLUG_*" /home/alex/.local/share/ov/pkg/create-2023.2.0/omni.create.sh
...
# script above gets executed..
HandleLayersDidChange received (stage with rootLayer @anon:0x12035890:RootLayer.usda@, sessionLayer @anon:0x1c4ada40:RootLayer-session.usda@)
</SomePrim> in @anon:0x12035890:RootLayer.usda@ changed.
Changed field: specifier
Adding paths that use </SomePrim> in layer @anon:0x12035890:RootLayer.usda@: [ /SomePrim ]
ProcessPendingChanges (stage with rootLayer @anon:0x12035890:RootLayer.usda@, sessionLayer @anon:0x1c4ada40:RootLayer-session.usda@)
Did Change Significantly: /SomePrim
Recomposing: /SomePrim
A comprehensive list of debug codes can be found here: Debugging USD.
Credits
Created by Marco Alesiani.
This book and its webGL frontpage use open source and royalty-free libraries and technologies. Some paragraphs also draw from freely shared material.
- glTF 2.0 viewer
- developer-office-hours sample extensions
- stemkoski Three.js selective glow
- Book of USD
- VFX USD Survival Guide
- USD Cookbook
- Omniverse workshop - SIGGRAPH 2022
- LowPoly Airship (royalty free)
A huge thank you to all people who made this possible.