Houdini 17.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

Python viewer states

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

The hou.Drawable API lets you display guide geometry. The geometry source can be a hou.Geometry object or a hou.drawablePrimitive value (currently the only possible value is hou.drawablePrimitive.Sphere).

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 SopNode.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 to distinguish it from "real" geometry. This is default display mode for the Drawable object. You can set the wireframe color using hou.Drawable.setWireframeColor(). You can change a Drawable object to shaded mode using hou.Drawable.setDisplayMode().

  • 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 enable and 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()
    

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 instatiated, 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.Drawable(
                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 /hom/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.

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 * parent_xform.extractRotates()
    # Could also write above line as
    # 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. It gets the parent transform and applies it to the cursor:

from stateutils import ancestorObject


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

        self._cursor = hou.Drawable(
            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
        ui_event = kwargs["ui_event"]
        ray_origin, ray_dir = ui_event.ray()

        # Intersect in local space!
        intersected = -1
        if node.inputs() and node.inputs()[0]:
            # Grab the incoming geometry
            geometry = node.inputs()[0].geometry()
            intersected, pos, _, _ = sopGeometryIntersection(geometry, origin, direction)
        if intersected < 0:
            # Either there was no incoming geometry or the ray missed, so
            # try intersecting the construction plane
            position = cplaneIntersection(self.scene_viewer, origin, direction)

        # 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"])
        # Use the container's transform to display the cursor in world space
        parent_xform = parent.worldTransform()
        world_pos = position * parent_xform
        # 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

Python viewer states

See also

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.