Creating a Usdview Plugin

Creating a Usdview Plugin


VERIFIED ON USD VERSION 21.08

In this tutorial, we'll create a new Python plugin for Usdview and learn how to use the Usdview API.

Setting Up a PluginContainer

First, we'll create a new directory to hold our new plugin. We can put this directory anywhere that our USD build will look for plugins, but it's a good idea to nest it in a directory that can hold other plugins in case we want to install more in the future.

mkdir -p <some path>/usdviewPlugins/tutorialPlugin/

We now want to create a new PluginContainer class in our plugin module's __init__.py file.

tutorialPlugin/_init_.py
from pxr import Tf
from pxr.Usdviewq.plugin import PluginContainer


def printMessage(usdviewApi):
    print("Hello, World!")


class TutorialPluginContainer(PluginContainer):

    def registerPlugins(self, plugRegistry, usdviewApi):

        self._printMessage = plugRegistry.registerCommandPlugin(
            "TutorialPluginContainer.printMessage",
            "Print Message",
            printMessage)

    def configureView(self, plugRegistry, plugUIBuilder):

        tutMenu = plugUIBuilder.findOrCreateMenu("Tutorial")
        tutMenu.addItem(self._printMessage)

Tf.Type.Define(TutorialPluginContainer)

PluginContainers are just objects that know how to register new command plugins and add them to Usdview's UI. This is done through 2 methods:

When a PluginContainer is loaded, Usdview's plugin system first calls registerPlugins() which gives the container a chance to add its command plugins to the plugin registry. Each command plugin needs an identifier string, display name, and callback function. The identifier must be globally unique, so it is good practice to prepend the name of the plugin container. All command plugin callbacks take a usdviewApi object as their only parameter (we'll learn how to use the API later). In our example, we registered a new plugin that prints "Hello, World!" when invoked.

After a PluginContainer has registered all of its command plugins, it is given the chance to expose them to Usdview's UI in the configureView() method. Currently, plugins can only create simple menus in Usdview's menu bar as well as open new Qt windows. Our example creates a new menu named "Tutorial" and adds our "printMessage" command plugin to the menu.

Since plugins are loaded through Pixar's libplug library, we also need to define our PluginContainer as a new Tf.Type. This just lets libplug find the container later. We also need to create a new plugInfo.json file in our plugin directory, so we'll do that now.

plugInfo.json
{
    "Plugins": [
        {
            "Type": "python",
            "Name": "tutorialPlugin",
            "Info": {
                "Types": {
                    "tutorialPlugin.TutorialPluginContainer": {
                        "bases": ["pxr.Usdviewq.plugin.PluginContainer"],
                        "displayName": "Usdview Tutorial Plugin"
                    }
                }
            }
        }
    ]
}

When making a plugin container, all we need to change from the above example is the "Name" field, to match our plugin's Python module name, change the "tutorialPlugin.TutorialPluginContainer" type to match our PluginContainer type name, and update the "displayName."

Lastly, we need to configure the environment. libplug loads Python plugins by importing the module directly, so we need to make sure our plugins directory (if trying the sendMail.py example in extras/usd/examples/usdviewPlugins/ it would be that directory, NOT tutorialPlugin/) is listed in our PYTHONPATH environment variable. If we want libplug to load our plugin, we also need to add the path to the plugin directory (this time tutorialPlugin/ in our example) to the PXR_PLUGINPATH_NAME environment variable.

At this point, if we open Usdview we should see a new "Tutorial" menu. If we open this menu and select "Print Message," we should see "Hello, World!" printed to the console.

Congratulations!  We have just created a new Usdview plugin!

Using the Usdview API

Now that we can create command plugins, we can start interacting with Usdview using the usdviewApi object. An overview of API features is given below below. For a full listing of all API features, open the Interpreter window in Usdview (Window > Interpreter), and type help(usdviewApi).

  • usdviewApi.dataModel - a full representation of Usdview's state. The majority of the data and functionality available to plugins is available through the data model.
    • stage - The current Usd.Stage object.
    • currentFrame - Usdview's current frame.
    • viewSettings - A collection of settings which only affect the viewport. Most of these settings are normally controlled using Usdview's 'View' menu. Some examples are listed below.
      • complexity - The scene's subdivision complexity.
      • freeCamera - The camera object used when Usdview is not viewing through a camera prim. Plugins can modify this camera to change the view.
      • renderMode - The mode used for rendering models (smooth-shaded, flat-shaded, wireframe, etc.).
    • selection - The current state of prim and property selections.
      • Common prim selection methods: getFocusPrim(), getPrims(), setPrim(prim), addPrim(prim), clearPrims()
      • Common property selection methods: getFocusProp(), getProps(), setProp(prop), addProp(prop), clearProps()
  • usdviewApi.qMainWindow - Usdview's Qt MainWindow object. It can be used as a parent for other Qt windows and dialogs, but it should not be used for any other purpose.
  • usdviewApi.PrintStatus(msg) - Prints a status message at the bottom of the Usdview window.
  • GrabViewportShot()/GrabWindowShot() - Captures a screenshot of the viewport or the entire main window and returns it as a QImage.

Deferring Imports

Usdview is designed to be quick to launch, so to be a good Usdview citizen we should make sure our plugin loads as quickly as possible. Sometimes, importing other Python modules takes a noticeable amount of time, so it is a good idea to import them lazily when our command plugin is called for the first time.

The easiest way to do this is by putting our plugin logic into a separate Python file and using the deferredImport(moduleName) method available on PluginContainers. Let's fix the above example to use this method.

First, we'll put our printMessage function into a new Python file called printer.py. This function doesn't require any heavy imports, so we'll just print a message when the file is imported so we know it was deferred properly.

tutorialPlugin/printer.py
print("Imported printer!")


def printMessage(usdviewApi):
    print("Hello, World!")

Then, we'll import the module the normal way (without deferring yet) in __init__.py and make sure to call the printMessage function off this new module.

tutorialPlugin/_init_.py - Normal Import
from pxr import Tf
from pxr.Usdviewq.plugin import PluginContainer

import printer


class TutorialPluginContainer(PluginContainer):

    def registerPlugins(self, plugRegistry, usdviewApi):

        self._printMessage = plugRegistry.registerCommandPlugin(
            "TutorialPluginContainer.printMessage",
            "Print Message",
            printer.printMessage)

    def configureView(self, plugRegistry, plugUIBuilder):

        tutMenu = plugUIBuilder.findOrCreateMenu("Tutorial")
        tutMenu.addItem(self._printMessage)

Tf.Type.Define(TutorialPluginContainer)

If we run Usdview now, we'll immediately see the message "Imported printer!" appear in the console. Now, we'll do a deferred import.

tutorialPlugin/_init_.py - Deferred Import
from pxr import Tf
from pxr.Usdviewq.plugin import PluginContainer


class TutorialPluginContainer(PluginContainer):

    def registerPlugins(self, plugRegistry, usdviewApi):

        printer = self.deferredImport(".printer")
        self._printMessage = plugRegistry.registerCommandPlugin(
            "TutorialPluginContainer.printMessage",
            "Print Message",
            printer.printMessage)

    def configureView(self, plugRegistry, plugUIBuilder):

        tutMenu = plugUIBuilder.findOrCreateMenu("Tutorial")
        tutMenu.addItem(self._printMessage)

Tf.Type.Define(TutorialPluginContainer)

All we did was remove printer from our imports and add line 9. The deferredImport method just returns a fake module object, and pretends to know about any function we access on it. The first time one of its functions is called, it actually imports the target module and calls its function instead.

deferredImport doesn't know anything about the target module until it is imported, so it assumes any object we reference is a function in the module. If it refers to a function that doesn't exist on the target module, we'll get an ImportError.

If we run Usdview again, we won't see the "Imported printer!" message until we invoke printMessage. The module is only imported once, so invoking printMessage multiple times will only print the message the first time.

SendMail Example Plugin

An example plugin file is provided with the USD distribution at USD/extras/usd/examples/usdviewPlugins/sendMail.py. We can add this plugin to our PluginContainer and specify sendMail.SendMail as the command callback function.

When SendMail is invoked, a dialog opens which prompts the user to send an email that contains a screenshot of Usdview. The user can modify the recipient, subject, and body of the email and select whether to send a screenshot of the entire main Usdview window or just the render viewport.

If we inspect sendMail.py, we can see it call usdviewApi.GrabWindowShot() and usdviewApi.GrabViewportShot() to capture both types of screenshot. We can also see an example of creating a dialog parented to the Usdview main window using usdviewApi.qMainWindow. The plugin also includes several pieces of data from the API to the email body.

Organizing usdview Plugins in a Production Environment

Although the PluginContainer system allows for any number of plugin modules to be discovered and executed, its design is meant to make it easier for "non-build experts" to add new usdview plugins.  Although in this simple tutorial our registerPlugins() method registered only a single command, it is capable of registering an arbitrary number of commands, and configureView() can create and configure any number of menus.  Putting all plugins in a single module, which is what we do at Pixar, has two advantages:

  1. Once that module is set up by a maintainer, users desiring to add new plugins do not need to know about or modify any plugInfo.json files
  2. It is much easier to organize all commands into a coherent, well-ordered set of menus when that setup happens in a single place.

Graphics Home