USD and Python

A very broad introduction to programming with OpenUSD in Python

USD and Python

Open USD is primarily built in C++ but offers bindings to python. If you know a little python this can be an easy way to get a lot of things done. Here's a high level tour of the python apis.

Getting the Packages

If you're working in a digital content creation tool like Blender 4, Houdini, Maya, it may ship with USD already available in their python interpreters. You can check the documentation for your tool, but in my experience this is getting pretty common. A quick check is

from pxr import Usd

If that import is successful USD's APIs should be available.

If you're not in a DCC and want access to USD APIs you can use the PyPI usd-core distribution if it's available for your platform. I often do this in a venv.

python -m pip install usd-core

It's called usd-core because it only has the "core" USD APIs, and doesn't include rendering or things like usdview. For inspecting and modifying USD files though it should have everything you need.

The last-ditch way to get access to USD in python is to build it yourself. This is not for the faint of heart 😆 I will say it has gotten easier in recent years. If you have a C++ build toolchain set up and access to cmake there is a script that can handle getting the dependencies and building for you. This is the simplest thing to try

cd <root of github checkout>
python build_scripts/build_usd.py <output_path>

Setting options and getting that to work is outside the scope of what I'm covering here, but check out the README and BUILDING files in OpenUSD on github. Best of luck if you take that path.

Using the APIs

Here's a script that would output the example file from USD File Basics.

from pxr import Usd, UsdGeom

stage = Usd.Stage.CreateNew("./cube-example.usda")

root_xform = UsdGeom.Xform.Define(stage, "/RootTransform")
stage.SetDefaultPrim(root_xform.GetPrim())

cube_mesh = UsdGeom.Mesh.Define(stage, root_xform.GetPath().AppendChild("Cube"))
cube_mesh.GetPointsAttr().Set([(-50, -50, -50), (50, -50, -50), (-50, -50, 50), (50, -50, 50), (-50, 50, -50), (50, 50, -50), (50, 50, 50), (-50, 50, 50)])
cube_mesh.GetFaceVertexCountsAttr().Set([4, 4, 4, 4, 4, 4])
cube_mesh.GetFaceVertexIndicesAttr().Set([0, 1, 3, 2, 0, 4, 5, 1, 1, 5, 6, 3, 2, 3, 6, 7, 0, 2, 7, 4, 4, 7, 6, 5])
cube_mesh.GetSubdivisionSchemeAttr().Set("none")

stage.Save()

python to generate cube-example.usda

This is a 50 unit cube mesh centered at the origin. Just so you don't have to click the link, here's the output

#usda 1.0
(
    defaultPrim = "RootTransform"
)

def Xform "RootTransform"
{
    def Mesh "Cube"
    {
        int[] faceVertexCounts = [4, 4, 4, 4, 4, 4]
        int[] faceVertexIndices = [0, 1, 3, 2, 0, 4, 5, 1, 1, 5, 6, 3, 2, 3, 6, 7, 0, 2, 7, 4, 4, 7, 6, 5]
        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)]
        token subdivisionScheme = "none"
    }
}

cube-example.usda

from pxr import Usd, UsdGeom

We import two libraries from the pxr namespace, Usd and UsdGeom. Usd is one of the main python libraries you'll interact with. UsdGeom is a library containing prim types that describe geometry, we want it for the Mesh and Xform types.

stage = Usd.Stage.CreateNew("./cube-example.usda")

Create a stage. Stages in USD are the container for a USD file you've opened, and all the content that file references. The term stage is used in the sense of a stage in a theater or on a movie set. It's the place where you're setting up a scene. In this code we're creating a new, empty stage.

root_xform = UsdGeom.Xform.Define(stage, "/RootTransform")
stage.SetDefaultPrim(root_xform.GetPrim())

Use the UsdGeom library to define (with def) a new Xform prim. We give the Define function a reference to the stage we're creating it on, and a prim path for our new transform. The last token in the path is the new transform's name.

The second line tells the stage to make this the default prim. This way when someone references our cube-example.usda file they'll get this prim as the target of the reference.

cube_mesh = UsdGeom.Mesh.Define(stage, root_xform.GetPath().AppendChild("Cube"))
cube_mesh.GetPointsAttr().Set([(-50, -50, -50), (50, -50, -50), (-50, -50, 50), (50, -50, 50), (-50, 50, -50), (50, 50, -50), (50, 50, 50), (-50, 50, 50)])
cube_mesh.GetFaceVertexCountsAttr().Set([4, 4, 4, 4, 4, 4])
cube_mesh.GetFaceVertexIndicesAttr().Set([0, 1, 3, 2, 0, 4, 5, 1, 1, 5, 6, 3, 2, 3, 6, 7, 0, 2, 7, 4, 4, 7, 6, 5])
cube_mesh.GetSubdivisionSchemeAttr().Set("none")

Use the UsdGeom library to define a Mesh. In this case I've used the root_xform object's path to build the path to our Mesh, named "Cube". We could also have passed "/RootTransform/Cube".

After creating the cube set the attributes. Each of these built-in attributes has a function for getting access to them. We get the attribute we'd like and pass in the value. I hard-coded these, but you could generate them in whatever way is useful. These functions can also accept numpy arrays if they have the right shape.

stage.Save()

Finally, save the stage. This line actually creates the cube-example.usda file, up until here there was no file output.

Finding Documentation

Python documentation for USD is not as widely available as it could be. There's a lot of thought going into this and I expect it will improve in the future (I'm writing in Feb. 2024), but for now it can take some digging to find the right APIs. Personally I tend to look in a few places.

First Stop ➡️ I look at the C++ API and see if there is a python function with the same name and parameters. This is often the case with USD libraries. You'll notice the function names don't follow pep8 or other python naming conventions, most functions and types in USD have the same name in C++ and python. The Sdf library is the big exception, it uses much more pythonic naming.

I look for C++ function names and parameters in the main docs here and in the github repo.

Second Stop ➡️ I check the tests in the USD repository. USD is written in C++ but the tests are almost entirely in python. This means the python APIs are indirectly documented in the tests.

Each library in the source will have a testenv folder that contains the tests. Here are the Usd library tests. If there is an API I'm having trouble getting to work and I can find it in the tests that usually gives me what I need to know. Here's a link to the tests for Usd.PrimRange. It demonstrates ways to traverse prims on a stage with an iterator.

Third Stop ➡️ So you've looked for matching C++ api and you've hunted in the tests, and you're still stuck. If you know a little C++ you may be able to find what you need in the "wrap" files. USD's Python bindings are implemented in files with names like wrapClassName.cpp. So for instance Usd.Stage is bound in a file called wrapStage.cpp in the Usd library. The function we used above looks like this in that file

        .def("CreateNew", (UsdStageRefPtr (*)(const string &,
                                              UsdStage::InitialLoadSet))
             &UsdStage::CreateNew,
             (arg("identifier"),
              arg("load")=UsdStage::LoadAll),
             return_value_policy<TfPyRefPtrFactory<> >())

We called this version with a string file name identifier and used the default second parameter.


Some good online resources

Here are a few places with USD Python examples I'm aware of. If you know of any more let me know!

USD Tutorials — Universal Scene Description 24.03 documentation
Working with USD Python Libraries
GitHub - ColinKennedy/USD-Cookbook: USD Example Projects
USD Example Projects. Contribute to ColinKennedy/USD-Cookbook development by creating an account on GitHub.