USD Independent Study

Progress Blog

The goal of this independent study was to explore the nature of USD (Universal Scene Description) and how one might interact with it via the Python API. A lot of time was committed to setting up the development environment for USD.

Furthermore, I wanted to investigate how I could use the utility usdview and develop a plugin for it. This tool is very powerful for previewing a USD file.

Unfortunately, a lot of the USD documentation online is lacking. As a result some of the things I wanted to try out and research were cut short by the lack of resources. For example, loading and unloading prims doesn't seem to be well explained or intuitive (to me, at least).

I chose to install this on my workstation in the systems office, since this is where I would end up doing a majority of my development. Obviously, I couldn't really get this pushed out to all the machines in the building, so this would suffice.

On Linux, installation was extremely trivial. The installer that comes from the USD repo downloaded all the necessary packages (most of which are needed for usdview). Stuff like PyOpenGL and PySide were installed. When I tried to run the same installation on my personal Macbook, I was not so succesful with installing PySide. Initially I installed it manually myself via pip, but that failed and some research suggested I install it via homebrew, which worked perfectly fine. Unfortunately, usdview still seemed a bit bugged and constantly shot me errors on my Macbook. This confirms some of the things I was reading online. Regardless, my Linux install was flawless.

Well, except for the USD Houdini plugin. I spent a long time messing with CMake in order to compile the Houdini plugin for USD. The Maya plugin installed perfectly fine with the installer, however I believe the C libraries of our current Houdini build were out of sync with what the USD repo wanted to be built against. This was unfortunate because I was really looking forward to messing around with the USD-Houdini pipeline. I ended up having to settle for strictly Maya.

To recap, I ran the python install script found in USD-repo/build_scripts and built the tree out to /opt (This build directory will become $USD_INSTALL_ROOT). I also added some environment variables:

export USD_INSTALL_ROOT=/opt/USD/
export PYTHONPATH=$PYTHONPATH:$USD_INSTALL_ROOT/lib/python
export MAYA_PLUG_IN_PATH=$MAYA_PLUG_IN_PATH:$USD_INSTALL_ROOT/third_party/maya/plugin/
export MAYA_SCRIPT_PATH=$MAYA_SCRIPTPATH:$USD_INSTALL_ROOT/third_party/maya/lib/usd/usdMaya/resources/
export XBMLANGPATH=$XBMLANGPATH:$USD_INSTALL_ROOT/third_party/maya/lib/usd/usdMaya/resources/
PATH=$PATH:$USD_INSTALL_ROOT/bin

To start off and become acquainted with USD, I frequently checked out the docs/tutorial Pixar has up online. The intro docs here are pretty well setup to get you in the USD headspace. I quickly ran through these with the help of the example files that come in the repo.

The main thing I was after with this independent study was exploring more about how I can interact with USD via its python API. I was mainly interested in: introspecting geometry, making/modifying prims, and loading/unloading prims. To cover the first 2, I worked on a simple exercise to "copy" spheres to every vertex on a given piece of geometry. This was really easy to do with the API, but I did encounter a pitfall when bringing the USD stage back into Maya. Here's the code snippet:

from pxr import Usd, UsdGeom

import os

def main():
    stage = Usd.Stage.Open('/home/souell20/mount/stuhome/SCAD 2018-2019/Q2/USD/export/test.usd')
    tempPath = '/home/souell20/Desktop/foobar.usd'

    if os.path.exists(tempPath):
        os.remove(tempPath)

    temp = Usd.Stage.CreateNew(tempPath)
    poly = stage.GetPrimAtPath('/pPlatonic1')
    points = poly.GetAttribute('points').Get()

    refGeo = temp.OverridePrim('/geo')
    refGeo.GetReferences().AddReference('/home/souell20/mount/stuhome/SCAD 2018-2019/Q2/USD/export/test.usd')

    for i, p in enumerate(points):
        xform = UsdGeom.Xform.Define(temp, '/verts/xform_{}'.format(i))
        sphere = temp.DefinePrim('/verts/xform_{}/sphere_{}'.format(i, i), 'Sphere')
        sphere.GetAttribute('radius').Set(0.2)

        xform.AddTranslateOp(opSuffix='offset').Set(value=(p[0], p[1], p[2]))

    temp.GetRootLayer().Save()

The main issue when bringing this back into Maya is that although the spheres appeared fine on every vertex in USD view, the Maya USD importer didn't properly import any of the sphere prims. This is because adding them via USD in this way treats it as a prim, which usdview can render, but Maya has no notion of what a sphere prim is. The only way, as far as I can tell, to get geometry in Maya from USD is to have explicit point data. This is definitely a shortcoming in my opinion. I would have liked to test out if this works with Houdini.

I also tried testing how USD would handle a NURBS sphere exported from Maya to see if I could define the spheres that way. This just makes point data as well, where the points in the USD file are the CVs of the NURBS sphere. The only solution to doing something like this would be to reference in a standalone stage that has a sphere already in it, and use that USD file as a sidecar to the script that would run and add spheres to the verts.

I think this limitation shows truely what USD is meant for, and where its power lies which is layer composition. The ability to reference in multiple stages and compile their opinions quickly effectively makes a pipeline highly modular. The USD API does not seem to be for geometry creation, unlike something we might expect from automating RIB creation.

USDView is very powerful for viewing a lot of geometry at once since the app itself is very lightweight and leverages the OpenGL renderer "Hydra" in order to visualize the USD hierarchy. The UI is basic and allows us to introspect various opinions set on the prims in the stage.

One thing I definitely wanted to do was make a simple Qt GUI plugin for USDView. Luckily, this was the most straightforward thing I pursued when studying USD. My idea was to make a "playblast" feature that would export any animation from usdview to an MP4. I needed a couple things in place to make this work. First, I made a python module "playblasterPlugin" somewhere on disk. Then I needed to export that location to the environment variable PXR_PLUGINPATH_NAME. This would queue up usdview to load in my module on startup. Then, all I had to do was setup some framework. In the module's __init__.py:

from pxr import Tf
from pxr.Usdviewq.plugin import PluginContainer

class PlayblasterContainer(PluginContainer):
    # Called on load to add itself to registry
    # Requires an identifier string, display name, and callback function
    # all callbacks take the usdviewApi as the SOLE parameter
    def registerPlugins(self, plugRegistry, usdviewApi):
        pluginTest = self.deferredImport('.pluginTest')
        self._playblast = plugRegistry.registerCommandPlugin(
            'PlayblasterContainer.playblast',
            'Playblast',
            pluginTest.main
        )

    # After registration, the plugin can add itself to the UI with this function
    def configureView(self, plugRegistry, plugUIBuilder):
        menu = plugUIBuilder.findOrCreateMenu('Playblaster')

        menu.addItem(self._playblast)

Tf.Type.Define(PlayblasterContainer)

Then, the plugin needed a file called: plugInfo.json

{
    "Plugins": [
        {
            "Type": "python",
            "Name": "playblasterPlugin",
            "Info": {
                "Types": {
                    "playblasterPlugin.PlayblasterContainer": {
                        "bases": ["pxr.Usdviewq.plugin.PluginContainer"],
                        "displayName": "Playblaster"
                    }
                }
            }
        }
    ]
}

Where "pluginTest" is mentioned refers to the actual python file that runs the Qt window. pluginTest.main is a function in pluginTest.py that effectively shows my Qt dialog.



The plugin allows the user to select an output directory and a frame range. The script then steps through the selected range and exports a JPG for each frame to the output directory. It then uses ffmpeg to compile the outputted JPG's into an MP4 in that same directory.

The biggest challenge with this plugin was finding a way to step through the frames in the usdview API. Surprisingly, there's no actual API for doing this. You can easily get the start and end frames, but you cannot explicitly set the current frame on the slider bar. There is actually a menu option to step forward and backward, but this must be connected to an internal function I can't quite see. My workaround is extremely hacky, but it works. Since we do have the menu option, and the whole UI is just a Qt app, I can easily get the QAction entry for the step forward and step backward commands and programmatically activate them to get to the frame(s) I want.

As I mentioned previously, I think the biggest takeaway from this entire study is more understanding about how USD really enables modular pipelines to exist. The ability to intertwine the work of several departments with very little overhead and without any attribute stomping is very unique to how the USD layers are composed. Given that, I think what is publicly available in terms of DCC support is lacking - but with a bit more experience with the USD api, it would be relatively straightforward to build out a proper toolset for supporting USD in Maya/Houdini.

On the USD site, I read through a "white paper" article about a proposed variation of USD that was pretty interesting. Coined "usdz", the proposition was to provide a compressed "zipped" version of the USD schema. The applications of something like this seemed tailored towards hosting USD on mobile devices, which is very promising for extensive visualizations.

There's definitely potential in the ability to selectively load/unload prims in order to make USDView even more efficient at visualization. Unfortunately, I was not able to figure out this aspect too well. A plugin for usdview with a hierarchy view and pattern search to selectively toggle load states on an initially empty stage would be pretty convenient for introspecting and debugging large scenes.