Back to Openusd

Creating a Usdview Plugin

docs/tut_usdview_plugin.rst

26.0512.5 KB
Original Source

.. include:: rolesAndUtils.rst

.. include:: tut_setup_version_badge.rst

========================= Creating a Usdview Plugin

In this tutorial, we'll create a new Python plugin for :program:usdview and learn how to use the :program: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.

.. code-block:: console

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

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

.. code-block:: python :caption: 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 :python:PluginContainer is loaded, usdview's plugin system first calls :python: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 :python: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 :python:PluginContainer has registered all of its command plugins, it is given the chance to expose them to usdview's UI in the :python: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 <api/plug_page_front.html>_ library, we also need to define our :python: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.

.. code-block:: json :caption: tutorialPlugin/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 :python: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 is listed in our PYTHONPATH environment variable. For this example, the path to :filename:usdviewPlugins/ created above would be added to the PYTHONPATH. If trying the sendMail.py example, :filename:extras/usd/examples/usdviewPlugins would be the relevant plugins directory.

If we want libplug to load our plugin, we also need to add the path to the plugin directory to the PXR_PLUGINPATH_NAME environment variable. In this example, that would be the path to :filename:tutorialPlugin/.

At this point, if we open :program: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. If the "Tutorial" menu does not appear, try using full paths in the above environment variables and ensure files are named exactly __init__.py and plugInfo.json.

Congratulations! We have just created a new :program:usdview plugin!

Using the usdview API

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

* :python:`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.

    * :python:`stage` - The current Usd.Stage object.

    * :python:`currentFrame` - usdview's current frame.

    * :python:`viewSettings` - A collection of settings which only affect 
      the viewport. Most of these settings are normally controlled using 
      usdview's 'Display' menu. Some examples are listed below.

        * :python:`complexity` - The scene's subdivision complexity.

        * :python:`freeCamera` - The camera object used when 
          :program:`usdview` is not viewing through a camera prim. Plugins 
          can modify this camera to change the view.

        * :python:`renderMode` - The mode used for rendering models 
          (smooth-shaded, flat-shaded, wireframe, etc.).

    * :python:`selection` - The current state of prim and property 
      selections.

        * Common prim selection methods: :python:`getFocusPrim()`, 
          :python:`getPrims()`, :python:`setPrim(prim)`, 
          :python:`addPrim(prim)`, :python:`clearPrims()`

        * Common property selection methods: :python:`getFocusProp()`, 
          :python:`getProps()`, :python:`setProp(prop)`, 
          :python:`addProp(prop)`, :python:`clearProps()`

* :python:`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.

* :python:`usdviewApi.PrintStatus(msg)` - Prints a status message at the 
  bottom of the :program:`usdview` window.

* :python:`GrabViewportShot()`/:python:`GrabWindowShot()` - Captures a 
  screenshot of the viewport or the entire main window and returns it as a 
  QImage.

Deferring Imports

:program:usdview is designed to be quick to launch, so to be a good :program: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 :python: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.

.. code-block:: python :caption: 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.

.. code-block:: python :caption: tutorialPlugin/init.py - Normal Import

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

from . 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 :program:usdview now, we'll immediately see the message "Imported printer!" appear in the console. Now, we'll do a deferred import.

.. code-block:: python :caption: 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.

.. note:: :python: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 :program:usdview again, we won't see the "Imported printer!" message until we invoke :python:printMessage. The module is only imported once, so invoking :python: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 :filename:USD/extras/usd/examples/usdviewPlugins/sendMail.py. We can add this plugin to our :python:PluginContainer and specify :python: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 :program:usdview. The user can modify the recipient, subject, and body of the email and select whether to send a screenshot of the entire main :program:usdview window or just the render viewport.

If we inspect :filename:sendMail.py, we can see it call :python:usdviewApi.GrabWindowShot() and :python:usdviewApi.GrabViewportShot() to capture both types of screenshot. We can also see an example of creating a dialog parented to the :program:usdview main window using :python: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 :python: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 :program:usdview plugins. Although in this simple tutorial our :python:registerPlugins() method registered only a single command, it is capable of registering an arbitrary number of commands, and :python: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:

#. 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

#. It is much easier to organize all commands into a coherent, well-ordered
   set of menus when that setup happens in a single place.