Houdini 20.5 Python scripting

Python state guide geometry

How to display guide geometry in the viewport based on your state’s data and user interactions.

On this page

Overview

(See Python states for the basics of how to implement a custom viewer state.)

Guide geometry is 3D “user interface” geometry that only appears while your state is active – it is seaprate from the “real” geometry generated by a SOP network. For example, in a “wind force” node, the guide geometry might be an arrow to show the direction and strength of the wind force. In the brush node, the guide geometry is the ring showing the brush’s area of influence on the geometry.

Your state could have no guide geometry, or it might display simple geometry you can generate with verbs, or it might display elaborate guides and previews by cooking geometry inside your asset.

Drawable and geometry objects

Houdini offers several drawable objects to let you display guide geometries. The geometry source can be a hou.Geometry object or a hou.drawablePrimitive value.

See the Drawable object help to familiarize yourself with the API.

  • The Drawable object keeps a reference to the Geometry object. If content of the underlying Geometry object changes, you do not need to recreate the Drawable object, it picks up the changes automatically.

  • If you get the geometry from a SOP node using hou.SopNode.geometry or from a drawable using hou.SimpleDrawable.geometry or hou.GeometryDrawable.geometry, the resulting Geometry object is a live, read-only reference to the node’s output. If the node’s output changes (for example, because it is driven by parameters on your asset), the contents of the Geometry object automatically update.

  • Guide geometry is often drawn in wireframe with hou.SimpleDrawable to distinguish it from “real” geometry. This is default display mode for the Simple Drawable object. You can set the wireframe color using hou.SimpleDrawable.setWireframeColor. You can change a Drawable object to shaded mode using hou.SimpleDrawable.setDisplayMode.

  • Guide geometry created with hou.GeometryDrawable is not displayed in wireframe by default, the API provides various options to display the geometry.

  • Drawables are always drawn in world space. See compensating guide transforms below for how to transform local coordinates into world space.

  • If you need to animate guide geometry, it is much more efficient if you can achieve the effect you want by manipulating the Drawable’s transform, rather than recreating the Drawable over and over. See positioning, rotating, and scaling guides below.

    For example, if you want a sphere to track the mouse location on the ground plane, you should create the sphere once and move it by changing its transform, rather than creating a new drawable with the sphere in a different position for every mouse move.

  • A reference to the Drawable object must exist for it to continue to appear in the viewer. You should store a reference to the drawable object on the state implementation object to prevent it from being deleted by the Python garbage collector.

  • The Drawable object may not appear the instant you first show the object. It will appear the next time the viewer redraws, for example when the user tumbles. You can force an individual viewport to redraw using hou.GeometryViewport.draw:

    scene_viewer.curViewport().draw()
    

Gadgets

Python state gadgets are specialized geometry drawables designed to provide visual support for picking and locating geometries. They can be used in various ways with Python states. For example, you can use them to improve guide geometries or to create your own private custom handles when the requirement of a full blown Python handle is not required.

The usage of Python state gadgets are similar to Python handle gadgets, you need to register them first in order to be used. The registered gadget instances are created automatically by Houdini and assigned to your Python state class as a dictionary attribute (state_gadgets) using the gadget name as the key.

The code snippet below shows how gadgets can be used in a Python state. onMouseEvent handles the user interaction by using self.state_context. There is one context per state and it’s created by Houdini, you don’t need to create this type of object yourself. self.state_context holds up the contextual information for the gadget being located or picked. onMouseEvent is simply updating the mouse cursor with the active gadget name and its component identifiers.

onDraw simply updates the face gadget drawable with the geometry polygon located under the cursor. Since self.state_context has already been updated by Houdini with the polygon id, the state doesn’t need to perform a geometry intersection to locate the polygon.

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 = "State gadget test"
    state_cat = hou.sopNodeTypeCategory()

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

    template.bindGadget( hou.drawableGeometryType.Line, "line_gadget", label="Line" )
    template.bindGadget( hou.drawableGeometryType.Face, "face_gadget", label="Face" )
    template.bindGadget( hou.drawableGeometryType.Point, "point_gadget", label="Point" )

    menu = hou.ViewerStateMenu(state_typename + "_menu", state_label)        
    menu.addActionItem("cycle", "Cycle Gadgets", hotkey=su.hotkey(state_typename, "cycle", "y"))
    template.bindMenu(menu)

    return template

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

    def onEnter(self, kwargs):
        """ Assign the node input geometry to gadgets
        """
        node = kwargs["node"]
        self.geometry = node.geometry()

        self.line_gadget = self.state_gadgets["line_gadget"]
        self.line_gadget.setGeometry(self.geometry)
        self.line_gadget.setParams({"draw_color":[.3,0,0,1], "locate_color":[1,0,0,1], "pick_color":[1,1,0,1], "line_width":2.0})
        self.line_gadget.show(True)

        self.face_gadget = self.state_gadgets["face_gadget"]
        self.face_gadget.setGeometry(self.geometry)
        self.face_gadget.setParams({"draw_color":[0,.3,0,1], "locate_color":[1,0,0,1],"pick_color":[1,1,0,1]})
        self.face_gadget.show(True)

        self.point_gadget = self.state_gadgets["point_gadget"]
        self.point_gadget.setGeometry(self.geometry)
        self.point_gadget.setParams({"draw_color":[0,0,.3,1], "locate_color":[0,0,1,1], "pick_color":[1,1,0,1], "radius":15.0})
        self.point_gadget.show(True)

    def onMouseEvent(self, kwargs):
        """ Computes the cursor text position and drawable geometry
        """
        # set the cursor label
        self.cursor.setParams(kwargs)

        ui_event = kwargs["ui_event"]

        gadget_name = self.state_context.gadget()
        if gadget_name in ["line_gadget", "face_gadget", "point_gadget"]:
            gadget = self.state_gadgets[gadget_name]

            label = self.state_context.gadgetLabel()
            c1 = self.state_context.component1()
            c2 = self.state_context.component2()

            self.cursor.setLabel("{} : {} {}".format(label, c1, c2 if c2 > -1 else ""))
            self.cursor.show(True)

        else:
            self.cursor.show(False)

        return True        

    def onDraw( self, kwargs ):
        """ This callback is used for rendering the drawables
        """
        handle = kwargs["draw_handle"]

        c1 = self.state_context.component1()

        gadget_name = self.state_context.gadget()
        if gadget_name == "face_gadget":
            self.face_gadget.setParams({"indices":[c1]})
        else:
            self.face_gadget.setParams({"indices":[]})

        self.face_gadget.draw(handle) 
        self.line_gadget.draw(handle) 
        self.point_gadget.draw(handle) 

        self.cursor.draw(handle)

Here’s a more complex code snippet to demonstrate the use of gadgets with hou.ViewerStateDragger for translating a geometry along the located polygon normal. The use of hou.ViewerStateDragger is similar to hou.ViewerHandleDragger as covered here.

See $HFS/Houdini/viewer_states/examples/state_gadget_demo.hip for the complete demo.

def onMouseEvent(self, kwargs):

    # set the cursor label
    self.cursor.setParams(kwargs)

    ui_event = kwargs["ui_event"]

    gadget_name = self.state_context.gadget()
    if gadget_name in ["line_gadget", "face_gadget", "point_gadget"]:
        gadget = self.state_gadgets[gadget_name]
        label = self.state_context.gadgetLabel()
        c1 = self.state_context.component1()
        c2 = self.state_context.component2()

        # update the cursor with the active gadget info
        self.cursor.setLabel("{} : {} {}".format(label, c1, c2 if c2 > -1 else ""))
        self.cursor.show(True)

        if gadget_name == "face_gadget":

            reason = ui_event.reason()          
            if reason == hou.uiEventReason.Start:

                # setup the dragger to translate the geometry along the 
                # located polygon normal
                self.scene_viewer.beginStateUndo("Drag")

                # Get the line starting position and direction
                line_orig = hou.Vector3()
                normal = hou.Vector3()
                uvw = hou.Vector3()
                (rpos, rdir) = ui_event.ray()
                self.geometry.intersect(rpos, rdir, line_orig, normal, uvw)

                line_dir = self.geometry.prim(c1).normal()                    
                self.dragger.startDragAlongLine(ui_event, line_orig, line_dir)                    

                # Position the guide line 
                self.guide_line_points[0].setPosition(line_orig)
                self.guide_line_points[1].setPosition(line_orig + line_dir*0.3)

                # ... and the arrow
                rot_mat = hou.Vector3(0, 1, 0).matrixToRotateTo(line_dir)
                self.rotate = hou.hmath.buildRotate(rot_mat.extractRotates())

                xform = self.rotate
                xform *= hou.hmath.buildTranslate(self.guide_line_points[1].position())
                self.guide_arrow.setTransform(xform)

                self.guide_line.show(True)
                self.guide_arrow.show(True)

            elif reason in [hou.uiEventReason.Active, hou.uiEventReason.Changed]:
                # Get the latest dragger values
                drag_values = self.dragger.drag(ui_event)
                delta = drag_values["delta_position"]

                # update the translate parms
                self.tx.set(self.tx.eval() + delta[0])
                self.ty.set(self.ty.eval() + delta[1])
                self.tz.set(self.tz.eval() + delta[2])

                # update the guide geometry
                self.guide_line_points[0].setPosition(self.guide_line_points[0].position() + delta)
                self.guide_line_points[1].setPosition(self.guide_line_points[1].position() + delta)
                xform = self.rotate
                xform *= hou.hmath.buildTranslate(self.guide_line_points[1].position())
                self.guide_arrow.setTransform(xform)

                if reason == hou.uiEventReason.Changed:
                    # Done dragging
                    self.dragger.endDrag()
                    self.guide_line.show(False)
                    self.guide_arrow.show(False)

            if reason != hou.uiEventReason.Located:
                # Update the guide geometry data id
                self.guide_line_geo.findPointAttrib("P").incrementDataId()
                self.guide_line_geo.incrementModificationCounter()
                self.guide_arrow_geo.incrementModificationCounter()

            if reason == hou.uiEventReason.Changed:
                self.scene_viewer.endStateUndo()

    else:
        self.cursor.show(False)

    return True        

Generating guide geometry

  • For simple guides, you could build them programmatically by applying SOP verbs to an empty hou.Geometry object.

    (Note that a verb overwrites the Geometry object you pass to execute(). To build up multiple generators, you need to execute() into a “buffer” Geometry and merge that into a “main” Geometry.)

    sops = hou.sopNodeTypeCategory()
    box = sops.nodeVerb("box")
    box.setParms({"scale": 0.25})
    
    geo = hou.Geometry()
    temp = hou.Geometry()
    
    for x in (-0.5, 0.5):
        for y in (-0.5, 0.5):
            for z in (-0.5, 0.5):
                box.setParms({
                    "t": hou.Vector3(x, y, z)
                })
                box.execute(temp, [])
                geo.merge(temp)
    

    You can use a Python SOP to preview, test, or debug your geometry generation script. It outputs a hou.Geometry object you build in the node’s Python code parameter.

  • For more complex guides, if your state is associated with an asset, you can cook an arbitrary SOP node inside your asset to generate the guide geometry.

    This is especially powerful because you can usually set up the “guide geometry” network inside your asset to build the guides based on the asset’s parameter values, without having to script anything.

    When the state implementation class is instantiated, it doesn’t yet have a reference to your node, so you can’t reference nodes inside it. You should create the Drawable in the onEnter() method instead.

    class MyState(object):
        def __init__(self, state_name, scene_viewer):
            self.state_name = state_name
            self.scene_viewer = scene_viewer
    
            # We can't create the Drawable for our guide geometry here,
            # because we don't have a reference to the node yet
            self._guide = None
    
        def onEnter(self, kwargs):
            # This method gives us a reference to the node using this state
            node = kwargs["node"]
            # We'll assume it's our asset and cook some SOPs inside
            geo = node.node("guide_output").geometry()
    
            self._guide = hou.SimpleDrawable(
                self.scene_viewer, geo,
                self.state_name + "_guide"
            )
            self._guide.enable(True)
            self._guide.show(True)
    
        def onInterrupt(self, kwargs):
            self._guide.show(False)
    
        def onResume(self, kwargs):
            self._guide.show(True)
    
  • You can, of course, cook arbitrary geometry nodes to generate guide geometry.

    For example, you can imagine a tool that lets you align one piece of geometry to another. You could script the selection of the primitives to align, then enter the state. The state could generate a guide Geometry object containing the selected primitives. As the user mouses over primitives to align to, you could display a preview of the alignment by drawing the guide geometry in wireframe in-place and aligned to the primitive under the mouse pointer.

Tip

The Control SOP is a useful convenience for generating cursors, guides, and/or markers, such as jacks and crosshairs.

Note

Guide Geometry should consist only of closed polygon meshes. Other types of geometry may not render correctly.

Moving, rotating, and scaling guides

  • Setting a Drawable object’s transform with a hou.Matrix4 object will update its position, orientation, and scale the next time the viewer redraws.

    Remember to keep each piece of guide geometry you want to transform separately in separate Drawable objects.

  • Unfortunately an introduction to linear algebra is beyond the scope of this documentation. The hou.hmath module contains useful functions for building matrices, and the hou.Matrix4 object itself has useful methods. You should familiarize yourself with these functions.

    xform = hou.hmath.buildTranslate(1, 0, 0)  # type: Matrix4
    xform *= hou.hmath.buildRotate(90, 180, 45)
    xform *= hou.hmath.buildScale(0.25, 0.25, 0.25)
    drawable.setTransform(xform)
    

Support objects and functions

It is beyond the scope of this documentation to explain linear algebra and transformation matrices. However, when you have to work with matrices, you should be aware of the different objects and functions available to help.

Note

Houdini uses row-major matrices. This may be different than the way tutorials or textbooks describe transformation matrices.

  • The hou.Matrix4 object represents a 4×4 matrix. It has utility methods such inverted() and transposed().

  • The hou.Vector3 object is used to represent positions (translates), direction vectors, normals, scales, and Euler rotation angles. It has utility methods such as dot() and cross().

    It also has a few utility methods related to specific uses. For example, if you have a position, you can get the distanceTo() or angleTo() another position. If you have a direction vector, you can get its length() or lengthSquared().

  • hou.hmath.identityTransform creates a 4×4 identity matrix object.

  • hou.Matrix4.explode extracts the different “parts” of a transform matrix. It returns a dictionary mapping “translate” to a Vector3 position, “rotate” to a Vector3 containing Euler angles in degrees, “scale” to a Vector3 containing scales, and so on.

  • hou.hmath.buildTransform creates a transformation Matrix4 from a dictionary like the one created by hou.Matrix4.explode. Whatever keys you include (for example, "transform", "rotate", "shear") will be used to build the matrix.

  • hou.hmath.buildTranslate, hou.hmath.buildRotateAboutAxis, hou.hmath.buildRotate, hou.hmath.buildScale each build a transformation Matrix4 containing only one “part”, for example, the translate, rotate, or scale. You can combine

  • hou.Matrix3.extractRotates extracts Euler angles in degrees from a 3×3 orientation matrix. hou.Matrix4.extractRotationMatrix3 extracts a 3×3 orientation matrix from a 4×4 transformation matrix.

Compensating guide transforms

Drawable objects always interpret their transform in world space, but the ray you get in a SOP state is in local space. If the parent Geo object has the default transforms, there is no difference. However, if you try to display guide geometry based on SOP transforms, or based on the pointing ray (which is in local space), and the parent Geo is transformed, the guide geometry will appear in the wrong place.

To position guide geometry properly in world space, you should find the parent Geometry object and apply its transform before setting the transform of a Drawable object.

The general method for transforming local position and rotate/vector into world space is:

# Assume you have a local position and a local rotate
local_position = ...  # type: hou.Vector3()
local_rotate = ...  # type: hou.Vector3()

# Compensate for the Geo object's transform
parent = ancestorObject(kwargs["node"])
if parent:
    parent_xform = node.parent().worldTransform()
    world_pos = local_pos * parent_xform
    world_rotate = local_rotate.multiplyAsDir(parent_xform)
else:
    world_pos = local_pos
    world_rotate = local_rotate

As an example, here’s a simple state that displays a sphere “cursor” guide under the mouse pointer:

from stateutils import ancestorObject
from stateutils import sopGeometryIntersection
from stateutils import cplaneIntersection

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

        self._cursor = hou.SimpleDrawable(
            scene_viewer,
            hou.drawablePrimitive.Sphere,
            state_name + "_cursor"
        )
        self._cursor.enable(True)
        self._cursor.show(False)

    def onMouseEvent(self, kwargs):
        # Get the ray origin and direction in local space
        ui_event = kwargs["ui_event"]
        ray_origin, ray_dir = ui_event.ray()

        # Find the container Geo... this is often just node.parent(), but we need to
        # handle the case where the node is inside one or more subnets
        parent = ancestorObject(kwargs["node"])
        parent_xform = parent.worldTransform()

        intersected = -1
        if node.inputs() and node.inputs()[0]:
            # Grab the incoming geometry
            geometry = node.inputs()[0].geometry()

            # Intersect in local space
            intersected, position, _, _ = sopGeometryIntersection(geometry, ray_origin, ray_dir)

        if intersected >= 0:
            # Convert intersection to world space
            world_pos = position * parent_xform
        else:
            # Either there was no incoming geometry or the ray missed, so
            # try intersecting the construction plane

            # cplaneIntersection() works in world space, so first convert
            # our ray to world space
            ray_origin_world = ray_origin * parent_xform
            ray_dir_world = ray_dir.multiplyAsDir(parent_xform)

            # Intersect in world space
            world_pos = cplaneIntersection(self.scene_viewer, ray_origin_world, ray_dir_world)

        # Build a Matrix4 from the world space translate
        m = hou.hmath.buildTranslate(world_pos)
        self._cursor.setTransform(m)
        self._cursor.show(True)

    def onInterrupt(self, kwargs):
        # Don't show the cursor guide when the tool is paused
        self._cursor.show(False)

Utility functions

Finding ancestor object node

In a SOP state, you sometimes need to access methods or parameters on the Object node containing the SOP node (for example, to compensate for object-level transformations. The containing object node is often just node.parent(), but you need to handle the case where the node is inside one or more subnets.

Given a SOP node, the following function returns its closest Object node ancestor.

# In stateutils
def ancestorObject(sop_node):
    objs = hou.objNodeTypeCategory()
    if sop_node.type().category() == objs:
        return sop_node

    parent = sop_node.parent()
    while parent and parent.type().category() != objs:
        parent = parent.parent()

    if not parent or parent.type().category() != objs:
        raise ValueError("Node %r is not inside an Object node")

    return parent
See also

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