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 |
---|---|---|
|
hou.PathBasedPaneTab subclass |
The pane in which the tool was invoked.
See the |
|
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 |
|
|
|
The name of the pane in which the tool was invoked (see |
|
|
The internal name of this tool. |
|
|
Whether the user was holding ⇧ Shift when they clicked the tool (or selected it from the ⇥ Tab menu). |
|
|
Whether the user was holding ⌃ Ctrl when they clicked the tool (or selected it from the ⇥ Tab menu). |
|
|
Whether the user was holding Alt (⌥ Option on Mac) when they clicked the tool (or selected it from the ⇥ Tab menu). |
|
|
Mac only. Whether the user was holding ⌘ when they clicked the tool (or selected it from the ⇥ Tab menu). |
|
|
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). |
|
|
Whether to create the new node in a branch instead of appending to the current display node. |
|
|
If this is 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. |
|
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 |
|
|
|
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 or on a node output, which lets the user choose a node to wire from that output. For backwards compatibility, the |
|
|
The name of a node to wire into the new node’s input, or |
|
|
The index of a the output on |
|
|
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 or on a node input, which lets the user choose a node to wire into that input. For backwards compatibility, the |
|
|
The name of a node to wire into the new node’s output, or |
|
|
The index of a the input on |
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 tostateutils.runSelectors()
isTrue
(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 isFalse
, 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)