Houdini 18.0 Python Scripting

Writing custom viewer states in Python

A viewer state controls how to interpret mouse movements, clicks, keys, and so on in the viewer.

On this page

Python viewer states

Overview

Viewer states control interaction in the viewport. For example, the Rotate tool is a view state. The Handles tool allows access to the viewer state associated with the current node. Houdini lets you create and register your own custom view states in Python.

A custom state can, for example:

A Houdini digital asset can specify a state (for the Handles tool to use when a node of that type is current). Eventually you will be able to programmatically enter a state by calling hou.SceneViewer.setCurrentState(), for example in a shelf tool script (for tools that don’t create a node, such as an inspection tool).

To start playing with the code right away, see implementing a state below, which shows how to implement, register, and launch a minimal viewer state.

Tip

To see viewer states in action, check out the demo scene and asset files under $HH/viewer_states/examples. These samples provide a detailed coverage of the viewer state features.

Limitations

  • Currently Python states are available in the following contexts: SOP (geometry), OBJ, DOP and LOP levels.

  • Handles cannot be bound to nodeless states.

State names

A state has an internal name and a human readable label that appears in the UI. If you create a new custom state, the internal name you choose must be unique: if two authors use the same state name, one of the states will fail to register. If you consider that a state or asset you create might someday be shared with other users/studios or even sold as a product, you should take the time to ensure proper uniqueness.

There is the additional complication that Houdini automatically creates generic states for each asset, so you can’t use the name of an existing node type as a state name.

  • If the state is shared between more than one node, or not tied to a node, you must still ensure that the state name does not conflict with the generic state of any asset. The best way to do this is to incorporate the same namespace string you would use for an asset.

    For example, if you work at the Example.com movie studio and use examplecom:: as the namespace prefix for your assets, when you want to create a "scrub" state, you would use the state name examplecom::scrub.

  • State names do not have the same character restrictions as node type names and node names. A state name is more or less an arbitrary string.

  • You may need to use the name as a file/directory name (for example, if you're storing the code in a file)

Tool + state vs. self-contained state

Many, if not most "native" Houdini node states do not handle their own selection or create the node. Instead, they rely on a shelf tool script to ask for a selection and create the node. The state is then simply responsible for displaying handles.

You can use this workflow with a custom Python state as well, especially when the state is closely associated with an asset. You can write a Shelf tool script to ask for a selection and create the node (and filling in the node’s Group field with the selection).

Alternatively, your state can be self contained: it can create a node when invoked from the viewer and have its own selector.

(If your state doesn’t require a node (for example, an inspector-type tool), see writing nodeless state for more information.)

  • If you need to ask for more than one type of selections (for example, "Select some curves" followed by "Select some points on those curves"), use a tool script. Currently, a Python state can only accept a single type of selection.

  • If the code needs to know what’s in the selection before it creates the node, use a tool script.

See working with a node for more information on manipulating a node instance in a state.

Installing a state in Houdini

The most convenient and easiest way of creating and installing a viewer state in Houdini is to use the Viewer State Code Generator dialog. This code generator will get you a fully functional state in no time and will help you understand all the details involved in writing a viewer state from scratch.

There are currently two ways to make a custom state available in Houdini: embed the state’s code with an asset (HDA viewer state), or put a Python file containing the code in the right directory on the Houdini path.

Embedding a state in an asset

For creating HDA viewer states, use the code generator available in the Viewer State Editor tab of the Operator Type Properties window.

The following instructions create an asset and a minimal state so you can try out the code.

  1. Start with an asset you want to add a state to. To simplify the example, let’s start by using an OBJ asset.

    Note

    If you just want to start with a blank asset, you can to create a SOP asset or for an OBJ asset. The viewer state created with the code generator should work for both asset types.

    To quickly create a "blank" SOP asset to experiment with:

    1. At the Object level, use the ⇥ Tab menu to create a Geo object.

    2. Double-click the geo1 node to dive into the Geometry network inside.

    3. Use the ⇥ Tab menu to create a Subnetwork node.

    4. Right-click the subnet1 node and choose Create digital asset.

    5. Set the Operator name to statedemo, the Operator Label to State Demo, and Save to library to Embedded.

      Setting the library location to Embedded saves the asset with the current scene file instead of in an asset library.

    To quickly create a "blank" OBJ asset to experiment with:

    1. At the Object level, use the ⇥ Tab menu to create a Subnetwork node.

    2. Right-click the subnet1 node and choose Create digital asset.

    3. Set the Operator name to statedemo, the Operator Label to State Demo, and Save to library to Embedded.

      Setting the library location to Embedded saves the asset with the current scene file instead of in an asset library.

  2. Open the type properties window for the asset. (Right click an instance of the asset type and choose Type properties).

  3. Click the Interactive|Viewer State tab.

  4. Click the New… button to generate the state code.

  5. Select the onMouseEvent event handler and click Accept.

You should now have a functional state listed in the Viewer State Browser tree as Statedemo.

import hou
import viewerstate.utils as su

class State(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer


    def onMouseEvent(self, kwargs):
        """ Process mouse events
        """
        ui_event = kwargs["ui_event"]
        dev = ui_event.device()
        self.log("Mouse:", dev.mouseX(), dev.mouseY(), dev.isLeftButton())

        # Must return True to consume the event
        return False


def createViewerStateTemplate():
    """ Mandatory entry point to create and return the viewer state 
        template to register. """

    state_typename = kwargs["type"].definition().sections()["DefaultState"].contents()
    state_label = "Statedemo"
    state_cat = hou.objNodeTypeCategory()

    template = hou.ViewerStateTemplate(state_typename, state_label, state_cat)
    template.bindFactory(State)
    template.bindIcon(kwargs["type"].icon())

    return template

See implementing a state below for more information on adding functionality to the state class.

To test the state, select the asset in the network editor. Move the mouse into the scene viewer and press Enter. The mouse coordinates should be logged in the Viewer State Browser console as you move the mouse.

Loading a state from the Houdini path

For creating file viewer states, use the code generator in the Viewer State Browser window.

The following instructions create a file viewer state that can be "shared" between multiple assets. File viewer states are automatically registered by Houdini on startup.

  1. Open the Viewer State Browser window with the New Pane Tab Type ▸ Viewer State Browser menu.

  2. Select the Object category from the toolbar.

  3. Open the Viewer State Code Generator with the File ▸ New State… menu.

  4. Enter statedemo as the state’s name in the Name field.

  5. Select the onMouseEvent event handler and click Accept.

The new file viewer state should be saved as $HOUDINI_USER_PREF_DIR/viewer_states/statedemo.py, listed in the Viewer State Browser tree as Statedemo.

You should now have a functional state listed in the Viewer State Browser tree as Statedemo.

import hou
import viewerstate.utils as su

class State(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer


    def onMouseEvent(self, kwargs):
        """ Process mouse events
        """
        ui_event = kwargs["ui_event"]
        dev = ui_event.device()
        self.log("Mouse:", dev.mouseX(), dev.mouseY(), dev.isLeftButton())

        # Must return True to consume the event
        return False


def createViewerStateTemplate():
    """ Mandatory entry point to create and return the viewer state 
        template to register. """

    state_typename = "statedemo"
    state_label = "Statedemo"
    state_cat = hou.objNodeTypeCategory()

    template = hou.ViewerStateTemplate(state_typename, state_label, state_cat)
    template.bindFactory(State)
    template.bindIcon("MISC_python")

    return template

To test the state, find an asset you want to add a state to, or create a blank SOP asset with or for an OBJ asset. To simplify the example, create an OBJ asset.

To quickly create a "blank" SOP asset to experiment with:

  1. At the Object level, use the ⇥ Tab menu to create a Geo object.

  2. Double-click the geo1 node to dive into the Geometry network inside.

  3. Use the ⇥ Tab menu to create a Subnetwork node.

  4. Right-click the subnet1 node and choose Create digital asset.

  5. Set the Operator name to statedemo, the Operator Label to State Demo, and Save to library to Embedded.

    Setting the library location to Embedded saves the asset with the current scene file instead of in an asset library.

To quickly create a "blank" OBJ asset to experiment with:

  1. At the Object level, use the ⇥ Tab menu to create a Subnetwork node.

  2. Right-click the subnet1 node and choose Create digital asset.

  3. Set the Operator name to statedemo, the Operator Label to State Demo, and Save to library to Embedded.

    Setting the library location to Embedded saves the asset with the current scene file instead of in an asset library.

  1. Open the type properties window for the asset. (Right click an instance of the asset type and choose Type properties).

  2. Click the Node tab.

  3. Set the Default state field to the state name (for example, in the code above, this is statedemo).

  4. At the bottom of the type properties window, click Accept.

    • If the asset was locked, Houdini will prompt you to unlock the asset so you can save your changes.

  5. To start testing, select the asset in the network editor. Move the mouse into the scene viewer and press Enter. The mouse coordinates should be logged in the Viewer State Browser console as you move the mouse.

    • The toolbar at the top of the viewer should display the state’s label on the left.

On startup, Houdini will call createViewerStateTemplate to access the state template and perform the registration. If omitted, Houdini will simply skip the state registration.

See implementing a state below for more information on adding functionality to the state class.

Tip

To reload a file state during a Houdini session, on the state name listed in the Viewer State Browser tree and select Reload in the context menu. This will allow you to test changes to your state without restarting Houdini.

You can also reload the state with python by calling hou.ui.reloadViewerState() with the name of the state.

Implementing the state

The following section shows a high-level overview of the methods you can add to the class implementing your state.

For guides to implementing specific functionality, see the following pages:

Initializer (required)

def __init__(self, state_name, scene_viewer):

state_name

The state name string this state was registered under.

scene_viewer

A hou.SceneViewer object representing the scene viewer the tool is operating in. This object has many useful methods you can use to implement your state, for example hou.SceneViewer.currentGeometrySelection() and hou.SceneViewer.setCurrentGeometrySelection().

In general, you’ll just want to store the arguments in object attributes in case other methods need them:

class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

    # Event handlers
    # ...

Event handlers

Event handlers are called with a single argument, a dictionary containing various useful items. The dictionary will typically (but not always) contain the following items:

node

Contains a hou.Node instance representing the node being operated on by the current state.

Menu items

The dictionary also contains the current values associated with menu items, using the menu item name as the key.

state_parms

Contains the names representing the state parameters bound to the current state. This dictionary is used for modifying the parameter states. See the details here.

state_flags

A dictionary containing various flags associated to the state. State flags can be set by all state handlers via their kwargs argument.

Flag

Notes

mouse_drag

Controls whether a mouse drag event (with the up) is generated or not. Mouse drag events are generated by default (True).

When mouse_drag is False, no mouse drag event is generated. If a selector is active, the selectable elements will not be highlighted.

If mouse drag events are not required by your state, consider setting mouse_drag to False to reduce the overhead.

class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

    onEnter( self, kwargs):
        kwargs['state_flags']['mouse_drag'] = False
        ...                

redraw

The flag triggers a redraw of the viewport on a mouse move or mouse wheel event when it’s set to True.

By default the viewport always redraw. To reduce performance issues with large scenes, redraw should be set to False when your state doesn’t require a redraw of the viewport.

class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

    onMouseEvent( self, kwargs):
        kwargs['state_flags']['redraw'] = False
        if __some_redraw_test__:
            kwargs['state_flags']['redraw'] = True
        ...                

The following tables list the event handlers you can define on your state class.

Lifecycle event handlers

Method name

Called by Houdini

Notes

onEnter

state is entered from the network

This method is called when the state is activated by the user creating a new node, or selecting an existing node and pressing Enter in the viewport.

onInterrupt

state is interrupted

This method is called when:

  • The window loses focus.

  • The pointer leaves the viewer (including moving over a menu).

  • The user pushes a "volatile" tool (for example, holding down S to enter the volatile selection tool).

onExit

state is about to be exited

This method is called when:

  • The user or Houdini switches to a different tool.

  • The user closes the viewer or switches to a different pane tab.

  • The user exits Houdini.

Note: kwargs['node'] may not be present if the node has been deleted.

onResume

interruption ends

This method is called when:

  • The pointer re-enters the viewer.

  • The user pops back to this state from a "volatile" tool (for example, releasing S after using the volatile selection tool).

onGenerate

state is entered without an existing node

This method is called when the state is activated by the user entering the state not from an existing node, for example a shelf tool script calling hou.SceneViewer.setCurrentState(). See nodeless states for information on states that work without a node.

For states associated with assets, you could theoretically create a node in this method, however we strongly recommend creating the node in the tool script instead. See working with a node in a Python state for more information.

The dictionary passed to this method does not contain the node item. You can get the network location corresponding to the viewer using hou.SceneViewer.pwd.

Additional notes:

  • Houdini calls either onEnter (if the state should work on an existing node) or onGenerate (if the should create a new node) when the tool activates, never both. The dictionary passed to onEnter has a node item to access the existing node as a hou.Node object. See editing nodes for more information.

  • Houdini will call onEnter/onGenerate when the state begins, even if the mouse pointer is not over the viewer at the time. If you want to display screen visuals around the cursor, wait for an onMouseEvent call to show them.

  • While the state is "interrupted" it does not receive UI events.

  • You cannot rely on every onInterrupt call being followed by a corresponding onResume. If the user exits the state while it’s interrupted, you will not receive an onResume call, just onExit.

  • If the user opens the state’s context menu, when the pointer moves over the menu and leaves the menu you will receive onInterrupt and onResume events.

  • If your state is current but interrupted because Houdini is in the background, and the user switches to Houdini, even if the mouse pointer is over the viewer, your state will not receive an onResume call until the user makes some input (such as moving the mouse).

UI event handlers

See UI event handling for more information.

The dictionary passed to these methods has the following extra item:

ui_event

Contains a hou.ViewerEvent instance with information about the event (for example, for a mouse event, the current mouse coordinates and whether a button was clicked).

Method name

Called by Houdini

Notes

onMouseEvent

mouse moves/clicks

See mouse handling.

onMouseWheelEvent

mouse wheel scroll

hou.UIEventDevice.mouseWheel() returns -1 or 1 depending on the scroll direction.

See mouse wheel handling.

onKeyEvent

For key events

See reading the keyboard device for more.

onKeyTransitEvent

For key transition events

See reading the keyboard device for more.

onMenuAction

context menu choice

See context menu handling.

onMenuPreOpen

update menu state

See updating context menu.

The handler kwargs argument contains the following items:

menu

The identifier of the menu to update.

menu_states

A dictionary of menu states holding update values. Use the menu value to index the dictionary. The value part holds a dictionary of states for the menu.

States supported

enable: Enables or disables a menu.

value: The value state is supported for radio strip menus only, it allows you to select the radio strip item with the menu item id (see hou.ViewerStateMenu.addRadioStripItem()).

menu_item_states

A dictionary of menu item states holding update values. Use a menu item handle to index the dictionary. The value part holds a dictionary of states for the menu item.

States supported

enable: Enables or disables a menu item.

value: Corresponds to the menu item intrinsic state. For instance, value can be set to True to set a toggle menu item.

Note

value is only supported for toggle menu items.

onParmChangeEvent

State parameter events

See state parameter handling.

onCommand

general purpose command events

Called by invoking hou.SceneViewer.runStateCommand(). The handler kwargs argument contains the following items:

command

The command string identifier.

command_args

A python object holding the command specific arguments.

Additional notes:

  • onCommand is used for implementing specific actions on the state. For instance, you can use onCommand to set state parameters or to implement a custom notification mechanism. For details see hou.SceneViewer.runStateCommand().

Handle event handlers

Houdini will call these methods if you have bound dynamic handles to your state. See Python state handles for more information.

Method name

Called by Houdini

Notes

onHandleToState

user interaction with a handle

This lets you update node parameters (and/or the state/display) when a handle changes.

The dictionary passed to this method contains the following extra items:

handle

The string ID of the handle.

parms

A dictionary containing the new handle parameter values.

mod_parms

A list of of the names of parameters that changed.

prev_parms

A dictionary containing the previous handle parameter values. This can be useful for computing deltas.

ui_event

hou.UIEvent object to know about the handle status such as start dragging or stop dragging.

onStateToHandle

node parameters change

This lets you update handle parameters when node parameters change.

The dictionary passed to this method contains the following extra items:

handle

The string ID of the handle.

parms

A dictionary containing the new node parameter values.

onBeginHandleToState

start of user interaction with a handle

This lets you know when the user has started to manipulate a handle.

The dictionary passed to this method contains the following extra items:

handle

The string ID of the handle.

ui_event

hou.UIEvent object to know about the handle status such as start dragging or stop dragging.

onEndHandleToState

end of user interaction with a handle

This lets you know when the user has ended the manipulation a handle.

The dictionary passed to this method contains the following extra items:

handle

The string ID of the handle.

ui_event

hou.UIEvent object to know about the handle status such as start dragging or stop dragging.

Selection event handlers

Houdini will call these methods if you have bound selectors to your state. See selection handling for more information.

Method name

Called by Houdini

Notes

onStartSelection

user starts selecting

The dictionary passed to this handler has the following item:

name

The name of the current active selector (see hou.ViewerStateTemplate.bindGeometrySelector()).

onSelection

user selected geometry

The dictionary passed to this handler has the following extra items:

selection

A hou.GeometrySelection object representing the completed selection.

name

The name of the current active selector.

You should Return True from this method to signal your state "accepts" the current selection and stop the selector. If the method returns any other value (or doesn’t contain a return statement), the selector will remain active.

onStopSelection

user stops selecting

This is called when the state "accepts" a selection by returning True from onSelection(), or if the state exits before accepting a selection.

The dictionary passed to this handler has the following item:

name

The name of the current active selector.

Drag and Drop event handlers

Houdini calls the methods below for handling the drag drop events occurring in the viewer when a python state is active. See Drag and Drop handling for more.

Method name

Note

onDragTest

Called when the user initiates a drag event.

onDropGetOptions

Called to build a list of drop options to let the user chooses one.

onDropAccept

Handles the selected drop option.

Drawing event handlers

These methods are called when drawing events are generated. For instance, these methods would be implemented for processing hou.AdvancedDrawable objects.

Method name

Called by Houdini

Notes

onDraw

drawing events

The method is called when:

  • The user causes an interactive event such as a mouse move and a mouse-click.

  • Houdini is forced to redraw the current viewport.

onDrawInterrupt

drawing events

The method is called when:

  • The user interrupts the state like for orbiting the view or scrubbing the timeline.

  • The mouse pointer leaves the viewer.

  • The window loses focus.

Additional notes:

  • onDraw is required for advanced drawables. The handle returned by kwargs["draw_handle"] must be passed to drawables for performing the render operation.

    def onDraw(self, kwargs):
        handle = kwargs['draw_handle']
    
        params = { 'translate': hou.Vector3(self.mouse_pos[0],self.mouse_pos[1],self.mouse_pos[2]) }
        self.cursor.render(handle, params )
    
        params = { 
            'translate': hou.Vector3(self.translate[0],self.translate[1],self.translate[2]),
            'color1': hou.Color(self.rgba[0],self.rgba[1],self.rgba[2],self.rgba[3]),
            'blur_width': self.blur_width
        }
        self.face_drawable.render(handle, params)
    
  • onDrawInterrupt is optional and only used with advanced drawables when drawing is required during a state interruption.

Inspecting Viewer States

Houdini lets you view all registered viewer states with the Viewer State Browser window. The browser can be opened via the Python Panel menu.

Debugging tips

Houdini provides some support for debugging python viewer states. The most useful is the viewer state browser which offers integrated tools for displaying debugging information. You can still use a more basic solution like the python print function, but the browser offers more functionality with regards to debugging.

Displaying debugging information

The most basic but still useful form of debugging is to print information, such as the contents of variables, as the script runs. Houdini provides a few ways of displaying this type of information for python states.

The main advantage of using prints is that you can output a lot of information, including multi-line blocks of text (such as the current Python call stack), and scroll back to read it later.

self.log method

The log method is available with your python state class and is similar to the python print function. Instead of printing to the standard output, it prints messages to the viewer state browser console.

  • The browser lets you control when to log messages by toggling the Debug log button. This allows you to keep the log method call in your code instead of commenting out when debugging is not required.

  • Houdini adds the log method dynamically to the python state after the object was created. log is therefore not yet available when __init__ is called. The workaround is to use viewerstate.utils.log from __init__ for logging messages in the Viewer State Browser console.

import traceback

class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

    def onMouseEvent(self, kwargs):
        # Log the mouse position
        ui_event = kwargs["ui_event"]
        device = ui_event.device()
        self.log("Mouse position x=", device.mouseX(), "y=", device.mouseY())

        # Log the current Python call stack
        self.log(''.join(traceback.format_stack()))

Python print function

The output appears in a console window, or Houdini’s Python shell window, or the shell you started Houdini from, depending on how you started Houdini and what windows are open.

Tip

It’s a good idea to use from __future__ import print to get used to using print as a function instead of as a statement. The function is easier to use and has more functionality.

from __future__ import print
import traceback

class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

    def onMouseEvent(self, kwargs):
        # Print the mouse position
        ui_event = kwargs["ui_event"]
        device = ui_event.device()
        print("Mouse position x=", device.mouseX(), "y=", device.mouseY())

        # Print the current Python call stack
        traceback.print_stack()

hou.SceneViewer.setPromptMessage

The hou.SceneViewer.setPromptMessage() and hou.SceneViewer.clearPromptMessage() functions let you display prompts for user interaction in the status line at the bottom of the main Houdini window. You can "abuse" these functions to display debugging information.

  • The advantage of this method is that the information is front-and-center in the window as you interact with Houdini.

  • The disadvantage is that the status line can only show one line of information at a time and when you change it the previous message is lost.

class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

    def onMouseEvent(self, kwargs):
        # Display the mouse position in the status line
        ui_event = kwargs["ui_event"]
        device = ui_event.device()
        message = "Mouse x=%d y=%d" % (device.mouseX(), device.mouseY())
        self.scene_viewer.setPromptMessage(message)

hou.ui.displayMessage

The hou.ui.displayMessage() function pops up a message window and waits for the user to click OK or Cancel.

  • This has a few advantages for debugging. One is that it pauses the script while it waits, which may be useful if you're trying to investigate changes that happen very quickly. It also lets you give some feedback to the script based on which button you click. Another is that the function has keyword arguments that let you attach a block of "details" text that is hidden by default but can be expanded. This is useful, for example, for displaying the current Python call stack.

  • You probably don’t want to use this function in a loop, where it would be annoying to have to click to dismiss multiple message windows.

import traceback

import hou

class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

    def onMouseEvent(self, kwargs):
        ui_event = kwargs["ui_event"]
        device = ui_event.device()
        # Only diplay the message on a click
        if device.isLeftButton():
            message = "Mouse x=%d y=%d" % (device.mouseX(), device.mouseY())

            # Get the call stack at this point in the script and format it
            # in a string so we can attach it to the message as "details"
            details = "".join(traceback.format_stack())

            # Display a message and wait for the user to click a button in
            # the message window
            clicked = hou.ui.displayMessage(
                message, buttons=("OK", "Error"),
                details_label="Current call stack",
                details=details
            )

            # If user clicked the "Error" button, raise an error
            if clicked == 1:
                raise Exception("Don't blame me!")

Reloading states

For states defined on disk, if the files on disk change, you can tell Houdini to reload the state using hou.ui.reloadViewerState(). Use the Viewer State Editor to reload HDA embedded states.

HOM API

Python viewer states

Python Scripting

Getting started

Next steps

Python viewer states

You can write viewer states in Python that let you customize user interaction in the viewport for your node.

Guru level

Reference

  • hou

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

  • Alembic extension functions

    Utility functions for extracting information from Alembic files.