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 theGeometry
object. If content of the underlyingGeometry
object changes, you do not need to recreate theDrawable
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 theGeometry
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 toexecute()
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 theonEnter()
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()
andtransposed()
. -
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()
andcross()
.It also has a few utility methods related to specific uses. For example, if you have a position, you can get the
distanceTo()
orangleTo()
another position. If you have a direction vector, you can get itslength()
orlengthSquared()
. -
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 |