Houdini 20.0 Python scripting

Tool scripts

How to write Python scripts for shelf/asset tools.

On this page

Overview

A tool script runs when you click a shelf tool or choose a tool from a tab menu in a viewer or network editor. Tools can do something simple, like change a setting, or perform complex actions like running scripted interactions with the user and using the results to create nodes.

There are two places in Houdini you can write tool scripts:

  • In an asset, you can embed tools for creating and/or editing the asset inside the asset. The user can add custom tools to their shelf tabs. The tool script is also used to create the asset node in the viewer or the network editor.

  • You can create custom tools directly on the shelf (not embedded in an asset). This script may create or a node or perform any other actions possible through Python scripting.

Notes

  • When you create an asset, Houdini gives it a default tool with a generic script that handles basic interaction. You don’t need to modify the script unless you want to customize how the tool interacts with the user (see also Python states).

  • When you drag a node onto the shelf, Houdini automatically creates a shelf item to create that type of node. It does not use your default tool script in the new shelf tool, but instead uses a generic script. If your asset has a tool with custom interaction, you should add that tool to the shelf instead of dragging the node. See how to customize the shelf for more information.

  • While running shelf scripts Houdini is still “Live”. This is most noticeable if you acquire DOP Objects and then change the DOP Network. After changing the DOP Network, the current frame will recook and invalidate your DOP Objects.

    To avoid recooking the current frame, you can call hou.setSimulationEnabled() to disable the simulation for the duration of your operation. Make sure you record the initial simulation state (hou.simulationEnabled()) before turning it off, and restore it at the end of the script.

Arguments

When Houdini calls your script, it adds a dictionary variable named kwargs to the script’s context. This dictionary contains the following keys:

Key

Type

Description

pane

hou.PathBasedPaneTab subclass

The pane in which the tool was invoked.

  • If the tool was invoked in a Scene Viewer, this will be hou.SceneViewer.

  • You must also handle the case where the user invoked the tool from a Context Viewer (a viewer type that adapts to the current network). In that case

  • If the tool was invoked from the shelf, this is None. In this case, if you need a viewer your script will have to find one, for example with hou.ui.paneTabOfType(hou.paneTabTypes.SceneViewer).

See the scene_viewer() utility function below for a function to get a scene viewer no matter what the value of pane is.

viewport

hou.GeometryViewport

The viewport in which the tool was invoked. If the tool was not invoked in a Scene Viewer (or Context Viewer viewing geometry), this will be None.

panename

str

The name of the pane in which the tool was invoked (see pane above).

toolname

str

The internal name of this tool.

shiftclick

bool

Whether the user was holding ⇧ Shift when they clicked the tool (or selected it from the ⇥ Tab menu).

ctrlclick

bool

Whether the user was holding ⌃ Ctrl when they clicked the tool (or selected it from the ⇥ Tab menu).

altclick

bool

Whether the user was holding Alt (⌥ Option on Mac) when they clicked the tool (or selected it from the ⇥ Tab menu).

cmdclick

bool

Mac only. Whether the user was holding when they clicked the tool (or selected it from the ⇥ Tab menu).

requestnew

bool

Indicates whether the tool should create a new instance of the node (the usual), or re-use an existing node (for example, the Edit SOP and UV Edit SOP nodes are capable of this).

branch

bool

Whether to create the new node in a branch instead of appending to the current display node.

autoplace

bool

If this is True, the tool should place the node in the network without asking the user for a position. (This is separate from checking if ⌃ Ctrl/ were pressed.)

This is set when a tool is invoked in way that has no concept of placement or modifier clicks, for example dragging a node from the Tool Palette in the network editor.

bbox

hou.BoundingBox

May not be present. If this value is in the dictionary, it contains a bounding box you can use to represent an object, for the purpose of placing.

This value does not come from Houdini but instead is set for a tool by the bbox attribute in a .shelf file. (Houdini uses this internally to record a bbox value for the shape tools based on the geometry they create.)

inputs

list of tuples of hou.OpNode and int

A list of nodes and output connector indices that should be wired into the inputs of the new node. This list will be non-empty if the user presses MMB or RMB on a node output, which lets the user choose a node to wire from that output.

For backwards compatibility, the kwargs dict also has inputnodename and outputindex, which were used previously when only one input node could be specified.

inputnodename

str

The name of a node to wire into the new node’s input, or "" (the empty string). See inputs above.

outputindex

int

The index of a the output on inputnodename to wire into the new node’s input, or -1. See inputs above.

outputs

list of tuples of hou.OpNode and int

A list of nodes and input connector indices that should be wired into the output of the new node. This list will be non-empty if the user presses MMB or RMB on a node input, which lets the user choose a node to wire into that input.

For backwards compatibility, the kwargs dict also has outputnodename and inputindex, which were used previously when only one output node could be specified.

outputnodename

str

The name of a node to wire into the new node’s output, or "" (the empty string). See outputs above.

inputindex

int

The index of a the input on outputnodename to wire into the new node’s input, or -1. See inputs above.

Utility functions

The current API for building tool scripts is very low level (for example, each script is responsible for manually checking the world up, orienting to the construction plane, checking modifier keys, wiring nodes into the correct place, and more). We want to make this much easier though a higher-level HOM API in future versions of Houdini.

For now, we provide the stateutils module in an attempt to abstract some of the details. You can see usages of the functions in the how to section. The functions in stateutils are geared toward scripting SOP assets.

“Factory” Houdini shelf tools use a variety of internal libraries (toolutils, soputils, doputils, and others). These libraries are undocumented, do not provide good examples of Python usage, and are subject to change/deletion without notice. The plan is to replace them with a higher-level HOM API as mentioned above. However, currently it is sometimes necessary to call functions from those libraries. In the “how to” section below, the code snippets will sometimes call these functions when the user is not interacting in a viewer.

How to

Get a scene viewer

There are a few utility functions for getting a reference to the active pane or a hou.SceneViewer instance. These account for edge cases like context viewers that are viewing the scene.

# In a tool script
import stateutils


# Get the currently active pane. If kwargs["pane"] is None (like if the script
# is run from a shelf tool), this will call findSceneViewer() to find a
# SceneViewer. Otherwise this might be another pane type, such as a
# hou.NetworkEditor if the tool was selected in the network editor. You should
# check the type before assuming it's a SceneViewer.
pane = stateutils.activePane(kwargs)

# Get a hou.SceneViewer. If kwargs["pane"] is a SceneViewer, this returns
# that, otherwise it uses findSceneViewer() to find any SceneViewer it can.
scene_viewer = stateutils.activeSceneViewer(kwargs['pane'])

# This looks for visible scene viewers first, then falls back to finding a
# viewer that is not visible and making it the current tab. May raise
# hou.NotAvailble if there really is no SceneViewer in the desktop.
scene_viewer = stateutils.findSceneViewer()

Put down a generator SOP node

A generator SOP is one which generates data without any input (as opposed to modifying an incoming node).

# In a tool script
import soptoolutils
import stateutils


pane = stateutils.activePane(kwargs)
if isinstance(pane, hou.SceneViewer):
    # This function asks for a position (or auto-places if the user ctrl/cmd-
    # clicked), then creates a Geometry object and puts your SOP inside (also
    # handles "create in context" setting)
    stateutils.createGeneratorSop(
        kwargs, "$HDA_NAME", prompt="Select where to put the new thing"
    )
else:
    # For interactions other than in a viewer, fall back to the low-level
    # function
    soptoolutils.genericTool(kwargs, "$NODE_NAME")

Put down a filter node

A filter node is one which takes one or more inputs, modifies them, and outputs the result.

The following shows how you could implement a tool script for the traditional Copy to Points SOP.

# In a tool script
import soptoolutils
import stateutils


pane = stateutils.activePane(kwargs)
if isinstance(pane, hou.SceneViewer):
    # First we'll ask for the primitive(s) to copy
    source = stateutils.Selector(
        name="select_polys",
        geometry_types=[hou.geometryType.Primitives],
        prompt="Select primitive(s) to copy, then press Enter",
        primitive_types=[hou.primType.Polygon],
        # Which paramerer to fill with the prim nums
        group_parm_name="sourcegroup",
        # Which input on the new node to wire this selection to
        input_index=0,
        input_required=True,
    )
    # Then, we'll ask for the points to copy onto
    target = stateutils.Selector(
        name="select_points",
        geometry_types=[hou.geometryType.Points],
        prompt="Select points to copy onto, then press Enter",
        group_parm_name="targetgroup",
        # Remember to wire each selection into the correct input :)
        input_index=1,
        input_required=True,
    )

    # This function takes the list of Selector objects and prompts the user for
    # each selection
    container, selections = stateutils.runSelectors(
        pane, [source, target], allow_obj_selection=True
    )

    # This function takes the container and selections from runSelectors() and
    # creates the new node, taking into account merges and create-in-context
    newnode = stateutils.createFilterSop(
        kwargs, "$HDA_NAME", container, selections
    )
    # Finally enter the node's state
    pane.enterCurrentNodeState()

else:
    # For interactions other than in a viewer, fall back to the low-level
    # function
    soptoolutils.genericTool(kwargs, "$HDA_NAME")

Here’s a simpler example where the script prompts the user to select an object (or proceeds if the viewer is already inside an object), and then creates the new node in that object, without any component selection. If the user presses Enter without selecting a Geometry object, the tool creates a new object for itself. This might be useful for a node that can add geometry to its input but doesn’t modify a selection.

# In a tool script
import soptoolutils
import stateutils


_, _, basename, _ = hou.hda.componentsFromFullNodeTypeName("$HDA_NAME")


pane = stateutils.activePane(kwargs)
if isinstance(pane, hou.SceneViewer):
    # Instead of using runSelectors(), which lets the user select components,
    # we'll just ask for an object if needed
    container = pane.pwd()
    if container.childTypeCategory() != hou.sopNodeTypeCategory():
        # We're not already inside an object, so ask the user where they want
        # the new SOP
        objects = pane.selectObjects(
            prompt='Select objects',
            quick_select=False,
            use_existing_selection=True,
            allow_multisel=False,
            allowed_types=['geo']
        )
        if objects:
            # If the user selected more than one object, just take the first
            container = objects[0]
            # Jump into the object
            pane.setPwd(container)
        else:
            # The user pressed Enter without selecting an object, so create
            # a new object
            cname = basename + "_object1"
            container = hou.node("/obj").createNode("geo", node_name=cname)

        # Dive into the container
        pane.setPwd(container)

    # Create the new node in the selected container (the empty list represents
    # no component selections)
    newnode = stateutils.createFilterSop(kwargs, "$HDA_NAME", [])
    # Finally enter the node's state
    pane.enterCurrentNodeState()

else:
    # For interactions other than in a viewer, fall back to the low-level
    # function
    soptoolutils.genericTool(kwargs, "$HDA_NAME")

Notes:

  • If the allow_obj_selection argument to stateutils.runSelectors() is True (the default), and the user starts the tool at the object level, the script will allow the user to select whole objects (rather than components). If the argument is False, if the user starts at the object level, the script will prompt the user to select an object and then dive inside and request a component selection.

  • If you want to run the selectors associated with a traditional Houdini node (for example, a node inside your asset), you can get a list of its selectors using hou.NodeType.selectors.

    selectors = nodetype.selectors()
    container, selections = runSelectors(scene_viewer, selectors)
    
  • When writing your own custom prompt text, remember to tell the user to press Enter to finish the selection.

  • If you try to test the scene_viewer.selectXXX methods in the Python Shell window, it may seem to freeze or not do anything. This is because the prompt only appears when the mouse is over the viewer.

Prompt the user for a position, multiple positions, or a path

To ask the user for a location, as when you place new geometry using the tools on the Create shelf tab.

For tools with a “placement” phase (for example, the Sphere tool which lets you place a new sphere in the scene), whether to skip placement and just pick a “natural” placement. (For example, for the sphere tool, this places the sphere at the origin. For the Camera tool, this positions the camera to match the current view). The user invokes “auto-placement” by ⌃ Ctrl-clicking on Linux/Windows or -clicking on Mac. You should check both:

import stateutils


# The selectPositions() method of the hou.SceneViewer object lets you prompt
# the user for a certain number of positions (using the min_number_of_positions
# and number_of_positions keywords), optionally connecting multiple positions
# (for example when prompting for a path), displaying a bounding box (when
# prompting to place geometry), etc.

scene_viewer = stateutils.activeSceneViewer(kwargs['pane'])
if kwargs['ctrlclick'] or kwargs['cmdclick']:
    position, orientation = \
        stateutils.defaultPositionAndOrientation(scene_viewer)
else:
    # The result is a tuple of Vector3 objects.
    positions = scene_viewer.selectPositions(
        prompt='Click to specify a position',
        number_of_positions=1,
        min_number_of_positions=-1,
        connect_positions=True,
        show_coordinates=True,
        bbox=kwargs.get("bbox", BoundingBox()),
        position_type=positionType.WorldSpace,
        icon=None,
        label=None
    )
    position = positions[0]

See the help for hou.SceneViewer.selectPositions, hou.BoundingBox, and hou.positionType.

Modify node parameters

# Get a list of all parameters on a node
all_parms = my_cam.parms()

# Get the current value of a parameter
current_lookat = my_cam.parm("lookat").get()

# Set the value of a parameter
my_cam.parm("lookat").set("/obj/torus1")

Tip

The argument to the Node.parm() method is the internal name of the parameter. To find out the internal name of a parameter, hover over its label in the parameter editor. The internal name appears in a tooltip.

Detect what network context the tool is called in

You might want to make a tool that works inside two or more different network types. For example, the Delete action on the Modify shelf tab works at both the object level, where it deletes the selected object, and at the geometry level, where it creates a Blast surface node.

There are a few ways you could write this part of your script. One is to follow the code used by the Delete action’s script:

# In a tool script
import stateutils


# Find out current context. Returns the active pane when the tool/action was
# called. If there is not an active pane when the tool/action is called, this
# returns None.
scene_viewer = stateutils.active_scene_viewer(kwargs['pane'])

# If the active pane is not a scene viewer, raise an error
if not scene_viewer
   raise hou.Error("The tool was not invoked in the scene viewer.")

# Get the network context of the viewer.
child_type = active_pane.pwd().childTypeCategory()

if child_type == hou.objNodeTypeCategory():
    ...
elif child_type == hou.sopNodeTypeCategory():
    ...
elif child_type == hou.dopNodeTypeCategory():
    ...

Manipulate the current viewport

To get the current viewport…

import stateutils

# Get the scene viewer
scene_viewer = stateutils.find_scene_viewer()

# Get the current viewport
viewport =  scene_viewer.curViewport()

Calling the settings() method on the GeometryViewport object returns a GeometryViewportSettings object. This object has a ton of methods for getting and setting information about the viewport.

# Get the viewport's settings object
settings = viewport.settings()

Among the most useful methods on the settings object are viewTransform() and setViewTransform(), which get and set the viewport’s transformation matrix respectively. You can

To set the viewport to look through a camera, use…

viewport.setCamera(camera_node)

To get the camera node that a viewport is currently looking through…

# Returns None if not looking through a camera
viewport.settings().camera()

There’s a special method on viewports to copy their view to a camera or light, so you can copy the ctrl-click behavior of the standard tools on the Lights and Cameras shelf tab…

viewport.saveViewToCamera(cam_or_light_object)

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