Houdini 20.5 Python scripting

Scene Import LOP object translator plugin

On this page

Overview

Expert users can write Python plugins to customize how the Scene Import LOP translates specific Houdini object node types into USD. This is especially useful to translate custom node types, such as light and camera types associated with proprietary renderers. It may also be useful to modify or add to the translation of common object node types to handle custom data or workflows, or generate custom data on the USD side.

Note

You must be familiar with Pixar’s USD Python APIs to implement a translator plugin.

  • Houdini looks for scene import plugins in HOUDINIPATH/husdplugins/objtranslators/.

    You can customize where Houdini looks for USD-related plugins by setting the $HOUDINI_HUSDPLUGINS_PATH environment variable.

  • Create a Python source code (.py) file that will be found in that path, for example $HOUDINI_USER_PREF_DIR/husdplugins/objtranslators/my_translator.py.

    Copy the template content below and paste it into your plugin file.

  • A plugin file should have at least one translator class (subclassed from husd.objtranslator.Translator), and a function named registerTranslators() that takes a single manager argument and uses it to register the translator classes in the file (see the plugin file template).

  • You can define and register multiple translators in a single .py file (this improves startup time). However, you may want to keep each plugin in a separate file to have the flexibility to move them into/out of the path individually.

  • Files earlier in the path override files with the same name later in the path.

  • When the Scene Import node encounters an object node whose type name is registered with a plugin class, it instantiates the class with the node instance as the single argument. The default implementation of Translate.__init__() saves the node instance as self._node, so you can use that attribute in the other methods to access the object node being translated.

    Scene Import then calls various methods on the object to check whether the current object node should be translated, what kind of USD prim it should be translated into, and so on. See the file template below for more detail on each plugin method.

Plugin file template

In the Python file, paste in the following skeleton:

from husd.objtranslator import Translator
from pxr import Sdf, Usd


# Your translator class should subclass husd.objtranslator.Translator.
# You probably want to name your class based on the node type it translates,
# for example StudioEnvLightTranslator.

class MyCustomTranslator(Translator):
    def shouldTranslateNode(self):
        # Use self._node to access a hou.OpNode instance representing the current
        # object node to be translated.

        # Return True if this node should be translated into USD, or False if not.
        # Use this if, for example, no nodes of a given type should ever be
        # translated, or if a node has a parameter that "hides" it, and you don't
        # want to translate objects that have the "hide" parameter turned on.

        return True

    def primType(self):
        # Return a string containing the name of the USD prim type to create
        # corresponding to the object node being translated. For example,
        # "Xform".

        return ""

    def sopNodeFlag(self):
        # If the object node has a SOP network inside, this method tells Scene
        # Import which node you want to use as the geometry output of the object:
        # the node with the display flag, or the node with the render flag.
        # It should return either self.SOP_RENDER_FLAG, or self.SOP_DISPLAY_FLAG.

        return self.SOP_RENDER_FLAG

    def referencedNodePaths(self):
        # Use self._node to access a hou.OpNode instance representing the current
        # object node to be translated.

        # This should return a dict representing all external nodes this node
        # has references to/dependencies on.
        #
        # Each key in the dict is an arbitrary keyword describing the
        # relationship with the external node. The corresponding value is a
        # node path to the referenced node.
        #
        # (The dict, with USD paths instead of node paths, will be passed the
        # populatePrim() method below, so the keywords only have to be
        # meaningful/match up between these two methods.)
        #
        # For example, if an environment light node has a parameter where the
        # user can enter the node path of a Geo object to use as portal
        # geometry, you could do something like:
        # return {"portal": self._node.evalParm("env_portal")}

        return {}

    def populatePrim(self, prim, referenced_node_prim_paths, force_active):
        # Use self._node to access a hou.OpNode instance representing the current
        # object node to be translated.

        # Given a USD prim, fill out its properties with all necessary info
        # from the object node being translated.
        # 
        # Use the USD API to create and set the attributres, such as
        # prim.CreateAttribute() and prim.CreatePrimvar().
        # 
        # Use the self.populateAttr() helper function to copy information
        # from parameters on the object node into properties on the prim.
        # It automatically takes care of bookkeeping related to time
        # dependency and time samples.
        # 
        # This method is called with the following arguments:
        # 
        # prim: The USD primitive, corresponding to the object node being
        # translated. You should create and fill-in properties on this prim
        # so it can act as the USD equivalent of the original object node.
        # 
        # referenced_node_prim_paths: This is the dictionary you returned from
        # referencedNodePaths() above, but with the node paths in the values
        # replaced with the corresponding Sdf paths in the translated USD
        # stage.
        # 
        # force_active: if this is True, the user has set an option that says
        # the translated prims should always be authored as "active", even if
        # the original object node was invisible in the scene.

        pass


# You must include the registration function: this is what Houdini looks for
# when it loads your plugin file

def registerTranslators(manager):
    # The first argument is the name of the Houdini object node type,
    # the second argument is your class for translating that node type
    manager.registerTranslator('node_type_name', MyCustomTranslator)

How to

Let’s say you want to translate your studio’s proprietary dome light object, named studio::domelight.

We’ll start by creating a plugin file. For now we’ll create the file in the user preferences ($HOUDINI_USER_PREF_DIR/husdplugins/objtranslators) so it’s only available to the current user. Once the plugin is perfected, you might want to move it to a location in the Houdini path where it is picked up by all artists in a department, or all users in the studio (studios often customize the Houdini path to include shared network locations, such as the studio-wide $HSITE or the shot-wide $JOB, but this is beyond the scope of this example).

  1. In a text editor, create the plugin file $HOUDINI_USER_PREF_DIR/husdplugins/objtranslators/studio_domelight.py.

    (It is possible to implement multiple translators in a single file. In that case, you could name the file based on what the group of translators have in common, rather than naming it for a single translator.)

  2. Copy the template content above and paste it into the file.

  3. Rename the class so it indicates the object type(s) it translates, for example StudioDomeLightTranslator.

  4. At the bottom of the file, edit the call to manager.registerTranslator().

    • Change the first argument to the internal name of the Object node type the class with translate, studio::domelight in this case.

    • Change the class name to the class name you used above (StudioDomeLightTranslator).

    def registerTranslators(manager):
        # The first argument is the name of the Houdini object node type,
        # the second argument is your class for translating that node type
        manager.registerTranslator('studio::domelight', StudioDomeLightTranslator)
    
  5. Implement the shouldTranslateNode() method on the translator class.

    Often, this will simply return True. However, let’s say the dome light type has a light_enable parameter, and we only want to translate the dome light to USD if that parameter is on. We can access a hou.OpNode object representing the node using self._node.

    def shouldTranslateNode(self):
        # This is a checkmark parm, so eval() returns 0 or 1
        return self._node.parm("light_enable").eval()
    
  6. Implement primType() on the translator class. It must return a string containing the name of the USD prim type to create. All Translators must implement this method.

    def primType(self):
        return "DomeLight"
    
  7. By default, for object nodes that contain a SOP network (like the Geo node), the translator will use the output of the node with the render flag. If for some reason you want to use the display flag instead, you can override the sopNodeFlag() method, though this will usually not be needed.

    For the dome light, we will not override this method. But as an example, the translator for hairgen does use the display geometry, because the “render” geometry for that node is meant as input for a procedural shader, which cannot run inside USD.

    def sopNodeFlag(self):
        return self.SOP_DISPLAY_FLAG
    
  8. Implement referencedNodePaths() on the translator class, if the object node has links to other nodes that you need to re-create in USD as links between the translated prims. If the object node type doesn’t reference other nodes, you don’t need to implement this method.

    Some Houdini nodes have links with other nodes that are important to capture. For example, an environment light might have a link to a geometry object representing a portal, with a portal_anable parameter to turn it on, and a portal_path parm containing the node path of the Geometry object representing the portal.

    The method should return a dictionary. The keys are arbitrary (they allow you get back the information in the populatePrim() method), the values are node paths.

    def referencedNodePaths(self):
        if self._node.evalParm("portal_enable"):
            return {"portal": self._node.evalParm("portal_path")}
        else:
            return {}
    
  9. Implement populatePrim() on the translator class.

    This is where the real work of the translator happens. The Scene Import node will call this method with the following arguments:

    prim

    The USD prim created to be the translated version of the original object node. The purpose of this method is to populate this prim with properties and/or child prims so it does the same “job” in the USD stage as the original object node did in the Houdini scene.

    referenced_node_prim_paths

    This is the dictionary returned by your referencedNodePaths() method, except with the object node path values translated into the Sdf prim paths of the equivalent USD prims.

    For example, if your referenced NodePaths() method returned a dictionary mapping "portal" to the path of a Geometry node representing portal geometry, this argument will be a dictionary mapping "portal" to the scene graph path of the USD geometry prim translated from that Geometry object.

    It’s up to you to recreate the equivalent links to the other prim(s) using the USD API. For example:

    def populatePrim(self, prim, referenced_node_prim_paths, force_active):
        # Wrap the "raw" prim we received in the UsdLux.DomeLight API
        lgt = UsdLux.DomeLight(prim)
        # Check if we were passed a "portal" link from referencedNodePaths()
        if "portal" in referenced_node_prim_paths:
            portalPath = referenced_node_prim_paths["portal"]
            # Use the DomeLight API to create a portal relationship in USD
            lgt.CreatePortalsRel().SetTargets([portalPath])
    

    force_active

    The Scene Import LOP has a Force Objects parameter which lets the user list object nodes that must always be translated into USD as “active”, even the translator might normally not translate it (for example, if the translator doesn’t translate nodes are hidden, or have an “Enabled”-type parameter turned off), or might translate it as an invisible or disabled USD prim.

    For nodes matching the Force Objects pattern, the Scene Import doesn’t call your shouldTranslatePrim() method, it just goes ahead and creates the prim and calls your populatePrim() method to set it up, with this argument set to true so you know that the user wants this prim to be authored as “active”.

    For example:

    def populatePrim(self, prim, referenced_node_prim_paths, force_active):
        # Wrap the "raw" prim we received in the UsdLux.DomeLight API
        lgt = UsdLux.DomeLight(prim)
        # Check if we were passed a "portal" link from referencedNodePaths()
        if "portal" in referenced_node_prim_paths:
            portalPath = referenced_node_prim_paths["portal"]
            # Use the DomeLight API to create a portal relationship in USD
            lgt.CreatePortalsRel().SetTargets([portalPath])
    
        # Use the DomeLight API to create the intensity attribute, and set
        # it based on the object node's "coneangle" and "conedelta" parms
        intensity_attr = lgt.CreateIntensityAttr()
        if force_active:
            # Set the intensity attr directly from the node's intensity parm
            self.populateAttr(intensity_attr, self._node.parm("intensity"))
        else:
            # If the original light object was disabled, set its intensity
            # to zero, otherwise use the value of the intensity parm
            self.populateAttr(intensity_attr,
                              [self._node.parm("enabled",
                               self._node.parm("intensity"))]
                              lambda vs: vs[0] * vs[1])
    

    You should use the populateAttr() method to translate parameter values into USD properties. It takes care of a lot of bookkeeping related to time dependence and time samples.

Using populateAttr()

In the Translator.populatePrim() method, you should use the self.populateAttr(usd_attr, parm, callback=None) help method to copy data from a parameter into a USD property.

You might think it’s easier to simply read the parameter values and set the property value directly, for example:

# Create an intensity attribute on the prim
intensity_attr = lgt.CreateIntensityAttr()
# Read the value of the object node's intensity value
intensity_value = self._node.evalParm("intensity")
# Set the attribute to the parameter value
intensity_attr.Set(intensity_value)

However, this will give incorrect results when parameters are animated. To take into account animation, you would need to check whether the parameter has animation, and set up USD metadata related to time dependence and time samples. The populateAttr() helper method does this for you.

  1. The first argument is a USD property to copy the data into. You must use the USD API to create the property first, for example with prim.CreateAttribute() or prim.CreatePrimvar().

  2. The second argument should be one of the following:

    • A hou.Parm object.

    • A hou.ParmTuple object.

    • A list of hou.Parm objects.

    • A scalar value (string, integer, or float).

    • None (in this case, the method will silently do nothing).

    Since you can pass a value as the second argument, you might be tempted in some cases to pre-compute a value (such as to avoid repitition). For example:

    # DON'T DO THIS
    intensity_value = self._node.evalParm("intensity")
    enabled_value = self._node.evalParm("enabled")
    if force_active:
        self.populateAttr(intensity_attr, intensity_value)
    else:
        self.populateAttr(intensity_attr, enabled_value * intensity_value)
    

    However, by passing the value instead of the original parameter, you are hiding vital information about the parameter (such as whether it was animated) from the populateAttr() method. Instead, try to always pass parameter objects instead of values:

    # DO THIS
    if force_active:
        self.populateAttr(intensity_attr, self._node.parm("intensity"))
    else:
        self.populateAttr(intensity_attr,
                          [self._node.parm("enabled"), self._node.parm("intensity")],
                          lambda vs: vs[0] * vs[1])
    
  3. The optional third argument is a callable object (a function or lambda) that will be called on the value extracted from the parameter(s). The returned value is used to fill in the USD property. Use this when you need to process the original parameter value into a different value for USD.

For example:

class MyLightTranslator(Translator):
    def populatePrim(self, prim, referenced_node_prim_paths, force_active):
        # Wrap the "raw" prim we received in the UsdLux.DomeLight API
        lgt = UsdLux.DomeLight(prim)

        # Use the DomeLight API to create the radius attribute, and set
        # it based on the object node's "coneangle" and "conedelta" parms
        self.populateAttr(lgt.CreateRadiusAttr(),
                          [self._node.parm("coneangle"),
                           self._node.parm("conedelta")],
                          lambda vs: vs[0] / 2.0 + vs[1])

Python scripting

Getting started

Next steps

Reference

  • hou

    Module containing all the sub-modules, classes, and functions to access Houdini.

Guru level

Python viewer states

Python viewer handles

Plugin types